Skip to content

feat(ci): isolated parallel runner instances and bounded CI cleanup - #11

Open
Chalvin96 wants to merge 6 commits into
masterfrom
feat/isolated-ci-runners
Open

Chalvin96 wants to merge 6 commits into
masterfrom
feat/isolated-ci-runners

Conversation

@Chalvin96

@Chalvin96 Chalvin96 commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

Isolated parallel runners. A repository can run N runners (instances: N on its github_runner_repos entry, kept in the private vault config; default 1). Each instance gets its own account, rootless Docker daemon, image store and network namespace, and the runner joins that namespace, so fixed ports, compose projects and registry logins cannot collide between concurrent jobs. The runner process stays unprivileged; CAP_SYS_ADMIN is only used once to join the namespace and is dropped before the runner starts.

  • CI daemons disable RootlessKit port forwarding and detached netns, so --network host and published ports resolve inside the runner's namespace.
  • Each daemon re-points /var/run/docker.sock to its own socket (job containers mount it; the host compatibility symlink belongs to one account).
  • The restart handler only restarts daemons whose config changed and restarts the runners that must rejoin the new namespace.
  • The shared actions-runner@.service template is replaced by one unit per instance. Instance 1 keeps the original account and unit name.

Bounded CI cleanup. Replaces unconditional image prune --all, the 2 GB build-cache cap and the "total /srv > 25 GB wipes workspaces and caches" rule with dangling-image pruning, a 5 GB per-builder BuildKit budget and 14-day age-based retention, per instance. Freeze, active-job skip, flock, thaw and metrics are unchanged and now cover every instance.

Notes for reviewers / operators

  • Per-instance CPU/memory caps are docker_ci_cpu_quota / docker_ci_memory_max (now 6G each). Budget total memory for the number of instances.
  • Layers and build cache are per daemon, so each instance stores its own images; plan disk accordingly.
  • If instance homes live on separate storage, mount them before running the role (it creates the homes).
  • Applying the change restarts the CI daemons and runners: apply when no jobs are running.
  • migrate-rootless.yml now loops over instances.

Review follow-ups

  • Runner readiness probe now runs with its capabilities cleared (found in Codex review).
  • Cleanup deletes inside each daemon's user namespace as the runner user instead of as root, closing a symlink race.
  • Cleanup starts from / so per-user find can restore its working directory.

Simplification

  • Removed the finished rootless migration playbook and its tests/docs.
  • Kept only behavioural tests (CI cleanup, Penpot backup, Proxmox health, nft firewall and isolation) and run them all in CI; dropped static YAML-shape and stub-only tests.
  • Service accounts and deploy are password-locked (key-only).

Testing

  • tests/test-ci-cleanup.sh covers multiple accounts (per-account Docker calls, busy account skips all, retention per instance).
  • Applied to the live CI host and exercised with real workflow runs: concurrent runners each in their own namespace, container jobs and host-network E2E pass, no OOM kills observed.

🤖 Generated with Claude Code

Chalvin96 and others added 6 commits October 27, 2026 13:13
Runners
- A repo can run N runners (`instances: N` in the private config). Every
  instance gets its own account, rootless Docker daemon, image store and
  network namespace; the runner unit joins its daemon's namespace through
  `actions-runner-exec`, so fixed ports, compose projects and registry logins
  can no longer collide between concurrent jobs. The runner still runs as an
  unprivileged user; CAP_SYS_ADMIN is only used to join the namespace.
- CI daemons disable RootlessKit port forwarding and detached netns so
  `--network host` and published ports resolve inside the runner's namespace.
- Each daemon re-points /var/run/docker.sock to its own socket so job
  containers use their own runner's daemon.
- Docker restart handler only restarts daemons whose config changed and
  restarts the runners that must rejoin the new namespace.

Cleanup
- Replace unconditional `image prune --all`, the 2 GB build cache cap and the
  "/srv > 25 GB wipes workspaces and caches" rule with dangling-image pruning,
  a 5 GB per-builder BuildKit budget and 14-day age-based retention, per
  instance. Existing protections (freeze, active-job skip, flock, thaw,
  metrics) are unchanged and now cover every instance.

Docs and tests updated; the cleanup test covers multiple accounts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ide the daemon's user namespace

Review findings:
- The runner unit's `docker info` readiness probe inherited the ambient
  CAP_SYS_ADMIN, so a CLI plugin planted in the runner home by a previous job
  could keep it. The probe now runs with ambient/inheritable capabilities
  cleared; only the namespace-entry step has them.
- ci-cleanup removed workspaces and caches as root, so a surviving job
  container could swap a path for a symlink between `find` and `rm`. Deletion
  now runs as the runner user inside its daemon's user namespace: it can still
  remove files owned by container sub-UIDs but cannot touch anything outside
  that account's data (verified: a symlink to /etc is refused).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ing directory

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…service accounts

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant