Run on Hetzner Cloud.
PreviewPut the controller on any persistent Docker host. It starts a clean Hetzner server for each job, verifies its deny-inbound firewall, and deletes it afterward.
01 · Isolate
Create a dedicated Hetzner project that cannot reach production. Install hcloud, authenticate to that project, and create a worker firewall with no inbound rules.
hcloud firewall create --name runneryard-workers
hcloud firewall describe runneryard-workers -o json | jq .idReady whenThe project is dedicated to CI and the worker firewall contains zero inbound rules.
02 · Scaffold
npx runneryard init \
--provider hetzner \
--region fsn1 \
--github https://github.com/acme/widgetsThe generated Compose setup keeps durable state on the controller host and mounts the GitHub App key read-only.
Ready whenThe generated diff contains Compose, an environment template, ignores for secret files, and the canary.
03 · Connect GitHub
Run this on the controller checkout. GitHub opens in your browser; the verified one-time App key is written directly to mode-0600 ignored files. No browser secret is copied into a terminal.
npx runneryard auth github create \
--github https://github.com/acme/widgets \
--sink fileReady when.runneryard/github-app.pem and github-app.env are private and ignored.
04 · Configure compute
Create .runneryard/controller.env on the controller host. Add only a read/write token from the dedicated Hetzner project and the firewall ID. Keep the file mode 0600.
cp .runneryard/controller.env.example .runneryard/controller.env
chmod 600 .runneryard/controller.env
# Set on the controller host only:
# HCLOUD_TOKEN, RUNNER_HETZNER_FIREWALL_IDReady whenThe controller files are private; no token or PEM is committed.
05 · Verify and start
npx runneryard doctor --provider hetzner --firewall-id 123456
docker compose -f .runneryard/hetzner.controller.compose.yml \
run --rm controller budget init \
--file /var/lib/runneryard/budget.json
docker compose -f .runneryard/hetzner.controller.compose.yml up -dReady whenDoctor passes and the private status receipt reports a healthy controller.
06 · Canary and route
gh workflow run runneryard-canary.yml --repo acme/widgets
docker compose -f .runneryard/hetzner.controller.compose.yml \
exec controller /usr/local/bin/runneryard status
hcloud server list --selector runneryard-managed-by=true
npx runneryard route enable \
--github https://github.com/acme/widgets \
--label acme-linux --confirm-canaryThe canary must prove the Node toolcache, root-filesystem-aware Docker storage, and a real BuildKit build and container run. Preview means the adapter has seam coverage but still needs a public release canary in a real Hetzner project. Use only trusted, low-risk workloads until then. Size capacity and budget, then read the complete Hetzner guide.
Upgrade
Disable the route and stop the old controller. Pin the same release in both generated files: RUNNER_IMAGE in controller.env for workers and image in Compose for the controller. Set the canary's expected version to match. Preserve durable state, start one controller, then prove the controller with status and the worker with the canary.
Outboard
Disable the route, stop the controller, delete owned servers, and revoke the project token. Delete a dedicated GitHub App. For a shared BYO App, remove this controller's local credential; change installation scope only after confirming no other consumer needs it. Then delete the dedicated project.
npx runneryard route disable --github https://github.com/acme/widgets