If you’re new to self-hosted runners: it’s GitHub’s runner program, but running on your own machine instead of GitHub’s hosted runners.
This is useful when you want to test code against a Linux host but you don’t want every workflow run to permanently
mutate the machine. The core idea is to keep the runner operationally predictable (systemd-managed, reliable at boot),
while running container-style job steps inside an ephemeral systemd-nspawn guest so filesystem changes made during a
job are discarded when the guest is torn down. Writing to real devices (USB/ACM/TTY) is not reverted on teardown.
What it aims to do:
- Keep a single host runner process managed by systemd.
- When a workflow uses job containers, run the job steps inside a short-lived
systemd-nspawnguest instead of using Docker. (Ifsystemd-nspawnis new to you: it’s a lightweight container that boots a small Linux userspace with systemd inside it.)
Works on systemd-based Linux with apt (for example Raspberry Pi OS / Debian / Ubuntu).
-
On GitHub, open your repo’s Settings → Actions → Runners → New self-hosted runner page.
-
On the host:
sudo apt-get update
sudo apt-get install -y --no-install-recommends ca-certificates curl git- Create
/etc/runner/config.envusing values from GitHub’s “New self-hosted runner” page:
sudo install -d -m 0755 /etc/runner
# This file contains a short-lived registration token; keep it root-readable only.
sudo install -m 0600 /dev/null /etc/runner/config.env
sudo tee /etc/runner/config.env >/dev/null <<'EOF'
# Optional: actions runner version to install.
# Default: 2.330.0 (may not be the latest).
# The GitHub “New self-hosted runner” page shows the current version in its commands.
# RUNNER_ACTIONS_RUNNER_VERSION=2.330.0
# GitHub URL to register against.
# Get it from your repo/org’s GitHub UI: Settings → Actions → Runners → New self-hosted runner.
# Use either:
# - Repo runner: https://github.com/<owner>/<repo>
# - Org runner: https://github.com/<org>
RUNNER_GITHUB_URL=
# Registration token from GitHub UI: Settings → Actions → Runners → New self-hosted runner.
# Note: short-lived (expires); not a PAT.
RUNNER_REGISTRATION_TOKEN=
# Optional: runner name (defaults to hostname)
# RUNNER_NAME=pi-runner
EOF- Clone this repo and run the installer:
sudo git clone https://github.com/theaussiepom/github-runner.git /opt/runner
cd /opt/runner
sudo ./scripts/install.sh- Verify it’s running:
systemctl status runner.service --no-pagerIf you want to run the repo’s checks locally, the easiest path is to use the devcontainer. It’s a Docker image with the exact lint/test tools CI uses.
Build the devcontainer image:
docker build -t runner-devcontainer -f .devcontainer/Dockerfile .Run the full CI pipeline inside it:
docker run --rm \
-v "$PWD:/work" \
-w /work \
runner-devcontainer \
bash -lc './scripts/ci.sh'Or, use the Makefile (requires make + Docker on your host):
make ciSee CONTRIBUTING.md for the required pre-PR checks.
At runtime, systemd manages two services:
runner-install.service(first-boot installer; retried until it succeeds)runner.service(runs the configured GitHub runner)
The normal install flow is designed for “first boot” setups (cloud-init or Pi Imager):
- cloud-init writes
/etc/runner/config.env. - cloud-init installs a one-time installer unit + bootstrap script.
- systemd runs
runner-install.serviceuntil install succeeds.
Examples:
Runtime configuration lives in /etc/runner/config.env.
Bootstrap repo pin (used when the host needs to fetch this repo to install/update itself):
RUNNER_BOOTSTRAP_REPO_URL(agit cloneURL for this repo or your fork)RUNNER_BOOTSTRAP_REPO_REF(branch/tag/commit; pinning to a tag/commit is recommended)
If you omit these, bootstrap defaults to this repo on main.
Optional:
APPLIANCE_CHECKOUT_DIR(default:/opt/runner)APPLIANCE_INSTALLED_MARKER(default:/var/lib/runner/installed)APPLIANCE_APT_PACKAGES(space-separated extra packages for install)APPLIANCE_DRY_RUN=1(do not modify system; record intended actions)
Runner paths:
RUNNER_ACTIONS_RUNNER_DIR(default:/opt/runner/actions-runner)RUNNER_HOOKS_DIR(default:/usr/local/lib/runner)
Job isolation (systemd-nspawn) settings:
RUNNER_NSPAWN_BASE_ROOTFS(default:/var/lib/runner/nspawn/base-rootfs)RUNNER_NSPAWN_READY_TIMEOUT_S(default:20)RUNNER_NSPAWN_BIND/RUNNER_NSPAWN_BIND_RO(space-separated bind mount entries)
These are the “what’s running?” commands you’ll use most often.
Inspect service status:
systemctl status runner.service --no-pagerInspect install/boot status:
systemctl status runner-install.service --no-pager
ls -l /var/lib/runner/installed || trueIf you can’t use cloud-init, you can still install over SSH.
- Install prerequisites (needed to fetch this repo):
sudo apt-get update
sudo apt-get install -y --no-install-recommends ca-certificates curl git- Create
/etc/runner/config.env(start from the example):
sudo mkdir -p /etc/runner
sudo cp /path/to/runner/examples/config.env.example /etc/runner/config.env
sudo nano /etc/runner/config.env
sudo chmod 600 /etc/runner/config.env- Clone the repo and run the installer as root:
git clone https://github.com/your-org/github-runner.git /opt/runner
cd /opt/runner
sudo ./scripts/install.shTo have the installer perform a full end-to-end setup (download + configure the official GitHub Actions runner),
set these in /etc/runner/config.env before running scripts/install.sh:
RUNNER_ACTIONS_RUNNER_TARBALL_URL(Linux ARM/ARM64 tarball URL from GitHub’s “New self-hosted runner” page)RUNNER_GITHUB_URL(repo or org URL)RUNNER_REGISTRATION_TOKEN(short-lived registration token)- Optional:
RUNNER_NAME(defaults to hostname)