Why move from GitHub to a self-hosted forge
GitHub remains the reference, but three trends converged in 2026 to make the move concrete rather than theoretical.
First: outages. Several incidents documented on githubstatus.com affected Git Operations and Actions in August 2026, blocking production pipelines at the worst time. The Hacker News thread "Alternatives to GitHub" reached 472 points and 299 comments on 2026-08-17 — a sign the question is no longer academic.
Second: CI minute costs. GitHub Actions is free up to a monthly quota on public repositories, but any team that exceeds that quota on private repos pays per minute — with a multiplier depending on the runner OS. Details are available on github.com/pricing.
Third: code sovereignty. Some teams prefer that the only access to their repository is root on their own VM, not a unilaterally revocable SaaS token.
What self-hosted Gitea delivers in practice
- Full history preserved: Gitea's API import consumes a GitHub tar.gz archive — commits, branches and tags intact.
- Issues and labels migrated: the
POST /api/v1/repos/migrateendpoint transfers issues, milestones and labels in one pass. - Native CI runner: gitea-runner (based on act) understands GitHub Actions workflow syntax — most pipelines work without rewriting.
- Predictable cost: a VPS with 1 vCPU and 1 GB RAM is enough for a small team; the software is open source, with no licence fee or minute quota.
- Compatible webhooks: Gitea exposes the same events (
push,pull_request,release) as GitHub — third-party integrations stay wired without changes. - Local API token: access is managed in your own interface, never at a third party.
Prerequisites
Before you start, gather the following.
GitHub side: a personal access token (Settings → Developer settings → Personal access tokens) with repo and read:org scopes. Keep it handy — you will use it in every curl call.
Gitea side: an already-installed Gitea instance (see our guide "Host Gitea on Your Own VPS") and an admin token (gitea-admin → Settings → Applications → Generate Token). Note your base URL as https://git.your-domain.com.
Local workstation: git 2.x, curl and jq installed. Allow 1 GB of RAM minimum on the VPS for importing average-sized repositories.
Migration step by step
Create the target Gitea organisation
In the Gitea interface, go to + → New Organization, pick a name matching your GitHub organisation (this simplifies the remote update later).
Or via the API:
curl -s -X POST https://git.your-domain.com/api/v1/orgs \
-H "Authorization: token YOUR_GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"username":"my-org","visibility":"private"}'List and export GitHub repositories
Retrieve your repository list (paginated at 100 per page):
curl -s -H "Authorization: token YOUR_GITHUB_TOKEN" \
"https://api.github.com/orgs/MY_ORG/repos?per_page=100&page=1" \
| jq -r '.[].name' > repos.txtThe GitHub endpoint used here is GET /repos/{owner}/{repo}/tarball/{ref} for a full archive, and GET /orgs/{org}/repos for the inventory. Check the official documentation at docs.github.com.
Import each repository via the Gitea API
The POST /api/v1/repos/migrate endpoint of Gitea API v1 accepts a GitHub source URL (official documentation: gitea.io). For each repository:
while IFS= read -r REPO; do
curl -s -X POST https://git.your-domain.com/api/v1/repos/migrate \
-H "Authorization: token YOUR_GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\n \\\"clone_addr\\\": \\\"https://github.com/MY_ORG/$REPO\\\",\n \\\"auth_token\\\": \\\"YOUR_GITHUB_TOKEN\\\",\n \\\"uid\\\": 2,\n \\\"repo_name\\\": \\\"$REPO\\\",\n \\\"issues\\\": true,\n \\\"labels\\\": true,\n \\\"milestones\\\": true,\n \\\"mirror\\\": false\n }"
done < repos.txtuid is the numeric ID of your Gitea organisation (visible via GET /api/v1/orgs/my-org). mirror: false means a one-time import — set it to true for continuous sync during the transition period.
Update local remotes
For each repository cloned on your workstation:
git remote set-url origin https://git.your-domain.com/my-org/my-repo.gitOr via SSH if you have added your public key in Gitea (Settings → SSH / GPG Keys):
git remote set-url origin [email protected]:my-org/my-repo.gitVerify with git remote -v, then test with git fetch to confirm the remote responds.
Update webhooks
In Gitea, for each repository: Settings → Webhooks → Add Webhook. Paste the URL of your CI or third-party service (Slack, Woodpecker CI, etc.), replacing github.com with git.your-domain.com.
The available events (push, pull_request, issues, release) are identical to GitHub's — payloads share the same base structure for most integrations.
Migrate the CI pipeline to gitea-runner
Install the official runner on your VPS:
wget -O gitea-runner https://dl.gitea.com/act_runner/latest/act_runner-latest-linux-amd64
chmod +x gitea-runner
./gitea-runner register --no-interactive \
--instance https://git.your-domain.com \
--token YOUR_RUNNER_TOKEN \
--name vps-runner \
--labels ubuntu-latest:docker://node:20-bullseye
./gitea-runner daemon &The runner token is generated in Gitea: Site Administration → Runners → Create new Runner Token.
Your existing .github/workflows/*.yml files are understood by gitea-runner without modification for common actions (actions/checkout, actions/setup-node, etc.). Rename .github/workflows/ to .gitea/workflows/ to use Gitea's native path (both are supported, but .gitea/ is the canonical one).
Post-migration checks
Once migration is complete, validate these three points before revoking GitHub access.
SSH clones: from a freshly cloned workstation, git clone [email protected]:my-org/my-repo.git should succeed without a password prompt if your public key is registered in Gitea.
Active webhooks: in each repository, open Settings → Webhooks and click Test Delivery. A 200 code in the delivery logs confirms your CI is receiving events.
Intact history: git log --oneline -10 on the migrated repository should display the same ten latest commits as on GitHub, in the same order.
If you are migrating dozens of repositories, run the migration curl calls in parallel using xargs -P 4 to cut the total wait time by four. Gitea handles concurrent imports without data loss — the only limit is GitHub's outbound bandwidth and your VPS's inbound bandwidth.
Common errors — troubleshooting
Here are the three most frequent situations during a migration.
SSH: `Permission denied (publickey)`
Symptom: git clone [email protected]:… fails with Permission denied (publickey).
Check: in Gitea, Settings → SSH / GPG Keys — the public key you are using must be listed there. Add it if absent (cat ~/.ssh/id_ed25519.pub | pbcopy or xclip).
If the key is present but the error persists, verify that Gitea's SSH port is 22 (or the configured port): ssh -vT [email protected] -p 22 will show the negotiation and Gitea's acceptance message if the key is recognised.
Remote: `fatal: repository not found`
Symptom: git fetch returns fatal: repository 'https://git.your-domain.com/…' not found.
Diagnosis: git remote -v — check that the URL points to your Gitea instance and not github.com. If the URL is correct, log in to the Gitea interface and confirm the repository exists under the correct organisation name. A silently failed POST /api/v1/repos/migrate leaves the repository absent with no error message in the local remote.
Silent webhooks after migration
Symptom: a git push does not trigger a build in your CI.
Diagnosis: in Gitea, Settings → Webhooks → last delivery: the "Response" field shows the HTTP code returned by your CI. A 404 means the webhook URL points to a non-existent route; a 401 means a misconfigured HMAC secret.
Enable runner debug mode: in the gitea-runner configuration file, set log_level to debug and restart the daemon. Logs will show each event received and the job activation code.
Host Gitea on a ServOrbit VPS
Gitea runs comfortably on a VPS with 1 vCPU and 1 GB RAM for a small team. For an organisation of ten developers with an active CI, 2 vCPU and 2 GB is a reasonable starting point — the runner and Gitea can share the same VM at that size.
If server administration is not your priority, ServOrbit's VPS administration option covers exactly this case: security updates, monitoring and incident response — you keep root access, we keep the VM healthy. Your forge stays yours, without sharing infrastructure with other teams.
Deploy the Gitea template from the Marketplace, select your OS (Ubuntu or Debian) and your VPS size, and you have a configured instance in minutes. VPS plans from {{vps.start.price}}.