Files
apf_portal/infra
Julien Gautier f5f0cd6369
CI / check (pull_request) Failing after 4s
CI / commits (pull_request) Failing after 4s
CI / scan (pull_request) Failing after 7s
CI / a11y (pull_request) Failing after 2s
CI / perf (pull_request) Failing after 4s
fix(ci): pin act_runner job image to catthehacker/ubuntu:act-22.04
Without an image suffix on the runner labels, act_runner uses a
minimal default container image that has no Node. Every JavaScript
action (actions/checkout, actions/setup-node, nrwl/nx-set-shas, etc.)
crashes during the action's launch step with `Cannot find: node in
PATH`, blocking every CI gate before any project code runs.

The fix is in the runner registration labels: act_runner accepts the
format `<label>:docker://<image>`, which makes act spawn jobs in the
specified container. catthehacker/ubuntu:act-22.04 is the de facto
standard image for act-compatible runners; it bundles Node, Python,
git, common build tools, and the Docker CLI, matching what GitHub
Actions / Gitea Actions workflows generally assume.

infra/ci-runners.compose.yml: GITEA_RUNNER_LABELS updated for the
three runners, with an inline comment explaining the format trap.

docs/decisions/0015-cicd-gitea-actions.md (§Runners): amend the
runner-image baseline. Previous wording said "a Debian image"
without specifying which; now states catthehacker/ubuntu:act-22.04
explicitly and explains the docker:// suffix requirement. Status
remains 'accepted' - the runner-image choice is implementation detail
under the existing decision.

Already-running runners need their labels updated in the Gitea UI
(Site Administration -> Actions -> Runners -> edit each runner) to
the new format. Compose change applies on next re-registration.
Documented as the operational follow-up.
2026-05-04 10:53:23 +02:00
..

infra/

Infrastructure-as-code artefacts for the project. Separate from application code and from documentation: this folder contains the recipes and configs that the team and ops use to stand up running infrastructure (CI runners, future local-dev databases, future on-prem deploy assets).

Subject File / Folder ADR / Reference
Self-hosted CI runners (Gitea Actions) ci-runners.compose.yml ADR-0015 §"Runners"
Runtime state of the runners data/ (git-ignored after .gitignore)
Env-vars template for the runners .env.example (.env is git-ignored)

Future folders / files that will land here as the corresponding ADRs ship:

  • local/ — Docker Compose for the developer's machine (Postgres + Redis + OTel collector). Triggered by the first feature that needs Redis (sessions, ADR-0010).
  • prod/ — On-prem deploy manifests (HA Postgres, Redis Sentinel, OTel collector + backend, secret manager). Triggered by the on-prem infrastructure ADR (phase 3b).

CI runners — ci-runners.compose.yml

Three self-hosted act_runner instances, registered with the project's Gitea organisation, labelled self-hosted + on-prem (the labels referenced by every job in .gitea/workflows/*). Three matches the floor recommended by ADR-0015 §"Runners" — one runner is enough to validate the pipeline; two leave no slack; three keep CI flowing if one runner is down for upgrade or maintenance.

First-time registration

cd infra/

# 1. Generate a registration token in Gitea.
#    Site Administration → Actions → Runners → "Create new Runner"
#    (or, for org-scoped runners: Organisation Settings → Actions → Runners).
#    The token is one-time and short-lived; don't lose it.

# 2. Configure .env (which is git-ignored).
cp .env.example .env
$EDITOR .env
#    Set GITEA_INSTANCE_URL (https, no trailing slash) and
#    GITEA_RUNNER_REGISTRATION_TOKEN.

# 3. Bring the runners up.
docker compose -f ci-runners.compose.yml up -d

# 4. Verify in Gitea: the three runners appear as online with the
#    self-hosted, on-prem labels. If a runner doesn't come online,
#    inspect its logs:
docker compose -f ci-runners.compose.yml logs runner-1

After the first successful boot, each runner stores its credentials under data/runner-N/.runner. The registration token is no longer needed and should be removed from .env. Subsequent restarts (docker compose restart) authenticate from the persisted credential.

Operational tips

  • Rotation of one runner at a time — to upgrade the image or change config, restart runners one by one (docker compose restart runner-1, wait, runner-2, …) so the CI pipeline is never starved.
  • Logsdocker compose logs -f --tail=100 runner-N for a single runner; jobs being executed appear here.
  • Disk pressure — the runner caches each job's container image in /var/lib/docker on the host. On a small host, prune periodically (docker system prune -af while no job is running).
  • Adding a fourth runner — copy any runner-N block in the compose file, increment the suffix in container_name, GITEA_RUNNER_NAME, and the data/ mount path. Then docker compose up -d. The runner registers using the same GITEA_RUNNER_REGISTRATION_TOKEN (which must be regenerated if it has expired).

Security — Docker socket exposure

The compose mounts /var/run/docker.sock into each runner so jobs can spawn containers. This grants the runner root-equivalent access to the host's Docker daemon. A malicious workflow could spawn arbitrary containers, mount host paths, escalate privileges. Mitigations:

  • Trust boundary: only register the runners against repositories controlled by the org. Gitea's runner-registration UI lets you scope a runner to an organisation, a single repository, or instance-wide. Prefer the narrowest scope.
  • Dedicated host: run these containers on a host that does not also run production services or hold sensitive data. The runner host is in the trust boundary of any developer who can push to a repo it serves.
  • No host filesystem mounts beyond the docker socket: the compose intentionally does not mount /, /etc, or any project source. Workflows that need data on the host must do so via Docker volumes.
  • Future hardening (out of scope of v1): migrate to rootless Docker on the runner host, or to a DinD (Docker-in-Docker) sidecar so the runner cannot escape into the host daemon. Decided when the org's RSSI confirms the security posture, or when the runner host is shared with anything else of value.

Image pinning

The compose pins gitea/act_runner:0.2.13. Update the pin deliberately, not via :latest:

  1. Read the act_runner release notes for breaking changes.
  2. Edit the three image references (runner-1, runner-2, runner-3).
  3. Commit on a feature branch with a chore(deps): Conventional Commits subject.
  4. Roll one runner at a time (rotation tip above).

The matching CI workflows refer to runner labels (not images), so a runner-image upgrade does not affect .gitea/workflows/*.


Future infra concerns — placeholders

These are listed here so a contributor knows where to expect related files; they don't exist yet.

File Purpose Triggered by
local/dev.compose.yml Postgres 17 + Redis (Sentinel-flavoured single-node) + OTel collector for local dev First feature that needs Redis or end-to-end OTel traces
local/init/postgres/*.sql Bootstrap SQL to create the Postgres roles audit_owner / audit_writer / audit_reader / audit_archiver (per ADR-0013) and provision the dev DB Same as above
prod/* On-prem deployment manifests (k8s, Compose, or whatever the on-prem infra ADR settles on) The on-prem infrastructure ADR (phase 3b)
runbooks/*.md Operational runbooks (incident response, secret rotation, runner upgrade procedure, …) First incident, or when ops cadence justifies them