Docker-in-Docker Forgejo Runners. Optimized for Coolify installations.
  • Shell 98.8%
  • Dockerfile 1.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Ismo Vuorinen 1383572993 feat!: register runners through the Forgejo UI (#5)
## Summary

Runners are now registered in Forgejo's web UI instead of with `forgejo-cli actions register`.

- `entrypoint.sh` takes `FORGEJO_RUNNER_UUID` and `FORGEJO_RUNNER_TOKEN` as Forgejo issues them and writes them into `server.connections.forgejo`. The UUID must be 8-4-4-4-12 hex (lowercased before use); the token must be non-empty with no whitespace or control characters. The UUID is no longer derived from a shared secret.
- `compose.yaml` requires both variables; `FORGEJO_RUNNER_SECRET` is removed.
- Tests, `.env.example` and the README follow.
- Design spec and plan: `docs/superpowers/specs/2026-09-26-ui-runner-registration-design.md`, `docs/superpowers/plans/2026-09-26-ui-runner-registration.md`.

This branch also carries the earlier CI-gated deploy design spec (`docs/superpowers/specs/2026-09-26-ci-gated-deploys-design.md`, docs only).

## Breaking change

Before deploying, set `FORGEJO_RUNNER_UUID` and `FORGEJO_RUNNER_TOKEN` in the Coolify resource and remove `FORGEJO_RUNNER_SECRET`. A deploy without the new variables fails at compose interpolation. The config in `runner-data` is rewritten on start; no migration.

## Testing

- `shellcheck entrypoint.sh healthcheck.sh tests/*.sh`, `hadolint Dockerfile`: clean
- `sh tests/entrypoint.test.sh`: 35 checks pass
- `sh tests/compose.test.sh`: 16 checks pass

Reviewed-on: #5
2026-09-26 21:34:11 +00:00
docs/superpowers feat!: register runners through the Forgejo UI (#5) 2026-09-26 21:34:11 +00:00
tests feat!: register runners through the Forgejo UI (#5) 2026-09-26 21:34:11 +00:00
.env.example feat!: register runners through the Forgejo UI (#5) 2026-09-26 21:34:11 +00:00
compose.yaml feat!: register runners through the Forgejo UI (#5) 2026-09-26 21:34:11 +00:00
Dockerfile feat: coolify-deployable forgejo runner with dind (#1) 2026-09-25 23:31:33 +00:00
entrypoint.sh feat!: register runners through the Forgejo UI (#5) 2026-09-26 21:34:11 +00:00
healthcheck.sh feat: coolify-deployable forgejo runner with dind (#1) 2026-09-25 23:31:33 +00:00
README.md feat!: register runners through the Forgejo UI (#5) 2026-09-26 21:34:11 +00:00
renovate.json chore: Configure Renovate (#3) 2026-09-26 16:28:42 +00:00

coolify-forgejo-runners

A Forgejo Actions runner for Coolify: one runner with configurable parallelism, and a Docker-in-Docker (DinD) sidecar that gives jobs a Docker daemon without exposing the host's.

How it works

  • dind runs dockerd on tcp://0.0.0.0:2375 inside its own network namespace. The port is never published.
  • runner shares that namespace (network_mode: service:dind). On every start its entrypoint validates the environment, writes /data/config.yml with the runner UUID and token from FORGEJO_RUNNER_UUID and FORGEJO_RUNNER_TOKEN, and starts forgejo-runner daemon. Coolify's variables are the only source of truth; the file is rewritten each start.
  • Job containers run inside DinD and reach it, and the runner's cache server, at dind.docker.internal.

Concurrent jobs share one Docker daemon and can see each other's containers.

Deploying in Coolify

  1. Create a runner in Forgejo (below).
  2. In Coolify, create a resource from this repository with the Docker Compose build pack and Docker Compose location /compose.yaml. It needs no domain and no ports.
  3. Set FORGEJO_RUNNER_UUID and FORGEJO_RUNNER_TOKEN in the resource's Environment Variables, mark the token as a secret, and adjust any other variable from the table below.
  4. Deploy. dind becomes healthy first, then runner. The runner shows as online on the Forgejo runners page it was created on.

Registering a runner

Create the runner in Forgejo's web UI at the scope it should serve:

  • every repository: Site Administration → Actions → Runners;
  • one organisation or user: its Settings → Actions → Runners;
  • one repository: its Settings → Actions → Runners.

Create a new runner there and copy the UUID and token Forgejo shows into Coolify as FORGEJO_RUNNER_UUID and FORGEJO_RUNNER_TOKEN. The runner's labels come from RUNNER_LABELS.

To replace a runner, delete it in Forgejo, create a new one and update both variables; a deleted runner's UUID and token stop working.

Variables

Variable Default Purpose
FORGEJO_RUNNER_UUID required UUID Forgejo shows for the runner
FORGEJO_RUNNER_TOKEN required Token Forgejo shows for the runner
FORGEJO_URL https://git.esle.fi Forgejo instance URL
RUNNER_CAPACITY 3 Jobs run in parallel
RUNNER_FETCH_INTERVAL 2s How often the runner polls Forgejo for jobs
RUNNER_LABELS docker:docker://code.forgejo.org/oci/node:lts,ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:runner-latest Comma-separated runner labels
RUNNER_CONTAINER_OPTIONS empty Extra docker run options for job containers
CACHE_PORT 8088 Cache server port
CACHE_PROXY_PORT 8089 Cache proxy port
RUNNER_VERSION 13 Tag of code.forgejo.org/forgejo/runner the image builds from
DIND_IMAGE docker:28-dind@sha256:2a232a42256f70d78e3cc5d2b5d6b3276710a0de0596c145f627ecfae90282ac DinD image
DIND_DNS_1 185.12.64.1 First DNS resolver for containers inside DinD (Hetzner's)
DIND_DNS_2 185.12.64.2 Second DNS resolver for containers inside DinD (Hetzner's)

.env.example lists the same variables for local runs.

Health

  • dind: docker info against its own daemon.
  • runner: healthcheck.sh requires the daemon process, the cache port, the DinD port and $FORGEJO_URL/api/healthz. It prints the failing check. Docker does not restart unhealthy containers; the status shows in Coolify and docker ps.

Troubleshooting

Symptom Check
runner restarts with entrypoint: <VARIABLE> … The named variable is missing or invalid; the message states the rule
Runner never shows as online FORGEJO_RUNNER_UUID and FORGEJO_RUNNER_TOKEN belong to different runners, the runner was deleted in Forgejo, or it was created on another instance; create a new runner and update both variables
Jobs fail with Cannot connect to the Docker daemon dind is unhealthy; check its logs
Cache restore/save times out CACHE_PORT or CACHE_PROXY_PORT collides with something inside DinD's namespace
Image pulls or builds inside jobs fail to resolve hosts DIND_DNS_1/DIND_DNS_2 must be resolvers reachable from this host; Docker's embedded DNS is not usable from nested containers, and dockerd otherwise falls back to 8.8.8.8/8.8.4.4

Print the config the runner is using, with the token redacted:

docker exec <runner-container> entrypoint.sh render

Testing

shellcheck entrypoint.sh healthcheck.sh tests/*.sh
hadolint Dockerfile
sh tests/entrypoint.test.sh   # builds the image; runs containers with --network none only
sh tests/compose.test.sh

The tests need Docker with Compose v2 and Python 3 with PyYAML. Job containers reaching DinD is not covered by the tests; it is verified by a real workflow run after deployment.