Skip to content
Ali Akbari
Menu

ADR-0010 · accepted · 9 October 2026

CI and deploys run on a self-hosted runner on the production host

Context

The owner registered a self-hosted GitHub Actions runner (label portfolio) on the production VPS, running as deployer, which is in the docker group. The pipeline had been written for disposable GitHub-hosted machines: it started the full stack under the production Compose project name, published test ports, installed packages with apt, and reached the host over SSH to deploy.

Options

  • Keep GitHub-hosted runners. Strongest isolation, but uses hosted minutes and ignores the owner's choice.
  • Move the jobs unchanged. Would have replaced the live containers on every test run and opened test ports to the internet.
  • Adapt the pipeline to share the host safely.

Decision

CI, evidence sync, CodeQL and the deploy run on the self-hosted runner, adapted to the shared host:

  • Language jobs run inside pinned containers as the runner's own user; Playwright, Lighthouse and pdftotext run in pinned images. Nothing needs apt or sudo on the host.
  • The CI stack uses its own Compose project (aliakbari-ci) and binds to 127.0.0.1; the deploy-script test uses another (aliakbari-deploytest). deploy.sh takes the project name and bind address from the environment, with production defaults unchanged.
  • CI images are named ci.local/akynte/*; production pruning only touches ghcr.io/akynte/* release tags, and CI cleanup only removes its own ci.local tags.
  • Images are scanned from docker save tarballs, so the scanner never gets the Docker socket.
  • The deploy job calls /srv/aliakbari/deploy.sh directly. The SSH deploy key and its secrets were removed.
  • The uptime probe and the post-deploy verification stay on GitHub-hosted machines: a host cannot report its own outage or see itself from the internet.
  • Pull requests run only when they come from this repository.

Consequences

  • Trust boundary: any workflow on the runner acts with deployer's rights, and Docker access is equivalent to root. Code in pull requests from this repository (including Dependabot updates) is built and tested on the production host. Most of it runs inside containers without the Docker socket, but repository scripts that orchestrate the stack run on the host. Only people with write access can trigger this.
  • Resources: builds and browser tests share 4 CPUs and 8 GB RAM with the live site; production containers keep their CPU and memory limits, but response times can rise while CI runs.
  • Availability: if the host is down, CI and deploys stop too (the uptime probe still reports it).
  • Disk: the build cache is capped with docker buildx prune --max-used-space 12gb after each stack job.

Source: docs/adr/0010-self-hosted-runner-on-production-host.md

← All decisions