Forgejo Installation - Installing Forgejo with Docker Compose
Status: Active
Last Updated: 2026-08-14
Category: CI/CD - Phase 2: Forgejo (Self-Hosted Git)
Prerequisites: Docker basics, docker-compose-patterns, forgejo-introduction, reverse proxy basics
Time: 2โ3 hours
Tags: forgejo, installation, docker-compose, postgresql, traefik, ssl, self-hosted
Summary
A production-grade Forgejo deployment in under an hour of hands-on time: Docker Compose with PostgreSQL, persistent volumes, a Traefik reverse proxy for automatic Let's Encrypt TLS, correct SSH passthrough so git clone works over port 22, and the initial-admin checklist that locks your instance down properly.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ Deploy Forgejo + PostgreSQL via Docker Compose with named volumes
- โ Route HTTPS through Traefik (or Caddy) with automatic certificates
- โ Pass Git SSH traffic through to the container cleanly
- โ Complete initial setup and create a locked-down admin account
- โ Verify and troubleshoot the full stack end-to-end
Prerequisites Checklist
# Confirm before starting:
docker --version # 24+ recommended
docker compose version # v2 plugin
# A DNS A/AAAA record pointing at this host, e.g.:
dig +short git.example.com # โ your server IP
# Ports free:
ss -tlnp | grep -E ':(80|443|22)\b'
Resource floor: 2GB RAM total is comfortable (Forgejo ~512MB idle, Postgres ~100MB, headroom for spikes); 20GB disk for repos + images grows with use.
Directory Layout
Keep everything declarative and backup-friendly:
/opt/forgejo/
โโโ docker-compose.yml
โโโ .env # secrets โ never commit
โโโ data/ # named volume targets below
โโโ forgejo/
โโโ postgres/
sudo mkdir -p /opt/forgejo/data/{forgejo,postgres}
cd /opt/forgejo
Environment File
# /opt/forgejo/.env
FORGEJO_VERSION=9
POSTGRES_PASSWORD=change-me-long-random-string # openssl rand -hex 32
GITEA__database__PASSWORD=${POSTGRES_PASSWORD} # passed into container
chmod 600 .env # secrets stay secrets
Docker Compose: Forgejo + PostgreSQL
# /opt/forgejo/docker-compose.yml
services:
forgejo:
image: codeberg.org/forgejo/forgejo:9
container_name: forgejo
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=db:5432
- GITEA__database__NAME=forgejo
- GITEA__database__USER=forgejo
- GITEA__database__PASSWD=${POSTGRES_PASSWORD}
- GITEA__server__DOMAIN=git.example.com
- GITEA__server__SSH_DOMAIN=git.example.com
- GITEA__server__ROOT_URL=https://git.example.com/
- GITEA__server__HTTP_ADDR=0.0.0.0
- GITEA__server__HTTP_PORT=3000
- GITEA__security__INSTALL_LOCK=true
- GITEA__mailer__ENABLED=false # enable later; see notes
volumes:
- ./data/forgejo:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
depends_on:
- db
labels:
# --- Traefik routing ---
- "traefik.enable=true"
- "traefik.http.routers.forgejo.rule=Host(`git.example.com`)"
- "traefik.http.routers.forgejo.entrypoints=websecure"
- "traefik.http.routers.forgejo.tls.certresolver=letsencrypt"
- "traefik.http.services.forgejo.loadbalancer.server.port=3000"
db:
image: postgres:16-alpine
container_name: forgejo-db
restart: unless-stopped
environment:
- POSTGRES_USER=forgejo
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=forgejo
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U forgejo"]
interval: 10s
timeout: 5s
retries: 5
networks:
default:
name: proxy-tier # join the network Traefik listens on
external: true
What Happens on up:
- Postgres initializes its data dir and creates the
forgejodatabase/user. - The healthcheck gates Forgejo's start until the DB accepts connections.
- Forgejo writes its config to
/data/gitea/conf/app.ini, seeded from theGITEA__*env vars (each__maps to a[section] KEY=line). - Traefik sees the router label, requests a Let's Encrypt cert for
git.example.com, and proxieshttps://git.example.comโ container port 3000.
If you don't already run Traefik, see kb/networking for the base setup; Caddy users can swap the labels for a one-line
reverse_proxy forgejo:3000in their Caddyfile.
SSH Passthrough for git@
Web UI alone isn't enough โ you want git clone git@git.example.com:you/repo.git. Two clean options:
Option A: Host sshd forwards to container (recommended)
Forward the host's SSH on a dedicated port or use command-based forwarding. Simplest robust pattern โ run the container's SSH on host port 2222 and advertise it:
ports:
- "2222:22" # add to forgejo service
Then set in app.ini (or env): GITEA__server__SSH_PORT=2222 and GITEA__server__SSH_LISTEN_PORT=22. Clone URLs will render as ssh://git@git.example.com:2222/user/repo.git.
Option B: True transparent passthrough on port 22
Use sshd's Match block on the host so real shell logins and Forgejo git traffic share port 22:
# /etc/ssh/sshd_config.d/forgejo.conf
Match User git
ForceCommand /usr/local/bin/forgejo-shell
PasswordAuthentication no
PermitTTY no
# /usr/local/bin/forgejo-shell
#!/bin/sh
exec ssh -p 2222 -o StrictHostKeyChecking=no git@127.0.0.1 "SSH_ORIGINAL_COMMAND=\"$SSH_ORIGINAL_COMMAND\" $0 $@"
Option A is simpler; choose B only if policy forbids extra ports. Either way, create the git user on the host with the same UID as inside the container (USER_UID) to avoid permission mismatches.
First Boot & Initial Admin Setup
cd /opt/forgejo
docker compose up -d
docker compose logs -f forgejo # wait for "Starting new Web server..."
curl -fsS https://git.example.com/api/healthz | jq .
Because INSTALL_LOCK=true skipped the web installer, create the admin via CLI:
docker compose exec -u git forgejo \
forgejo admin user create \
--admin --username opsadmin \
--password 'use-a-password-manager-value' \
--email ops@example.com --must-change-password=false
Post-install hardening checklist:
| Setting | Where | Why |
|---|---|---|
| Disable open registration | Site Admin โ Users โ SHOW_REGISTRATION_BUTTON=false; config SERVICE/DISABLE_REGISTRATION=true |
Strangers shouldn't self-register on your private forge |
| Require 2FA for admins | Profile โ Security | Your forge holds all your code; treat like root |
| Hide email domains | service/EMAIL_DOMAIN_WHITELIST or hide-email default |
Prevents enumeration |
| Enable fail2ban on auth endpoints | Host firewall | Brute-force protection (see fail2ban-setup) |
SSL/TLS Verification
echo | openssl s_client -connect git.example.com:443 -servername git.example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
# Expect: issuer=Let's Encrypt, notAfter ~90 days out (auto-renewed by Traefik)
curl -sI https://git.example.com | head -1 # HTTP/2 200
curl -sI http://git.example.com | head -1 # expect 308 โ https
Backup Before You Build On It
# Consistent cold backup (stop world briefly):
docker compose stop forgejo
tar czf forgejo-$(date +%F).tgz data/ docker-compose.yml .env
docker compose start forgejo
# Or hot DB dump + file copy for larger instances:
docker compose exec db pg_dump -U forgejo forgejo | gzip > forgejo-db-$(date +%F).sql.gz
Schedule it now โ see kb/cloud/restic-setup patterns for automated encrypted backups.
๐ ๏ธ Troubleshooting & Common Issues
| Symptom | Likely cause | Fix |
|---|---|---|
502 Bad Gateway from Traefik |
Containers not on same Docker network | external: true network must match Traefik's; check docker network inspect proxy-tier |
| Certificate never issued | DNS not propagated or port 80 blocked (LE HTTP-01 needs it) | dig the record; open 80; check Traefik logs for ACME errors |
docker compose up fails on .env var |
Unset/empty POSTGRES_PASSWORD | Recreate .env, chmod 600, re-run |
| Push asks for password repeatedly over HTTP | Remote URL uses https without credential helper | Switch remote to SSH: git remote set-url origin git@...:repo.git |
| Permission denied writing repos | UID/GID mismatch between host mount owner and USER_UID |
chown -R 1000:1000 ./data/forgejo |
| SSH clones hang | Host sshd intercepting before container | Verify port mapping/passthrough path; test ssh -p 2222 git@git.example.com verbosely (-v) |
| DB connection refused at first boot | Forgejo raced Postgres init | Healthcheck handles it normally; if removed, restart forgejo after db healthy |
๐ Related
- Previous: forgejo-introduction
- Next: forgejo-organization
- Reverse proxy deep dive: traefik concepts, tls-configuration
- Hardening: ssh-security-hardening
Change Log
- 2026-08-14 โ Initial version written as part of the KB course build-out (cicd directory).