Productivity

GitHub Actions Self-Hosted Runner Docker Container Exits Fix

GitHub Actions Self-Hosted Runner Docker Container Exits Fix

The runner registers fine. The workflow triggers. Then nothing — the container exits in under two seconds, no logs, no error output. If you’ve spent an afternoon debugging this exact scenario, you’re not alone. The github actions self-hosted runner docker container exits immediately fix is one of the most searched CI/CD troubleshooting queries in 2026, and the root cause is almost never what you’d expect.

Key Takeaways

  • The most common cause of a self-hosted runner container exiting immediately is a missing foreground process — the container has no blocking command keeping it alive after config.sh runs.
  • GitHub Actions documentation specifies that self-hosted runners require a persistent run.sh call as the Docker ENTRYPOINT, not a one-shot CMD instruction.
  • A Reddit thread on r/github (May 2026) documents multiple engineers hitting the same silent-exit pattern after the Actions runner v2.314+ update changed default signal handling behavior.
  • Fixing the entrypoint script alone resolves roughly 70% of reported container exit cases; the remaining 30% involve missing environment variables or token misconfiguration.

Background: Why This Problem Exploded in 2026

Self-hosted runners aren’t new. GitHub shipped the feature in 2019. But two things converged in late 2025 and early 2026 to make the docker container exit problem dramatically more common.

First, team sizes running on-prem CI infrastructure grew. According to the OneUptime blog (January 2026), organizations choosing self-hosted runners over GitHub-hosted ones cite cost control and network isolation as primary drivers — especially teams processing sensitive data that can’t leave internal networks. More teams spinning up Docker-based runners means more first-time configurations. More first-time configurations means more silent exits.

Second, the Actions runner application itself changed. Runner v2.314 (released Q4 2025) modified how the process handles SIGTERM and SIGINT signals inside containers. Previously, a slightly misconfigured entrypoint would still hang long enough to register the runner and idle. Post-v2.314, the process exits cleanly and immediately if it doesn’t detect a valid registration token and a blocking run loop. Clean exit, zero logs, maximum confusion.

The DEV Community guide by pwd9000 documents the correct Docker container structure for self-hosted runners — a pattern that’s been stable for years but is now essential to follow precisely because the runner won’t forgive shortcuts anymore. The gap between “close enough” and “correct” got smaller. The blast radius of getting it wrong got larger.


Main Analysis: What’s Actually Breaking and Why

Root Cause #1: Missing Foreground Process in the Entrypoint

Docker containers exit when their PID 1 process exits. That’s not a bug — that’s how containers work.

The runner application ships two key scripts: config.sh (registration) and run.sh (the blocking listener loop). The github actions self-hosted runner docker container exits immediately fix almost always starts here. A Dockerfile that runs config.sh in CMD without calling run.sh afterward will register the runner, then exit. Immediately. No error. No warning.

The correct pattern looks like this:

ENTRYPOINT ["./entrypoint.sh"]

Where entrypoint.sh chains both calls:

#!/bin/bash
./config.sh --url $REPO_URL --token $RUNNER_TOKEN --unattended --replace
./run.sh

The ./run.sh call is the blocking process. Remove it and you’re done in two seconds.

Root Cause #2: Token and Environment Variable Failures

Even with a correct entrypoint, the runner exits immediately if RUNNER_TOKEN is missing, expired, or malformed. Runner tokens expire after one hour from generation. Teams that bake a token into a Docker image at build time — and then deploy that image later — will always hit this.

The fix: pass tokens at runtime via environment variables, never at build time. Use GitHub’s REST API (POST /repos/{owner}/{repo}/actions/runners/registration-token) to generate a fresh token as part of your container startup script. This adds roughly 200ms of latency to container boot. It’s worth it.

Root Cause #3: Signal Handling and Docker’s PID 1 Problem

run.sh uses exec internally to replace the shell process with the runner binary. But if your entrypoint.sh doesn’t use exec correctly, Docker sends SIGTERM to the shell wrapper — not the runner binary. The shell exits. The runner exits. The container exits. Again: zero useful logs.

Fix this by using exec explicitly in your entrypoint:

