Self-Hosted Git Server: Gitea, GitLab CE and Forgejo Compared
GitHub dominates the Git hosting landscape, but depending on a single centralized platform for your source code comes with risks: policy changes, outages, pricing adjustments, and the fundamental loss of control over your most valuable asset. Self-hosting a Git server gives you full ownership of your repositories, eliminates vendor lock-in, and allows integration with your internal infrastructure on your own terms.
Three open-source platforms stand out for self-hosted Git: Gitea (lightweight and fast), GitLab CE (full-featured DevOps platform), and Forgejo (community-governed Gitea fork). Each makes different trade-offs between resource usage, features, and philosophy. This guide compares them in depth and shows how to deploy each with Docker.
The Three Contenders
Gitea: Lightweight and Fast
Gitea is a single Go binary that provides a GitHub-like experience with minimal resource overhead. It was forked from Gogs in 2016 and has grown into a mature, feature-rich platform. Gitea focuses on being lightweight, easy to install, and easy to maintain.
GitLab CE: The Full DevOps Platform
GitLab Community Edition is far more than a Git server. It is an entire DevOps lifecycle platform with built-in CI/CD, container registry, package registry, security scanning, issue tracking, and project management. The trade-off is significant resource requirements.
Forgejo: Community-First Fork
Forgejo forked from Gitea in late 2022 after governance concerns when Gitea Ltd. was formed as a for-profit company. Forgejo is maintained by Codeberg e.V. (a non-profit) and aims to keep the project purely community-governed. It is API-compatible with Gitea and shares most of its codebase, with some diverging features.
Feature Comparison
| Feature | Gitea | GitLab CE | Forgejo |
|---|---|---|---|
| Language | Go | Ruby / Go | Go |
| Minimum RAM | 256 MB | 4 GB (8 GB recommended) | 256 MB |
| Idle RAM usage | ~150 MB | ~2.5 GB | ~150 MB |
| Built-in CI/CD | Gitea Actions (GitHub Actions compatible) | GitLab CI (mature, powerful) | Forgejo Actions |
| Container registry | Yes (packages) | Yes (full registry) | Yes (packages) |
| Package registry | npm, PyPI, Maven, NuGet, etc. | Comprehensive | npm, PyPI, Maven, NuGet, etc. |
| Issue tracking | Yes (labels, milestones, projects) | Yes (boards, epics, weights) | Yes (labels, milestones, projects) |
| Wiki | Yes (per-repo) | Yes (per-repo + group wikis) | Yes (per-repo) |
| Code review | Pull requests with review | Merge requests with approval rules | Pull requests with review |
| LDAP/OAuth | Yes | Yes (extensive) | Yes |
| Migration tools | GitHub, GitLab, Bitbucket importers | GitHub, Bitbucket importers | GitHub, GitLab importers |
| API | Swagger REST API | Comprehensive REST + GraphQL | Swagger REST API |
| License | MIT | MIT (CE) | GPL-3.0 |
Docker Deployment: Gitea
Gitea is the easiest to deploy and the lightest on resources:
# docker-compose.yml for Gitea
version: "3.8"
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=gitea-db:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=${DB_PASSWORD}
- GITEA__server__ROOT_URL=https://git.example.com/
- GITEA__server__SSH_DOMAIN=git.example.com
- GITEA__server__SSH_PORT=2222
- GITEA__mailer__ENABLED=true
- [email protected]
- GITEA__service__DISABLE_REGISTRATION=true
volumes:
- gitea_data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"
depends_on:
- gitea-db
gitea-db:
image: postgres:16-alpine
container_name: gitea-db
restart: unless-stopped
environment:
- POSTGRES_DB=gitea
- POSTGRES_USER=gitea
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- gitea_db:/var/lib/postgresql/data
volumes:
gitea_data:
gitea_db:
Gitea Actions (CI/CD) requires a separate runner:
# Add to docker-compose.yml for CI/CD
gitea-runner:
image: gitea/act_runner:latest
container_name: gitea-runner
restart: unless-stopped
environment:
- GITEA_INSTANCE_URL=http://gitea:3000
- GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_TOKEN}
- GITEA_RUNNER_NAME=default-runner
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- gitea_runner_data:/data
depends_on:
- gitea
volumes:
gitea_runner_data:
Docker Deployment: GitLab CE
GitLab CE requires significantly more resources but provides an all-in-one DevOps platform:
# docker-compose.yml for GitLab CE
version: "3.8"
services:
gitlab:
image: gitlab/gitlab-ce:latest
container_name: gitlab
restart: unless-stopped
hostname: gitlab.example.com
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'https://gitlab.example.com'
gitlab_rails['gitlab_shell_ssh_port'] = 2222
# PostgreSQL (use built-in or external)
# postgresql['enable'] = true
# Redis
# redis['enable'] = true
# Email configuration
gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.example.com"
gitlab_rails['smtp_port'] = 587
# Container registry
registry_external_url 'https://registry.example.com'
# Monitoring
prometheus_monitoring['enable'] = true
# Reduce memory usage (optional)
puma['worker_processes'] = 2
sidekiq['max_concurrency'] = 10
prometheus_monitoring['enable'] = false
grafana['enable'] = false
ports:
- "80:80"
- "443:443"
- "2222:22"
volumes:
- gitlab_config:/etc/gitlab
- gitlab_logs:/var/log/gitlab
- gitlab_data:/var/opt/gitlab
shm_size: '256m'
volumes:
gitlab_config:
gitlab_logs:
gitlab_data:
Docker Deployment: Forgejo
Forgejo is a drop-in replacement for Gitea with the same lightweight footprint:
# docker-compose.yml for Forgejo
version: "3.8"
services:
forgejo:
image: codeberg.org/forgejo/forgejo:latest
container_name: forgejo
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- FORGEJO__database__DB_TYPE=postgres
- FORGEJO__database__HOST=forgejo-db:5432
- FORGEJO__database__NAME=forgejo
- FORGEJO__database__USER=forgejo
- FORGEJO__database__PASSWD=${DB_PASSWORD}
- FORGEJO__server__ROOT_URL=https://code.example.com/
- FORGEJO__server__SSH_DOMAIN=code.example.com
- FORGEJO__server__SSH_PORT=2222
volumes:
- forgejo_data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"
depends_on:
- forgejo-db
forgejo-db:
image: postgres:16-alpine
container_name: forgejo-db
restart: unless-stopped
environment:
- POSTGRES_DB=forgejo
- POSTGRES_USER=forgejo
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- forgejo_db:/var/lib/postgresql/data
volumes:
forgejo_data:
forgejo_db:
Resource Usage Comparison
Real-world resource usage with 50 repositories and 5 active users:
| Metric | Gitea | GitLab CE | Forgejo |
|---|---|---|---|
| Idle RAM | ~150 MB | ~2.5 GB | ~150 MB |
| Active RAM (CI running) | ~300 MB + runner | ~4 GB | ~300 MB + runner |
| Disk (base install) | ~200 MB | ~3 GB | ~200 MB |
| Startup time | ~3 seconds | ~60-120 seconds | ~3 seconds |
| Docker image size | ~110 MB | ~2.8 GB | ~110 MB |
CI/CD Capabilities
Gitea Actions / Forgejo Actions
Both use a GitHub Actions-compatible workflow syntax, making migration straightforward:
# .gitea/workflows/build.yml (or .forgejo/workflows/build.yml)
name: Build and Test
on:
push:
branches: [main]
pull_request:
jobs:
build:
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 -race ./...
GitLab CI
GitLab CI uses its own syntax, which is more powerful but requires learning a different DSL:
# .gitlab-ci.yml
stages:
- build
- test
- deploy
build:
stage: build
image: golang:1.22
script:
- go build ./...
artifacts:
paths:
- binary
test:
stage: test
image: golang:1.22
script:
- go test -race -cover ./...
coverage: '/coverage: \d+.\d+%/'
deploy:
stage: deploy
script:
- docker build -t registry.example.com/myapp:$CI_COMMIT_SHA .
- docker push registry.example.com/myapp:$CI_COMMIT_SHA
only:
- main
Migration from GitHub
All three platforms support importing repositories from GitHub, including issues, pull requests, labels, and milestones.
Gitea/Forgejo Migration
# Via the web UI:
# New Migration > GitHub > Enter repository URL and token
# Via API:
curl -X POST "https://git.example.com/api/v1/repos/migrate" \
-H "Authorization: token YOUR_GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clone_addr": "https://github.com/org/repo.git",
"auth_token": "ghp_YOUR_GITHUB_TOKEN",
"repo_name": "repo",
"repo_owner": "myorg",
"service": "github",
"mirror": false,
"issues": true,
"labels": true,
"milestones": true,
"pull_requests": true,
"releases": true
}'
Bulk Migration Script
#!/bin/bash
# migrate-github-org.sh - Migrate all repos from a GitHub org
GITHUB_ORG="my-org"
GITHUB_TOKEN="ghp_xxx"
GITEA_URL="https://git.example.com"
GITEA_TOKEN="xxx"
GITEA_ORG="my-org"
# Get all GitHub repos
repos=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/orgs/$GITHUB_ORG/repos?per_page=100" | \
jq -r '.[].full_name')
for repo in $repos; do
repo_name=$(basename "$repo")
echo "Migrating: $repo -> $GITEA_ORG/$repo_name"
curl -s -X POST "$GITEA_URL/api/v1/repos/migrate" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"clone_addr\": \"https://github.com/$repo.git\",
\"auth_token\": \"$GITHUB_TOKEN\",
\"repo_name\": \"$repo_name\",
\"repo_owner\": \"$GITEA_ORG\",
\"service\": \"github\",
\"mirror\": false,
\"issues\": true,
\"labels\": true,
\"pull_requests\": true,
\"releases\": true
}"
sleep 2 # Rate limiting
done
Which One Should You Choose?
- Choose Gitea if you want a lightweight, fast Git server with good CI/CD support and GitHub-compatible workflows. Best for small to medium teams with limited server resources.
- Choose GitLab CE if you need a full DevOps platform with mature CI/CD, container registry, security scanning, and project management. Best for larger teams willing to dedicate 8+ GB RAM.
- Choose Forgejo if you want Gitea's lightweight approach with a commitment to community governance and open-source principles. Best for those who want to avoid any commercial influence in their tooling.
For most self-hosters managing a handful of projects, Gitea or Forgejo provides 90% of what you need at 10% of GitLab's resource cost. GitLab CE is worth the overhead only if you actively use its advanced CI/CD features, security scanning, or project management capabilities.
Whichever platform you choose, managing it alongside your other Docker services is straightforward with container management tools like usulnet, which provides visibility into resource usage, container health, and backup status across your entire self-hosted stack.