How to set up a self-hosted Forgejo with CI runners & pushes to your own registry
This is a quick, reproducible path from zero to running Forgejo + a runner that builds Docker images and pushes to your Forgejo registry, plus optional GitHub mirroring.
Prerequisites
- A domain pointing to your box (e.g., git.example.com)
- Docker & Compose installed
- A persistent data folder, e.g.
/path/to/your/data/forgejo
⚠️ CRITICAL WARNING ⚠️
🛑 DO NOT START THE CONTAINERS BEFORE COMPLETING ALL SETUP STEPS!
Starting the containers before proper setup will cause initialization failures and require cleanup.
Docker Compose File
# MODIFY $MYSQL_ROOT_PASSWORD, $MYSQL_PASSWORD, $MAILER_PASSWORD and required volume paths
services:
server:
image: codeberg.org/forgejo/forgejo:12
container_name: forgejo
environment:
- PUID=1000
- PGID=100
- TZ=Europe/Madrid
- GITEA__database__DB_TYPE=mysql
- GITEA__database__HOST=db:3306
- GITEA__database__NAME=forgejo
- GITEA__database__USER=dbuser
- GITEA__database__PASSWD=$MYSQL_PASSWORD
- ROOT_URL=https://git.example.com
- GITEA_CUSTOM=/data/gitea
- FORGEJO__mailer__ENABLED=true
- FORGEJO__mailer__SMTP_ADDR=example.com
- FORGEJO__mailer__SMTP_PORT=587
- FORGEJO__mailer__FROM=noreply@example.com
- FORGEJO__mailer__USER=contact@example.com
- FORGEJO__mailer__PASSWD=$MAILER_PASSWORD
- FORGEJO__mailer__MAILER_TYPE=smtp
- FORGEJO__mailer__IS_TLS_ENABLED=true
networks:
- forgejo
volumes:
- /path/to/your/data/forgejo/data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- 3002:3000
- 223:22
depends_on:
- db
restart: unless-stopped
db:
image: mysql:8
container_name: forgejo-db
environment:
- PUID=1000
- PGID=100
- TZ=Europe/Madrid
- MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
- MYSQL_USER=dbuser
- MYSQL_PASSWORD=$MYSQL_PASSWORD
- MYSQL_DATABASE=forgejo
networks:
- forgejo
volumes:
- /path/to/your/data/forgejo/mysql:/var/lib/mysql
restart: unless-stopped
docker-daemon:
image: docker:27-dind
container_name: forgejo-runner-dind
hostname: docker
privileged: true
environment:
- DOCKER_TLS_CERTDIR=/certs
- DOCKER_DRIVER=overlay2
volumes:
- /path/to/your/data/forgejo/runner-data/docker-certs-ca:/certs/ca
- /path/to/your/data/forgejo/runner-data/docker-certs-client:/certs/client
- /path/to/your/data/forgejo/runner-data/docker-data:/var/lib/docker
networks:
- forgejo
restart: unless-stopped
forgejo-runner:
image: data.forgejo.org/forgejo/runner:9
container_name: forgejo-runner
user: "1000:100"
working_dir: /data
environment:
- PUID=1000
- PGID=100
- TZ=Europe/Madrid
- DOCKER_HOST=tcp://docker:2376
- DOCKER_TLS_VERIFY=1
- DOCKER_CERT_PATH=/certs/client
- DOCKER_BUILDKIT=1
volumes:
- /path/to/your/data/forgejo/runner-data:/data:rw
- /path/to/your/data/forgejo/runner-data/docker-certs-client:/certs/client:ro
depends_on:
- docker-daemon
# Security hardening (adjusted for DinD)
security_opt:
- no-new-privileges:true
- apparmor:docker-default
cap_drop:
- ALL
cap_add:
- DAC_OVERRIDE
read_only: true
tmpfs:
- /tmp:size=500M,noexec,nosuid,nodev
- /var/tmp:size=200M,noexec,nosuid,nodev
- /var/log:size=100M,noexec,nosuid,nodev
# Use existing forgejo network for communication
networks:
- forgejo
# wait if not registered yet, so it doesn't crash-loop
command: ["/bin/sh","-c","while [ ! -f /data/.runner ] || [ ! -s /data/.runner ]; do echo 'Runner not registered yet. Please register first:'; echo 'docker exec forgejo-runner forgejo-runner register --instance http://git.example.com --token YOUR_TOKEN'; sleep 30; done; echo 'Runner registered, starting daemon...'; exec forgejo-runner daemon --config /data/config.yml"]
restart: unless-stopped
networks:
forgejo:
name: forgejo
1) Prepare directories and create config.yml file
a) Prepare data directories
mkdir -p /path/to/your/data/forgejo/data
mkdir -p /path/to/your/data/forgejo/mysql
mkdir -p /path/to/your/data/forgejo/runner-data
chown -R 1000:1000 /path/to/your/data/forgejo/data
chown -R 1000:1000 /path/to/your/data/forgejo/runner-data
b) Create config.yml file
Create a config.yml file in /path/to/your/data/forgejo/runner-data/config.yml
log:
level: info
runner:
envs:
DOCKER_HOST: tcp://docker:2376
DOCKER_CERT_PATH: /certs/client
DOCKER_TLS_VERIFY: "1"
container:
network: host
options:
-v /certs/client:/certs/client
-v /etc/ssl/certs/ca-certificates.crt:/etc/pki/tls/certs/ca-bundle.crt:ro
-v /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro
-v /usr/local/share/ca-certificates/homelab-ca.crt:/usr/local/share/ca-certificates/homelab-ca.crt:ro
valid_volumes:
- /certs/client
- /etc/pki/tls/certs/ca-bundle.crt
- /etc/ssl/certs/ca-certificates.crt
- /usr/local/share/ca-certificates/homelab-ca.crt
2) Bring up the Compose
Setup verification
Make sure you have:
- Set
ROOT_URL=https://<your domain>
- Used strong passwords for the DB
- Completed step 1 (directory setup and config.yml creation)
Start
docker compose -f compose.yml up -d
Open https://<your domain>
and finish the initial setup.
Enable Actions
Instance-wide: Admin > Actions > Runners (or per org/repo in Settings > Actions > Runners).
Click Create new runner and copy the runner token (you can also do org/repo-scoped tokens).
docker exec -it forgejo-runner /bin/bash
cd /data
forgejo-runner register
# Instance URL: https://<your domain>
# Token: <paste the token you created>
# Runner name: <anything>
# Labels: e.g. ubuntu-24.04:docker://node:22-bookworm,ubuntu-22.04:docker://node:22-bookworm,ubuntu-18.04:docker://node:20-bookworm,docker:docker://node:20-bookworm
exit
docker restart forgejo-runner
Make builds reliable
Generate a config and enable auto-pull + Docker socket automount into job containers:
docker exec -it forgejo-runner /bin/sh -lc 'forgejo-runner generate-config > /data/config.yml'
docker exec -it forgejo-runner /bin/sh -lc "sed -i 's/^ docker_host:.*/ docker_host: automount/' /data/config.yml"
docker exec -it forgejo-runner /bin/sh -lc "sed -i 's/^ force_pull:.*/ force_pull: true/' /data/config.yml"
Make sure your runner starts with that config (in compose command, run
forgejo-runner daemon --config /data/config.yml
) and that the container user can
access /var/run/docker.sock
. If you see "permission denied", add the host docker
group GID via group_add
or (as a quick test) run the container as root.
3) CI: build & push to your Forgejo registry
Create .forgejo/workflows/docker-compose.yml
in your repo:
name: Build & push to Forgejo Registry
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
# ---- change these to match your setup ----
REGISTRY: git.example.com # your Forgejo domain (no scheme)
NAMESPACE: youruser # your Forgejo user/org
IMAGE_NAME: yourimage # repo/image name
TAG: latest
# -----------------------------------------
FORGEJO_USER: ${{ secrets.FORGEJO_USER }} # username (not email)
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} # Personal Access Token (PAT) with read:package + write:package
jobs:
build:
# Use a runner label that has Docker available (common label is 'docker')
runs-on: docker
steps:
- uses: actions/checkout@v4
# Install the Docker CLI inside the job container so we can build/push
- name: Install Docker CLI (Debian)
run: |
apt-get update
apt-get install -y docker.io ca-certificates
docker info
# Build (tag with full registry path)
- name: Build
run: docker build -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG} .
# Log in (only on push; secrets aren't exposed on PRs from forks)
- name: Login to registry (push only)
if: github.event_name == 'push'
run: |
docker logout "${REGISTRY}" || true
printf '%s' "${FORGEJO_TOKEN}" | docker login "${REGISTRY}" -u "${FORGEJO_USER}" --password-stdin
# Push (skip on PRs)
- name: Push (push only)
if: github.event_name == 'push'
run: docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG}
Secrets needed in your Forgejo repo:
FORGEJO_USER
= your usernameFORGEJO_TOKEN
= Personal Access Token with read:package + write:package. If pushing to an org namespace, make sure you're a member allowed to publish.
4) Optional: mirror to GitHub
Add .forgejo/workflows/mirror-to-github.yml
:
name: Mirror to GitHub
on:
push:
branches: [ "master" ]
tags: [ "*" ] # mirror tags, too
delete:
branches: [ "*" ]
tags: [ "*" ]
env:
# ---- change this to your GitHub repo HTTPS URL (no token in it) ----
GITHUB_MIRROR_URL: https://github.com/<user>/<repo>.git
# -------------------------------------------------------------------
jobs:
mirror:
runs-on: docker
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for --mirror
# Configure Git auth header using Basic with a PAT (safer than embedding in URL)
- name: Configure Git auth header
run: |
TOKEN="${{ secrets._GITHUB_MIRROR_TOKEN }}"
B64="$(printf '%s' "x-access-token:${TOKEN}" | base64 | tr -d '\n')"
git config --global http.https://github.com/.extraheader "Authorization: Basic ${B64}"
git config --global user.email "ci@local"
git config --global user.name "forgejo-ci"
# Mirror everything (branches + tags; creates/prunes to keep remote identical)
- name: Push mirror
run: |
git remote add mirror "${GITHUB_MIRROR_URL}" || git remote set-url mirror "${GITHUB_MIRROR_URL}"
git push --mirror mirror
Secrets in Forgejo repo:
_GITHUB_MIRROR_TOKEN
= GitHub PAT with repo scope (or granular "Contents: Read/Write")
5) Troubleshooting notes (the gotchas I hit)
- Install page loop: your Forgejo container can't write
/data
or can't reach DB. Fix volume ownership (1000:1000) and DB creds. - Runner says
.runner: EOF
: you "touched" the file but didn't register. Exec into the runner,cd /data
, runforgejo-runner register
, then restart. - "cannot ping the docker daemon": the runner user can't access
/var/run/docker.sock
. Add the socket's GID viagroup_add
, or run container as root (quick test). docker: command not found
in jobs: install the Docker CLI inside the job container, and setcontainer.docker_host: automount
in the runner config.- Registry 401 (authGroup.Verify): use username + PAT (not email), token must have read/write:package; only attempt login on push events.
- Docker Hub pulls time out (regional/CDN block): pre-pull via mirror.gcr.io and retag, or route CI pulls via a VPN-namespaced Docker daemon (gluetun + dind).
6) One-liner you'll run during runner registration
docker exec -it forgejo-runner /bin/bash
# then inside:
cd /data
forgejo-runner register
# Instance: https://<your domain>
# Token: (Settings > Actions > Runners at instance/org/repo scope)
# Labels: ubuntu-24.04:docker://node:22-bookworm,ubuntu-22.04:docker://node:22-bookworm,ubuntu-18.04:docker://node:20-bookworm,docker:docker://node:20-bookworm
exit
docker restart forgejo-runner