GitHub works, until it doesn't. Price hikes, Copilot training on your private code, AI-generated issues spammed into your repos, Microsoft telemetry, and the creeping feeling that your source code lives on infrastructure you don't control. For many teams the annual GitHub Team bill has quietly crossed the threshold where self-hosting a Git server pays for itself in a single month.
Gitea and its community fork Forgejo are the two most popular lightweight, self-hosted alternatives. They're written in Go, ship as a single static binary, run happily on 512MB of RAM, and give you pull requests, issues, wiki, container registry, package registry, LFS, SSH access, and a GitHub Actions–compatible CI system called Forgejo Actions.
This guide covers everything you need in 2026: the history behind the Forgejo fork, resource sizing, a production Docker Compose setup, SSL with Caddy, runner installation, migrating from GitHub, backups, and how to pick between Gitea and Forgejo. We'll deploy on a DanubeData VPS in Falkenstein for €7.49–€12.49/month.
Why Self-Host Git in 2026?
Managed Git forges have real costs that aren't obvious from the sticker price:
- GitHub Team is $4/user/month billed annually — a 10-person team pays $480/year, and prices have risen three times in four years.
- Copilot training: even with opt-outs, many enterprises are uncomfortable with source code crossing into third-party AI training pipelines.
- AI issue spam: public GitHub repos now receive drive-by AI-generated "bug reports" and pull requests at unprecedented rates.
- Vendor lock-in: GitHub Actions, Projects, Packages, and Advanced Security all assume you stay on GitHub forever.
- Data sovereignty: for EU companies subject to GDPR, DORA, or NIS2, keeping source code inside an EU-hosted, auditable server is increasingly table stakes.
Self-hosting Gitea or Forgejo doesn't mean giving up modern DX. You still get a web UI, CI/CD, webhooks, OAuth providers, pull request reviews, branch protection, code search, container registries, and IDE integrations. You just run it on your own VPS, on your own domain, under your own rules.
Gitea vs Forgejo: What Actually Happened in 2022
If you're new to this space, the Gitea/Forgejo split is worth understanding because it shapes which project you should pick.
Gitea launched in 2016 as a community fork of Gogs, itself a lightweight Go-based reimplementation of GitHub's UX. For years Gitea was a pure volunteer-run project under a small non-profit umbrella.
In October 2022, the Gitea trademark and domain were transferred to a newly incorporated for-profit company, Gitea Limited, without prior consultation with the broader contributor community. The company announced plans for a hosted SaaS product (gitea.com) alongside the open-source project. Several long-time maintainers and contributors objected to the process, the governance implications, and the potential for "open-core" features being held back from the community edition.
Within weeks, those contributors forked the project as Forgejo (pronounced "for-JAY-oh") under the umbrella of Codeberg e.V., a German non-profit. Forgejo is now governed by an elected council, all source code is free software (no proprietary SaaS companion), and releases ship under Codeberg.org trust anchors.
Both projects remain highly compatible at the database and API level — Forgejo can still be set up as a drop-in replacement for Gitea, and migrations in either direction are documented. But their roadmaps have diverged: Forgejo ships federation features (ActivityPub-based repo federation) first, and Forgejo Actions shipped GitHub Actions compatibility ahead of Gitea Actions.
| Dimension | Gitea | Forgejo |
|---|---|---|
| Governance | Gitea Limited (for-profit) | Codeberg e.V. (non-profit, elected council) |
| Licensing | MIT (community edition) | GPLv3+ (fully copyleft) |
| SaaS offering | Yes (gitea.com) | No — Codeberg.org is the reference hosted instance |
| Federation (ActivityPub) | Experimental | Production — issues/PRs can cross instances |
| CI system | Gitea Actions | Forgejo Actions (shipped first, fully GH-compatible) |
| Release cadence | Quarterly major, rolling patches | Monthly patches, predictable LTS line |
| Migration path | Same Go stack | Drop-in for Gitea (same DB schema) |
Our recommendation in 2026: pick Forgejo unless you have a specific dependency on Gitea Enterprise features. The non-profit governance, monthly security patches, and GPL licensing are the safer long-term bet for most teams. We'll use Forgejo in the setup below, but every command translates to Gitea by changing one image name.
Who Is This For?
- Indie hackers / solo developers tired of GitHub Pro and wanting one place for personal repos, container registry, and CI.
- Small teams (3-20) that would pay €40-€100/month to GitHub and want to spend €12/month instead.
- EU businesses needing GDPR-friendly hosting in Germany with full data sovereignty.
- Consultancies / agencies hosting client repositories under their own domain and branding.
- Open-source maintainers who want federation (Forgejo) and don't want AI scraping their PRs.
- Airgapped / on-prem environments where the same Docker setup deploys behind a corporate firewall.
Comparison: GitHub Team vs Gitea vs Forgejo vs GitLab CE
| Feature | GitHub Team | Gitea | Forgejo | GitLab CE |
|---|---|---|---|---|
| Cost (10 users) | $40/mo (~€38) | €7.49/mo VPS | €7.49/mo VPS | €25+/mo VPS (RAM-heavy) |
| Minimum RAM | Managed | 512 MB | 512 MB | 4 GB (8 GB recommended) |
| Install complexity | Zero | 1 Docker container | 1 Docker container | Omnibus (multi-service) |
| Pull requests / MRs | Yes | Yes | Yes | Yes |
| CI/CD built-in | GH Actions | Gitea Actions | Forgejo Actions | GitLab CI |
| GH Actions compatible | Native | ~90% | ~95% | Own YAML syntax |
| Container registry | Yes | Yes (OCI) | Yes (OCI) | Yes |
| Package registry | npm, NuGet, Maven, etc. | 20+ formats | 20+ formats | Many formats |
| Git LFS | Yes | Yes | Yes | Yes |
| Federation | No | Experimental | Yes (ActivityPub) | No |
| Data sovereignty | US (Microsoft) | You control | You control | You control |
| AI training on code | Opt-out required | Never | Never | Never |
Resource Requirements (Minimal Footprint)
This is where Gitea and Forgejo shine compared to GitLab. A single binary, under 100 MB on disk, idles at 40–80 MB RAM, and handles small-to-medium teams on the cheapest VPS you can buy.
| Team size | Recommended VPS | DanubeData Plan | Database |
|---|---|---|---|
| Solo / personal | 2 vCPU, 2GB RAM, 40GB NVMe | DD Nano (€4.49/mo) | SQLite |
| Small team (2-10) | 2 vCPU, 4GB RAM, 80GB NVMe | DD Micro (€7.49/mo) | SQLite or Postgres |
| Team (10-50) + CI | 4 vCPU, 8GB RAM, 160GB NVMe | DD Small (€12.49/mo) | Managed Postgres |
| Business (50+) | 8 vCPU, 16GB RAM, 320GB NVMe | DD Medium or dedicated | Managed Postgres + replica |
For this guide we'll deploy on DD Micro (€7.49/mo) which is the sweet spot: 2 vCPU, 4GB RAM is plenty of headroom for Forgejo plus one small Actions runner. If you're running CI workloads for a real team, step up to DD Small (€12.49/mo) and consider moving the database to DanubeData Managed Postgres (€19.99/mo) so you don't have to babysit backups or upgrades yourself.
Step 1: Provision the VPS
- Create a VPS on DanubeData — pick DD Micro (2 vCPU, 4GB, 80GB NVMe) for €7.49/mo.
- Choose Ubuntu 24.04 LTS as the OS.
- Add your SSH public key during provisioning.
- Note the public IPv4 and IPv6 addresses.
- New accounts get €50 signup credit — that's 6+ months free.
Initial Server Hardening
# SSH in
ssh root@YOUR_SERVER_IP
# Update everything
apt update && apt upgrade -y
# Install essentials
apt install -y curl wget git ufw fail2ban unattended-upgrades
# Firewall: HTTP(S) and a dedicated git SSH port
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 2222/tcp # SSH port for git pushes
ufw enable
# Auto security updates
dpkg-reconfigure --priority=low unattended-upgrades
# Hostname
hostnamectl set-hostname git
Note: we'll use port 2222 for git SSH pushes instead of the default 22. This lets Forgejo listen on its own SSH port inside Docker without clashing with the host's SSH daemon.
Step 2: Install Docker
curl -fsSL https://get.docker.com | sh
apt install -y docker-compose-plugin
docker --version
docker compose version
systemctl enable --now docker
Step 3: DNS
Create two A records (or just the first if you're IPv4-only):
Type Name Value TTL
A git YOUR_IPV4 300
AAAA git YOUR_IPV6 300
Wait for propagation:
dig git.yourdomain.com +short
# Should return your IPs
Step 4: Docker Compose — Forgejo + Postgres + Caddy
Create the working directory:
mkdir -p /opt/forgejo
cd /opt/forgejo
SQLite vs Postgres: Which Should You Pick?
| Criterion | SQLite | PostgreSQL |
|---|---|---|
| Setup effort | Zero — just works | One extra container |
| Backup | Copy one file | pg_dump |
| Concurrent writes | Single writer | Multi-writer |
| Team size | 1-5 active devs | Any size |
| CI load | Struggles under high Actions load | Handles heavy CI fine |
| Upgrade path | Convert via dumper | Straight upgrades |
Our recommendation: start with Postgres from day one. The marginal effort is 15 lines of YAML, and you never have to think about migration later. If you're really just hosting dotfiles for yourself, SQLite is fine — swap the DB env vars in the compose file below for FORGEJO__database__DB_TYPE=sqlite3.
docker-compose.yml
cat > docker-compose.yml << 'EOF'
services:
forgejo:
image: codeberg.org/forgejo/forgejo:10
container_name: forgejo
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- FORGEJO__database__DB_TYPE=postgres
- FORGEJO__database__HOST=db:5432
- FORGEJO__database__NAME=forgejo
- FORGEJO__database__USER=forgejo
- FORGEJO__database__PASSWD=CHANGE_ME_STRONG_PASSWORD
- FORGEJO__server__DOMAIN=git.yourdomain.com
- FORGEJO__server__ROOT_URL=https://git.yourdomain.com/
- FORGEJO__server__SSH_DOMAIN=git.yourdomain.com
- FORGEJO__server__SSH_PORT=2222
- FORGEJO__server__SSH_LISTEN_PORT=22
- FORGEJO__server__START_SSH_SERVER=true
- FORGEJO__service__DISABLE_REGISTRATION=false
- FORGEJO__service__REQUIRE_SIGNIN_VIEW=false
- FORGEJO__mailer__ENABLED=false
- FORGEJO__lfs__PATH=/data/lfs
volumes:
- ./data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "2222:22"
depends_on:
- db
db:
image: postgres:16-alpine
container_name: forgejo-db
restart: unless-stopped
environment:
- POSTGRES_USER=forgejo
- POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD
- POSTGRES_DB=forgejo
volumes:
- ./postgres:/var/lib/postgresql/data
caddy:
image: caddy:2-alpine
container_name: forgejo-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy-data:/data
- caddy-config:/config
depends_on:
- forgejo
volumes:
caddy-data:
caddy-config:
EOF
Important: replace CHANGE_ME_STRONG_PASSWORD (in two places — they must match) with a real password. Generate one with openssl rand -base64 32.
To use Gitea instead of Forgejo, change just the image line:
image: gitea/gitea:1.22
# and prefix env vars with GITEA__ instead of FORGEJO__
Caddyfile
cat > Caddyfile << 'EOF'
git.yourdomain.com {
reverse_proxy forgejo:3000
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
-Server
}
# Gzip and zstd
encode gzip zstd
# Don't buffer large pushes/LFS uploads
request_body {
max_size 5GB
}
log {
output file /var/log/caddy/access.log
format json
}
}
EOF
Step 5: Launch and Install
# Create the data dirs with correct ownership (UID 1000 = forgejo inside container)
mkdir -p data postgres
chown -R 1000:1000 data
# Bring everything up
docker compose up -d
# Watch the logs until you see "Listen: http://0.0.0.0:3000"
docker compose logs -f forgejo
Open https://git.yourdomain.com. You'll land on the installation wizard.
Key settings to confirm:
- Database: PostgreSQL, host
db:5432, databaseforgejo, userforgejo— should be pre-filled from env vars. - Application URL:
https://git.yourdomain.com/ - SSH server domain:
git.yourdomain.com - SSH port:
2222(what external clients see) - Admin account: create one now — otherwise the first user who registers becomes admin.
Click Install. After 10–20 seconds you're redirected to a logged-in dashboard.
Lock Down Registration
Unless you want a public forge, disable signup immediately:
docker compose exec forgejo sh -c "sed -i 's/DISABLE_REGISTRATION = false/DISABLE_REGISTRATION = true/' /data/gitea/conf/app.ini"
docker compose restart forgejo
Step 6: SSH Access for Git
Clients clone over port 2222:
git clone ssh://git@git.yourdomain.com:2222/yourname/yourrepo.git
That's ugly. Make it clean by configuring a host alias in ~/.ssh/config on each developer's machine:
Host git.yourdomain.com
HostName git.yourdomain.com
Port 2222
User git
IdentityFile ~/.ssh/id_ed25519
Now clones work with the natural form:
git clone git@git.yourdomain.com:yourname/yourrepo.git
Users add their SSH public keys via Settings → SSH / GPG Keys in the web UI.
Step 7: Forgejo Actions — GitHub Actions on Your VPS
Forgejo Actions uses the same YAML syntax as GitHub Actions. .forgejo/workflows/*.yml files with steps using actions/checkout@v4 just work.
Enable Actions
# Add to docker-compose.yml environment section of the forgejo service:
- FORGEJO__actions__ENABLED=true
- FORGEJO__actions__DEFAULT_ACTIONS_URL=https://code.forgejo.org
docker compose up -d
Register a Runner
Runners execute the jobs. They can run on the same VPS (fine for small teams) or on a separate machine.
In the Forgejo UI: Site Administration → Actions → Runners → "Create new Runner" — copy the registration token.
cat >> docker-compose.yml << 'EOF'
runner:
image: code.forgejo.org/forgejo/runner:6
container_name: forgejo-runner
restart: unless-stopped
depends_on:
- forgejo
volumes:
- ./runner-data:/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- FORGEJO_INSTANCE_URL=https://git.yourdomain.com
- FORGEJO_RUNNER_REGISTRATION_TOKEN=PASTE_TOKEN_HERE
- FORGEJO_RUNNER_NAME=vps-runner-1
- FORGEJO_RUNNER_LABELS=docker,ubuntu-latest:docker://node:20-bookworm
command: |
sh -c "
if [ ! -f /data/.runner ]; then
forgejo-runner register --no-interactive
--instance $$FORGEJO_INSTANCE_URL
--token $$FORGEJO_RUNNER_REGISTRATION_TOKEN
--name $$FORGEJO_RUNNER_NAME
--labels $$FORGEJO_RUNNER_LABELS
fi
forgejo-runner daemon
"
EOF
mkdir -p runner-data
docker compose up -d runner
docker compose logs -f runner
You should see Runner registered successfully. Back in the web UI the runner now shows as "Idle".
Example Workflow
Create .forgejo/workflows/ci.yml in any repository:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: docker
container:
image: node:20-bookworm
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
- run: npm run build
Push to the repo and watch the job light up under the Actions tab. Every primitive from GitHub Actions — matrix builds, artifacts, caching, secrets, environments — works identically.
Step 8: Container Registry
Forgejo ships an OCI-compatible container registry at the same URL. Push with:
echo "YOUR_API_TOKEN" | docker login git.yourdomain.com -u yourname --password-stdin
docker tag myapp:latest git.yourdomain.com/yourname/myapp:latest
docker push git.yourdomain.com/yourname/myapp:latest
Generate API tokens in Settings → Applications → Generate New Token, with the write:package scope.
The registry is also usable from Actions workflows via the built-in ${{ secrets.GITHUB_TOKEN }} (yes, Forgejo aliases this for compatibility).
Step 9: Git LFS
LFS is already enabled by the FORGEJO__lfs__PATH setting. Push large binaries normally:
cd your-repo
git lfs install
git lfs track "*.psd" "*.mp4" "assets/**"
git add .gitattributes
git commit -m "Track large files with LFS"
git push
LFS objects land under data/lfs inside your volume. Plan disk accordingly if you host video or design assets.
Step 10: Migrate From GitHub
Forgejo has a first-class GitHub migration flow — issues, PRs, labels, milestones, releases, and wiki all come across.
- In the Forgejo UI click + → New Migration → GitHub.
- Paste the source repo URL.
- Generate a GitHub Personal Access Token with
reposcope and paste it. - Pick what to migrate — at minimum: wiki, milestones, labels, issues, pull requests, releases.
- Hit Migrate.
For bulk migrations, use the API:
# migrate.sh — migrate all repos from a GitHub org
GITEA_TOKEN="your_forgejo_token"
GITHUB_TOKEN="your_github_token"
GITHUB_ORG="your-github-org"
FORGEJO_OWNER_UID="1" # your admin user UID
gh api "orgs/$GITHUB_ORG/repos?per_page=100" --paginate --jq '.[].full_name' | while read repo; do
echo "Migrating $repo..."
curl -X POST "https://git.yourdomain.com/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",
"uid": $FORGEJO_OWNER_UID,
"repo_name": "$(basename $repo)",
"mirror": false,
"private": true,
"wiki": true,
"milestones": true,
"labels": true,
"issues": true,
"pull_requests": true,
"releases": true,
"service": "github"
}"
echo ""
done
Convert GitHub Actions Workflows
Most workflows need zero changes. The main gotchas:
- Move files from
.github/workflows/to.forgejo/workflows/(Forgejo also reads.github/workflows/for compatibility). - Replace
runs-on: ubuntu-latestwithruns-on: docker(or whatever labels your runner advertises). - Marketplace actions like
actions/checkout@v4work viahttps://code.forgejo.orgmirrors. Third-party actions may need to be vendored or hosted internally. ${{ secrets.GITHUB_TOKEN }}is aliased to the Forgejo token — most workflows just work.
Step 11: Backups
Three things to back up: the database, the data directory (repos + attachments + LFS), and the config.
cat > /opt/forgejo/backup.sh << 'EOF'
#!/bin/bash
set -e
BACKUP_DIR="/opt/forgejo/backups"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
S3_REMOTE="danubedata:forgejo-backups"
mkdir -p "$BACKUP_DIR"
cd /opt/forgejo
echo "=== Forgejo Backup: $DATE ==="
# 1. Postgres dump
docker compose exec -T db pg_dump -U forgejo forgejo | gzip > "$BACKUP_DIR/postgres_$DATE.sql.gz"
# 2. Repos + LFS (pause Forgejo for consistency on active writes)
docker compose exec -T forgejo forgejo dump --type tar.gz --file /tmp/forgejo-dump.tar.gz --skip-log
docker compose cp forgejo:/tmp/forgejo-dump.tar.gz "$BACKUP_DIR/dump_$DATE.tar.gz"
docker compose exec -T forgejo rm /tmp/forgejo-dump.tar.gz
# 3. Docker compose config
cp docker-compose.yml "$BACKUP_DIR/compose_$DATE.yml"
cp Caddyfile "$BACKUP_DIR/caddy_$DATE.conf"
# 4. Combined archive
tar -czf "$BACKUP_DIR/forgejo_$DATE.tar.gz"
"$BACKUP_DIR/postgres_$DATE.sql.gz"
"$BACKUP_DIR/dump_$DATE.tar.gz"
"$BACKUP_DIR/compose_$DATE.yml"
"$BACKUP_DIR/caddy_$DATE.conf"
# 5. Offsite to DanubeData S3
rclone copy "$BACKUP_DIR/forgejo_$DATE.tar.gz" "$S3_REMOTE/"
# 6. Retain 14 days local, 90 days remote (lifecycle rule on bucket)
find "$BACKUP_DIR" -name "forgejo_*.tar.gz" -mtime +14 -delete
find "$BACKUP_DIR" -name "postgres_*.sql.gz" -mtime +2 -delete
find "$BACKUP_DIR" -name "dump_*.tar.gz" -mtime +2 -delete
find "$BACKUP_DIR" -name "compose_*.yml" -mtime +2 -delete
find "$BACKUP_DIR" -name "caddy_*.conf" -mtime +2 -delete
echo "=== Backup complete ==="
EOF
chmod +x /opt/forgejo/backup.sh
# Nightly at 03:00
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/forgejo/backup.sh >> /var/log/forgejo-backup.log 2>&1") | crontab -
DanubeData S3 storage is €3.99/mo for 1TB, which is plenty for years of nightly backups. Configure rclone once with your DanubeData S3 access keys and you're done.
Test the Restore
A backup you haven't tested is theoretical. Every three months:
# On a scratch VPS
rclone copy danubedata:forgejo-backups/forgejo_LATEST.tar.gz .
tar -xzf forgejo_LATEST.tar.gz
# Restore postgres + extract dump tarball + boot a fresh compose stack
Step 12: Production Hardening Checklist
- Webhook secrets: always set a secret on webhooks so pushed events can be verified downstream.
- Two-factor auth: Settings → Security → enable TOTP for every admin user.
- Branch protection: require PR reviews + CI green on
mainfor production repos. - Rate limits: Caddy's
rate_limitplugin is worth adding if the instance is public. - OAuth: hook up GitHub/Google/Microsoft OAuth under Authentication Sources so team members log in with SSO.
- Monitoring: Forgejo exposes
/metricsin Prometheus format — scrape it and alert on error rate, response time, and queue depth. - Log rotation: Docker's default JSON driver fills disk. Switch to
json-filewithmax-size: 10mandmax-file: 3in/etc/docker/daemon.json. - Upgrades: bump the
codeberg.org/forgejo/forgejoimage tag quarterly; Forgejo follows semver and has been painless to upgrade since v1.20.
Upgrading to Managed Postgres (When to Move)
The bundled Postgres container is fine for 1-10 active users. Move to DanubeData Managed PostgreSQL (€19.99/mo) once any of these is true:
- More than 20 active developers or 50+ repos with active CI.
- You're tired of handling backups/WAL/PITR yourself.
- You want a read replica for reporting or disaster recovery.
- Compliance requires point-in-time recovery and documented backup SLAs.
The migration is straightforward:
# 1. Create the managed Postgres at danubedata.ro/databases/create
# 2. Dump the current DB
docker compose exec -T db pg_dump -U forgejo forgejo > forgejo.sql
# 3. Restore into managed Postgres
PGPASSWORD="..." psql -h your-managed-pg.danubedata.ro -U forgejo -d forgejo < forgejo.sql
# 4. Update docker-compose.yml env:
# FORGEJO__database__HOST=your-managed-pg.danubedata.ro:5432
# FORGEJO__database__SSL_MODE=require
# 5. Remove the db: service from compose, restart
docker compose up -d
Cost Analysis: 3-Year TCO
| Option | Users | Monthly | 3-Year Cost | Notes |
|---|---|---|---|---|
| GitHub Team | 10 | $40 (~€38) | ~€1,368 | CI minutes billed on top |
| GitHub Enterprise | 10 | $210 | ~€7,128 | SAML, audit log |
| Self-hosted Forgejo on DD Micro | 10 | €7.49 | €270 | Unlimited CI, unlimited repos |
| Self-hosted Forgejo on DD Small + Managed PG | 30 | €32.48 | €1,169 | SLA'd Postgres, backups |
Even in the most generous GitHub comparison, self-hosting pays for itself in under two months for a small team, and the €50 DanubeData signup credit covers your first half-year.
Troubleshooting
"Permission denied (publickey)" on git push
- Is your public key in Settings → SSH Keys?
- Are you hitting port 2222? Add
Port 2222in~/.ssh/config. - Does
ssh -vT -p 2222 git@git.yourdomain.comshow "Hi yourname!"? If not, check container logs:docker compose logs forgejo | grep -i ssh.
Actions runner stuck "registering"
- Token expires fast — get a fresh one and recreate the runner container.
- Outbound HTTPS from the runner to the Forgejo URL must work — test with
docker compose exec runner wget -O- https://git.yourdomain.com/api/v1/version.
LFS push fails with 413
- Caddy's
request_body max_sizeneeds to be above your largest LFS object. The config above allows 5GB.
Container registry "unauthorized" on push
- Personal Access Token must have
write:packagescope. Regenerate anddocker loginagain.
FAQ
Can I migrate from GitHub with full issue and PR history?
Yes — the built-in migration tool imports issues, PRs, comments, labels, milestones, releases, and wiki pages. Commits, branches, and tags come across via a normal mirror clone. The only things that don't transfer cleanly are GitHub-specific features like Projects v2 boards, Discussions, and GitHub Pages. For teams moving off GitHub, most report 95%+ of useful history survives.
Are Forgejo Actions 100% compatible with GitHub Actions?
Not quite — about 95%. Syntax is identical, most marketplace actions (checkout, setup-node, setup-python, cache, upload-artifact) work unchanged, and secrets/env/matrix/artifacts behave the same. The gaps are: GitHub-specific APIs (GH_TOKEN is aliased but octokit calls to api.github.com obviously don't work), some advanced concurrency primitives, and a few actions that hard-code github.com. In practice, converting a typical CI workflow takes minutes, not hours.
Can Gitea or Forgejo scale to 100+ developers?
Yes, comfortably. Forgejo instances like Codeberg.org serve 100,000+ users on modest hardware. The bottleneck at scale is usually the database and Git storage, not Forgejo itself. Past ~50 active developers or heavy CI load, move to DD Small (€12.49/mo) + Managed PostgreSQL (€19.99/mo), and put LFS on S3-backed storage. Beyond ~500 users you'll want dedicated runners and possibly horizontal Forgejo replicas behind a load balancer.
Who should pick Forgejo vs Gitea?
Pick Forgejo if you value non-profit governance, prefer GPL copyleft licensing, want the most aggressive federation/ActivityPub roadmap, or are an EU organization that appreciates EU-based stewardship (Codeberg e.V. is registered in Germany). Pick Gitea if you need Gitea Enterprise features (SAML, audit features) or are already deeply invested in the Gitea ecosystem. For 90% of new self-hosters in 2026, Forgejo is the safer default.
How do I handle SSO and LDAP?
Both projects support LDAP, SMTP, OAuth2 (GitHub, Google, GitLab, Microsoft, Discord, generic OIDC), SAML 2.0, and PAM out of the box. Configure under Site Administration → Authentication Sources. For a small team, OAuth2 via Google or Microsoft 365 is the least-friction path — no extra servers, and offboarding is automatic when you remove the user from your identity provider.
What about Git LFS storage costs?
LFS objects live on the same disk as your repos by default. For teams storing video, design files, or game assets, enable the built-in MinIO/S3 LFS backend and point it at DanubeData S3 (€3.99/mo for 1TB). The config is one section in app.ini: set [lfs] STORAGE_TYPE=minio with the endpoint, access key, and bucket. Objects move off local disk and you get versioning + lifecycle rules for free.
Can I run CI for private repos without exposing my runner publicly?
Yes — runners initiate outbound polling to the Forgejo server, there's no inbound connection. You can run the runner on a laptop, a homelab server, or a second VPS with no public ports open. This is safer than GitHub's self-hosted runner model, which requires an inbound webhook path.
What happens if the Forgejo maintainers abandon the project?
Two layers of insurance. First, Forgejo is governed by an elected council under Codeberg e.V., a registered German non-profit — there's no single owner who can walk away. Second, your data is standard Git + PostgreSQL + filesystem. Even in a worst-case future where both Gitea and Forgejo disappeared, your repos are plain git, your issues are in a documented Postgres schema, and you could migrate to the next fork, GitLab CE, or build a tiny custom UI in weeks. This is the opposite of GitHub lock-in.
Get Started Today
- Create a DanubeData VPS — DD Micro (€7.49/mo) for solo/small teams, DD Small (€12.49/mo) for production.
- Follow the Docker Compose walkthrough above (30 minutes from blank server to working Forgejo).
- Migrate your GitHub org via the built-in migration tool.
- Register one Forgejo Actions runner and port your CI.
- Set up nightly backups to DanubeData S3.
Why DanubeData for your Git server:
- AMD EPYC NVMe — Git operations feel instant.
- Falkenstein, Germany — GDPR-native, low latency across the EU.
- 20TB included traffic — clones, CI artifacts, container pulls won't bite you.
- €50 signup credit — your first six months are effectively free.
- Managed PostgreSQL from €19.99/mo when you outgrow SQLite.
- S3 object storage from €3.99/mo for backups and LFS.
Ready to own your source code?
Questions about migrating a large GitHub org or tuning Forgejo for heavy CI? Contact our team — we help DanubeData customers plan migrations at no extra cost.