Self-Hosting Gitea as a GitHub Alternative: Setup, CI/CD, and Mirroring

Gitea is the lightest full-featured Git hosting platform you can self-host. The current stable release, version 1.25, runs comfortably in under 200 MB of RAM as a single Go binary or Docker container. It covers pull requests with code review, issue tracking, CI/CD through GitHub Actions-compatible runners, package registries for containers and NPM, and bidirectional repository mirroring with GitHub. If you want ownership of your source code without the resource overhead of GitLab, Gitea is the strongest option available right now.
Why Gitea Over Forgejo, GitLab, or Gogs
Several self-hosted Git platforms compete for attention, and the right pick depends on your priorities.
Gitea vs. Forgejo. Forgejo forked from Gitea in late 2022 after a for-profit company took ownership of the Gitea project. As of 2026, both are actively developed with nearly identical core features. Forgejo operates under Codeberg e.V., a non-profit, and prioritizes community governance and ActivityPub federation. Forgejo actually has more contributors (232 vs 153) and higher commit velocity in recent months. If federation or governance philosophy matters to you, Forgejo is the better pick. If you want a larger plugin ecosystem and faster feature adoption from upstream, Gitea still has the edge.
Gitea vs. GitLab CE. GitLab Community Edition requires 4 GB of RAM minimum (8 GB recommended) and pulls in PostgreSQL, Redis, Sidekiq, Puma, and a fleet of background services. Gitea runs in 200-512 MB of RAM with a single binary and one database. For a homelab or a team under 50 users, GitLab’s resource overhead is hard to justify.
Gitea vs. Gogs. Gogs is Gitea’s ancestor - Gitea forked from Gogs in 2016. Gogs lacks pull request reviews, CI/CD, and package registries. Development has slowed significantly. There is no compelling reason to choose Gogs for a new installation in 2026.
| Feature | Gitea | Forgejo | GitLab CE | Gogs |
|---|---|---|---|---|
| Minimum RAM | ~200 MB | ~200 MB | 4 GB+ | ~100 MB |
| CI/CD | Gitea Actions | Forgejo Actions | Built-in | None |
| Package Registry | Yes | Yes | Yes | No |
| Federation | Experimental | Active (ActivityPub) | No | No |
| Migration Tool | GitHub, GitLab, Bitbucket | GitHub, GitLab, Bitbucket | GitHub, Bitbucket | Limited |
| Governance | For-profit company | Non-profit (Codeberg) | For-profit | Single maintainer |
Resource requirements are modest. Gitea with PostgreSQL and an Actions runner runs comfortably on a Raspberry Pi 4 (4 GB) or a $5/month VPS. It handles repositories with 100K+ commits and serves 20+ concurrent users on 2 CPU cores and 1 GB of RAM.

