docs(runner-images): stock images work; drop the "must extend the runner base" claim - #170
Merged
Conversation
…ner base" claim
The guide asserted that custom images must extend
ghcr.io/actions/actions-runner because "the base includes the runner binary
that ephemerd needs". The code does not require that, and the claim
discourages the simplest thing that works.
What the code actually does:
- pkg/runner embeds the Actions runner archive in the ephemerd binary
(runner.go:18) and extracts it onto the host (runner.go:64).
- pkg/runtime bind-mounts a per-job copy into the container at
/actions-runner (runtime.go:952, :1514) and execs
/actions-runner/run.sh (runtime.go:860, :930).
- The Windows default image is bare Server Core with no runner in it at
all (image_windows.go:9-13), so the "stock image" path is already what
every Windows job takes.
Deriving from the official image is the special case, not the rule:
isOfficialRunnerImage (runtime.go:1660) matches a short ref allowlist and
is the only thing that suppresses the mount.
Changes:
- Rewrite "How it works" and the defaults table around the two real
paths, with file:line citations.
- Add "Use a stock image" with golang:1.26.6-bookworm as the worked
example, including native arm64 from the same tag and dropping
run-time toolchain installs.
- Add "What the image actually has to provide": bash, glibc + the .NET
runner's libs (musl/Alpine does not work), coreutils, CA certs; Node
is not needed. Note the capability set and the read-only /etc mounts.
- Document the "error obtaining VCS status: exit status 128" failure and
the safe.directory "*" fix, and why pointing safe.directory at
${{ github.workspace }} cannot work.
- Note that the Windows Server Core default ships no Docker CLI.
- Fix images/runner-ci-windows' base: it is Server Core, not the
nonexistent actions-runner:latest-win.
- Fix the per-repo override example: the key is [runner.images.<repo>]
with per-OS entries, not [runner.repo_images].
…two overreaches Review follow-ups on the stock-image guide. 1. Missed requirement: the mounted runner dir must be writable by the container's uid. run.sh writes run-helper.sh into its own directory before the job starts, and that directory arrives owned by the ephemerd daemon uid. There is no Chown or Chmod in pkg/runner or pkg/runtime; extraction is MkdirAll(0o755) as the daemon uid (runner.go:79) and the per-job copy is cp -al (runtime.go:1589), so hardlinks carry the original ownership and mode through unchanged. Add a "Running as a non-root USER" subsection: root, or a uid that can write a root-owned 0755 dir. ephemerd does grant CAP_DAC_OVERRIDE for this — the capability list annotates it "write to dirs owned by other users" (runtime.go:60) — so non-root is expected to work, but it is a dependency worth naming, and the Linux example's trailing `USER runner` now says so. 2. run.sh does not exec node20. The chain is run.sh -> run-helper.sh -> bin/Runner.Listener (.NET); runsvc.sh is the service path and ephemerd does not use it. The conclusions were right, the citation was not. 3. The github.workspace explanation claimed more than the source supports. The runner does translate paths for a container step host, and part of that sits behind the server-delivered flag DistributedTask.UseContainerPathForTemplate, so "the two never agree" is wrong. Reframe as observed behavior, and fix the path: on the mounted-runner path _work is under /actions-runner, not /home/runner. Also: complete the official-ref list (@digest, docker.io/ prefix), add the global [runner] default_image rung to the resolution order (runtime.go:806-811), and scope "One Image, Every Host" to Linux jobs so it stops contradicting the Windows notes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
docs/guides/runner-images.md:37claimed "custom images must extend the upstream GitHub Actions runner base image", and line 12 asserted the same more quietly ("the runner binary lives inside the image").Both are wrong, and the false premise had spawned three further errors.
What the code does — all three mechanisms verified
pkg/runnerextracts the runner.go:embed all:embedatrunner.go:18;Manager.Extract()at:64unpacks to<data-dir>/runners/<version>-<goos>(:43). Fetched at build time by mage, SHA-pinned — never at runtime.pkg/runtimebind-mounts it in. Per-job copy atruntime.go:952;withRunnerMountat:1514mountsrbind,rwat/actions-runner(Linux) or an HCS mapped dir atC:\actions-runner(Windows). Entrypointoci.WithProcessArgs("/actions-runner/run.sh", ...)at:930.pkg/runtime/image_windows.go:9-13returns baremcr.microsoft.com/windows/servercore. Every Windows job already runs the stock-image path.The decisive detail:
customImage := image != "" && !isOfficialRunnerImage(image)(runtime.go:805). Deriving from the official base is the special case that suppresses the mount. Stock is the normal path.Errors that followed from the false premise
ghcr.io/actions/actions-runner:latest-win, which does not exist. The doc had invented a Windows runner base because it believed one was required. The real base ismcr.microsoft.com/windows/servercore:ltsc2025.[runner.repo_images], not in the schema. Real shape is[runner.images.<repo>]withlinux/windowskeys (pkg/config/config.go:1357-1365).images/link pointed at a feature branch.Real constraints, documented rather than hand-waved
Nothing runner-related, but genuine ones exist. Linux:
/bin/bash(the mountedrun.shis#!/bin/bash,execved directly), and a glibc userland withlibkrb5-3,zlib1g,liblttng-ust,libssl,libicu. musl/Alpine does not work — which also explains why the runner's bundlednode20will not run there. Node.js is not needed. NoUSER/WORKDIRis set andRUNNER_ALLOW_RUNASROOT=1, so root-by-default stock images are fine. Capability set excludesCAP_SYS_ADMIN/CAP_NET_ADMIN;/etc/hostsand/etc/resolv.confare mounted read-only over whatever the image ships. Windows needscmd.exe(Server Core, not Nano).Added
golang:1.26.6-bookwormas a worked example — resolves arm64 natively from the same tag, and lets a project delete an entire run-time C-toolchain step.Plus the gotcha:
go buildinside a container fails witherror obtaining VCS status: exit status 128, and${{ github.workspace }}is the host path so marking it safe does not work.git config --global --add safe.directory "*"is what works — documented with why the obvious fix fails (_work→/__wremap).Also notes that the servercore default ships no Docker CLI (
image_windows.go:12vsimages/runner-ci-windows/Dockerfile:86-89), so nodockerin steps, no dind, nocontainer:.