Troubleshooting
Most problems here share a trait: the symptom does not name the cause. This page is ordered by what you see, not by what is wrong.
The container restarts in a loop, with a 404 on “Obtaining the token”
Section titled “The container restarts in a loop, with a 404 on “Obtaining the token””Obtaining the token of the runnercurl: (22) The requested URL returned error: 404...Invalid configuration provided for token. Terminating unattended configuration.The registration token is in the wrong variable. It belongs in
RUNNER_TOKEN. In RUNNER_PAT it is sent as a personal access token — the
image uses that to call the API for a registration token — and GitHub answers
404 for a credential it does not accept. The message points at a URL; the
problem is a variable.
If the same log also contains ORG_NAME required for org runners, read on.
”ORG_NAME required for org runners”, and then it carries on
Section titled “”ORG_NAME required for org runners”, and then it carries on”Exactly what it says, except for the “and then it carries on”: the image’s exit
runs in a subshell, so the script continues with an empty organisation and
requests /orgs//actions/runners/registration-token. That is a second 404,
unrelated to the first, from the same log.
Either set RUNNER_SCOPE=repo (with REPO_URL), or set RUNNER_ORG_NAME
alongside RUNNER_SCOPE=org.
If you did set RUNNER_SCOPE=repo and still see this, check what Portainer
actually passed. Its environment editor parses every line as KEY=VALUE, and a
comment line containing an = can arrive as a variable of its own — which is
why the paste block in stack.env.example has no comments in it.
”Ephemeral option is enabled” when you set it to false
Section titled “”Ephemeral option is enabled” when you set it to false”RUNNER_EPHEMERAL=false turns ephemeral mode on. The image tests the
variable for presence, not truth, so any non-empty value enables it — false
included.
Off means empty: RUNNER_EPHEMERAL=
The runner never appears under Settings → Actions → Runners
Section titled “The runner never appears under Settings → Actions → Runners”If the log shows a plain registration failure rather than the 404 above, the
token expired — it is valid for one hour and single-use. Fetch a fresh one and
set RUNNER_TOKEN again. Nothing else is lost.
The job queues and nothing happens, then fails a day later
Section titled “The job queues and nothing happens, then fails a day later”No runner carries the label the job asks for. Compare RUNNER_LABELS with the
runs-on list, and check the runner is Idle rather than Offline.
A runs-on label with no runner behind it does not fail fast: the job waits 24
hours first.
A container starts and serves nothing. Or a build cannot find files that are plainly there
Section titled “A container starts and serves nothing. Or a build cannot find files that are plainly there”The work root is not mounted at its own host path. This is the most common failure and the least obvious.
The runner uses the host’s Docker daemon, so any path it hands to Docker is
resolved on the host. If RUNNER_WORK_ROOT, the host side of the volume and the
container side of the volume are not the same string, Docker silently creates an
empty directory on the host and mounts that instead of your checkout.
Check: while a job runs, ls the work root on the host. Your checkout should
be there. If it is empty, that is this.
See How it works.
A job fails binding a port
Section titled “A job fails binding a port”Something else on the host holds it:
ss -ltnpDev-environment tooling that fronts services with its own router usually wants 80 and 443, and those are the ones a Portainer host is most likely to have taken already.
Moving the tool’s ports is rarely enough on its own: whatever URLs your tests use have to follow.
Tests fail in ways that move between runs
Section titled “Tests fail in ways that move between runs”State from the previous run. A seeded test database is the usual culprit: on a hosted runner the machine is discarded after each job, so the workflow never needed to clean up. On a persistent runner the last run’s data is still there.
No prune finds this — the volume belongs to a container that is running perfectly well. Tear it down in the workflow, before the job as well as after. See How it works.
This is worth suspecting early, because it reads exactly like flakiness.
A long job reports as cancelled, with no reason
Section titled “A long job reports as cancelled, with no reason”Something restarted the runner mid-job. The usual cause is an auto-updater on the host — a Watchtower that watches every container by default will update the runner container while it is working.
Both services in this stack carry
com.centurylinklabs.watchtower.enable=false. Any service you add needs the
same label.
The disk fills up
Section titled “The disk fills up”Check what is actually using it:
./cleanup.shThen look at which of these it is:
- Images and build cache — the
pruneservice should be handling these. Check theprunecontainer is running and look atPRUNE_KEEP. - Volumes — not pruned by default, deliberately: on a shared host that
destroys other stacks’ data. If the host runs nothing but runners, either
PRUNE_VOLUMES=trueor./cleanup.sh --volumes. - The work root —
_work/_tooland_work/_actionsare caches that grow slowly and are worth keeping../cleanup.sh --work PATHclears everything else.
A host that fills up does not report a full disk. It reports an unhealthy container, a database that cannot write, or an image that cannot be pulled. Rule the disk out early.
A tool installation step works on GitHub’s runners and fails here
Section titled “A tool installation step works on GitHub’s runners and fails here”Actions that install tooling are usually written for GitHub’s images: a known
Ubuntu, sudo available, a predictable PATH. Inside a runner container some
of that differs.
If an installer keeps failing, bake the tool into a derived image rather than fighting the action:
FROM myoung34/github-runner:2.338.0-ubuntu-nobleRUN ... install your tool ...and point RUNNER_IMAGE at it.