- Shell 98.8%
- Dockerfile 1.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
## 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 |
||
| docs/superpowers | ||
| tests | ||
| .env.example | ||
| compose.yaml | ||
| Dockerfile | ||
| entrypoint.sh | ||
| healthcheck.sh | ||
| README.md | ||
| renovate.json | ||
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
dindrunsdockerdontcp://0.0.0.0:2375inside its own network namespace. The port is never published.runnershares that namespace (network_mode: service:dind). On every start its entrypoint validates the environment, writes/data/config.ymlwith the runner UUID and token fromFORGEJO_RUNNER_UUIDandFORGEJO_RUNNER_TOKEN, and startsforgejo-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
- Create a runner in Forgejo (below).
- 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. - Set
FORGEJO_RUNNER_UUIDandFORGEJO_RUNNER_TOKENin the resource's Environment Variables, mark the token as a secret, and adjust any other variable from the table below. - Deploy.
dindbecomes healthy first, thenrunner. 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 infoagainst its own daemon.runner:healthcheck.shrequires 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 anddocker 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.