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:


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:

  1. Postgres initializes its data dir and creates the forgejo database/user.
  2. The healthcheck gates Forgejo's start until the DB accepts connections.
  3. Forgejo writes its config to /data/gitea/conf/app.ini, seeded from the GITEA__* env vars (each __ maps to a [section] KEY= line).
  4. Traefik sees the router label, requests a Let's Encrypt cert for git.example.com, and proxies https://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:3000 in 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

Change Log

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse