Tech Digest

Migration

Replace GitHub with a forge you run

Your repositories move in an afternoon. Your issues move badly, your Actions history does not move at all, and the people who found your project through GitHub do not follow you.

Last reviewed

Can I replace GitHub with a self-hosted git server?

Forgejo replaces GitHub's git hosting, issues, pull requests, package registry and CI on one container that idles around 150 MB, and Gitea is the same product under different governance. What you cannot replace is the network. GitHub is where people find code, file drive-by issues and check who you are, and no amount of self-hosting reproduces that. Most people who do this keep a push mirror on GitHub for exactly that reason.

Git is distributed, so moving repositories is close to free. Everything GitHub built on top of git is not, and that is what this decision is actually about.

What you are actually giving up#

GitHub is not really a git host. It is a network with a git host attached, and you cannot self-host a network.

Concretely, here is what stops happening the day you move a public project. Nobody stumbles across it. Nobody stars it, so nobody else sees it in a trending list or a dependency graph. The stranger who would have opened a two-line issue does not, because filing it now costs them an account on a server they have never heard of. Your commit history stops appearing on a profile that hiring managers actually look at. Dependabot stops opening pull requests. The GitHub Advisory Database stops matching your dependencies. The Actions marketplace, the container registry other people can pull from without credentials, Codespaces, Pages, sponsors: all of it is the network, and none of it is a feature you can install.

For a private team repository, none of that matters and a self-hosted forge is straightforwardly better: no per-seat bill, no US SaaS dependency, and Forgejo idles at about 150 MB. For a public project, moving off GitHub entirely is a decision to be less findable, and people who make it usually regret the drop in contributions rather than the hosting.

The second thing to be honest about is availability. GitHub's uptime is not something you can match with one machine. If your forge is a mini PC under a desk, your git remote is now as reliable as your home power and as reachable as your home broadband, and it is in the same building as the laptop holding the only other copy of your work.

Get your data out first#

Repositories are the easy part. Do them with a mirror clone so you get every branch, tag and note, not just the default branch:

bash
gh repo list YOURNAME --limit 500 --no-archived \
  --json nameWithOwner -q '.[].nameWithOwner' \
| while read r; do git clone --mirror "https://github.com/$r.git" "${r##*/}.git"; done

Wikis are a separate repository and everyone forgets them:

bash
git clone --mirror https://github.com/YOURNAME/REPO.wiki.git

Gists are separate again, at gist.github.com, and are not in any repository listing.

For everything that is not git, use the user migration API, which produces a tar.gz of JSON plus git data covering issues, issue comments, issue events, milestones, pull requests, pull request reviews, review comments, releases, protected branches, projects and an attachments directory of uploaded files:

bash
gh api --method POST /user/migrations -f 'repositories[]=YOURNAME/REPO'
gh api /user/migrations                      # poll until state is "exported"
gh api /user/migrations/ID/archive > archive.tar.gz

The archive is available for seven days and then deleted. Download it the same day.

Now the honest part about quality:

  • Repositories: perfect. A mirror clone is byte-identical history. This is the only part with no caveats.
  • Issues and comments: good. Both Forgejo and Gitea have a GitHub importer that reads the API directly with a personal access token, and it brings across issues, comments, labels, milestones and releases with authorship intact. Rate limits mean a large repository takes a while.
  • Pull requests: degraded. They arrive as issues carrying a diff. Review threads, approval state, required checks and the merge button's history do not survive as first-class objects.
  • Actions: gone. Run history, logs, artifacts, caches and workflow run numbers are not exported by anything, including the migration API. Your workflow YAML files are in the repository, so the definitions survive; the record of what ran does not.
  • Releases: mostly. Release notes and tags come across. Binary release assets are separate downloads and some importers skip them, so check the file counts.
  • Never exported: secrets, environment variables, deploy keys, branch protection rules, webhook secrets, Discussions, Projects boards, Sponsors, and the Packages registry. Write these down by hand before you stop paying attention.

Which one to run#

The two forges are close enough on features that the decision is governance, not capability. Forgejo is the default recommendation: nonprofit-owned by Codeberg e.V., GPL-3.0-or-later since v9.0, so features cannot quietly migrate behind a paywall later. Gitea is the pragmatic pick when GPL-3.0 is a legal blocker, when you want a commercial support contract, or when you are already on Gitea v1.23 or newer, where there is no supported direct migration to Forgejo any more. Forgejo vs Gitea has the full argument.

Your situationRunWhy
Choosing a forge for the first timeForgejoNonprofit governance and a copyleft licence, same product otherwise
Legal policy permits permissive licences onlyGiteaMIT, and the enterprise support contract exists if you need one
Already on Gitea v1.23 or newerStay on GiteaForgejo v10.0 was the last version reading a Gitea v1.22 or older database
You need merge queues, security scanning, a full DevOps platformGitLab CEThe only one here that has them, at a 4 GB memory floor
CI is the reason you are leavingWoodpecker CIApache-2.0, two Go processes, under 100 MB, and it never went proprietary
You need a real container registry with scanningHarborBoth forges ship a package registry, neither does RBAC and Trivy scanning

Set up the push mirror on day one. In the repository settings, add a GitHub personal access token and the remote https://github.com/YOU/REPO.git, and the forge pushes branches, tags and commits on a schedule. Be aware that a push mirror overwrites the remote, so the GitHub copy has to be read-only.

