feat: coolify-deployable forgejo runner with dind #1

Merged
ivuorinen merged 3 commits from feat/runner into main 2026-09-25 23:31:34 +00:00
Owner

Adds a Forgejo Actions runner that Coolify deploys from this repository as a Docker Compose resource: one runner with configurable parallelism and a Docker-in-Docker sidecar, every value set through Coolify environment variables.

Design: docs/superpowers/specs/2026-09-26-forgejo-runner-design.md. Plan: docs/superpowers/plans/2026-09-26-forgejo-runner.md.

What's in it

  • compose.yaml: dind (privileged docker:28-dind, pinned by digest, TCP 2375 inside its own network namespace, never published) and runner (shares dind's namespace, no host Docker socket, waits for dind to be healthy).
  • Dockerfile: builds on code.forgejo.org/forgejo/runner:${RUNNER_VERSION} and adds two scripts.
  • entrypoint.sh: validates the variables, derives the runner UUID from FORGEJO_RUNNER_SECRET (the forgejo-cli actions register scheme), writes /data/config.yml with mode 600 on every start, then runs forgejo-runner daemon. entrypoint.sh render prints the config with the token redacted.
  • healthcheck.sh: checks the daemon process, the cache port, the DinD port and $FORGEJO_URL/api/healthz, and names the check that failed.
  • .env.example and README.md: the variables, Coolify setup, registering a secret, troubleshooting.

Only FORGEJO_RUNNER_SECRET is required. The compose file uses ${FORGEJO_RUNNER_SECRET:?…}, so a deploy without it fails immediately.

Verification

  • sh tests/entrypoint.test.sh: 30/30. It builds the image and checks the rendered config (defaults, overrides, escaping), every validation error, that the secret never appears in any output, and that the healthcheck reports a missing daemon. It also starts the real daemon, which loads the config and fails pinging the configured DinD address. A malformed config fails this check instead. Containers run with --network none only.
  • sh tests/compose.test.sh: 12/12. The secret is required, and the defaults, overrides and dind/runner wiring come out right.
  • shellcheck and hadolint: clean.
  • The README variable table, compose.yaml and .env.example agree on all 10 variables.

Not covered by the tests

Job containers reaching DinD for Docker and the cache at dind.docker.internal only get exercised by the first real workflow run after deployment. Testing that locally would mean starting privileged containers on the production host.

Coolify 4.3.23 skips injecting networks: for a service with network_mode: its serviceParser checks for it. So network_mode: service:dind should deploy, but that is confirmed only by the first deploy.

Cutover (after merge)

Task 4 of the plan: reuse runner 1's registered secret, stop the old forgejo-runner service before deploying this one, then delete the runner-2 and runner-3 registrations and the old volumes once a job has run.

Adds a Forgejo Actions runner that Coolify deploys from this repository as a Docker Compose resource: one runner with configurable parallelism and a Docker-in-Docker sidecar, every value set through Coolify environment variables. Design: `docs/superpowers/specs/2026-09-26-forgejo-runner-design.md`. Plan: `docs/superpowers/plans/2026-09-26-forgejo-runner.md`. ## What's in it - `compose.yaml`: `dind` (privileged `docker:28-dind`, pinned by digest, TCP 2375 inside its own network namespace, never published) and `runner` (shares `dind`'s namespace, no host Docker socket, waits for `dind` to be healthy). - `Dockerfile`: builds on `code.forgejo.org/forgejo/runner:${RUNNER_VERSION}` and adds two scripts. - `entrypoint.sh`: validates the variables, derives the runner UUID from `FORGEJO_RUNNER_SECRET` (the `forgejo-cli actions register` scheme), writes `/data/config.yml` with mode 600 on every start, then runs `forgejo-runner daemon`. `entrypoint.sh render` prints the config with the token redacted. - `healthcheck.sh`: checks the daemon process, the cache port, the DinD port and `$FORGEJO_URL/api/healthz`, and names the check that failed. - `.env.example` and `README.md`: the variables, Coolify setup, registering a secret, troubleshooting. Only `FORGEJO_RUNNER_SECRET` is required. The compose file uses `${FORGEJO_RUNNER_SECRET:?…}`, so a deploy without it fails immediately. ## Verification - `sh tests/entrypoint.test.sh`: 30/30. It builds the image and checks the rendered config (defaults, overrides, escaping), every validation error, that the secret never appears in any output, and that the healthcheck reports a missing daemon. It also starts the real daemon, which loads the config and fails pinging the configured DinD address. A malformed config fails this check instead. Containers run with `--network none` only. - `sh tests/compose.test.sh`: 12/12. The secret is required, and the defaults, overrides and `dind`/`runner` wiring come out right. - `shellcheck` and `hadolint`: clean. - The README variable table, `compose.yaml` and `.env.example` agree on all 10 variables. ## Not covered by the tests Job containers reaching DinD for Docker and the cache at `dind.docker.internal` only get exercised by the first real workflow run after deployment. Testing that locally would mean starting privileged containers on the production host. Coolify 4.3.23 skips injecting `networks:` for a service with `network_mode`: its `serviceParser` checks for it. So `network_mode: service:dind` should deploy, but that is confirmed only by the first deploy. ## Cutover (after merge) Task 4 of the plan: reuse runner 1's registered secret, stop the old `forgejo-runner` service before deploying this one, then delete the runner-2 and runner-3 registrations and the old volumes once a job has run.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
ivuorinen/coolify-forgejo-runners!1
No description provided.