Self-hosted Git forges in 2026: revisit Gitea, Forgejo and GitLab CE before you move another repository

Picture a software company in Kochi that builds billing and export-documentation software for seafood exporters in Aroor, Alappuzha, Kollam and Thoothukudi (an invented example, not a real client). About 25 developers push to a Gitea 1.21 server that someone set up on a spare VM in 2023. CI runs on an old Jenkins box. Test fixtures contain invented but realistic invoices with GST numbers, and one client contract says source code and build logs stay on servers the company controls.

A client audit then asks two plain questions: who can push to a release branch, and where do CI secrets live? Nobody can answer quickly, so the team decides to “fix the Git server” this quarter. Three proposals arrive. The person who installed it wants to upgrade Gitea to the new 28.x line and switch on Gitea Actions. A developer who follows the Codeberg community wants to move to Forgejo, for its non-profit governance and copyleft licence. The lead who used it at a previous job wants GitLab CE, so that code, CI and the container registry live in one product.

This post compares those three as they stand on 19 October 2026: history and governance, licences, versions and release rhythm, architecture and footprint, CI options, migration paths, backups and upgrades, security records and day-to-day operations. Hosted plans (Gitea Cloud, Codeberg, GitLab.com) and paid editions are out of scope except where they explain the free one. The question is narrow: what actually moves, and what you must rebuild, when you move a team from one forge to another.

Facts come from GitHub, Codeberg and GitLab release APIs and LICENSE files, official docs and blog posts, CVE records (checked on cveawg.mitre.org), GitHub advisories and vendor patch notes, and a drill that ran Gitea 28.2.0 and Forgejo 15.0.10 with their runners on one shared box. GitLab CE was not run (reasons in the drill section). It is not a benchmark, and nothing here is legal advice.

The short version

  • A forge move is three moves, and only the first is easy. Git data (commits, branches, tags) moved perfectly in the drill. Collaboration data (issues, pull requests, reviews, labels, releases, wiki) mostly moved. Platform data (users, assignees, branch protection, webhooks, CI secrets, variables, run history, collaborators) did not move at all and had to be rebuilt by hand.
  • The forks have diverged for real. Forgejo upgrades in place only from Gitea up to 1.22. When I pointed Forgejo 15.0.10 at a copy of a Gitea 28.2.0 database, it refused to start: database migration version 358 against the 305 it understands. If you are on Gitea 1.22 or older and might want Forgejo, decide before you upgrade Gitea.
  • A green CI run can hide a missing secret. After the move, Forgejo ran the workflow unchanged and the job passed, but the secret it needed was empty. Test for secrets explicitly after any migration.
  • CI runners are remote code execution by design. Both runners registered in a few milliseconds with a host label and ran jobs directly on the VM, with no isolation. Several critical Gitea advisories in 2026 were about Actions: fork pull requests getting past the approval gate, and the runner passing host options into job containers.
  • Patch cadence matters more than features. Gitea’s GitHub repository lists 155 security advisories published between 19 April and 19 October 2026. At least 166 GitLab CE/EE CVEs were published in 2026, two of them CVSS 10.0 and unauthenticated. Forgejo shipped five security patch sets since 30 July. Whichever you pick, budget for patching every couple of weeks.
  • Footprint still splits the choice. Gitea and Forgejo each used about 200 MB of RSS in the drill on SQLite. GitLab documents 8 vCPU and 16 GB of RAM as the single-node baseline (8 GB at minimum), PostgreSQL only, plus Redis or Valkey, Gitaly and Sidekiq.

Where things stand on 19 October 2026

Dates are release published_at or tag commit dates from each project’s API, converted to IST (UTC+5:30). Licences are from LICENSE files.

Gitea

  • Latest: 28.2.0, 18 October 2026, 11:55 PM. Before it, 28.1.0 (7 October) and 28.0.0 (30 September), the release that dropped the old “1.” prefix. The last 1.x line is 1.27.3 (29 August 2026).
  • Rhythm: 14 stable releases in 2026 so far, and four new feature lines in twelve months (1.25.0 in October 2025, 1.26.0 in April, 1.27.0 in July and 28.0.0 in September 2026), each followed by frequent patch releases.
  • Licence: MIT (copyright “The Gitea Authors” 2016 and “The Gogs Authors” 2015).
  • Runner: gitea-runner 5.1.1 (16 October 2026), MIT, now hosted at gitea.com/gitea/runner.
  • Attention: about 58,560 GitHub stars and 2,195 open issues and pull requests.

