Forgejo Actions runner with dind
A CI runner whose job containers reach Forgejo at the same address the runner does, from inside a nested Docker daemon.
Names and addresses are placeholders. The list says what each one stands for.
What it does here
The runner connects out to Forgejo and runs each job in a container on its own Docker daemon (docker-in-docker, or dind). This card turns on one rule: the runner and its job containers must reach Forgejo at the same address, and that address has to work from inside dind.
Canonical setup
Get a registration token from Site Administration, then Actions, then Runners, then Create new runner. Register the runner with it, then run the runner's daemon next to a dind service that executes the jobs.
Upstream: Forgejo Actions: installation with Docker.
In this build
- Registered against the public URL
https://git.dev.tailnet.example, nothttp://forgejo:3000. Job containers live on dind's own daemon, where the nameforgejomeans nothing. - Runs as root
user: "0:0", because the data folder is root-owned. - An explicit commandThe image has no entrypoint, so
daemonalone doesn't run and no command just prints help. - The dind service is named dockerIts TLS certificate is issued for that name.
- DNS set twice for dindA
dns:block for its own image pulls, and a mounteddaemon.jsonfor the job containers it starts. - One firewall rule for the hairpinContainers reach Caddy on 443; see the UFW card.
mkdir -p /srv/stacks/forgejo-runner && cd /srv/stacks/forgejo-runner
printf '{ "dns": ["1.1.1.1", "1.0.0.1"] }\n' > dind-daemon.jsonRegister on one line, as root, with the token pasted in place of PASTE_TOKEN (no angle brackets). Each attempt needs a fresh token; a failed registration uses it up.
sudo rm -f /srv/data/forgejo-runner/data/.runner
docker run --rm -it --user root -v /srv/data/forgejo-runner/data:/data code.forgejo.org/forgejo/runner:6 forgejo-runner register --no-interactive --instance https://git.dev.tailnet.example --token PASTE_TOKEN --name devbox-runner --labels docker:docker://node:24-bookwormservices:
runner:
image: code.forgejo.org/forgejo/runner:6
container_name: forgejo-runner
user: "0:0"
working_dir: /data
command: ["forgejo-runner", "daemon"]
restart: unless-stopped
environment:
- DOCKER_HOST=tcp://docker:2376
- DOCKER_CERT_PATH=/certs/client
- DOCKER_TLS_VERIFY=1
volumes:
- /srv/data/forgejo-runner/data:/data
- dind-certs:/certs
depends_on: [docker]
networks: [runner]
docker:
image: docker:dind
container_name: forgejo-runner-dind
privileged: true
restart: unless-stopped
environment:
- DOCKER_TLS_CERTDIR=/certs
dns:
- 1.1.1.1
- 1.0.0.1
volumes:
- dind-certs:/certs
- /srv/stacks/forgejo-runner/dind-daemon.json:/etc/docker/daemon.json:ro
networks: [runner]
networks:
runner:
volumes:
dind-certs:Seams
Where this meets the rest of the build. Each one leads with what you will actually see.
SymptomCould not resolve host: forgejo
The runner was registered with the internal address. It works for the runner's own API calls, then checkout fails inside the job. Re-register with https://git.dev.tailnet.example.
Symptomx509: certificate is valid for docker
dind issues its certificate for the hostname docker. A service with any other name fails verification. Name the service docker.
Symptoma shell error before registration starts
< is a shell redirect, so a token pasted as <TOKEN> breaks the command. Paste the bare token.
Symptomcontainer name already in use
Renaming a service that has an explicit container_name needs docker compose down first. up --force-recreate doesn't free the old name.
Symptomsomething can't be resolved, and it isn't clear which daemon is asking
docker pull fails inside jobs: fix dind's dns: block or daemon.json. checkout can't resolve forgejo: re-register with the public URL. code.forgejo.org fails in the runner's own log: fix the host's daemon.json (see the Docker card).
Verify
docker compose up -d && docker compose logs -f shows the runner connecting, with no help text and no restart loop. Forgejo lists it as online under Actions, then Runners. Online only means it connected; a green run is the real proof.
on: [push]
jobs:
test:
runs-on: docker
steps:
- uses: actions/checkout@v4
- run: echo ok- 17 Oct 2026First written.