Runner groups
Runs execute on runner groups that you operate. A runner group is a long-lived coordinator that brings ephemeral runners online — one short-lived worker per run, which clones the repo, runs OpenTofu, uploads logs and the plan, then disappears. GantryCD keeps no standing fleet and no persistent agent holding access to your cloud.
Two kinds of group
| Type | Where runners run | Launcher |
|---|---|---|
self-hosted (pull-based) | A process you run | Local, Docker, Kubernetes |
github-actions (backend-dispatched) | Inside your GitHub Actions | GitHub Actions |
Both speak the same protocol; they differ only in who plays the runner and how it
starts. You create either in Org settings → Runner groups. When you create a
self-hosted group, GantryCD shows its private key once — save it and point
RUNNER_PRIVATE_KEY_FILE at it.
| Launcher | Runs each runner as | Use when |
|---|---|---|
| Local | A child process | Dev, or a dedicated host with git/bash/OpenTofu. |
| Docker | A container | You want per-run isolation on a single host. |
| Kubernetes | A Pod | You’re already on a cluster; scales horizontally (Helm chart). |
| GitHub Actions | A dispatched workflow | You’d rather not host runner infra at all. |
Routing work with labels
A runner group advertises capability labels (region=eu-west, arch=arm64).
A stack’s runner selector — its gantrycd:runner-group:* labels — must be a subset
of a group’s labels for that group to be eligible. This is how you steer stacks to
the right pool.
Stack selector: region=eu-west, network=private
Group A labels: region=eu-west, network=private, arch=amd64 ✅ eligible
Group B labels: region=us-east, network=private ❌ no match
A stack with no selector labels uses the org’s default runner group — an admin picks it from the Default group control at the top of the Runners page. If none is set, such runs may go to any group.
Routing vs. per-stack overrides
Labels do two distinct jobs — don’t confuse them:
gantrycd:runner-group:*routes a stack to a group (the table above). Use it for genuinely different fleets: a separate image by default, a different region/arch, an isolated cloud account. Each fleet is its own runner group.gantrycd:runner-spec:*tunes the pod a chosen group launches for that stack’s runs — currently the Kubernetes image and CPU/memory request/limit. Use it to size individual stacks within one pool without standing up a near-identical second group.
Reach for routing when the difference is operational (where/what fleet); reach for overrides when it’s just sizing within a fleet. Image-by-default is routing; image-for-one-stack is an override (and gated — see Kubernetes).
Concurrency and health
- Each group caps how many runs it handles at once (
MAX_CONCURRENTfor self-hosted,max_concurrent_runsfor github-actions). - Self-hosted groups heartbeat so the UI shows them connected; GitHub Actions groups show connected while GantryCD’s dispatcher is healthy.
- If a group claims a run but can’t bring a runner online, that one run is failed and the queue moves on — a clear error rather than a stuck stack.
Upgrading: groups first, then the backend
Upgrade your runner groups before, or together with, the backend. A released
backend refuses to hand work to a group that does not report a gantrycd version,
and versions of the group older than v0.0.2 do not send one — the header did
not exist yet. Upgrade the backend first and every self-hosted, docker,
kubernetes, and standalone github group stops receiving work until you catch them
up.
The symptom is deliberately quiet on the group’s side and loud on the backend’s:
- The group keeps heartbeating, so it is reachable — the UI shows it as unversioned rather than connected or disconnected, with an empty version and a note on the group page explaining that its runs stay queued.
- Its polls return 403, so it is assigned nothing and runs stay
pending. - The backend logs
refusing work to an unversioned runner groupon every poll.
Both Helm charts take the version from Chart.appVersion and are published in
lockstep with each release, so helm upgrade on the runner-group chart is all
this needs — just do it no later than the backend.
Groups of type github-actions are unaffected: the backend dispatches them
itself, so there is no separate process to upgrade. They have their own upgrade
step instead — the workflow template changed, see
GitHub-Actions runner groups.
How the runner’s version is chosen
A runner group pins the ephemeral runner it starts to a gantrycd version, so the two agree on the wire contract. How strong that guarantee is depends on the launcher:
| Launcher | How the runner is pinned | Can it drift? |
|---|---|---|
github-actions | The group sends its own version as a workflow input, and the workflow downloads that release. | No — same build by construction. |
kubernetes, docker | The runner image tag. The Helm chart defaults ephemeralRunner.image.tag to Chart.appVersion, matching the group. | Yes — see below. |
local | Whatever the configured EXEC command runs. | Yes — it is entirely yours. |
For kubernetes and docker there are two ways the runner can end up on a
different version from its group, and both are things you opt into:
- Pinning
ephemeralRunner.image.taginstead of leaving it empty. The default tracksChart.appVersion, so an explicit tag is a decision to manage the version yourself. - A per-stack image override — the
gantrycd:runner-spec:kubernetes_imagestack label, which replaces the image for that stack’s runs. It is ignored unless the group setsKUBERNETES_ALLOW_STACK_IMAGE_OVERRIDE=true(a requested-but-disabled override is dropped with a warning), because the Pod runs under the group’s ServiceAccount. Enabling it hands the version choice — and keeping it compatible — to whoever writes stack labels.
Where drift is possible it is also invisible: runners do not report their version to the backend, only groups do. Neither path is wrong, but both move the guarantee from gantrycd to you: leave the tag empty and the override disabled unless you want that, and keep the version in step with the group if you do.
Internals
The dispatch engine, ephemeral-runner JWT signing, and the runner protocol are in GitHub-Actions runner groups and Runner Polling. Every launcher’s environment is in Configuration.