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 orforgejo admin user create.REVERSE_PROXY_TRUSTED_PROXIES = *withREVERSE_PROXY_LIMIT = 1— required because Cloudflare Tunnel terminates TLS and forwardsX-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
- Snapshot
/opt/forgejo/data/while stopped. - Bump pin in compose (e.g.
15.0.2→15.1.0). Read the Forgejo release notes — major versions sometimes need migrations. docker compose pull && docker compose up -d- 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.
Navigation
Quick Links
Projects
Server (discourse)
- Host
- Cloudflare Tunnel
- Tailscale
- SFTP
- Discourse
- Forgejo
- Filedrop
- Filebrowser
- Landing Sites
- Onboarding
- Backups
- Recovery
Window Managers
Homelab
Docker
Software
Thoughts
Archived
Connect
Butterbian — my Debian 13 distro · butterrepo — community APT repo · butterknife — TUI installer
justaguylinux.com · Butterforge · Butter Lab · The Churn · YouTube · Mastodon · Links · Contact · Support on Buy Me a Coffee
"Not an expert. Just A Guy." — Licensed CC BY-SA 4.0 · © 2026 JustAGuy Linux