Dockerized GitHub Actions self-hosted runners for Linux (x64) and macOS (ARM64). Deploy in minutes, scale with replicas, deregister cleanly on shutdown.
git clone https://github.com/youssefbrr/self-hosted-runner.git
cd self-hosted-runner
cp .env.example .env # fill in REPO, REG_TOKEN, NAMELinux (x64)
docker-compose -f docker/linux/docker-compose.yml up -dmacOS / ARM64
docker-compose -f docker/mac/docker-compose.yml up -dREG_TOKEN expires after 1 hour. Generate a fresh one from
GitHub → Settings → Actions → Runners → "New self-hosted runner" before each deploy.
- Zero-config start — set 3 env vars and run
- Clean shutdown — SIGINT/SIGTERM deregisters the runner automatically
- Scalable — Linux defaults to 2 replicas; tune with
deploy.replicas - Ephemeral mode — run once and self-destruct (
EPHEMERAL=true) - Docker-in-Docker — macOS image mounts the Docker socket for nested builds
- Healthchecks — built-in
pgrep run.shhealth monitoring on both variants
docker/
├── linux/ Ubuntu 24.04, x64, runner v2.331.0
│ ├── Dockerfile user: docker, workdir: /home/docker/actions-runner
│ ├── docker-compose.yml 2 replicas · 0.5 CPU · 512M each
│ └── start.sh
└── mac/ Ubuntu 24.04, ARM64, runner v2.331.0
├── Dockerfile user: runner, workdir: /home/runner/actions-runner
├── docker-compose.yml 1 replica · 1 CPU · 1G · Docker socket mounted
└── start.sh
Both start.sh scripts: configure via config.sh → trap SIGINT/SIGTERM for deregistration → exec run.sh.
Copy .env.example to .env and set your values. The .env file is gitignored.
| Variable | Description |
|---|---|
REPO |
owner/repo for repo-level or owner for org-level runners |
REG_TOKEN |
Registration token from GitHub Settings (expires in 1 hour) |
NAME |
Display name shown in GitHub Actions UI |
| Variable | Default | Description |
|---|---|---|
LABELS |
(none) | Comma-separated labels, e.g. self-hosted,linux,x64,gpu |
RUNNER_GROUP |
(default) | Runner group name — org/enterprise only |
WORK_DIR |
_work |
Workspace directory inside the container |
EPHEMERAL |
false |
true → deregister after one job |
DISABLE_AUTO_UPDATE |
false |
true → prevent runner self-updates |
docker build --build-arg RUNNER_VERSION=2.332.0 -t custom-github-runner:latest ./docker/linuxSet REPO to just the org name:
REPO=my-orgThe runner will register at org level and be available to all repositories in that org.
Reference your runner in any workflow file:
jobs:
build:
runs-on: [self-hosted, linux]
steps:
- uses: actions/checkout@v4
- run: echo "Running on self-hosted runner"Use your custom labels to target specific runners:
runs-on: [self-hosted, linux, gpu]Adjust replicas in docker-compose.yml under deploy.replicas. GitHub will distribute jobs across all registered runners automatically.
deploy:
replicas: 4 # spin up 4 concurrent runners
resources:
limits:
cpus: '0.5'
memory: 512MRunner doesn't appear in GitHub Settings
- Check
REG_TOKEN— it expires after 1 hour. Generate a new one. - Verify
REPOformat:owner/repo(no leading slash, no trailing slash).
Logs
docker-compose -f docker/linux/docker-compose.yml logs -fHealth status
docker-compose -f docker/linux/docker-compose.yml psRunner stuck / won't deregister
docker-compose -f docker/linux/docker-compose.yml downdown sends SIGTERM → start.sh cleanup → runner deregisters cleanly.
See SECURITY.md for the vulnerability reporting policy.
Key practices in this project:
- Runners execute as non-root users (
dockeron Linux,runneron ARM64) - Secrets live in
.env(gitignored) — never hardcoded in compose files REG_TOKENis used only at registration time; not stored after config
Contributions welcome. Please:
- Fork the repo and create a branch from
main - Keep changes scoped — one feature or fix per PR
- Test your change by actually spinning up the container
- Open a pull request with a clear description of what and why
For bugs or feature requests, open an issue.
MIT — use freely, attribution appreciated.