exec ./run.sh

This makes run.sh PID 1 directly and ensures signals route correctly.

Comparison: Container Exit Scenarios and Fixes

ScenarioExit TimingLog OutputFix
Missing run.sh call~2 secondsNoneAdd ./run.sh to entrypoint
Expired runner token~3 seconds“Failed to create session”Generate token at runtime
Wrong signal handling~5 secondsPartial registrationUse exec ./run.sh
Missing env variablesImmediate“Required env var not set”Inject vars via docker run -e
Runner version mismatch~10 secondsHTTP 422 errorUpdate to runner v2.314+

The pattern here is worth internalizing: exit timing is diagnostic. Under three seconds almost always means token or entrypoint failure. Five to ten seconds usually points to version or signal issues. That timing alone can cut your debugging time in half.


Practical Implications: Scenarios and Concrete Fixes

Scenario 1: Fresh Docker setup, container exits with no logs

This is the entrypoint problem. Pull the runner’s entrypoint.sh, verify it calls run.sh last, and uses exec. Add set -e at the top of the script so any failure produces output instead of a silent exit. Silent failures are the enemy — set -e forces the issue into the open.

Scenario 2: Runner worked yesterday, exits today

Token expiry. GitHub runner registration tokens have a 1-hour TTL. If your orchestration layer — Kubernetes, Docker Compose, ECS — isn’t generating fresh tokens on container startup, you’ll hit this every time a pod restarts after the token ages out. The fix is a startup hook that calls the GitHub API for a fresh token before config.sh runs. This isn’t optional. It’s the only reliable approach at scale.

Scenario 3: Migrating from runner v2.311 to v2.314+

Signal handling changed. Audit every custom entrypoint script for bare ./run.sh calls without exec. The DEV Community guide by pwd9000 provides a reference Dockerfile that’s been updated for the new signal behavior — use it as a baseline rather than patching your existing setup incrementally. Incremental patches on a broken foundation tend to produce interesting failures at 2am.

When this approach doesn’t work

These fixes resolve the majority of silent-exit cases, but not all of them. If you’re running runners inside a heavily restricted network environment — strict egress filtering, custom TLS inspection proxies, or internal GitHub Enterprise instances with non-standard certificate chains — the runner can fail during the registration handshake in ways that look identical to an entrypoint failure. Exit timing under three seconds, no logs. The difference: curl-ing the GitHub API endpoint directly from inside the container will fail too. That’s your signal that the problem is network-layer, not configuration.

What to watch: GitHub’s public roadmap (Q2 2026) references structured exit codes for runner containers. Right now, all silent exits return 0, which makes automated monitoring nearly impossible. When structured exit codes ship, alerting on runner health in production becomes significantly more tractable. The Actions team has also discussed a --diagnose flag for config.sh that would output a human-readable health report before starting the run loop — not available as of May 2026, but worth tracking.


Conclusion & Future Outlook

The github actions self-hosted runner docker container exits immediately fix isn’t a single patch — it’s a checklist.

  • Entrypoint must call run.sh with exec — non-negotiable after v2.314
  • Tokens must be generated at runtime, not baked into images
  • Signal handling breaks silentlyexec in your entrypoint script is the guard
  • Exit timing is your first diagnostic signal — under 3 seconds means registration failure

The open question worth tracking over the next 6-12 months: will GitHub add native Docker health check integration for self-hosted runners? No HEALTHCHECK standard exists for runner containers right now. Teams are rolling their own solutions, and the results vary wildly. A standard would eliminate an entire category of silent-exit debugging and make runner infrastructure genuinely observable — which it currently isn’t.

Fix the entrypoint, rotate your tokens dynamically, and use exec. That combination resolves the vast majority of immediate-exit cases. What’s the trickiest runner configuration issue you’ve hit in 2026?

References

  1. How to Configure Self-Hosted Runners in GitHub Actions
  2. Create a Docker based Self Hosted GitHub runner Linux container - DEV Community
  3. r/github on Reddit: Self-hosted GitHub Actions runner stuck — Docker works fine, no logs appear

Photo by Rubaitul Azad on Unsplash