Skip to content

Naming and platforms

Zoomies has a grammar for every name an operator reads and writes, and the name says what the thing is:

zoomies-4vcpu-ubuntu-2404                      a pool
zoomies-16vcpu-32gb-ubuntu-2404-tuck           the host running it
zoomies-4vcpu-ubuntu-2404-biscuit-a3f9qz2m     one of the pool's runners

The reason is narrow and practical. runs-on: linux-x64 tells a workflow author nothing about how much machine they are asking for, and tells whoever is reading a slow build even less. A name that carries the size and the platform answers both at a glance, and a pool's name is copied into every workflow file that uses it, so it is read far more often than it is written.

Runner names matter for a different reason: they are the one thing Zoomies puts in somebody else's GitHub account. GitHub shows a runner's name in the runner list, in a job's header, and in the "Set up job" step of every log, three places a reader arrives at knowing nothing, and often the place they are looking because something landed somewhere surprising. The name answers them there.

The grammar

zoomies-<vcpu>vcpu[-<memory>gb]-<os>-<version>[-<arch>][-<suffix>]
Part Example Notes
zoomies Always. It is how you tell Zoomies' runners and containers from everything else on a host.
<vcpu>vcpu 4vcpu A pool's per-runner share, or a host's total. A fraction rounds up.
<memory>gb 32gb Only when there is a memory limit to state. A host name carries what the machine has; a pool name carries what one of its runners gets, and only when that is not the fleet's default; a figure every pool shares says nothing about any of them, which is the same reason amd64 is left out.
<os>-<version> ubuntu-2404 The distribution and its release, with the dots removed. ubuntu-2404, debian-12, fedora-42, rocky-9.
<arch> arm64 Omitted for amd64, which is the default. Spelled out for everything else.
<suffix> tuck, biscuit-a3f9qz2m What tells two things of the same shape apart: a host's machine name, or a runner's kennel word and token. Pools have none: a pool is its shape.

Every part after the prefix is optional and left out when it is not known. A host that will not say what distribution it runs gets a name without one, rather than a name that claims something nobody established.

Any name is still legal. Nothing rejects gpu-builders, and a fleet that has always called its pool linux-x64 keeps working exactly as it did. The grammar is what Zoomies suggests, what zoomies init prints, and what an unnamed host is given -- not a rule it enforces.

The name is capped at 64 characters, because a pool's name is what its labels are built from and GitHub rejects a runner name longer than that.

Every pool name carries the brand whether or not it is in the grammar: a name saved as gpu is stored as zoomies-gpu. That is internal/store/brand.go, and it is about the label a workflow in somebody else's repository has to write, not about the grammar here.

Runner names

A runner is named after the pool it belongs to, the host it was placed on, and a token that tells it from its siblings, zoomies-4vcpu-ubuntu-2404-fido-a3f9qz2m:

Part Example What it is for
brand zoomies The only sign that a registration on GitHub is this fleet's.
shape 4vcpu-ubuntu-2404 The pool's, in the grammar above.
host fido The machine the runner was placed on.
token a3f9qz2m Eight random characters, which is what makes the name unique.

The host is there because a failed job is first looked at on GitHub, in the runner list, the job header or the "Set up job" line of the log, and GitHub knows a runner by its name and nothing else. With the host in the name, "this job ran on fido" needs no lookup in the controller, so a disk that filled on one machine shows up as one machine's name repeating in the failures. It is the part of the host's name after its shape (zoomies-8vcpu-ubuntu-2404-fido contributes fido, a host an operator named build-box-7 contributes all of it), cut a word at a time to 20 characters.

A host with no name the grammar allows, and a runner named before any host is known, take a kennel word instead, one of thirty-two cocker spaniel names, the same list the pool editor offers. Runners created before this change keep the kennel word they were given.

The token is eight random characters, and it is what actually guarantees uniqueness. GitHub requires a runner's name to be unique within the target, and two runners on one host share everything else.

A pool that has no shape to report (no resources, no platform) lends its own name instead, so a pool created in the UI gets zoomies-biscuit-docker-linux-truffle-a3f9qz2m rather than a runner called nothing in particular.

When a name will not fit in 64 characters, the shape is what gives way, one whole segment at a time. Neither the brand nor the token may be spent: without the brand, the reaper cannot tell this registration from one somebody made by hand; without the token, two runners truncated to the same name are one registration fighting itself. Segments go whole rather than mid-word, because ...ubuntu-24 claims a release that does not exist, where a name one segment shorter only says less.

