Skip to content

Configuration

Paste stack.env.example into Portainer’s Advanced mode environment box and fill it in. Five values are required; the rest have defaults.

The repository this runner serves — https://github.com/ORG/REPO.

With RUNNER_SCOPE=org, the organisation URL instead. Read Security first: an org runner accepts jobs from every repository in the organisation, including ones somebody adds later.

How you recognise it under Settings → Actions → Runners. Anything; something that names the host and the purpose ages well, like ci-box-e2e.

Must be unique across the runners of that repository or organisation.

What a workflow asks for:

runs-on: [self-hosted, linux, x64, your-label]

self-hosted, linux and x64 are added by the runner itself; this is the specific one.

Always set something specific. With only self-hosted, any job in scope lands here — including jobs this host cannot serve.

The registration token, from Settings → Actions → Runners → New self-hosted runner. Valid one hour, single-use; the runner holds its own credentials afterwards.

If the deploy fails with a registration error, the token expired. Fetch a fresh one and set it again — nothing else is lost.

Optional, and an alternative to RUNNER_TOKEN rather than a companion to it: a personal access token with repo scope (or admin:org for an org runner), which the image uses to fetch a registration token whenever it needs one. That saves refreshing a short-lived token by hand; the trade is a credential that lives as long as you let it.

Fill one or the other. If both are set, this one wins.

Only read when RUNNER_SCOPE is org or enterprise. Leave it empty for a repository runner.

The runner’s work directory, as an absolute host path. Create it first:

Terminal window
sudo mkdir -p /srv/e2e-ci/_work
sudo chown 1000:1000 /srv/e2e-ci/_work

Two things about it:

  • It must be unique per stack on the same host. Two runners sharing a work root overwrite each other’s checkouts.
  • It appears three times in the compose file — as this variable and as both sides of the volume — and all three have to stay identical. See the identical-paths rule. Get it wrong and Docker mounts empty directories without erroring.

repo (default) or org. See REPO_URL and Security.

Default myoung34/github-runner:2.338.0-ubuntu-noble.

Pin it — a runner that pulls a new image mid-job loses the job. Pick the OS your workflows were written against: ubuntu-noble is Ubuntu 24.04, which is what GitHub’s ubuntu-latest resolved to before the Ubuntu 26 migration beginning 19 October 2026. Pinning is also what stops your runner following that migration silently.

Default empty, which means off.

Empty keeps the container between jobs, so pulled images, package stores and browser downloads stay warm. The cost is that state leaks from one job to the next.

On gives a clean container per job. Correct for a public repository or one that takes fork pull requests, at the price of re-downloading everything each run.

Default 24h. How much to keep: images, build cache and stopped containers younger than this survive.

Do not set it very low. Keeping a day’s worth of images is what makes a self-hosted runner faster than a hosted one; pruning aggressively means every job re-downloads what the last one used.

Default 3600.

Default false, and think before changing it.

true runs docker volume prune -af, which on a shared host destroys other stacks’ data — a volume whose container has been removed counts as unused. Only sensible on a host that runs nothing but runners.

Default docker:29-cli. The image the prune service runs; it needs nothing but the Docker CLI.

cleanup.sh in the repository runs on the host, reports by default and changes nothing until you name a scope:

Terminal window
./cleanup.sh # what is using disk, and what each flag would do
./cleanup.sh --containers --images # the usual reset
./cleanup.sh --work /srv/e2e-ci/_work # clear a work root, keeping _tool and _actions
KEEP=24h ./cleanup.sh --all # spare anything younger than 24h