3 Forgejo
Drew edited this page 2026-05-22 18:55:45 -04:00

Forgejo + Runner — git.thelinuxcast.org

Self-hosted git forge with CI runner. Lives at /opt/forgejo/. Web on :3000, git-over-ssh on :2222. Public access is via the second Cloudflare Tunnel (cloudflared-thelinuxcast) — see Cloudflare Tunnel.

The same compose file also runs the tunnel container, since the tunnel only exists to expose Forgejo — keeping them together means one docker compose up -d brings both up or down.

/opt/forgejo/docker-compose.yml

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared-thelinuxcast
    command: tunnel --no-autoupdate run --token <REDACTED>
    restart: unless-stopped
    network_mode: host

  forgejo:
    image: codeberg.org/forgejo/forgejo:15.0.2
    container_name: forgejo
    environment:
      - USER_UID=1000
      - USER_GID=1000
    volumes:
      - ./data:/data
    ports:
      - "3000:3000"
      - "2222:22"
    restart: unless-stopped

  forgejo-runner:
    image: code.forgejo.org/forgejo/runner:6
    container_name: forgejo-runner
    user: root
    volumes:
      - ./runner-data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    working_dir: /data
    command: forgejo-runner daemon
    depends_on:
      - forgejo
    restart: unless-stopped

USER_UID=1000 matches host drew, so files in data/ are readable from the shell without sudo. The Forgejo image is pinned (15.0.2) — everything else uses :latest.

Configuration — data/gitea/conf/app.ini

Highlights (full file is on the box; only the meaningful settings are reproduced here, secrets redacted):

APP_NAME = Forgejo
RUN_USER = git
WORK_PATH = /data/gitea

[server]
DOMAIN          = git.thelinuxcast.org
SSH_DOMAIN      = git.thelinuxcast.org
HTTP_PORT       = 3000
ROOT_URL        = https://git.thelinuxcast.org/
DISABLE_SSH     = false
SSH_PORT        = 2222          # advertised in clone URLs (host port)
SSH_LISTEN_PORT = 22            # in-container listener (mapped from :2222)
LFS_START_SERVER = true
OFFLINE_MODE    = true          # no avatar/CDN fetches to external services

[database]
DB_TYPE = sqlite3
PATH    = /data/gitea/gitea.db

[security]
INSTALL_LOCK             = true
REVERSE_PROXY_LIMIT      = 1
REVERSE_PROXY_TRUSTED_PROXIES = *
INTERNAL_TOKEN           = <REDACTED>
PASSWORD_HASH_ALGO       = pbkdf2_hi

[service]
DISABLE_REGISTRATION         = true
REGISTER_EMAIL_CONFIRM       = true
DEFAULT_ALLOW_CREATE_ORGANIZATION = true

[lfs]
PATH = /data/git/lfs

[mailer]
ENABLED   = true
SMTP_ADDR = smtp.protonmail.ch
SMTP_PORT = 587
FROM      = <REDACTED>
USER      = <REDACTED>
PASSWD    = <REDACTED>

[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true

[oauth2]
JWT_SECRET = <REDACTED>

[actions]
ENABLED = true

Things worth knowing:

  • Backend is sqlite3 at data/gitea/gitea.db. No external Postgres/MySQL. Backups are simple: stop, copy, start.
  • OFFLINE_MODE = true — Forgejo doesn't reach out to gravatar etc. Avatars are local.
  • DISABLE_REGISTRATION = true — invite-only forge. Accounts created by admin via web UI or forgejo admin user create.
  • REVERSE_PROXY_TRUSTED_PROXIES = * with REVERSE_PROXY_LIMIT = 1 — required because Cloudflare Tunnel terminates TLS and forwards X-Forwarded-For. Forgejo trusts one proxy hop.
  • LFS enabled with storage in /data/git/lfs.
  • SMTP via Protonmail — used for password resets, notifications, registration confirmation.
  • OAuth2 + OpenID — both enabled for sign-in.
  • Forgejo Actions enabled — see runner section below.

Persistent data

  • /opt/forgejo/data/git/repositories/ — all bare repos, organised <owner>/<repo>.git
  • /opt/forgejo/data/git/lfs/ — LFS object storage
  • /opt/forgejo/data/gitea/gitea.db — sqlite (users, issues, PRs, settings)
  • /opt/forgejo/data/gitea/conf/app.ini — config (above)
  • /opt/forgejo/data/ssh/ — host SSH keys for git-over-ssh
  • /opt/forgejo/runner-data/.runner — runner registration token + URL
  • /opt/forgejo/runner-data/cache/ — actions cache

Cloning over SSH

git clone ssh://git@git.thelinuxcast.org:2222/<owner>/<repo>.git

Or in ~/.ssh/config:

Host git.thelinuxcast.org
    Port 2222
    User git

Then git clone git@git.thelinuxcast.org:<owner>/<repo>.git works.

Runner registration

The runner authenticates to Forgejo with a token issued from Forgejo's admin UI (Site Administration → Actions → Runners → Create new Runner Token).

To re-register after rotating the token or rebuilding the runner:

docker exec -it forgejo-runner sh
cd /data
forgejo-runner register \
  --no-interactive \
  --instance http://forgejo:3000 \
  --token <NEW_TOKEN> \
  --name discourse-runner \
  --labels docker:docker://node:20,ubuntu-latest:docker://node:20-bullseye

Mounting /var/run/docker.sock lets the runner spawn job containers.

Operating

cd /opt/forgejo
docker compose up -d                          # start / apply changes
docker compose logs -f forgejo                # tail logs
docker compose pull && docker compose up -d   # upgrade (forgejo is pinned, runner is :6, cloudflared is :latest)
docker compose restart forgejo                # restart only

# Admin user (run inside container)
docker exec -it forgejo forgejo admin user create --username admin --password <pw> --email admin@example.com --admin
docker exec -it forgejo forgejo admin user change-password --username <name> --password <new>

Upgrade procedure

  1. Snapshot /opt/forgejo/data/ while stopped.
  2. Bump pin in compose (e.g. 15.0.215.1.0). Read the Forgejo release notes — major versions sometimes need migrations.
  3. docker compose pull && docker compose up -d
  4. Watch first-run logs: docker compose logs -f forgejo. Migrations run automatically; if they fail, restore from the snapshot.

Backup

See Backups. Short version:

cd /opt/forgejo
docker compose stop forgejo
tar czf forgejo-$(date +%F).tar.gz data/
docker compose start forgejo

Stopping is necessary because sqlite + repos can be mid-write. The tarball is self-contained — restore is tar xzf into a fresh /opt/forgejo/ and docker compose up -d.