Deploying Forgejo with Traefik and Docker Compose
Table of Contents ๐
Forgejo is a lightweight self-hosted Git forge with repositories, issues, pull requests, releases, packages, and optional Actions-compatible CI. It is a good fit when GitLab is too heavy but a plain Git server is not enough.
This guide installs Forgejo in an existing Traefik v3 and CrowdSec Docker ecosystem. Forgejo runs as a rootless Docker container, PostgreSQL stores the application database, Traefik handles HTTPS, and Forgejoโs built-in SSH server exposes Git over a dedicated host port.
| โน๏ธ FRESH INSTALL FIRST |
The main path below is a clean Forgejo installation. GitHub import, container registry usage, and Forgejo Runner are covered as optional sections after the base instance is working. |
Changelog
| Date | Change |
|---|---|
| 2026-07-01 | Initial Version: Fresh Forgejo install with PostgreSQL, rootless Docker, Traefik labels, SSH, registry, and backups. |
1. Architecture
The target architecture looks like this:
Internet
|
| 443/tcp
v
Traefik -> Forgejo web UI/API/registry on port 3000
Internet
|
| 2222/tcp
v
Forgejo built-in SSH server for Git
Forgejo -> PostgreSQL on a private Docker networkThis setup deliberately starts without a runner. CI jobs can execute arbitrary project code, so it is better to enable the runner only after the Git service itself is stable.
2. Prerequisites
You need:
- A working Traefik v3 and CrowdSec stack.
- Docker and Docker Compose.
- An external Docker network named
proxy. - A subdomain such as
forgejo.your-domain.com. - DNS
A/AAAArecords pointing the subdomain to your server. - Open firewall ports
80/tcp,443/tcp, and one Git SSH port, e.g.2222/tcp.
This guide uses:
- Forgejo
15.0.3-rootless - PostgreSQL
16-alpine - Traefik TLS resolver named
tls_resolver
Check the Forgejo Docker installation docs before changing major versions. Upgrading from one major Forgejo version to the next should be done deliberately, not through latest.
3. Directory Structure
sudo mkdir -p /opt/containers/forgejo
cd /opt/containers/forgejo
sudo mkdir -p data config postgres
sudo chown -R 1000:1000 data configThe rootless Forgejo container runs as UID/GID 1000:1000. Adjust this if your Docker host uses a different unprivileged user.
4. Environment File
Create /opt/containers/forgejo/.env:
sudo install -m 0600 /dev/null .env
sudo nano .envExample:
TZ=Europe/Berlin
FORGEJO_DOMAIN=forgejo.your-domain.com
FORGEJO_DB_PASSWORD=REPLACE_WITH_LONG_RANDOM_VALUE
FORGEJO_SECRET_KEY=REPLACE_WITH_LONG_RANDOM_VALUE
FORGEJO_INTERNAL_TOKEN=REPLACE_WITH_LONG_RANDOM_VALUE
FORGEJO_JWT_SECRET=REPLACE_WITH_LONG_RANDOM_VALUEGenerate secrets with:
openssl rand -hex 32
openssl rand -hex 645. Docker Compose
Create /opt/containers/forgejo/docker-compose.yml:
services:
forgejo-db:
image: docker.io/postgres:16-alpine
container_name: forgejo-db
restart: unless-stopped
environment:
POSTGRES_DB: forgejo
POSTGRES_USER: forgejo
POSTGRES_PASSWORD: ${FORGEJO_DB_PASSWORD}
volumes:
- ./postgres:/var/lib/postgresql/data
networks:
- forgejo-internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U forgejo -d forgejo"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
forgejo:
image: codeberg.org/forgejo/forgejo:15.0.3-rootless
container_name: forgejo
user: "1000:1000"
restart: unless-stopped
depends_on:
forgejo-db:
condition: service_healthy
environment:
USER_UID: 1000
USER_GID: 1000
FORGEJO____APP_NAME: "My Forgejo"
FORGEJO__database__DB_TYPE: postgres
FORGEJO__database__HOST: forgejo-db:5432
FORGEJO__database__NAME: forgejo
FORGEJO__database__USER: forgejo
FORGEJO__database__PASSWD: ${FORGEJO_DB_PASSWORD}
FORGEJO__database__SSL_MODE: disable
FORGEJO__security__INSTALL_LOCK: "true"
FORGEJO__security__SECRET_KEY: ${FORGEJO_SECRET_KEY}
FORGEJO__security__INTERNAL_TOKEN: ${FORGEJO_INTERNAL_TOKEN}
FORGEJO__oauth2__JWT_SECRET: ${FORGEJO_JWT_SECRET}
FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN}
FORGEJO__server__ROOT_URL: https://${FORGEJO_DOMAIN}/
FORGEJO__server__HTTP_ADDR: 0.0.0.0
FORGEJO__server__HTTP_PORT: 3000
FORGEJO__server__PROTOCOL: http
FORGEJO__server__START_SSH_SERVER: "true"
FORGEJO__server__SSH_DOMAIN: ${FORGEJO_DOMAIN}
FORGEJO__server__SSH_PORT: 2222
FORGEJO__server__SSH_LISTEN_PORT: 2222
FORGEJO__server__LFS_START_SERVER: "true"
FORGEJO__repository__DEFAULT_PRIVATE: private
FORGEJO__service__DISABLE_REGISTRATION: "true"
FORGEJO__service__REQUIRE_SIGNIN_VIEW: "true"
FORGEJO__openid__ENABLE_OPENID_SIGNIN: "false"
FORGEJO__openid__ENABLE_OPENID_SIGNUP: "false"
FORGEJO__packages__ENABLED: "true"
FORGEJO__actions__ENABLED: "false"
TZ: ${TZ}
volumes:
- ./data:/var/lib/gitea
- ./config:/etc/gitea
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "2222:2222"
networks:
- proxy
- forgejo-internal
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.forgejo.rule=Host(`forgejo.your-domain.com`)"
- "traefik.http.routers.forgejo.entrypoints=websecure"
- "traefik.http.routers.forgejo.tls.certresolver=tls_resolver"
- "traefik.http.routers.forgejo.middlewares=security-headers@file,crowdsec-bouncer@docker"
- "traefik.http.services.forgejo.loadbalancer.server.port=3000"
networks:
proxy:
external: true
forgejo-internal:
external: falseOpen the Git SSH port on the host:
sudo ufw allow 2222/tcp comment 'Forgejo SSH'Start Forgejo:
docker compose pull
docker compose up -d
docker compose logs -f forgejo6. Create the Admin User
With the rootless container image, the generated app.ini usually lives at:
/var/lib/gitea/custom/conf/app.iniCreate the first admin user:
docker compose exec -T forgejo \
forgejo --config /var/lib/gitea/custom/conf/app.ini \
--work-path /var/lib/gitea \
admin user create \
--username youradmin \
--password 'REPLACE_WITH_TEMPORARY_PASSWORD' \
--email you@your-domain.com \
--admin \
--must-change-password=falseLog in at:
https://forgejo.your-domain.com/Then add your SSH public key under:
Settings -> SSH / GPG KeysTest SSH:
ssh -T -p 2222 git@forgejo.your-domain.com7. Optional: Mail via Mox
Forgejo works without mail, but password resets and notifications require SMTP. If you run Mox, create a dedicated mailbox such as forgejo@your-domain.com, then add:
FORGEJO__mailer__ENABLED: "true"
FORGEJO__mailer__PROTOCOL: smtp+starttls
FORGEJO__mailer__SMTP_ADDR: mail.your-domain.com
FORGEJO__mailer__SMTP_PORT: 587
FORGEJO__mailer__USER: forgejo@your-domain.com
FORGEJO__mailer__PASSWD: "REPLACE_WITH_MAILBOX_PASSWORD"
FORGEJO__mailer__FROM: "Forgejo <forgejo@your-domain.com>"Restart Forgejo after changing mail settings:
docker compose up -d8. Container Registry
Forgejo includes a Docker/OCI-compatible container registry. With the setup above, the registry uses the same hostname as the web UI:
docker login forgejo.your-domain.com
docker build -t forgejo.your-domain.com/youruser/myapp:v1.0.0 .
docker push forgejo.your-domain.com/youruser/myapp:v1.0.0For production, prefer immutable tags:
v1.2.0
main-a1b2c3d
2026-07-01-a1b2c3dUsing only latest makes rollbacks and audits harder.
9. Optional: Import or Mirror GitHub Repositories
For a simple source-code backup, mirror the Git repository first. Do not start by copying thousands of old container images or CI artifacts.
git clone --mirror https://github.com/ORG/REPO.git
cd REPO.git
git push --mirror https://forgejo.your-domain.com/ORG/REPO.gitFor private GitHub repositories, use the GitHub CLI locally:
gh auth login
gh auth refresh -h github.com -s repo -s read:org
git clone --mirror https://github.com/ORG/REPO.gitIf you want to avoid importing GitHub-specific pull request refs as hundreds of Forgejo branches, push only normal branches and tags:
git push https://forgejo.your-domain.com/ORG/REPO.git '+refs/heads/*:refs/heads/*'
git push https://forgejo.your-domain.com/ORG/REPO.git '+refs/tags/*:refs/tags/*'GitHub Actions caches are rebuildable. Old Playwright reports and expired artifacts are usually safe to delete once they are no longer needed for debugging. Container package versions should be reduced to release tags and a small number of recent main builds; stale untagged image versions should not be mirrored blindly into your new registry.
10. Optional: Forgejo Runner
Forgejo Actions can be enabled later. The conservative path is:
- Keep Forgejo itself on the production VPS.
- Add a runner only when you know which workflows you need.
- Avoid running untrusted CI jobs on the same host as production data.
- If you start on the same VPS, keep it as a temporary convenience and move the runner to a separate build host later.
The risky part is Docker access from CI jobs. Mounting /var/run/docker.sock gives jobs effective host-root power. If you need container builds, prefer a dedicated runner host or a rootless build tool such as BuildKit, Buildah, or Kaniko.
11. Backups
Back up at least:
/opt/containers/forgejo/docker-compose.yml
/opt/containers/forgejo/.env
/opt/containers/forgejo/data
/opt/containers/forgejo/config
/opt/containers/forgejo/postgresA simple dump:
cd /opt/containers/forgejo
docker compose exec -T forgejo-db \
pg_dump -U forgejo forgejo > forgejo-db.sql
tar -czf forgejo-data-backup.tar.gz data config forgejo-db.sqlStore backups off-server and test restores before relying on them.
12. Updates
For patch updates within the same major version:
cd /opt/containers/forgejo
docker compose pull
docker compose up -d
docker compose logs -f forgejoFor major upgrades, read the Forgejo release notes first. Do not jump major versions unattended, and do not use latest in production.





