Practical guide

JIT Runner Kit v0.3.0: Cloudflare-controlled GitHub Actions on Hetzner

AtlasRepo now runs trusted private CI/CD on a Cloudflare-controlled, scale-to-zero Hetzner pool with at most two isolated JIT runners.

JIT Runner Kit v0.3.0: Cloudflare-controlled GitHub Actions on Hetzner

AtlasRepo has moved its trusted private-repository build and deployment workloads from the original one-VM-per-job GitHub control-job flow to the Cloudflare-controlled shared-host mode in JIT Runner Kit.

The production path now uses GitHub as the workflow scheduler, a repository-scoped GitHub App and Cloudflare Worker as the runner control plane, and Hetzner as temporary compute. The controller scales from zero to one CX33 host, runs at most two disposable runner/Docker-in-Docker pairs, and deletes the host after the configured idle window.

What changed in v0.3.0

  • A Cloudflare Worker accepts signed workflow_job webhooks from selected repositories only.
  • Cloudflare Queues and a SQLite Durable Object serialize lifecycle operations, retries, leases, and cleanup.
  • The Hetzner pool adapter creates at most one temporary host for a pool and reuses it for adjacent jobs.
  • Each job still receives an isolated disposable GitHub runner container and its own Docker-in-Docker daemon. Jobs do not share a workspace or the host Docker socket.
  • The pool is capped at two concurrent runners. The steady idle state remains zero servers, Primary IPs, firewalls, SSH keys, and GitHub runner records.
  • The runner image now includes version-pinned Node.js, Docker CLI/Compose, GitHub CLI, PHP CLI, Python, ShellCheck, and native build tools. The documentation shows how to add any shell, runtime, system package, or fully custom immutable runner image.
  • Primary IPv4 readiness, provider retry classification, ambiguous create recovery, DinD plaintext readiness, non-root JIT configuration ownership, and idle cleanup were hardened with deterministic regression coverage.

The production flow

```text

trusted push or deliberate workflow_dispatch on main

-> GitHub workflow_job webhook

-> Cloudflare Worker + Queue + Durable Object

-> zero or one temporary Hetzner CX33

-> one disposable runner/DinD pair per job

-> build, test, deploy, exact-SHA verification

-> runner containers removed on job completion

-> host, Primary IPv4, and firewall removed after 600 idle seconds

```

The application workflow contains one substantial job with static jit-runner and unique jit-run-${{ github.run_id }} labels. It has no GitHub-hosted provision or cleanup job, so the migrated private workflows consume no GitHub-hosted Actions minutes. GitHub still stores workflow state and logs.

What we verified on AtlasRepo

The controller has now run real trusted release workloads for the AtlasRepo Platform, Scout, Admin, and Web repositories. These were application builds and deployments, not synthetic hello-world jobs.

  • Platform run 32940790444, job 98091187515: Docker Compose build, GitHub CLI/Zeabur deployment wait, migrations, API and cache verification completed successfully.
  • Scout run 32944184488, job 98101276988: the migrated PostgreSQL integration topology, build, deploy, and runtime checks completed successfully.
  • Scout run 32946780433, job 98109098199: the follow-up authenticated release completed successfully.
  • Admin run 32991984714, job 98251511790: quality gates, the exact-release container image, Zeabur deployment, private-edge checks, and authenticated deep-link browser verification completed successfully.
  • Web run 32986683555, job 98234483296: build, Zeabur deployment, public routes, 404 handling, analytics, and cache checks completed successfully.
  • A delayed automatic Web push run, 32987167529, reused the same temporary host instead of creating another server, then completed and cleaned its runner normally.
  • The final Platform workflow cutover run 32994477386 and Scout workflow cutover run 32994479984 arrived together after a GitHub Actions incident delay. They ran concurrently in two isolated runner/DinD pairs on one CX33, both completed successfully, and created no second server.

After the final idle window, independent provider inventory returned to zero servers, zero Primary IPs, zero firewalls, and zero SSH keys. Repository runner inventories also returned to zero.

This infrastructure result is not a claim that every AtlasRepo product-readiness gate is green. Scout still reports explicit launch blockers, including semantic coverage below its target and incomplete quality, catalogue, and alert evidence; the new CI/CD path preserves and surfaces those fail-closed states.

Lessons from the cutover

Do not turn a delayed push into a manual duplicate

GitHub recorded the Web push event before it exposed the corresponding Actions run. A manual fallback was authorized after a ten-minute observation window, but the automatic push run appeared later and caused a duplicate deployment of the same SHA.

The shared host prevented a second VM charge by reusing the existing CX33, but the workflow duplication was still incorrect. JIT Runner Kit v0.3.0 therefore documents a stricter rule: if an exact-SHA GitHub PushEvent exists, do not dispatch the same release manually just because the Actions API is temporarily empty. A timer is not a deduplication primitive. An automatic fallback needs a durable repository-and-SHA release claim.

DinD changes localhost semantics

The runner and its Docker daemon are separate containers on a per-job network. Services published inside the nested Docker daemon are not reachable from the runner at 127.0.0.1. Workflows must connect through the DinD hostname and published port, or execute the client in the same job network. We added a real PostgreSQL conformance gate for this boundary.

Runner images are part of the release contract

The first Platform migration exposed two image gaps: Docker-in-Docker had to be reachable over the exact runner-network endpoint, and the release workflow required GitHub CLI. Both are now part of the immutable runner image and verified at runtime. Projects can extend the image for Bash alternatives, PowerShell, Java, Go, Ruby, PHP extensions, browsers, or other native packages without changing the controller core.

Security boundaries

  • The GitHub App is installed on selected repositories, not the entire account.
  • Only exact branch-pinned workflow files on main and trusted push or deliberate workflow_dispatch events are allowed.
  • Pull-request code never receives Hetzner, Cloudflare, GitHub App, or production deployment credentials.
  • Checkout credentials are not persisted.
  • Controller credentials remain Worker secrets and are never copied into runner containers or Durable Object state.
  • Provider resources are ownership-labeled, independently swept, and verified after every production run.
  • A disposable container is a practical workload boundary, not a separate kernel. For hostile multi-tenant workloads, use one VM per trust boundary instead of the shared-host mode.

Cost model

The original adapter created one minimum-billed VM for every job. Shared-host mode instead creates one temporary burst host, reuses it for nearby trusted jobs, and returns to zero after ten idle minutes. This preserves job-level runner isolation while reducing minimum-hour churn during a release burst.

The main remaining cost controls are operational: consolidate pushes, keep one heavy job per workflow, avoid overlapping release triggers, cap concurrency at two, and prove scale-to-zero after the final job.

Read the v0.3.0 release notes, follow the production cutover runbook, and see custom runner image examples. Questions and provider-adapter ideas are welcome in the existing AtlasRepo forum discussion.