← Providers

Run on Hetzner Cloud.

Preview

Put 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.

Terminal
hcloud firewall create --name runneryard-workers
hcloud firewall describe runneryard-workers -o json | jq .id

Ready whenThe project is dedicated to CI and the worker firewall contains zero inbound rules.

02 · Scaffold

Terminal
npx runneryard init \
  --provider hetzner \
  --region fsn1 \
  --github https://github.com/acme/widgets

The 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.

Terminal
npx runneryard auth github create \
  --github https://github.com/acme/widgets \
  --sink file

Ready 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.

Terminal
cp .runneryard/controller.env.example .runneryard/controller.env
chmod 600 .runneryard/controller.env

# Set on the controller host only:
# HCLOUD_TOKEN, RUNNER_HETZNER_FIREWALL_ID

Ready whenThe controller files are private; no token or PEM is committed.

05 · Verify and start

Terminal
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 -d

Ready whenDoctor passes and the private status receipt reports a healthy controller.

06 · Canary and route

Terminal
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-canary

The 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.

Terminal
npx runneryard route disable --github https://github.com/acme/widgets