Forgejo

  • Latest: 17.0.0, 15 October 2026, 10:06 PM. Its release page carries a warning: the project advises against upgrading to 17.0.0 while it investigates an issue that can block pushes, merges and other code operations.
  • Maintained lines: 16.0.6 and 15.0.10 (both 12 October 2026). 15.0 is the current LTS, released 16 April 2026 and supported until 15 July 2027. 16.0 reaches end of life on 29 October 2026, and 17.0 on 28 January 2027.
  • Rhythm: a major release every three months; the first-quarter release each year is LTS. 35 releases across all lines in 2026.
  • Licence: GPL v3 or later since v9.0 (16 October 2024). Earlier versions remain MIT, and the project notes that many individual files are still MIT.
  • Runner: forgejo-runner 13.2.0 (19 September 2026), GPL v3.
  • Attention: about 5,674 stars on Codeberg, with 1,571 open issues and 152 open pull requests.

GitLab CE

  • Latest: 19.5.0, 15 October 2026. Monthly minor releases on the third Thursday; recent patch releases 19.4.2, 19.3.5 and 19.2.8 (12 October 2026, a critical security release).
  • Rhythm: the maintenance policy schedules a minor release on the third Thursday of each month and a major each May (20.0 is scheduled for 20 May 2027). Security fixes are backported to the current release and the two before it; patch releases come twice a month, with ad-hoc critical patches in between.
  • Licence: open core. Content outside the ee/ and jh/ directories is MIT Expat, the doc/ directory is CC BY-SA 4.0, and ee/ has its own licence. GitLab CE is the MIT part; GitLab Inc. is the steward.
  • Runner: GitLab Runner 19.5.0 (14 October 2026), MIT.
  • Attention: the main gitlab-org/gitlab project shows about 6,167 stars on GitLab.com; stars there are not comparable with GitHub.

Stars show interest, not suitability for your audit.

Timeline

  • 1 November 2016: the go-gitea/gitea repository is created on GitHub as a community fork of Gogs.
  • 8 December 2016: “Welcome to Gitea” post explains the fork: contributors found the single-maintainer model of Gogs frustrating.
  • 24 to 30 October 2022: the Gitea blog announces Gitea Limited, says the domains and trademark were transferred to the company, and the project stays MIT. A community open letter (28 October) asks for a non-profit to hold them instead.
  • 15 December 2022: Forgejo is announced as a soft fork, with Codeberg e.V. (a German non-profit association) as custodian of its domains and trademarks.
  • 15 February 2024: Forgejo announces it is becoming a hard fork.
  • 22 August 2024: Forgejo announces the move to copyleft; v9.0 (16 October 2024) is the first GPL v3+ release.
  • 16 January 2025: Forgejo v10.0, the last stop for in-place upgrades from Gitea (up to 1.22).
  • 16 April 2026: Forgejo 15.0 LTS.
  • 21 May 2026: GitLab 19.0.
  • 4 July 2026: CVE-2026-20896 (Gitea Docker image trusts reverse-proxy headers from any IP by default) published.
  • 30 September 2026: Gitea 28.0.0 drops the “1.” prefix.
  • 12 October 2026: GitLab critical patch release for CVE-2026-97229 (CVSS 10.0); same day, Forgejo 16.0.6 and 15.0.10 security releases.
  • 15 October 2026: GitLab 19.5 and Forgejo 17.0.0.
  • 18 October 2026: Gitea 28.2.0 (11:55 PM), followed about 20 minutes later by 10 newly published advisories for the issues it fixed.

First question: what exactly are you moving?

“Move the Git server” sounds like git push --mirror. It is really three layers, and tools differ at each one:

  1. Git data. Commits, branches, tags, and usually LFS objects. Every forge can import this, and it is easy to verify with commit counts and hashes.
  2. Collaboration data. Issues, pull or merge requests, review comments, labels, milestones, releases and attachments, wiki. Importers move most of it, but the people attached to it are the weak point: authors, assignees, reviewers and mentions only map if the importer can match accounts.
  3. Platform data. Users and teams, permissions, branch protection, required approvals, webhooks, deploy keys, CI secrets and variables, runner registrations, package registries, and the CI run history your auditor may ask for. In the drill, none of this moved. It is configuration, and it has to be written down and rebuilt.

Write the promise in one sentence and get it signed before you install anything:

