Ephemeral Docker-based self-hosted runners for GitHub Actions
Go
1
62 commits
updated Aug 24, 2026
Your own GitHub Actions runners on one Ubuntu machine.
Every job runs in a fresh disposable virtual machine with Docker inside. When the job ends, the VM is destroyed. No leftovers, no security surprises, no per-minute bills.
docker.sock.ubuntu-latest from — so your VMs carry the same git, gh, node, python, cmake, docker tooling. Pick ubuntu-24.04, ubuntu-26.04, or ubuntu-22.04 and how much of it you want.docker pull your workflows do on every run is instant and rate-limit-proof. Local docker save tarballs work too: gh-runnerd.local/my-ci:1.0.doctor checks the host; serve probes the whole VM datapath (DHCP, control channel, DNS, TCP 443) before booting a single runner, logs a concrete fix when something is broken, and pauses runner creation instead of minting Offline runners in GitHub.gh-runnerd runners cleanup purges by hand whenever you want.ls /dev/kvm — if that file exists, you are good.Actions: Read-only, Administration: Read and writeSelf-hosted runners: Read and write; plus Actions: Read-only on the repos it watches for jobsDownload the two files into a folder and run the wizard:
mkdir gh-runnerd && cd gh-runnerd
gh release download --repo RefireLab/gh-runnerd --pattern "gh-runnerd_*_linux_$(dpkg --print-architecture).tar.gz"
tar -xzf gh-runnerd_*.tar.gz
sudo ./gh-runnerd init
curl -fsSL https://raw.githubusercontent.com/RefireLab/gh-runnerd/main/scripts/install-binary.sh | sudo bash
This installs the binaries into /usr/local/bin. Then run sudo gh-runnerd init.
That's it. init is a wizard that walks you through everything:
owner/repo) or organization should get the runners and verifies the access too,systemd service, and starts it on every boot. Set up and forget.gh-runnerd.toml, gh-runnerd-data/). Start it yourself with sudo ./gh-runnerd serve.Just press Enter at every question to accept the defaults.
The default VM is minimal Ubuntu + Docker + the runner. If your workflows expect ubuntu-latest tools (node, gh, cmake, ...), bake them in — gh-runnerd runs GitHub's own image build scripts, no Packer needed:
| Flavor | What's inside | Image | Bake time |
|---|---|---|---|
minimal (default) | Docker + runner + git/curl/jq | ~2 GB | 3-5 min |
essential | + everyday tools from GitHub's images: git/git-lfs/gh, node + nvm, python + pipx, cmake, ninja, gcc, zstd, yq, pwsh, docker plugins | ~10 GB | ~10 min |
full | everything ubuntu-latest ships: browsers, JDKs, Android SDK, CodeQL, toolcache... | ~60-80 GB | ~30 min, ~130 GB free disk |
gh-runnerd runner-image available # which GitHub images can I mirror?
sudo gh-runnerd runner-image bake --image ubuntu-24.04 --flavor essential
Details, pinning, and per-script control: docs/runner-images.md.
In any repository you connected, create .github/workflows/ci.yml:
jobs:
build:
runs-on: gh-runnerd
steps:
- uses: actions/checkout@v4
- run: echo it works
Push. The job runs on your machine in a clean Ubuntu VM. Done.
Want the job itself inside a container (Alpine, Node, your own image)? Same as on GitHub-hosted runners:
jobs:
build:
runs-on: gh-runnerd
container:
image: alpine:3.22
steps:
- run: apk add --no-cache git
- run: uname -a
services: (Postgres, Redis), private GHCR images, digest pins, and docker/build-push-action all work the way you expect — see docs/containers.md and examples/workflows.
| I want to... | Run |
|---|---|
| Check that everything is healthy | gh-runnerd doctor |
| See the pool and network state | gh-runnerd status |
| See every runner registered in GitHub | gh-runnerd runners list |
| Purge stale Offline runners from GitHub now | gh-runnerd runners cleanup (add --idle for dead runners GitHub still shows Idle) |
| See the service | systemctl status gh-runnerd |
| See live logs | journalctl -u gh-runnerd -f |
| Rebuild the VM image (newest Ubuntu + runner + tools) | sudo gh-runnerd runner-image update |
Get GitHub's ubuntu-latest tools in the VM | sudo gh-runnerd runner-image bake --image ubuntu-24.04 --flavor essential |
| See which GitHub images can be mirrored | gh-runnerd runner-image available |
| Add a VM image from a file | sudo gh-runnerd runner-image import (it's a wizard too) |
| Cache a container image for instant pulls | gh-runnerd image pull node:22-bookworm |
| Use a local tarball as a job container | gh-runnerd image import ./my-ci.tar --name my-ci --tag 1.0 |
| Re-run the whole setup | sudo gh-runnerd init |
gh-runnerd doctor — tells you what is missing and how to fix it.journalctl -u gh-runnerd -e — the daemon's own words; network problems come with the exact fix and runner creation pauses until it passes.sudo gh-runnerd init — it is safe to run again; it keeps your config if you want.Three steps, in this order:
# 1. Update both binaries to the latest release
curl -fsSL https://raw.githubusercontent.com/RefireLab/gh-runnerd/main/scripts/install-binary.sh | sudo bash
# 2. Rebuild the VM image so the in-VM agent is updated too
# (safe while the old daemon is still running: the image is replaced
# atomically and live VMs keep the old one)
sudo gh-runnerd runner-image update
# 3. Restart the daemon — pick a quiet moment: a restart tears down VMs
# that are mid-job, and those jobs fail
sudo systemctl restart gh-runnerd
Verify with gh-runnerd --version and gh-runnerd doctor. Step 2 matters:
the guest agent is baked into the VM image, so skipping it leaves new
daemon + old agent (they stay compatible, but fixes and features that live
in the agent only arrive with a rebake).
Portable mode (no system service): download the new tar.gz over the old
binaries in your folder, run sudo ./gh-runnerd runner-image update, then
restart sudo ./gh-runnerd serve.
sudo systemctl disable --now gh-runnerd
sudo rm -f /etc/systemd/system/gh-runnerd.service /usr/local/bin/gh-runnerd /usr/local/bin/gh-runnerd-guest
sudo rm -rf /etc/gh-runnerd /var/lib/gh-runnerd
(Portable mode: just delete the folder.)
Ubuntu host
└── gh-runnerd (daemon)
├── isolated bridge: DHCP, guest control, pull-through registry cache
└── disposable Ubuntu VM ← created per job, destroyed after
├── GitHub Actions Runner (official, JIT, one job)
├── Docker Engine + Compose
├── ubuntu-latest tooling # if baked with essential/full
└── job container (any image) # only if the workflow sets container:
runs-on: gh-runnerd picks your infrastructure; container.image picks the job's Linux environment.docker.sock.More detail: install · runner images · job containers · registry & cache · architecture · security · GitHub App · troubleshooting · examples
MIT
Crafted by Refirelab, co-piloted by a few AI agents.
Go
98.6%
Shell
1.1%
Ephemeral Docker-based self-hosted runners for GitHub Actions
Go
1
62 commits
updated Aug 24, 2026
Your own GitHub Actions runners on one Ubuntu machine.
Every job runs in a fresh disposable virtual machine with Docker inside. When the job ends, the VM is destroyed. No leftovers, no security surprises, no per-minute bills.
docker.sock.ubuntu-latest from — so your VMs carry the same git, gh, node, python, cmake, docker tooling. Pick ubuntu-24.04, ubuntu-26.04, or ubuntu-22.04 and how much of it you want.docker pull your workflows do on every run is instant and rate-limit-proof. Local docker save tarballs work too: gh-runnerd.local/my-ci:1.0.doctor checks the host; serve probes the whole VM datapath (DHCP, control channel, DNS, TCP 443) before booting a single runner, logs a concrete fix when something is broken, and pauses runner creation instead of minting Offline runners in GitHub.gh-runnerd runners cleanup purges by hand whenever you want.ls /dev/kvm — if that file exists, you are good.Actions: Read-only, Administration: Read and writeSelf-hosted runners: Read and write; plus Actions: Read-only on the repos it watches for jobsDownload the two files into a folder and run the wizard:
mkdir gh-runnerd && cd gh-runnerd
gh release download --repo RefireLab/gh-runnerd --pattern "gh-runnerd_*_linux_$(dpkg --print-architecture).tar.gz"
tar -xzf gh-runnerd_*.tar.gz
sudo ./gh-runnerd init
curl -fsSL https://raw.githubusercontent.com/RefireLab/gh-runnerd/main/scripts/install-binary.sh | sudo bash
This installs the binaries into /usr/local/bin. Then run sudo gh-runnerd init.
That's it. init is a wizard that walks you through everything:
owner/repo) or organization should get the runners and verifies the access too,systemd service, and starts it on every boot. Set up and forget.gh-runnerd.toml, gh-runnerd-data/). Start it yourself with sudo ./gh-runnerd serve.Just press Enter at every question to accept the defaults.
The default VM is minimal Ubuntu + Docker + the runner. If your workflows expect ubuntu-latest tools (node, gh, cmake, ...), bake them in — gh-runnerd runs GitHub's own image build scripts, no Packer needed:
| Flavor | What's inside | Image | Bake time |
|---|---|---|---|
minimal (default) | Docker + runner + git/curl/jq | ~2 GB | 3-5 min |
essential | + everyday tools from GitHub's images: git/git-lfs/gh, node + nvm, python + pipx, cmake, ninja, gcc, zstd, yq, pwsh, docker plugins | ~10 GB | ~10 min |
full | everything ubuntu-latest ships: browsers, JDKs, Android SDK, CodeQL, toolcache... | ~60-80 GB | ~30 min, ~130 GB free disk |
gh-runnerd runner-image available # which GitHub images can I mirror?
sudo gh-runnerd runner-image bake --image ubuntu-24.04 --flavor essential
Details, pinning, and per-script control: docs/runner-images.md.
In any repository you connected, create .github/workflows/ci.yml:
jobs:
build:
runs-on: gh-runnerd
steps:
- uses: actions/checkout@v4
- run: echo it works
Push. The job runs on your machine in a clean Ubuntu VM. Done.
Want the job itself inside a container (Alpine, Node, your own image)? Same as on GitHub-hosted runners:
jobs:
build:
runs-on: gh-runnerd
container:
image: alpine:3.22
steps:
- run: apk add --no-cache git
- run: uname -a
services: (Postgres, Redis), private GHCR images, digest pins, and docker/build-push-action all work the way you expect — see docs/containers.md and examples/workflows.
| I want to... | Run |
|---|---|
| Check that everything is healthy | gh-runnerd doctor |
| See the pool and network state | gh-runnerd status |
| See every runner registered in GitHub | gh-runnerd runners list |
| Purge stale Offline runners from GitHub now | gh-runnerd runners cleanup (add --idle for dead runners GitHub still shows Idle) |
| See the service | systemctl status gh-runnerd |
| See live logs | journalctl -u gh-runnerd -f |
| Rebuild the VM image (newest Ubuntu + runner + tools) | sudo gh-runnerd runner-image update |
Get GitHub's ubuntu-latest tools in the VM | sudo gh-runnerd runner-image bake --image ubuntu-24.04 --flavor essential |
| See which GitHub images can be mirrored | gh-runnerd runner-image available |
| Add a VM image from a file | sudo gh-runnerd runner-image import (it's a wizard too) |
| Cache a container image for instant pulls | gh-runnerd image pull node:22-bookworm |
| Use a local tarball as a job container | gh-runnerd image import ./my-ci.tar --name my-ci --tag 1.0 |
| Re-run the whole setup | sudo gh-runnerd init |
gh-runnerd doctor — tells you what is missing and how to fix it.journalctl -u gh-runnerd -e — the daemon's own words; network problems come with the exact fix and runner creation pauses until it passes.sudo gh-runnerd init — it is safe to run again; it keeps your config if you want.Three steps, in this order:
# 1. Update both binaries to the latest release
curl -fsSL https://raw.githubusercontent.com/RefireLab/gh-runnerd/main/scripts/install-binary.sh | sudo bash
# 2. Rebuild the VM image so the in-VM agent is updated too
# (safe while the old daemon is still running: the image is replaced
# atomically and live VMs keep the old one)
sudo gh-runnerd runner-image update
# 3. Restart the daemon — pick a quiet moment: a restart tears down VMs
# that are mid-job, and those jobs fail
sudo systemctl restart gh-runnerd
Verify with gh-runnerd --version and gh-runnerd doctor. Step 2 matters:
the guest agent is baked into the VM image, so skipping it leaves new
daemon + old agent (they stay compatible, but fixes and features that live
in the agent only arrive with a rebake).
Portable mode (no system service): download the new tar.gz over the old
binaries in your folder, run sudo ./gh-runnerd runner-image update, then
restart sudo ./gh-runnerd serve.
sudo systemctl disable --now gh-runnerd
sudo rm -f /etc/systemd/system/gh-runnerd.service /usr/local/bin/gh-runnerd /usr/local/bin/gh-runnerd-guest
sudo rm -rf /etc/gh-runnerd /var/lib/gh-runnerd
(Portable mode: just delete the folder.)
Ubuntu host
└── gh-runnerd (daemon)
├── isolated bridge: DHCP, guest control, pull-through registry cache
└── disposable Ubuntu VM ← created per job, destroyed after
├── GitHub Actions Runner (official, JIT, one job)
├── Docker Engine + Compose
├── ubuntu-latest tooling # if baked with essential/full
└── job container (any image) # only if the workflow sets container:
runs-on: gh-runnerd picks your infrastructure; container.image picks the job's Linux environment.docker.sock.More detail: install · runner images · job containers · registry & cache · architecture · security · GitHub App · troubleshooting · examples
MIT
Crafted by Refirelab, co-piloted by a few AI agents.
Go
98.6%
Shell
1.1%