Because a runner's name is in the grammar, anything holding one can read it back into the pool's shape, which is what makes it worth the characters rather than just longer.

Platforms

A pool's platform is the machine its runners need: a distribution, a release, and an architecture. It does two things.

It picks the runner image. A pool that names ubuntu 24.04 boots ghcr.io/eyupio/zoomies-runner:ubuntu-2404 without anyone keeping the pool and the image in step by hand. Naming an image explicitly still wins -- an operator who has built their own is not second-guessed.

It restricts placement. The scheduler will not put an Ubuntu 24.04 runner on a Debian host, or an arm64 runner on an amd64 one. Before this existed, a pool said which backend it wanted and nothing about the machine underneath, so the only thing that noticed a mismatch was the job that failed on it.

Every field is optional, and an empty field is a promise nobody made:

  • A pool that names no platform goes anywhere, which is what every pool created before platforms existed does.
  • A host that has not reported its distribution -- an agent older than this feature -- is not ruled out by one. Adding a platform to a pool narrows where its runners can go; it never widens it, and it never strands a host.

When no host matches, the Overview says so in those words, and names the machine to add:

cannot scale zoomies-4vcpu-ubuntu-2404-arm64 0 -> 2: no host can take a new
docker runner (1 not Ubuntu 24.04, arm64); add a Ubuntu 24.04, arm64 host, or
change the pool's platform to one you have

The runner image

ghcr.io/eyupio/zoomies-runner is published with one tag per operating system, and the tag is the same <os>-<version> that appears in a pool name.

Tag Base Architectures zoomies-runner-full
ubuntu-2404 ubuntu:24.04@sha256:224a1869083a311ef3f13648a154ba79832fbef6364d31493642ca03082da254 amd64, arm64 yes
ubuntu-2604 ubuntu:26.04@sha256:da6fc2be547864451aa253836dd926da33623312df4a9a243e35dc877c378a78 amd64, arm64 yes
ubuntu-2204 ubuntu:22.04@sha256:829f6df217bcbae2b371026e81711d1a787c61b2967ad09d015063663ebafbf7 amd64, arm64 yes
debian-13 debian:13-slim@sha256:a99cfc517144bc59b1978475ec53b46ecabec7e43635402ee5b77cc54cd1b20a amd64, arm64 -
debian-12 debian:12-slim@sha256:88200866dfff7ea7f5cbcb6ec7c8a701889efe6fe859fe64d6990e4b07ea4171 amd64, arm64 -
fedora-42 fedora:42@sha256:99e203b80b1c3d8f7e161ec10a68fd02b081ef83a3963553e513c82846b97814 amd64, arm64 -
rocky-9 rockylinux/rockylinux:9@sha256:8101994123cf3d0a8fee517bee7f39e555c7d92bd2d9eb3303cc988a0eeed00f amd64, arm64 -

:latest points at ubuntu-2404, which is what a pool that names no platform gets. Each release also publishes <tag>-<version> for pinning one operating system without pinning the controller.

Every variant is built for both architectures, and each architecture on a machine of its own kind: the amd64 images on an x86-64 builder, the arm64 images on an arm64 one, and the two merged into a single multi-architecture tag, so a host pulls the same reference whatever it runs on. The arm64 images used to be cross-built under QEMU, whose aarch64 emulation segfaults inside ldconfig on Ubuntu 22.04's glibc; that is why 22.04 was once amd64 only, and why nothing is emulated now.

Every variant is published twice: as zoomies-runner and as zoomies-runner-docker, the same image plus a Docker CLI, which a pool is switched to when its docker_mode gives jobs a daemon. The Ubuntu variants are published a third time, as zoomies-runner-full: the Docker image plus the language toolchains the setup-* actions install, which a pool opts into by naming it; the table above says which. All three are targets of one deploy/Dockerfile.runner.

That file takes the base image and the package family (apt or dnf) as build arguments; everything a distribution names differently lives in one deploy/runner-*.sh script per concern -- the baseline tools, the build toolchain, the GitHub CLI, the Docker client. actions/runner's own .NET dependencies are left to the tarball's installdependencies.sh, which already knows every distribution's package names for them; naming them by hand is what made this image Ubuntu 24.04 and nothing else.

The apt variants share one script more: every package installation goes through deploy/runner-apt.sh, which fetches the index again and retries when a download fails. The Ubuntu and Debian archives are each several machines behind one name, and a package the index has just started listing can 404 on whichever of them the build happens to reach; a mirror mid-sync, not a missing package, and not a reason to lose a whole image build. The dnf variants need no equivalent: dnf reads a metalink, knows about several mirrors at once and fails over between them itself.