After the move, [every repository] has the same commits and tags, [open issues and pull requests] keep their numbers and history, [release branches] accept pushes only from [named people] after [N approvals], CI secrets live [only in the forge, scoped to the repository], runners execute untrusted code [only in disposable containers], and we can restore everything to [a new server] within [N hours], losing at most [N hours] of work.

The web UI comes last; all three have a good one.

What each forge actually is

Gitea 28.2.0

Gitea is a single Go binary with SQLite, MySQL, PostgreSQL or MSSQL behind it, an embedded web UI, a built-in SSH server option, a package registry with more than twenty formats, and Gitea Actions for CI. The 2016 founding post is clear about why it exists: Gogs had one maintainer who would not share write access, so the community forked it with elected owners. Gogs itself is still maintained (v0.14.3, 8 June 2026, MIT). In October 2022 the project announced Gitea Limited, said the founder had transferred the domains and trademark to it, and promised that the code would stay open source and MIT. Today the official site presents its paid Gitea Cloud and Gitea Enterprise (an enhanced edition with features such as organisation-wide branch protection inheritance and dependency scanning) under the name CommitGo; how CommitGo relates to Gitea Ltd is Not documented in the sources I read. The community edition stays MIT.

Version 28.0.0 (30 September 2026) dropped the “1.” prefix, so the line went from 1.27.3 straight to 28.0.0. Its highlights were audit logging, bot accounts, HTTPS deploy tokens, administrator impersonation of users and code-owner approval rules. Two breaking changes matter for operations: Git network operations now go through an internal proxy with new egress settings, and a new RUN_RETENTION_DAYS setting deletes old Actions runs. Release binaries no longer include 32-bit x86 or gogit builds, and download file names changed, so check your download scripts.

Gitea Actions is “designed to be compatible with GitHub Actions”, with listed differences: uses: accepts absolute URLs and a self: prefix, some syntax such as jobs.<job_id>.environment is ignored, and the job token cannot publish to the package registry. The runner documentation describes three modes: Docker, Docker-in-Docker, and host, which it marks plainly as “no isolation between jobs”.

Forgejo 15.0.10 LTS and 17.0.0

Forgejo started in December 2022 as a soft fork of Gitea, “in reaction to control of Gitea being taken away from the community” by the new company, after the open letter went unanswered. Codeberg e.V., the non-profit association that runs Codeberg.org on Forgejo, holds the domains and trademarks. In February 2024 Forgejo became a hard fork: it no longer follows Gitea’s code, and its own runner and Actions system had already diverged. In August 2024 it moved to GPL v3 or later, starting with v9.0, explaining that copyleft protects users from modified versions that take freedoms away.

The practical results show up in three places. First, the release model is predictable: a major every three months, the first-quarter release is LTS for 15 months, and security releases are pre-announced (without details) through a security-announcements repository so administrators can plan. Second, in-place upgrades from Gitea stop at 1.22: the upgrade guide supports Gitea up to 1.22.x, through Forgejo v10.0.x, and nothing newer. Third, the products are drifting apart in features: v17.0 added repository funding links, Bitbucket Data Center migrations, revamped LFS support and Actions job summaries, and changed how absolute links in rendered documents resolve.

Forgejo Actions reads workflows from .forgejo/workflows, falling back to .github/workflows according to its docs; in my run it also picked up a .gitea/workflows file, which I would not rely on. Its security guide is blunt: the runner “performs remote code execution”, and with the host label, steps run “as the same user as the Forgejo Runner” on the same machine “without any isolation”.

On 19 October 2026, the safe choice for production is 15.0.10 LTS or 16.0.6 (until 29 October). 17.0.0 carries the project’s own advice not to upgrade yet.

GitLab CE 19.5.0

GitLab is a different kind of product: a Ruby on Rails application with Puma and Sidekiq, Gitaly for repository storage, PostgreSQL (the only supported database; 17.x is the minimum for GitLab 19), Redis or Valkey for sessions and background jobs, and optional services such as the container registry, Pages, Prometheus and advanced search. The Linux package installs most of these on one machine, with Prometheus bundled and enabled by default. CE and EE code live in one repository, and the LICENSE file draws the line: everything outside ee/ and jh/ is MIT.

The installation requirements page lists 8 vCPU and 16 GB of RAM as the single-node baseline, with at least 8 GB in a memory-constrained setup, 40 GB for the application node, swap disabled where possible, and SSD storage for Gitaly. Each Sidekiq process needs at least 200 MB and grows under load.

