Skip to content

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.

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-playwright

See Caching for the full list of pre-wired ecosystem caches and how to point your own tools at the volume.

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.

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 as labels: in compose), then run a final step:

    Terminal window
    docker ps -aq --filter "label=run-id=$GITHUB_RUN_ID" | xargs -r docker rm -f

    This catches what docker compose down missed.