Adding an operating system, or swapping one for its next release, is a row in internal/naming/images.go and then make generate, which rewrites the table above, the Makefile's build variants and both workflows' matrices from it. Go tests fail, naming that command, if any of them is edited by hand instead.

There is no Alpine variant. actions/runner ships glibc binaries and .NET dependencies that musl does not satisfy, so an Alpine image would build and then fail at the first job.

Baking in a toolchain

The image already carries what a build usually reaches for (a compiler, the headers native extensions link against, python3, node, git-lfs, shellcheck and the GitHub CLI) because a runner missing cc is a workflow that fails in the middle of somebody's afternoon with an error about a missing compiler rather than about this image.

When a whole fleet needs something beyond that, EXTRA_PACKAGES bakes it in without forking the Dockerfile:

docker build -f deploy/Dockerfile.runner \
  --build-arg BASE=ubuntu:24.04 --build-arg OS_FAMILY=apt \
  --build-arg OS_ID=ubuntu --build-arg OS_VERSION=24.04 \
  --build-arg EXTRA_PACKAGES="ruby-dev libpq-dev" \
  -t ghcr.io/acme/our-runner:ubuntu-2404 .

The packages are named for the variant's own package manager, so an apt variant wants libpq-dev and a dnf one libpq-devel. They are installed in the image's last layer, so changing the list costs one package install rather than rebuilding the toolchain above it, and an unknown name fails the build, which is a much better place to find out than a workflow.

Then point a pool at it with --image, which overrides whatever its platform would have selected.

Building locally

make image-runner                          # the default variant
make image-runner RUNNER_VARIANT=debian-12 # one other
make images-runner                         # all of them

What a name is built from

Pools. zoomies init suggests a first pool named for the host it just set up, dividing the machine among the runners it will hold:

zoomies pools create --name zoomies-4vcpu-ubuntu-2404 \
  --labels linux,x64,zoomies,zoomies-4vcpu-ubuntu-2404 --backend docker --max 4 \
  --installation ins_... --cpus 4 --os ubuntu --os-version 24.04 --arch amd64

The pool advertises its canonical name as a label, the zoomies brand label every pool answers to, and the kernel and architecture labels actions/runner advertises anyway. Declaring the last two is what makes the scheduler refuse an x64 job on an arm64 pool.

The Pools page's editor fills in the same name, following the answers as they are given: choose Debian 12 and it says zoomies-debian-12, ask for four CPUs and it says zoomies-4vcpu-debian-12. It used to lead with a kennel word instead, which left an operator who met both the pool editor and zoomies init with two conventions for one thing.

A new pool opens at the fleet's default size (runners.default_cpus, runners.default_memory_mb), and that size is left out of the name: every pool here gets it, so saying it would tell a workflow author nothing. Ask for more than the usual and the name says so.

The spaniel is still there, for the two cases the shape cannot cover on its own: a pool created before anything about it is known (the first one, before a host has connected) is zoomies-biscuit, and a second pool of a shape the fleet already has becomes zoomies-4vcpu-ubuntu-2404-truffle rather than colliding. The dice ask for one outright, for an operator who wants a handle whether or not the name needs one.

Any name is still legal, and the platform rather than the name is what picks the image and restricts placement.

Hosts. An agent that joins without --name is named for what it is: zoomies-16vcpu-32gb-ubuntu-2404-build01. The size is what the agent may actually use, which is the cgroup's share when the agent runs in a container -- so the controller in a two-core container does not claim the host's sixty-four.

A host's name is otherwise free text (Build Box 1 and ip-10-0-31-44.eu-west-1.compute.internal are both fine) with three limits: at most 128 characters, no control characters or line breaks, and no backtick. The last one is there because Zoomies quotes commands in backticks and the UI draws each pair as a command with a copy button, so a name carrying one would put text of the agent's choosing in front of every administrator as something to copy and run. The controller refuses a join under such a name before it redeems the join token, so the same token works once the name is fixed, and zoomies agent join says so before it contacts the controller at all. Renaming a host, in the UI, with zoomies hosts edit --name or over MCP, is held to the same rule.

A host enrolled before the rule existed keeps working. It may join again under the name it already has, a sentence that names it shows a backtick as an apostrophe and a control character as a space, and renaming it to a name the rule accepts puts it right everywhere.

Runners. The pool's shape, a kennel word and a token, as above. The zoomies- prefix is also how the uninstaller and the agent's orphan sweep know which registrations and containers are Zoomies' to reap, so it must not appear in front of anything else.