What the migration actually costs#

For twenty repositories with a few hundred issues between them:

  • 1 to 2 hours for the forge itself. One container, one volume, SQLite. The two things that go wrong are USER_UID/USER_GID not matching the bind mount owner, and the SSH clone URL: the web UI builds it from SSH_DOMAIN and SSH_PORT in app.ini, not from what Docker published, so map host 222 to container 22 without setting SSH_PORT = 222 and every URL you hand out points at a closed port.
  • 1 hour cloning and re-pushing repositories.
  • 2 to 6 hours running importers, fixing pull requests that landed as issues, and rewriting remotes across every checkout on every machine.
  • 3 to 8 hours if you also move CI, because Actions workflows that use marketplace actions need those actions to be reachable, and a self-hosted runner is not the same environment as GitHub's ubuntu-latest image.
  • Ongoing: upgrades that you must not skip several majors at once, an offsite forgejo dump on a schedule, TLS renewal, and the runner host's disk filling with cached images.

You now personally maintain the availability of your own git remote. That is the real cost, and it is why the mirror matters.

What breaks and how to tell early#

  • The first-run installer is unauthenticated. Publish port 3000 before you complete the wizard and whoever finds it becomes your instance administrator. Bind to localhost for the first boot or pre-write app.ini with INSTALL_LOCK = true.
  • Repository hooks bake in absolute paths. Move the install, switch from a package to a container, or restore into a different layout, and pushes fail with an opaque hook error while the web UI looks perfectly healthy. Regenerate hooks after any relocation.
  • Live backups tear. forgejo dump and gitea dump are documented as needing the service stopped, because a live copy can catch the database and the repository files at different instants. You find that out during a restore.
  • Nobody can reach it. Test from a phone on mobile data, from a coworker's laptop, and from CI. Remote access without port forwarding covers doing this without opening a hole in your router.
  • Runners are root. The standard runner configuration bind-mounts the Docker socket, so anyone who can push a workflow file can read the host filesystem. Keep runners off the forge machine.

The point of no return#

Before you delete anything on GitHub:

  1. Push to your forge, from a second machine, over SSH, with a key you just generated. Then clone the same repository fresh into an empty directory and build it.
  2. Restore a dump onto a different host and log in. Backups that actually restore exists because the untested backup is the standard failure here.
  3. Confirm the push mirror actually pushed. Check a commit hash on GitHub, not the mirror's green tick.
  4. Leave the GitHub repositories in place, archived and read-only, with a README line pointing at the new home. Archiving costs nothing, keeps inbound links alive and keeps the stars.

Do not delete GitHub repositories at all if they are public. Archive them. The only thing deletion buys you is the loss of every link anyone ever made to your code.

Next#

Read An update strategy that does not lose data before your first major version bump, then set up an offsite dump with restic and check it against the Backup planner. If you are weighing a full platform instead, Gitea vs GitLab CE covers what the extra 4 GB buys.

Questions#

Forgejo or Gitea?

Forgejo if you are choosing today. Gitea's trademark and domains belong to Gitea Ltd, contributors sign a copyright assignment, and the subsidiary CommitGo sells a proprietary Gitea Enterprise on top of the MIT core. Forgejo belongs to Codeberg e.V., a German nonprofit, and has shipped under GPL-3.0-or-later since v9.0 in August 2024, which makes that open core pattern structurally impossible. Stay on Gitea if your legal policy blocks GPL-3.0, or if you already run it happily.

Can I migrate my GitHub issues and pull requests?

Partly. Both forges have a GitHub importer that pulls repository content, issues, comments, labels, milestones and releases over the GitHub API with a personal access token, and it works well enough. Pull requests come across as issues with a diff attached rather than as mergeable branches with review state. Actions run history, artifacts, Discussions, Projects boards, branch protection rules, secrets and Dependabot alerts do not migrate at all and there is no tool that moves them.

Should I keep mirroring to GitHub?

For anything public, yes. Both forges support push mirrors: you add a GitHub personal access token and a remote URL, and the forge pushes branches, tags and commits on a schedule. Your forge becomes the source of truth and GitHub becomes a read-only shop window that keeps your project discoverable and keeps your code reachable when your house loses power. Note that a push mirror overwrites the remote, so do not accept pull requests on the mirror.

What about CI after leaving GitHub Actions?

Both forges ship an Actions-compatible runner that reads workflow YAML close enough to GitHub's that simple pipelines run unchanged. Woodpecker CI is the alternative: Apache-2.0, forked from Drone 0.8 before Drone went proprietary, two small Go processes idling under 100 MB. Either way the runner needs a Docker socket, which is root-equivalent on that host, so put it on a machine that is not your forge.

How much does a self-hosted forge cost to run?

Forgejo and Gitea are one container with one volume, SQLite by default, roughly 150 MB idle and 450 MB under load. That is a 2 out of 5 on ongoing attention: pull, restart, read the release notes on a major bump. GitLab CE is the other end of the scale at a 4 GB floor. The real cost is not RAM, it is that you are now the person who restores it.

Is my code safe if my server is in my house?

Only if it is somewhere else too. A forge on a mini PC under your desk has the same uptime as your electricity and the same reachability as your home broadband, and if your laptop and your only git remote burn in the same fire, git clone from a colleague is your backup. Run forgejo dump on a schedule to offsite storage, and keep a push mirror somewhere that is not your house.

Sources#

Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.