A runner that takes exactly one job: "0" still turns the flag on
Gitea Actions jobs sat queued because act_runner was registering as ephemeral. The image only checks that GITEA_RUNNER_EPHEMERAL is non-empty, so setting it to "0" enables it, and the pod looks like a crash loop.
After I merged a run of changes to this site, none of the deploys happened.
Gitea Actions showed some runs queued forever and others cancelled. The
runner pod was up, but not in a way I liked: 1/2 CrashLoopBackOff, 25
restarts in three days. I already run a self-hosted runner as described in
the runner notes, so I looked at
the pod first.
A crash loop that wasn’t crashing
The runner container’s last state was Completed with exit code 0. No
probes, no OOM kill. kubectl describe showed only BackOff, which is what
Kubernetes says about any container that keeps exiting, however cleanly.
The runner’s own log was clearer. Each start printed that .runner was
missing, registered again, picked up one task, and then logged shutdown initiated about 45 seconds later. The job it had picked up finished
successfully at the same second. That timing is the test: if the runner shuts
down right as its first job completes, it is running in ephemeral mode.
Why it was ephemeral
My Deployment had GITEA_RUNNER_EPHEMERAL: "0", and I had put it there
meaning “off”. The image’s entrypoint script adds --ephemeral when the
variable is non-empty. It never looks at the value, so the string "0"
counts as set. The same goes for "false", and presumably for
GITEA_RUNNER_ONCE, which is checked the same way.
The fix is to delete the variable. An ephemeral runner in a Deployment is almost always a mistake, because the Deployment restarts it with an ever-growing back-off, so throughput falls to one job per few minutes.
Why the deploys were cancelled, not just late
This part I only understood afterwards. The deploy workflow uses a
concurrency group with cancel-in-progress: false, which sounds like
“never cancel anything”. It still replaces older pending runs with the
newest one. With a runner that took one job every five minutes, every new
merge cancelled the deploy queued before it. For a “deploy the latest main”
workflow that’s fine when the runner keeps up, and it’s exactly why four
merges in a row produced no deploys when it didn’t.
Things I ran into while cleaning up
- With no state volume for
.runner, every restart registers a new runner in Gitea. Expect a couple of dozen stale runner records to delete in the admin UI. - Deleting a runner record while its pod is up leaves the pod logging
unregistered runnerevery few seconds. Registration only happens in the entrypoint, so it never recovers on its own; delete the pod. - I used
rollout restartfor that at first. Flux owns the Deployment and reverted the restart annotation within about a minute, which replaced the new pod and killed the job it had just picked up. That orphaned my re-run deploy a second time. Delete the pod instead. - Jobs the removed runner was executing stay
in_progressuntil Gitea’s zombie-task timeout. My bot token could neither cancel nor re-run them, so that was a job for the repository owner in the UI. - Right after a pod restart the runner container often exits once or twice
with
could not read CA certificate. It races the Docker-in-Docker sidecar that writes the certificates, and settles after about fifteen seconds. It’s not the crash loop.