Gitea’s feature coverage against GitHub is broad: issues, pull requests with code review, branch protection rules, webhooks, OAuth2 login, organization and team management, container and NPM package registries, project boards (Kanban), wiki, and Actions-compatible CI/CD. The built-in migration tool (Settings > Migrations) imports repositories including issues, pull requests, labels, milestones, and releases from GitHub, GitLab, Bitbucket, and other Gitea instances.
Installation with Docker Compose and PostgreSQL
While Gitea supports SQLite for small setups, PostgreSQL provides the reliability needed for anything beyond experimentation. Here is a production-ready Docker Compose configuration:
services:
gitea:
image: gitea/gitea:1.25
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=db:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=${DB_PASSWORD}
volumes:
- gitea_data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"
depends_on:
db:
condition: service_healthy
restart: unless-stopped
mem_limit: 512m
cpus: 1.0
db:
image: postgres:16-alpine
container_name: gitea-db
environment:
- POSTGRES_DB=gitea
- POSTGRES_USER=gitea
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "gitea"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
mem_limit: 256m
volumes:
gitea_data:
postgres_data:Create an .env file alongside this compose file with DB_PASSWORD=your_secure_password_here. The GITEA__section__key format allows overriding any app.ini setting through environment variables.
After running docker compose up -d, navigate to http://your-server:3000 to complete the installation wizard. Set the site title, create your admin account, and configure the server domain and SSH port. These settings land in /data/gitea/conf/app.ini inside the container.
A few app.ini settings to adjust after installation:
[server]
ROOT_URL = https://gitea.yourdomain.com/
SSH_DOMAIN = gitea.yourdomain.com
SSH_PORT = 22
[service]
DISABLE_REGISTRATION = trueSet ROOT_URL to match your reverse proxy URL exactly. Set SSH_PORT to the public-facing port (not the container internal port). Disable public registration after creating your accounts.
For backups, run gitea dump -c /data/gitea/conf/app.ini inside the container to create a ZIP archive of all repositories, the database, and configuration. Schedule this with cron. For faster database restores, also run pg_dump separately against the PostgreSQL container.
SSH Access, Git Operations, and LFS
Git over SSH is the preferred protocol for push and pull operations - no password prompts, faster than HTTPS for large repositories.
SSH passthrough maps the container’s SSH port to the host. With the compose file above, users clone with:
git clone ssh://git@gitea.yourdomain.com:2222/user/repo.gitAdd this to ~/.ssh/config for cleaner URLs:
Host gitea.yourdomain.com
Port 2222
User gitUsers add their public keys in the Gitea web UI under Settings > SSH/GPG Keys. Gitea supports ED25519 (recommended), RSA (4096-bit minimum), and ECDSA key types. For CI/CD systems, use deploy keys - read-only SSH keys assigned to specific repositories, created under repository Settings > Deploy Keys.
Git LFS (Large File Storage) handles binary files that do not belong in regular Git history - images, models, datasets. Enable it in app.ini:
[lfs]
PATH = /data/lfs
MAX_FILE_SIZE = 1073741824Gitea supports local filesystem, MinIO/S3-compatible object storage, and Azure Blob Storage as LFS backends. Each LFS object is stored once regardless of how many branches reference it.
For environments where SSH is blocked, HTTPS cloning works with git clone https://gitea.yourdomain.com/user/repo.git. Users authenticate with their Gitea password or a personal access token.
Gitea Actions: CI/CD with GitHub Actions Compatibility
Gitea Actions
, production-ready since version 1.22, runs GitHub Actions-compatible workflow YAML files using the act_runner agent.
Enable Actions by adding this to app.ini and restarting Gitea:
[actions]
ENABLED = trueThis adds an Actions tab to every repository. Without a registered runner, workflows queue but never execute.
Install act_runner by downloading the binary from the Gitea releases page
or pulling the Docker image:
docker pull gitea/act_runner:latest
# Register the runner
act_runner register --no-interactive \
--instance https://gitea.yourdomain.com \
--token YOUR_RUNNER_TOKEN \
--name my-runner \
--labels ubuntu-latest,self-hosted,linuxGet the registration token from Gitea’s admin panel under Site Administration > Runners. The runner uses Docker containers (default: catthehacker/ubuntu:act-latest) to provide isolated build environments.

Workflow compatibility covers roughly 90% of GitHub Actions syntax: on: push/pull_request triggers, jobs with steps, uses: for third-party actions, environment variables, secrets, matrix builds, and artifacts. Notable gaps in version 1.25 include limited workflow_dispatch inputs UI and restricted caching action support.
Create workflows in .gitea/workflows/ (not .github/). Here is a basic CI pipeline for a Go project:
name: CI
on: [push, pull_request]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- run: go build ./...
- run: go test ./...Add repository secrets in Settings > Actions > Secrets and reference them with ${{ secrets.DEPLOY_KEY }}. Organization-wide secrets are available at the organization settings level.
Repository Mirroring and GitHub Backup
Self-hosting Gitea does not mean leaving GitHub behind. Mirror syncing lets you maintain both.
To push from Gitea to GitHub, go to repository Settings > Mirror Settings and add a push mirror with the GitHub remote URL and a personal access token. Gitea pushes all branches and tags on every push, with a configurable sync interval from 1 minute to 24 hours.
To pull from GitHub into Gitea, create a new repository via New Migration, select GitHub, and enable the Mirror option. Gitea pulls changes on a configurable interval (default 8 hours). This is the simplest way to maintain a local backup of important GitHub repositories.
In practice, you use Gitea as your primary development platform and push-mirror to GitHub for visibility. Your GitHub profile stays active, collaborators can find your code, and you retain full control over your primary Git host.
For bulk migration, combine the Gitea API with the gh CLI:
# List all your GitHub repos
gh repo list --json nameWithOwner --limit 500 -q '.[].nameWithOwner'
# Migrate each one via Gitea API
curl -X POST "https://gitea.yourdomain.com/api/v1/repos/migrate" \
-H "Authorization: token YOUR_GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clone_addr": "https://github.com/user/repo.git",
"mirror": true,
"repo_name": "repo",
"repo_owner": "your-gitea-user",
"service": "github",
"auth_token": "YOUR_GITHUB_TOKEN"
}'Gitea webhooks send GitHub-compatible payloads on push, pull request, and issue events, so many GitHub webhook receivers for Discord, Slack, Matrix, or ntfy work without modification.
Looking ahead, both Gitea and Forgejo are developing ForgeFed/ActivityPub support for cross-instance collaboration - pull requests between different instances without mirroring. As of early 2026, this remains experimental but is progressing steadily.