GitLab CI is built in and mature, with GitLab Runner (MIT) and its own executors. Upgrades have a published rhythm: required upgrade stops at x.2, x.5, x.8 and x.11, so 19.2 and 19.5 are stops this year, and you must let background migrations finish at each stop. The backup page is honest about scope: the backup command does not include /etc/gitlab/gitlab-secrets.json or gitlab.rb, Redis, object storage on Linux package installs, or Elasticsearch, and a consistent backup needs no pending Sidekiq jobs.

Side by side

No source, no claim: where I found no primary source, the bullet says “Not documented”.

Licence, governance and maintainer health

  • Gitea: MIT. Gitea Ltd holds the domains and trademark; release posts are signed by members of a Technical Oversight Committee. No foundation. Paid Cloud and Enterprise editions alongside.
  • Forgejo: GPL v3+ since v9.0 (MIT before). Community governance, with Codeberg e.V. as non-profit custodian of the name and domains. Funded by volunteers, grants, donations and employee time, according to its FAQ. No paid edition.
  • GitLab CE: MIT for CE, with ee/ code under its own licence in the same repository. Steward is GitLab Inc., which calls its model open core; its stewardship page promises that a feature, once open source, will not move to a paid tier. Premium and Ultimate features (for example the new Secrets Manager, GA in 19.5 for paid tiers) are not in CE.
  • Licence changes so far: Gitea none; Forgejo MIT to GPL v3+ (2024); GitLab CE none for CE.

Architecture and footprint

  • Gitea: one Go binary; SQLite, MySQL, PostgreSQL or MSSQL. In the drill: about 172 MB RSS at start and 217 MB after the test work, on SQLite. Minimum hardware: Not documented as a number in the install pages I read.
  • Forgejo: one Go binary; SQLite, MySQL or PostgreSQL. In the drill: about 181 MB at start and 200 MB after. Minimum hardware: Not documented as a number in the pages I read.
  • GitLab CE: many services (Rails, Puma, Sidekiq, Gitaly, PostgreSQL, Redis or Valkey, bundled Prometheus, optional registry, Pages and search). Baseline 8 vCPU and 16 GB RAM for one node, 8 GB minimum. Not run in the drill.

CI options

  • Gitea Actions: built in, GitHub Actions style YAML in .gitea/workflows. Runner gitea-runner 5.1.1 with Docker, Docker-in-Docker, Kubernetes or host labels. Fork pull requests from first-time contributors need approval.
  • Forgejo Actions: built in, similar YAML in .forgejo/workflows. Runner forgejo-runner 13.2.0 with Docker, LXC or host labels. Same approval idea for forks, and secrets are withheld from fork runs.
  • GitLab CI: built in, .gitlab-ci.yml, different syntax from Actions. GitLab Runner 19.5.0 (MIT). Moving from Actions to GitLab CI is a rewrite, not a copy.
  • In all three: a host runner is the forge’s own machine (or whatever machine runs it) offered to whoever can trigger a workflow.

Migration paths

  • Into Gitea: built-in migration (the 28.2.0 source has downloaders for GitHub, GitLab, Gitea, Gogs, OneDev, GitBucket, Codebase and AWS CodeCommit), with issues and pull requests where the source API allows.
  • Into Forgejo: the same family of importers (17.0 adds Bitbucket Data Center), plus in-place upgrade from Gitea up to 1.22 through Forgejo v10. From Gitea 1.23 or later, including 28.x, only API or UI migration per repository.
  • Into GitLab CE: a Gitea importer that brings repository data, issues, pull requests, milestones, labels and diff notes, with post-migration user contribution mapping since 17.8. Releases and wiki are not in its documented list.
  • Out of any of them: git clone --mirror for code always works; everything else depends on the target’s importer.

Backup and upgrade

  • Gitea: gitea dump makes a ZIP (config, data, repositories, SQL dump). The docs say stop the instance for consistency, prefer native pg_dump or mysqldump because the XORM SQL dump has open issues, and note there is no restore command: restore is manual, then gitea admin regenerate hooks. Upgrades migrate the database on start; you cannot downgrade without a backup.
  • Forgejo: forgejo dump works the same way. Upgrade guide lists known problematic upgrade paths per version; flush queues before upgrading.
  • GitLab CE: gitlab-backup create plus a separate copy of gitlab-secrets.json and gitlab.rb; object storage and Redis need their own handling. Required upgrade stops each year.

