GitHub Actions lets custom runner images stack on other custom images
Maya Okonkwo
Custom images for GitHub-hosted runners can now be built on top of other custom images, per the June 18 changelog. For a platform team running a fleet of hosted runners, that single change converts image management from a flat collection of one-off recipes into a layered chain — a base image, a shared middle layer, a team-specific tip — that mirrors how container images have been organised for years.
The changelog itself is narrow. It says custom runner images are gaining new capabilities that give teams more flexibility over how they compose and manage image-generation pipelines, and the headline capability is stacking custom images on other custom images. Beyond that, the post does not enumerate flags, image tags or pipeline syntax, so the operational specifics still live in the GitHub Actions docs rather than the announcement.
The mechanic
A custom runner image used to be a leaf node. You built it once from whatever base GitHub published, shipped the resulting image, and that artefact was what a workflow booted. After this change, the leaf can itself become a base for another custom image. The chain the platform now allows is therefore something like vendor base → shared org image → team image, with each layer expressing only the deltas above the one below it.
Why this matters at fleet scale
For a single project, layering is a curiosity. For a platform team that owns the runner image catalogue, it is the difference between "every team duplicates the same hardening recipe" and "the hardening lives in one image that everyone inherits." The familiar argument for layered images applies one level up the stack: a shared layer is built once, audited once and revoked once. Drift between teams shrinks. When a CVE lands in something the base layer installs, you rebuild that layer and everything downstream inherits the patch on its next rebuild, rather than each team chasing the fix separately.
Using the layered chain
The changelog does not publish the exact build-time syntax for declaring a parent custom image, so the practical recipe belongs to the docs rather than this post. The shape, though, is familiar from container workflows — a build that references a parent image, a registry the runner image system pulls from, and a tag scheme that promotes layers up the chain. Two habits port over from container image management without much editing:
- Pin parent images to immutable references, not floating tags. A team image that points at
<org>/base-runner:latestwill produce silently different fleets across rebuilds. - Treat each layer as a release artefact. Tag, sign and changelog it the way you would a base container image; the fact that it boots a CI runner rather than an application does not change the supply-chain footprint.
The catch on a 3am call
Layering simplifies governance and complicates the diff. When a workflow starts failing on a fresh runner, the question "what changed in the runner image" used to terminate at one Dockerfile. With three layers in the chain, the change might sit in any of them — and the team that owns the tip image may not own the middle layer that introduced the regression. The mitigation is unsurprising: lock the parent reference per release, keep build provenance attached to each layer, and rehearse the rollback on the tip image before the rollback is the only thing standing between you and a stalled merge queue.
There is a registry-availability tail to this, too. A flat custom image is one fetch on cold start; a three-deep chain is potentially three, with whatever the runtime's pull behaviour does on a partial failure. Worth pinning down against the docs once a fleet starts composing more than two layers.
Continuity caveat
Image stacking is a build-time convenience, not a guarantee about what reaches a runner at boot. The same content-trust questions a platform team would ask about a container base image — who signed it, how is it scanned, when is it rebuilt — now apply layer-for-layer to a runner image that has a parent. The changelog moves the mechanism. The policy that decides which parent images are allowed to sit underneath a production runner is still the operator's to write.
Source: GitHub Changelog (github.blog)