Skip to content

    Back to articles

    5 min read

    Treat custom runner images as build dependencies

    An image is part of the build

    A runner image decides what software is present before a workflow starts. That makes it more than a convenience layer: it is an input to the build, alongside the workflow, actions and project dependencies. I would version and review it accordingly.

    GitHub announced custom images for GitHub-hosted runners in public preview in October 2025, describing image lifecycle controls and the ability to use a custom image in later workflows (GitHub’s custom-images announcement). The capability reached general availability in May 2026, making the image lifecycle a production design decision rather than a preview experiment. GitHub has since added support for building custom images from other custom images, a pattern that can separate a shared foundation from team-specific additions (GitHub’s custom-image layering update).

    The architectural question is not simply which tools belong in an image. It is who owns that image, how a change is tested, and how the repositories using it move to a new version. If many repositories share one runner foundation, an image change is a shared dependency update. Treating it casually makes the blast radius shared too.

    Put stable tools in the image

    I would put stable, broadly useful tools and configuration in a custom image: the runtimes and system packages common to a class of builds, required certificates, or organization-wide configuration. The image should provide a dependable starting point, not try to encode every project’s exact build environment.

    Fast-changing items belong elsewhere. Pin workflow actions separately, and pin project dependencies through the project’s own dependency-management and lock files. That division makes it easier to identify what changed when a build breaks. If an image update also silently changes a project library or an action version, the runner becomes a catch-all bundle with poorly defined ownership.

    For example, a workflow can select a runner backed by the organization’s image while keeping its action references visible in the workflow:

    name: Build
    
    on:
      push:
    
    permissions:
      contents: read
    
    jobs:
      build:
        runs-on: org-linux-build
        steps:
          - uses: actions/checkout@<reviewed-commit-sha>
          - name: Build
            run: dotnet build --configuration Release

    The runner name is illustrative; configure it to match the larger runner or runner group using the custom image. Replace the action placeholder with the reviewed commit SHA before using this example. The important boundary is that the image supplies the stable host environment, while the workflow states which action it invokes and the project controls its own dependencies.

    This separation is not a claim that every tool should be installed at workflow time. Repeated setup can add delay and variation. It is a choice to keep the image’s responsibilities narrow enough that an image update can be reviewed as an image update, rather than as an opaque change to the whole build.

    Version, test and roll out deliberately

    An image needs an identity that a workflow can refer to and a history that an owner can review. I would build it from a declared definition, publish a new version when its contents change, and avoid silently replacing the contents behind a name that workflows already trust. The exact mechanism depends on the runner setup; the principle is to make the change and its adoption visible.

    I would test a candidate image with representative workflows before making it the default for every repository. That means checking not only whether the image builds, but whether the kinds of jobs that depend on it still work: compilation, tests, packaging and any required system-level setup. Where repositories have different needs, I would validate against more than one representative rather than treating one successful build as universal proof.

    Rollout should also have an explicit path back. A repository that fails after an image update needs to be able to identify which image version it used and, where practical, return to a known-good version while the problem is investigated. That does not mean keeping every old image indefinitely. It means deciding how long versions remain available and who can authorize a rollback, instead of discovering those questions during an incident.

    The operational cost is real. Someone must update base software, rebuild and validate images, communicate changes, and retire versions that should no longer be used. A custom image can improve consistency and reduce repeated setup, but it does not remove maintenance. It moves some maintenance from individual workflows into a shared artifact with a larger potential blast radius.

    Shared foundations need shared ownership

    The case for a custom image is strongest when many repositories need the same runner foundation. Reusing one image can avoid each workflow independently assembling a slightly different environment. But every shared dependency needs an owner. Without one, repositories can keep depending on an image that nobody is patching, or a well-intended update can break consumers that had no part in the change.

    I would assign ownership for the image definition and its release process, then make consumers’ migration visible. The image owner is responsible for producing and validating versions; repository owners remain responsible for their workflows and project dependencies. That division avoids the assumption that whoever owns a shared runner automatically owns every build running on it.

    Layering can help where teams share a common base but need different tools. GitHub’s update on building custom images from other custom images describes this kind of shared-foundation pattern (GitHub’s image-layering announcement). I would use it only when the ownership boundary is clear: the base layer has a maintainer and release policy, and each derived image has an owner for its additional contents. Otherwise, layers can obscure where a tool came from and which team should update it.

    My default is therefore straightforward: use a custom runner image when there is a stable environment worth sharing, keep volatile dependencies pinned in their own places, and treat every image revision as a build-input change that needs review and a rollout plan. The speed and consistency are useful; the patching responsibility is the price of getting them.

    Drafted with AI assistance, reviewed and edited by Ali.