Security record in 2026

  • Gitea: 155 GitHub advisories published between 19 April and 19 October 2026 (151 with CVE IDs; GitHub severity 12 critical, 49 high, 78 medium, 16 low). Gitea is its own CNA.
  • Forgejo: six CVE records published in 2026 found by an NVD keyword search, assigned by MITRE, VulnCheck and VulDB rather than by Forgejo. Fixes ship as dated security patch sets (five since 30 July 2026) and are described in release notes.
  • GitLab: at least 166 CVEs naming GitLab CE/EE published by GitLab’s CNA in 2026 (NVD data), five of them CVSS 9.0 or higher.
  • These counts measure disclosure practice as much as code quality. A project that files a CVE for every finding will look worse in a count than one that does not.

Worked example for the Kochi scenario

The Kochi team (invented) ran the steps below on a copy of one real-sized repository before deciding.

Step 1. Inventory platform data first. List users and teams, branch protection rules, required approvals, webhooks, deploy keys, CI secrets and variables, and runner labels. This is the part no importer moved in the drill, and it is also what the auditor asked about.

Step 2. Check the in-place door before upgrading anything. Their Gitea is 1.21, so an in-place move to Forgejo is still possible through Forgejo v10. Upgrading Gitea to 28.x first would close that door.

Step 3. Migrate one repository through the API and diff the result. For Forgejo, the call looks like this (tokens shortened):

curl -s -X POST http://forgejo.internal/api/v1/repos/migrate \
  -H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" \
  -d '{"clone_addr":"https://gitea.internal/kochi-seafood/invoice-service",
       "service":"gitea","auth_token":"'"$GITEA_TOKEN"'",
       "repo_owner":"kochi-seafood","repo_name":"invoice-service","private":true,
       "issues":true,"labels":true,"milestones":true,"pull_requests":true,
       "releases":true,"wiki":true,"lfs":true}'

Then compare, item by item, branches, tags, commit counts, issues, labels, milestones, pull requests, releases, wiki, protection rules, webhooks, secrets and collaborators on both sides. A small script against both APIs is enough.

Step 4. Make secrets fail loudly. Add a first CI step that exits if a required secret is empty, for example test -n "$DEPLOY_KEY" || exit 1. In the drill, the job passed with an empty secret.

Step 5. Put runners in containers and keep fork runs on approval. No host labels on any runner that can run pull requests. Keep the first-time contributor approval gate on, and patch the forge and the runner together.

Step 6. Practise the restore before the move. Dump, restore to a new directory or VM, regenerate hooks, push to a branch, confirm protection still rejects a push to main, and confirm a secret still decrypts in a CI run.

Security and hygiene

Each CVE ID below was checked in the CVE API (cveawg.mitre.org); times are the CVE record’s publication time in IST. Scores are the CNA’s, or GitHub’s advisory score where the CVE record has none.

Gitea (CNA: Gitea):

  • CVE-2026-20896 (4 July 2026, 1:49 AM): the Docker image shipped REVERSE_PROXY_TRUSTED_PROXIES = * by default, so with reverse-proxy authentication enabled any source IP could impersonate a user. CVSS 3.1 9.8. Affected up to 1.26.2.
  • CVE-2026-59774 (14 October 2026, 7:58 PM): the Org-mode renderer processed INCLUDE directives, letting an unauthenticated attacker with a suitable public repository read server files and reach code execution. GitHub score 9.8. Affected 1.22 to before 1.27.1.
  • CVE-2026-103059 (7 October 2026, 12:52 AM): authentication bypass in the built-in SSH server through key case folding. GitHub score 9.1. Affected up to 1.27.3, fixed in 28.0.0.
  • CVE-2026-104632 (7 October 2026, 12:52 AM): fork pull request approval bypass through cancel and rerun, giving an untrusted first-time contributor a job on a self-hosted runner. GitHub score 9.0. Fixed in 28.0.0.
  • CVE-2026-95106 (7 October 2026, 12:53 AM): Gitea accepted Git trees with duplicate entry names, so the web view could show one file while checkout and CI used another. GitHub score 9.0. Fixed in 28.0.0.
  • CVE-2026-73802 (14 October 2026, 8:07 PM): Gitea Runner before 3.0.0 passed workflow-controlled container options (host namespaces, capabilities) into job containers. GitHub score 9.9.
  • CVE-2026-109553 (19 October 2026, 12:21 AM): with a trusted SSH user CA, certificate principals could match other users’ keys. GitHub score 7.5. Fixed in 28.2.0.

