Back openDesk Edu for a sovereign, open-source education — every vote counts.
Vote nowSave products you love by clicking the heart icon.
A practical guide to DevOps certifications in 2026. Compare LPI DevOps Tools Engineer, CKAD, CKA, Terraform Associate, and AWS DevOps Engineer — which to choose, what they cover, how they map to real-world skills, and how they fit together in a career path from beginner to expert.
A pipeline is only as reliable as its least reliable runner. I run four sites on GitHub Actions with self-hosted runners, and for weeks one repository behaved haunted: pushes queued for hours, jobs executed in mysterious bursts at 4am, then everything went quiet again. The workflow files were fine. The YAML was fine. The machine was the problem — and the fix turned out to be half an hour of systemd.
This is the field guide I wish I'd had: how to diagnose a flapping runner, how to make a runner survive reboots, and the small print about labels and registration tokens that the setup docs skip.
Cloud runners pick up jobs in seconds. A self-hosted runner picks up jobs only while its listener process is alive and connected. When the machine sleeps, shuts down, or the process dies, jobs don't fail — they wait. Indefinitely. There is no timeout that saves you.
The diagnosis path that works:
GET /repos/{owner}/{repo}/actions/runs → for each run, the jobs endpoint
includes runner_name. That tells you which registration picked the job
up last — and whether it matches a runner that still exists.runner_name says
ede-website-runner but no box you can SSH to knows that name, you have a
ghost registration — and possibly a machine you forgot you owned.My haunted runner turned out to live on a machine nobody could name — it picked up jobs whenever its human happened to boot it. The queued runs weren't lost; they were hostages.
The GitHub runner ships with svc.sh, and this is the whole ceremony on a
Linux box that should host runners permanently:
# one-time: download and register
./config.sh --url https://github.com/<org>/<repo> \
--token <REGISTRATION_TOKEN> \
--name my-runner --work _work --unattended
# persist: install as a systemd service
sudo ./svc.sh install <username> # writes /etc/systemd/system/*.service
sudo ./svc.sh start
Two properties matter more than any other detail: the unit lands in
multi-user.target.wants (starts on boot), and systemd restarts the
listener when it crashes. That single step eliminates the entire class of
"runner was fine until someone closed the terminal / the machine slept / the
SSH session ended."
On macOS the same script installs a launchd agent — same idea, but a laptop still sleeps, and launchd won't stop that. Laptops are for runners that accept flakiness; servers are for runners that don't.
Labels decide routing. Every runner gets defaults (self-hosted, its OS,
its architecture), and jobs match on runs-on: labels. Two classic failures:
a workflow says runs-on: [self-hosted, linux] and the only idle runner is a
macOS box (waits forever), or your Docker build job lands on a runner without
Docker. Audit with:
gh api repos/<org>/<repo>/actions/runners \
--jq '.runners[] | .name + " | " + .status + " | " + (.labels|map(.name)|join(","))'
This one command answers "what runners exist, are they online, and what can they actually run" — worth running monthly.
Registration tokens are minted with POST. The API endpoint for
.../actions/runners/registration-token returns an unhelpful 404 on GET;
gh api -X POST ... returns the one-hour token. Tokens are single-purpose and
expire fast — mint, register, done.
Stale registrations are ghosts. A machine that dies without
config.sh remove leaves a permanently offline entry. It's harmless until it
isn't — you'll build debugging sessions around a runner that can never pick up
work again. Delete by ID:
gh api -X DELETE repos/<org>/<repo>/actions/runners/<id>
One runner means serialization. With a single runner registered, every job across every workflow runs one after another. That's fine — predictable, even — but budget for it: my four-site fleet runs a full validate → test → deploy cycle in about thirty minutes on one box precisely because the jobs queue politely. Add a second runner on the same box (register again with a different name) the day that stops being fine.
In order of how much I'd trust them with a 3am deploy:
run.sh in a terminal. This is not hosting;
this is a runner-shaped dice roll. It is also, notably, how most people
start — and how most people learn all of the above.The meta-lesson from the haunted-runner saga: pipelines fail in inverse proportion to how well you know your fleet. One API call lists every runner, its labels, and its aliveness. Point it at your repos tonight — whatever it prints is probably more interesting than you expect.