Writing Workflows for Runaway Runners
Runaway runners aren’t identical to GitHub-hosted ones (ubuntu-latest). Most workflows run
unchanged, but a few patterns that work on a hosted runner behave differently here.
Routing jobs to a scale set
Section titled “Routing jobs to a scale set”Each scale set registers its runners with one or more labels. Match
those labels in your workflow’s runs-on to route a job to that pool. self-hosted is always
implied, so include it alongside the scale set’s label:
jobs: build: runs-on: [self-hosted, my-label] steps: - run: echo "running on Runaway"With one scale set, runs-on: self-hosted is enough. With several, give each a distinct label and
match it so GitHub’s dispatcher sends each job to the right pool.
Skip GitHub’s Actions Cache for tool binaries
Section titled “Skip GitHub’s Actions Cache for tool binaries”Every scale set has a persistent /cache volume mounted into every runner, so pnpm stores,
Playwright browsers, apt downloads, and ecosystem caches survive across runs locally. Actions like
actions/cache@v4 and actions/setup-node@v4 with cache: pnpm tar and zstd-compress the path and
round-trip it through GitHub’s Cache service every run — slower than reading the local volume.
Point your tools at /cache instead:
# No `cache: pnpm` — the store already lives at /cache/pnpm.- uses: actions/setup-node@v4 with: node-version: 22
# Browser binaries on the persistent volume; `playwright install`# no-ops on warm runs.env: PLAYWRIGHT_BROWSERS_PATH: /cache/ms-playwrightSee Caching for the full list of pre-wired ecosystem caches and how to point your own tools at the volume.
The runner is itself a container
Section titled “The runner is itself a container”Applies when the scale set mounts the Docker socket into its runners (the “Mount Docker socket inside runners” option on Scale sets).
When a job runs docker compose up or docker run -p, the new containers spawn as siblings on
the host’s Docker daemon, not as children of the runner. The runner’s localhost:PORT is its own
loopback, so it can’t reach ports published on the host. Two ways to deal with it:
-
Connect the runner to the new stack’s network and address services by name:
Terminal window docker network connect <stack>_default $(hostname)# then talk to http://my-service:3000 -
Put both sides on a shared Docker network from the start by setting
networks:on both the runner side and the services in your compose file.
Hygiene for jobs that spawn containers
Section titled “Hygiene for jobs that spawn containers”Containers your job creates show up on the host’s Docker daemon, not nested inside the runner. Keep them tidy so concurrent runs on the same host don’t collide:
-
Use unique names per run. Don’t use a fixed
container_name:. Pass--project-name "$GITHUB_RUN_ID"(or a UUID) and let compose’s default<project>-<service>-<index>naming keep two jobs on one host from clashing. -
Stamp a sweep label and clean up at the end. Add
--label run-id="$GITHUB_RUN_ID"(or aslabels:in compose), then run a final step:Terminal window docker ps -aq --filter "label=run-id=$GITHUB_RUN_ID" | xargs -r docker rm -fThis catches what
docker compose downmissed.