Forgejo (CVE records filed by others):

  • CVE-2026-89094 (11 September 2026, 2:12 AM; MITRE; CVSS 3.1 9.9): remote code execution through a crafted template repository and .forgejo/template expansion. Fixed in 16.0.4.
  • CVE-2026-82556 (30 August 2026, 11:15 PM; VulDB; CVSS 3.1 6.3): SSRF in the repository migration allow-list through DNS lookup handling. Affected up to 15.0.4.
  • CVE-2026-59102 (3 July 2026, 1:14 AM; VulnCheck; CVSS 3.1 5.4): stored XSS through a user’s full name in Actions run descriptions. Fixed in 15.0.3.
  • CVE-2026-90679 (13 September 2026, 9:25 AM; MITRE; 4.3) and CVE-2026-89151 (11 September 2026, 7:53 AM; MITRE; 3.5): ActivityPub signature checks when federation is enabled, and restricted tokens reaching the “allow maintainer edit” feature. Both fixed after 16.0.4.
  • CVE-2025-68971 (published 16 March 2026, 5:30 AM; MITRE): denial of service through multi-gigabyte attachments, through 13.0.3.
  • The 12 October 2026 releases (16.0.6, 15.0.10) fixed fork pull request workflows on review events being treated as same-repository runs, which disabled the approval gate and secret withholding. No CVE ID was listed in the notes.

GitLab (CNA: GitLab):

  • CVE-2026-97229 (13 October 2026, 6:35 AM; CVSS 3.1 10.0): unauthenticated code execution through a crafted regular expression in CI/CD configuration. Fixed in 19.4.2, 19.3.5 and 19.2.8 (12 October).
  • CVE-2026-85706 (12 September 2026, 8:16 AM; 10.0): unauthenticated arbitrary file read through the repository commits API.
  • CVE-2026-19478 (18 August 2026, 1:34 AM; 9.4): unauthenticated modification or deletion of public projects and user data through a GraphQL directive.
  • CVE-2026-89078 and CVE-2026-93577 (24 September 2026, 4:35 and 4:34 AM; 9.9 each): authenticated code execution through crafted regular expressions (a double free and an integer overflow).
  • The 12 October patch post listed 24 fixes in total, including EE-only ones.

Habits that matter more than counting CVEs:

  • Patch the forge and the runner on the same day. One of the critical Gitea advisories of 2026 was in the runner, not the server.
  • Never offer host runners to pull requests. Use containers you throw away, keep the fork approval gate on, and do not give fork runs secrets.
  • Keep the UI and API private where you can. Several critical issues in Gitea and GitLab this year needed no login.
  • Review reverse-proxy and SSH settings after every upgrade. Defaults and behaviour changed in 2026 in Gitea (trusted proxies in the Docker image, SSH key handling).
  • Treat a dump file as a secret. In the drill, the Gitea dump held app.ini with the internal token and JWT secret, the JWT private key and the whole database.
  • Subscribe to the security feed. Gitea’s GitHub advisories, Forgejo’s security-announcements repository, GitLab’s patch release RSS.

Decision guide

Lean towards Gitea if

  • You want one small binary, a broad package registry and Actions, and you are happy with a company-stewarded MIT project.
  • You need paid support or enterprise features later, from the same steward.
  • You can keep up with a fortnightly patch rhythm; 2026 had many critical fixes.

Lean towards Forgejo if

  • Community governance and a non-profit custodian matter to your organisation, and GPL v3+ is acceptable (check with your legal team; this is not legal advice).
  • You want a documented LTS line (15.0 until July 2027) and pre-announced security releases.
  • You are on Gitea 1.22 or older and can still move in place.

Lean towards GitLab CE if

  • You want CI, registry, issue boards and merge request workflows in one mature product, and can give it 8 vCPU and 16 GB or more.
  • Your team already writes .gitlab-ci.yml, or you are willing to rewrite Actions workflows.
  • You can follow required upgrade stops and monthly patching, and you know which features need a paid tier.

Be careful whichever you pick

  • Moving from Gitea 1.23 or newer to Forgejo: plan per-repository migration, not an in-place upgrade.
  • Offering CI to outside contributors: container runners only, approval gates on.
  • Writing “open source” in a procurement note: GitLab CE is MIT, but the product you see in demos often includes paid features.

