Skip to content
GantryCD

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

TypeWhere runners runLauncher
self-hosted (pull-based)A process you runLocal, Docker, Kubernetes
github-actions (backend-dispatched)Inside your GitHub ActionsGitHub 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.

LauncherRuns each runner asUse when
LocalA child processDev, or a dedicated host with git/bash/OpenTofu.
DockerA containerYou want per-run isolation on a single host.
KubernetesA PodYou’re already on a cluster; scales horizontally (Helm chart).
GitHub ActionsA dispatched workflowYou’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_CONCURRENT for self-hosted, max_concurrent_runs for 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 group on 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:

LauncherHow the runner is pinnedCan it drift?
github-actionsThe group sends its own version as a workflow input, and the workflow downloads that release.No — same build by construction.
kubernetes, dockerThe runner image tag. The Helm chart defaults ephemeralRunner.image.tag to Chart.appVersion, matching the group.Yes — see below.
localWhatever 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:

  1. Pinning ephemeralRunner.image.tag instead of leaving it empty. The default tracks Chart.appVersion, so an explicit tag is a decision to manage the version yourself.
  2. A per-stack image override — the gantrycd:runner-spec:kubernetes_image stack label, which replaces the image for that stack’s runs. It is ignored unless the group sets KUBERNETES_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.