
Setup Forgejo Runner for Codeberg
This stack runs a Forgejo Actions runner (data.forgejo.org/forgejo/runner:12) together with a docker:dind sidecar, so job containers get their own isolated Docker daemon (DOCKER_HOST=tcp://docker-in-docker:2375).
How it’s wired up
compose.yml— two services:docker-in-docker: privilegeddocker:dind, exposes the Docker API ontcp://0.0.0.0:2375(no TLS, only reachable from inside the compose network).runner: the Forgejo runner daemon, runs as uid/gid1001:1001, mounts./datato/data, and starts withforgejo-runner daemon --config runner-config.yml.data/runner-config.yml— the runner’s config file. Registration is done declaratively in this file underserver.connections, not via the olderforgejo-runner registerinteractive command. Each entry underconnectionsis one Forgejo/Codeberg instance (or scope) this runner logs into.data/is git-ignored (see.gitignore) because it holds the registration token — never commit it.
Setup the Runner Service
To setup the runner you can use this compose.yml file:
services:
docker-in-docker:
image: docker:dind
container_name: 'docker_dind'
privileged: 'true'
command: ['dockerd', '-H', 'tcp://0.0.0.0:2375', '--tls=false']
restart: 'unless-stopped'
runner:
image: 'data.forgejo.org/forgejo/runner:12'
links:
- docker-in-docker
depends_on:
docker-in-docker:
condition: service_started
container_name: 'runner'
environment:
DOCKER_HOST: tcp://docker-in-docker:2375
# User without root privileges, but with access to `./data`.
user: 1001:1001
volumes:
- ./data:/data
restart: 'unless-stopped'
command: 'forgejo-runner daemon --config runner-config.yml'
Registering the runner with Codeberg
1. Enable Actions on the repo (or org)
Actions are off by default on Codeberg. For a single repository:
- Go to the repo → Settings → Units/Overview.
- Enable Actions.
(For an organization-wide runner, this isn’t needed per-repo — see scope note below.)
2. Get a registration token from Codeberg
Runners can be registered at four levels, and a repo also picks up runners registered at any broader level that owns it:
| Scope | Where |
|---|---|
| Instance-wide (admin only) | /admin/actions/runners |
| Organization | /org/<org>/settings/actions/runners |
| User | /user/settings/actions/runners |
| Single repository | /<owner>/<repo>/settings/actions/runners |
Pick the narrowest scope that covers what you need. Steps:
- Open the relevant Actions → Runners page.
- Click Create new runner.
- Give it a name/description.
- Codeberg shows a UUID and a Token (shown once — copy both immediately).
3. Add the connection to data/runner-config.yml
Under server.connections, add or edit an entry:
server:
connections:
codeberg: # arbitrary name for this connection
url: https://codeberg.org/
uuid: <uuid-from-step-2>
token: <token-from-step-2>
4. Restart the runner
docker compose up -d --force-recreate runner
On startup the runner reads server.connections, registers/authenticates each one, and starts polling for jobs. Registration state per connection is tracked internally — you don’t run a separate register step.
5. Verify
- Check logs:
docker compose logs -f runner— look for a successful connection message per instance. - On Codeberg, the runner should show as online on the same Actions → Runners page used to create it.
- Push a commit with a workflow under
.forgejo/workflows/in the target repo and confirm a job gets picked up.
Labels / job containers
runner.labels in runner-config.yml maps a label used in a workflow’s runs-on: to how the job container is built, e.g.:
labels:
- "ubuntu-latest:docker://node:lts-bookworm"
- "ubuntu-22.04:docker://node:22-bookworm"
Since the runner talks to the docker-in-docker sidecar (via DOCKER_HOST), job containers are spun up there rather than on the host Docker daemon.
Rotating/revoking access
If a token in data/runner-config.yml is ever exposed, revoke it immediately from the same Actions → Runners page on Codeberg (delete the runner entry) and generate a fresh one, then repeat step 3.