Kochi (invented) pick. The team chose Forgejo 15.0 LTS, moving in place from Gitea 1.21 through Forgejo v10.0 on a copy first, then to 15.0.10 (a path I could not test on this box). Reasons: the in-place path keeps users, permissions, protection rules, webhooks and secrets, which the per-repository migration did not; the LTS line covers the next audit cycle; and the footprint fits the existing VM. Runners moved to a separate VM with Docker labels only, and the first step of every workflow checks its secrets. They will not take 17.0.0 until the project lifts its warning. GitLab CE stayed the fallback if they later want one product for CI and registry, and Gitea 28.x was ruled out only because upgrading it would close the in-place door to Forgejo, not because of any flaw found in the drill. They practised a forgejo dump restore to a new VM before the switch.

Checklist

  1. Write the one-sentence promise: what moves, who can push, where secrets live, how fast you restore.
  2. Inventory platform data: users, teams, protection rules, webhooks, deploy keys, secrets, runners.
  3. Check your current version against the target’s in-place upgrade limits before upgrading anything.
  4. Migrate one repository and diff every item through both APIs.
  5. Map authors and assignees, or accept that history will show original names without accounts.
  6. Recreate protection rules, webhooks, secrets and variables from your inventory, then review them.
  7. Add a fail-fast secret check to every workflow.
  8. Run CI for pull requests only in disposable containers, with fork approval on.
  9. Practise a dump and restore on a new machine, and test push, protection and a secret.
  10. Subscribe to the forge’s and runner’s security feeds and set a patch day.

Common mistakes

  1. Treating git push --mirror as a forge migration.
  2. Upgrading Gitea past 1.22 and then planning an in-place move to Forgejo.
  3. Assuming issues keep their authors and assignees after an import.
  4. Trusting a green pipeline after a move without checking secrets.
  5. Registering a host runner “just for now” on the forge’s own server.
  6. Patching the forge but not the runner.
  7. Copying only gitea.db and forgetting its -wal file, or the app.ini that holds the keys.
  8. Skipping a GitLab required upgrade stop, or not waiting for background migrations.
  9. Backing up GitLab without gitlab-secrets.json.
  10. Taking a fresh major (such as Forgejo 17.0.0) into production in its first week.

A drill I ran on 19 October 2026

Shared Linux VM (Debian 13, 8 vCPU, 15 GiB RAM with about 6.7 GiB available at the start, other workloads running), no Docker. All runs were between about 2:24 PM and 2:30 PM IST. One box, one small invented repository and single runs, so nothing here is a speed claim.

Setup:

  • Gitea 28.2.0 and Forgejo 15.0.10 (the LTS line, chosen because of the 17.0.0 warning) as Linux binaries, SHA-256 matched the published checksum files, on SQLite, bound to 127.0.0.1, SSH off.
  • gitea-runner 5.1.1 and forgejo-runner 13.2.0, also checksum-verified, each registered with a single host label.
  • An invented kochi-seafood/invoice-service private repository on Gitea: 12 commits on two branches, a tag, three labels, a milestone, two issues (one assigned to a second user, one closed), a pull request with a comment and a review, a release with an attachment, a wiki page, branch protection on main, a webhook, an Actions secret, an Actions variable, a collaborator and topics. The workflow lived in .gitea/workflows/ci.yml and printed the host name and the length of the secret.

Actions on Gitea. The runner registered in 20 ms and picked up four queued runs (two branch pushes, a tag push and the pull request), all green. The job printed the VM’s own host name: host mode means the job is a process on the server. The secret was masked as *** and had length 23.

Migration to Forgejo. The /repos/migrate call with service: gitea and a token returned 201 in 0.23 s. Then I compared both APIs:

  • Came over: private flag, both branches and the tag, all 11 commits on main, issue and pull request numbers, titles, states, labels, the milestone, comments, the review, the release with its attachment, the wiki page and topics.
  • Did not come over: the assignee, branch protection, the webhook, the Actions secret and variable, the four runs of history and the collaborator.
  • Changed: authorship. The API showed issues and comments as posted by “Ghost”; the database kept the original name in an original_author column, not linked to the local account with the same name.

Actions on Forgejo. The Forgejo runner registered in 16 ms. A new push ran the unchanged .gitea/workflows/ci.yml (there was no .forgejo directory) and the job passed, printing secret length is 0. The secret had not been migrated, and nothing failed.

