Gitea vs Forgejo: Self-Hosted Git Compared
One codebase, one governance row, and two forges that have quietly stopped being the same program

Contents
For a couple of years the honest answer to “Gitea or Forgejo?” was “flip a coin, they’re the same program with different logos”. That answer has expired. The two projects have been diverging on purpose since 2024, the licences no longer match, the code flows in one direction only, and — the part that catches people — migration is a one-way door.
I’ve run both. My own forge started as Gitea because that’s what existed, moved to Forgejo during the period when doing so was a docker compose image swap, and stayed. That history makes me a slightly biased witness, so let me lay out the mechanics first and save my opinion for the end, where you can discount it appropriately.
If you’re still on GitHub and wondering why anyone bothers, self-hosting your Git is a separate argument I’ve already made. This is the argument after that one.
Why there are two of these
Gitea itself was a fork — of Gogs, in 2016, after that project’s maintainer proved unable to accept help at the rate the community wanted to give it. Gitea’s founding principle was community governance. Remember that; it’s load-bearing.
In October 2022 the Gitea maintainers announced that the domain and the trademark had been transferred to a newly formed for-profit company, Gitea Ltd. The community found out from the announcement. For a project whose entire origin story was “we forked because one person controlled everything”, handing control of the name to a company without discussion was a poor look, and the response was predictable.
Forgejo launched that December, run by Codeberg e.V., a German non-profit association that operates a public Git hosting service and needed the software it depended on to remain outside anyone’s asset register. The trademark sits with the association. The stated reason for existing is governance, which is why the code was identical at the start.
The governance argument is more relevant to a homelab than it sounds. Your forge is where your infrastructure-as-code lives. It’s the thing your GitOps loop reconciles against. Whoever controls it has a lever on your entire lab, five years from now, when you’ve forgotten you made this choice.
What actually differs now
The fork went hard in early 2024. For the first year Forgejo was a soft fork: it tracked Gitea’s releases and applied a patch set. That stopped. Forgejo declared itself a hard fork and began developing independently, cherry-picking from Gitea where useful and going its own way where not.
The licences diverged. Gitea is MIT. Forgejo relicensed to GPLv3 starting with its v9 series. The consequence is asymmetric and permanent: Forgejo can take Gitea’s MIT-licensed code; Gitea cannot take Forgejo’s GPL code. Every fix Forgejo writes now stays on Forgejo’s side of the wall. This is the mechanism by which two identical programs become two different programs, and it operates whether or not anyone intends it.
The version numbers stopped rhyming. Gitea continued its long 1.x line. Forgejo jumped to its own scheme in 2024 and has been running major versions since. Two projects, two numbering conventions, and a lot of confused blog posts comparing versions that have nothing to do with each other.
Gitea Ltd sells things. There’s a commercial Gitea offering with enterprise features and a hosted service. That’s a legitimate business model that funds full-time development, and it also means some work happens where you can’t read it. Forgejo has no proprietary edition, being run by an association whose members would notice.
Forgejo builds what Codeberg needs. Codeberg hosts thousands of strangers’ repositories, so Forgejo gets moderation tooling, abuse handling and accessibility work that a corporate roadmap wouldn’t prioritise. It’s also where the federation experiment lives — ActivityPub between forges, so an issue on one instance could be opened by a user on another. It has been experimental for years and it may stay that way. If it lands, it’s the most interesting thing either project is doing.
Running them
Here’s the thing that makes this comparison bearable: the deployment is near-identical. Same Go binary shape, same database options, same resource profile.
| |
Swap the image for gitea/gitea:latest and the env prefix from FORGEJO__ to GITEA__ and you have the other one. That double underscore is section-then-key from the INI config; FORGEJO__server__ROOT_URL writes ROOT_URL under [server]. It’s the tidiest config-via-environment scheme I’ve used.
Two notes from experience. SQLite is the correct default here — a forge for a handful of humans generates almost no write contention, and I’ve argued at length that SQLite is a production database for a homelab. Reach for Postgres when you’ve got dozens of concurrent users or CI hammering the API.
And SSH_PORT versus the container’s port 22 confuses everyone once. The container listens on 22 internally; you publish it as 2222 on the host; SSH_DOMAIN and SSH_PORT control the clone URL the web UI displays. Get them out of step and the UI cheerfully hands users a URL that cannot work.
The features you’ll actually compare
CI. Both ship a GitHub Actions-compatible runner — Gitea Actions with act_runner, Forgejo Actions with forgejo-runner, which is a fork of the same thing. Your workflow YAML is portable between them and mostly portable from GitHub. The gotcha is identical on both sides: the default runner labels decide which container image your jobs get, and a mismatch produces a job that queues forever with no error. I went through the setup in Gitea Actions: self-hosted CI that boots in seconds, and every word applies to Forgejo with the names changed. If you want to debug workflows before they reach either, act runs them locally.
Package registries. Both do containers, npm, PyPI, Maven, Cargo, Debian, and a dozen more, from the same code lineage. Both work. Using your forge as your container registry is the underrated feature of this entire category — one auth system, one backup, one thing to keep alive.
Mirrors and migration-in. Pull mirrors from GitHub, with issues and releases, work well on both. This is how most people start: mirror everything, discover you never open GitHub, flip the switch.
Renovate. Both are supported platforms for automated dependency updates, which matters more than it sounds — keeping homelab Compose files current is the single highest-value thing a self-hosted forge can automate for you.
Web UI. Forgejo has done more visual and accessibility work; Gitea’s UI has moved closer to GitHub’s. Both are fine. Nobody has ever chosen a forge on this and been happy about the reasoning.
Migration, and the one-way door
Gitea to Forgejo is supported. Forgejo publishes which Gitea versions can migrate to which Forgejo versions, and the procedure is: stop Gitea, back up everything, start Forgejo on the same data directory, let it run its own schema migrations. In the early days it was a pure image swap. Post hard-fork it’s a supported path with version constraints, and you must read the current table rather than trusting a blog post — including this one.
Forgejo to Gitea is not supported. Once Forgejo has migrated your schema, Gitea won’t read it. The path back is git clone for the repositories and re-entering everything else by hand.
That asymmetry should shape the order in which you try things. Gitea first costs you nothing. Forgejo first commits you.
| |
dump produces a portable archive containing the database, repositories, config and attachments. Take one before every upgrade of either project. It has saved me twice, both times from my own confidence.
Troubleshooting
Clone URLs in the UI are wrong after moving to a non-standard SSH port. SSH_DOMAIN and SSH_PORT are display values and don’t reconfigure anything real:
| |
Note the config path still says gitea on Forgejo, for compatibility. That trips people up during a migration and it’s cosmetic.
Actions jobs queue forever with no error. The runner registered with labels that no workflow requests. List what it actually claimed:
| |
Then match runs-on: in your workflow to a real label.
Web UI works, git push over SSH fails with permission denied. The container’s SSH is a shim that maps keys to users via the authorized_keys file it maintains. If you’ve bind-mounted /data from a filesystem with awkward permissions, the shim silently fails to write:
| |
An empty or unwritable file is your answer. USER_UID/USER_GID must match the host directory’s owner.
A migration leaves the instance up but repositories missing. The database migrated, the repository paths didn’t. Check that ROOT under [repository] points where your repositories actually are — this is the failure mode when someone changes the volume layout at the same time as the image. Change one thing at a time.
Post-upgrade 500 on every page. Almost always a schema migration that died halfway. Restore the dump, read the startup log properly, and check whether you’ve skipped a major. Both projects support upgrading one major at a time and neither promises anything about jumping three.
The case against running either
A comparison that only compares is doing half the job, so here’s the argument for closing this tab.
A self-hosted forge is a circular dependency waiting to happen. Mine holds the Compose files for every service I run — including the reverse proxy that serves the forge and the monitoring that would tell me the forge is down. The first time that box died, my recovery instructions were in a repository on the box. This is a genuinely stupid failure mode and it is the default one. The fix is a push mirror to somewhere else and a copy of your dump off the machine, both of which take an hour to set up and which almost nobody does before their first outage. Consider this your warning.
The bus factor is you. GitHub has an operations team. Your forge has you, on a Sunday, when a schema migration goes sideways and your CI is down and the thing you were actually trying to do that afternoon has been abandoned. I think this trade is worth it. I also think people underestimate it consistently, which is why I keep coming back to the argument that self-hosting is not free — the electricity is the cheap part.
CI runners are a security surface you built yourself. An Actions runner executes arbitrary code from your repositories, and the usual setup gives it a Docker socket so it can build images. That’s root on the runner host, one bad workflow away, and if the runner shares a host with anything else you care about, you’ve quietly connected your CI to your data. Give runners their own machine or their own VM. The same pinning and housekeeping discipline you apply to services applies double to the images your CI pulls.
latest will eventually eat you. Both projects publish a latest tag and both do schema migrations on startup. An unattended pull that crosses a major boundary is how a working forge becomes a Sunday. Pin the major in your Compose file — the example above says :12 deliberately — and upgrade when you’re at a keyboard and awake.
None of this makes me want GitHub back. It does mean the honest version of “just run your own forge” comes with three chores attached, and the people who skip them are the people writing the forum posts.
The verdict
Choose Gitea if you want the larger deployment base and the longer paper trail, if you might one day want a commercial support contract, or if MIT licensing matters to what you’re building. It is a good forge with full-time people on it and it isn’t going anywhere.
Choose Forgejo if you want the software your infrastructure depends on to be held by an association rather than a company that can be acquired, if the federation experiment interests you, or if you value that Codeberg runs the same code at real scale and finds the bugs before you do.
For a homelab the technical difference is close to zero, and I’d be lying if I claimed otherwise. Both are one binary, roughly two hundred megabytes of RAM, a SQLite file, and a forge that outperforms your expectations. You will not notice a difference in daily use, and anyone telling you they’ve benchmarked a meaningful gap is measuring their own configuration.
So it comes down to what you want from the ten-year version of this decision, and that’s a governance question wearing a technical costume. I run Forgejo because a forge full of my infrastructure code should belong to an association that cannot sell it. That reasoning has almost nothing to do with the software and I’m at peace with it. Your lab, your call — just make it with the one-way door in view, and take the dump before you find out.




