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

DateChange
2026-07-01Initial 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 network

This 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/AAAA records 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 config

The 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 .env

Example:

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_VALUE

Generate secrets with:

openssl rand -hex 32
openssl rand -hex 64

5. 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: false

Open 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 forgejo

6. Create the Admin User

With the rootless container image, the generated app.ini usually lives at:

/var/lib/gitea/custom/conf/app.ini

Create 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=false

Log in at:

https://forgejo.your-domain.com/

Then add your SSH public key under:

Settings -> SSH / GPG Keys

Test SSH:

ssh -T -p 2222 git@forgejo.your-domain.com

7. 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 -d

8. 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.0

For production, prefer immutable tags:

v1.2.0
main-a1b2c3d
2026-07-01-a1b2c3d

Using 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.git

For 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.git

If 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:

  1. Keep Forgejo itself on the production VPS.
  2. Add a runner only when you know which workflows you need.
  3. Avoid running untrusted CI jobs on the same host as production data.
  4. 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/postgres

A 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.sql

Store 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 forgejo

For major upgrades, read the Forgejo release notes first. Do not jump major versions unattended, and do not use latest in production.