Backup and restore of Gitea. With Gitea stopped, gitea dump took 91 ms and wrote a 200 KB ZIP with mode 0600, containing app.ini (internal token and JWT secret), the JWT private key, the SQLite file, an SQL dump, repositories and Actions logs. A manual restore into a new directory (unzip, copy config with paths changed, copy data and repositories, gitea admin regenerate hooks, start) took about 0.5 s including start. Of 18 checked items, all matched except one counter: the pull request’s comment count read 1 before and 2 after, with the comment and the review both present. A push to a new branch worked, a push to protected main was rejected, the old runner reconnected with its existing registration file, and a new run saw the secret with length 23. Restoring to the same paths and keys is what made that work.

unzip gitea-dump.zip -d restore-tmp
sed 's#/srv/gitea-old#/srv/gitea-new#g' restore-tmp/app.ini > /srv/gitea-new/custom/conf/app.ini
cp -a restore-tmp/data/. /srv/gitea-new/data/
cp -a restore-tmp/repos/. /srv/gitea-new/data/repositories/
gitea -c /srv/gitea-new/custom/conf/app.ini admin regenerate hooks

In-place move from Gitea 28.2.0 to Forgejo. Forgejo 15.0.10 against a copy of the Gitea 28.2.0 data refused to migrate: “Your database (migration version: 358) is for a newer Forgejo, you can not use the newer database for this old Forgejo release (305)”, and exited without changing the database. That is the safe failure, and it matches the upgrade guide.

Forgejo dump. With Forgejo stopped, forgejo dump took 87 ms and wrote 6.9 MB, most of it a 4.2 MB SQLite -wal file next to the database. If you copy SQLite by hand, copy the WAL too, or use the SQL dump.

Footprint after the drill. Gitea about 217 MB RSS, its runner 23 MB; Forgejo about 200 MB, its runner 26 MB. Data directories 3.2 MB and 7.0 MB. Single readings on a shared box.

What this box could not test: GitLab CE at all (its baseline is 8 vCPU and 16 GB, and the box had about 6.7 GiB free with other work running), Docker or LXC runner isolation, PostgreSQL or MySQL, LFS and SSH, upgrades between versions, the Gitea 1.22 to Forgejo v10 in-place path, GitLab’s Gitea importer, load and concurrency. All drill processes (both servers, the restored Gitea and both runners) were stopped by about 2:30 PM IST. Scripts, logs and results are under drill49/.

Unlearn

  • Unlearn that moving a forge means pushing Git data.
  • Unlearn that Gitea and Forgejo are the same software with different logos.
  • Unlearn that a green pipeline means the secrets arrived.
  • Unlearn that a runner is just a build tool; it is code execution on your network.
  • Unlearn that the newest major release is the safest one.

Re-learn

  • Re-learn the three layers: Git data, collaboration data, platform data.
  • Re-learn your upgrade path, including the doors it closes.
  • Re-learn runner labels and what each one isolates.
  • Re-learn your restore: keys, config, database, repositories and hooks.
  • Re-learn the security feed of both the forge and its runner.

Closing

Self-hosted Git forges in 2026 are less about the web UI and more about what you can rebuild and defend. Gitea 28.2 is a fast-moving, company-stewarded MIT forge with a heavy 2026 advisory load. Forgejo 15.0 LTS is a community-governed GPL v3+ hard fork with a predictable release model, and its 17.0.0 is not yet one to take. GitLab CE 19.5 is the full platform under MIT, with the footprint and upgrade stops to match. In the posts on container image builds and file backups, the question was how a build runs and whether a restore works. Here it is what survives a move from one forge to another. Learn the three layers. Unlearn “just mirror it”. Re-learn your runners. Practise the restore. Apply it before you move another repository.

Sources

Gitea

Forgejo

GitLab

Security records

Internal

comments powered by Disqus

Releted Posts

Container registries in 2026: revisit Harbor, Zot and CNCF Distribution before you push another image

Picture a product engineering team in Bengaluru that runs about forty microservices on Kubernetes for a B2B payments product used by NBFCs across Karnataka and Maharashtra (an invented example, not a real client).

Read more

Feature flags in 2026: revisit Unleash, Flagsmith and GrowthBook before you hard-code another release toggle

Picture a test-preparation and online classes platform in Jaipur (an invented example, not a real client). Students use an Android app, an iOS app and a React web app.

Read more

Load testing in 2026: revisit Grafana k6, Locust and Gatling before you run another performance test

Picture a company in Lucknow that hosts exam results and admit cards for several state boards and universities across Uttar Pradesh and Bihar (an invented example, not a real client).

Read more