Self-hosted GitHub Actions runner in Docker
  • Python 33.9%
  • HTML 24.3%
  • Shell 21.8%
  • PowerShell 14%
  • Dockerfile 6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Fill84 b177b173a8 fix: never record a runner install that did not happen
`curl … | tar -xz` with neither exit status checked, followed by an
unconditional write of .runner_version. A transient download failure recorded
"v2.336.0 installed" over binaries still on v2.333.1 - and because the marker
then matched the pin, every subsequent boot's check passed and it never
retried. github-runner-6 sat on deprecated binaries until GitHub refused to
send it work, then restart-looped 11 times.

This is the self-perpetuating form of the failure the RUNNER_VERSION comment
above already warns about: the false marker is precisely what prevents the
repair.

Now downloads to a file, checks curl and tar separately, and confirms the
extracted listener reports the expected version before recording anything.
On any failure it removes the marker and exits non-zero so the container
restarts and tries again.

Verified: a 404 download yields exit=1 with no marker written; a good one
yields exit=0, marker 2.336.0, binary 2.336.0. The download on this host has
been observed taking anywhere from 20s to 255s, so the failure window is real.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 02:41:34 +02:00
.github/workflows fix: change runner environment to ubuntu-latest for build job 2026-04-16 16:07:06 +02:00
dashboard fix: stop recreate-fleet cascade that destroyed six runners 2026-08-01 02:09:57 +02:00
docs/superpowers fix: harden prune-gating, log caps/redaction, and stale-state handling 2026-07-29 16:05:28 +02:00
install macOS: reboot survival, and the two macOS-only fixes verified on hardware (#2) 2026-07-28 04:54:01 +02:00
scripts fix: never record a runner install that did not happen 2026-08-01 02:41:34 +02:00
.env.example feat(runner): scalable runner group with docker compose 2026-03-25 22:30:15 +01:00
.gitattributes chore: add .gitattributes for consistent line endings 2026-03-16 05:51:09 +01:00
.gitignore chore: untrack __pycache__, add Python entries to .gitignore 2026-07-29 04:50:45 +02:00
docker-compose.runners.yml feat: turn the dashboard into a control plane 2026-07-27 00:54:34 +02:00
docker-compose.yml fix: correct start.sh mount path (/root/start.sh) 2026-04-16 18:52:25 +02:00
dockerfile fix: bump pinned runner version 2.335.1 -> 2.336.0 2026-07-29 16:29:05 +02:00
README.md docs: installer guide, and correct two false claims in the README 2026-07-27 23:54:40 +02:00

GitHub Actions Self-Hosted Runner (Docker)

This project provides a Dockerized GitHub Actions runner that supports scaling, custom labels, runner groups, and Docker-in-Docker (DinD) for CI/CD workflows.

Quick Start

1. Clone and configure

Clone this repo and copy .env.example to .env:

cp .env.example .env

Edit .env and set:

  • GH_TOKEN — GitHub PAT with admin:org or repo scope
  • GITHUB_ORG — Your GitHub organization name
  • RUNNER_LABELS — (Optional) Comma-separated labels for your runner (e.g. yourname,team,customtag)
  • RUNNER_GROUP — (Optional) Runner group name (must exist in your org)

2. Build and run with Docker Compose

docker compose up --build -d

To scale runners:

docker compose -f docker-compose.runners.yml up -d

Runners are defined as six explicit services rather than deploy.replicas. Replicas all receive identical mounts, and several Docker-in-Docker daemons sharing one data root corrupt each other.

Setting up runners on another machine

To add runners from a different machine, use the guided installers in install/ rather than this compose file. They work on Windows, Linux and macOS, and give the runners their own storage separate from whatever Docker that machine already runs.

3. Docker-in-Docker (DinD)

Each runner starts its own Docker daemon inside the container. The host's Docker socket is not mounted, so a job cannot see or touch containers outside its own runner.

The inner daemon uses the fuse-overlayfs storage driver: the kernel cannot stack native overlay2 on top of the host's overlay filesystem when the container is itself an overlay mount.

Build cache in that inner daemon is capped by a builder.gc policy in scripts/start.sh, and a janitor loop sweeps dead images, stale workspaces and old diagnostic logs every six hours. Without those the daemons grow without limit - this is not theoretical, they once filled a 1 TB disk.

Environment Variables

Set these in your .env file:

Variable Description
GH_TOKEN GitHub PAT (admin:org or repo scope)
GITHUB_ORG GitHub organization name
RUNNER_LABELS (Optional) Comma-separated runner labels
RUNNER_GROUP (Optional) Runner group name

Compose Configuration

See the included docker-compose.yml file in this repository for the latest and recommended configuration example.

Token Verification

You can verify your token with:

curl -L -X POST \
	-H "Accept: application/vnd.github+json" \
	-H "Authorization: Bearer {token}" \
	https://api.github.com/orgs/{org}/actions/runners/registration-token

Notes

  • The runner name is randomized per instance.
  • The container runs as root and privileged, which its own Docker daemon requires.
  • For repository-level runners, adjust the API endpoint and variables accordingly.