skill
incus-gha-runners
Several GitHub Actions self-hosted runners on one Linux host, each in its own Incus system container with its own Docker. Bash scripts plus an agent skill; switches for mainland China networks.
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/riverscn/incus-gha-runners itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 25, 2026incus-gha-runners
Several GitHub Actions self-hosted runners on one Linux host, each in its own Incus system container.
Using an AI coding agent? Install the skill:
npx skills add riverscn/incus-gha-runnersThen open the agent in your project and ask, for example:
Set up 4 GitHub Actions self-hosted runners for this repository on my host `ssh myhost`, each in its own Incus container, using incus-gha-runners. Read the workflows to find the label and the system packages the jobs need. Show me every change to the host before making it, and never print a token.Later: "add two more runners", "e2e says a shared library is missing after the Playwright upgrade", "why is runner 3 offline?". The agent runs the scripts and asks before anything that changes the host or a runner.
Self-hosted runners that share one host as processes of one user get in each other's way: parallel jobs
rewrite the same tool directories in $HOME, one job's docker compose down changes the network under
another job's browser (net::ERR_NETWORK_CHANGED), and the docker group is root on the host. Here every
runner gets an unprivileged Incus system container with its own systemd, dockerd, network namespace and
runner user, copied from a template. Jobs see a machine of their own; workflows are written as for
GitHub-hosted runners.
- Bash scripts, run as root on the host:
prepare-host,build-template,add-runner,remove-runner,apply-hooks,status,doctor. All idempotent, with--dry-run. - One configuration file per pool: repository or organisation, labels, names, how many.
- Long-lived containers: caches, images and build cache stay between jobs. Not ephemeral runners.
- Switches for mainland China: Debian mirror, a locally built base image instead of images.linuxcontainers.org, a Docker Hub pull-through cache.
- Project-specific system dependencies (such as Playwright's browser libraries) as hook scripts.
- Registration tokens are piped in from
ghon your computer; the host needs no GitHub credentials. - An agent skill that runs the scripts and asks before anything that changes the host or a runner.
Quick start
On the host:
sudo git clone https://github.com/riverscn/incus-gha-runners /opt/incus-gha-runners
sudo install -d /etc/incus-gha-runners
sudo cp /opt/incus-gha-runners/config.example.env /etc/incus-gha-runners/config.env
sudoedit /etc/incus-gha-runners/config.env # GITHUB_URL, RUNNER_LABELS, RUNNER_COUNT, NETWORK_PRESET
sudo /opt/incus-gha-runners/bin/prepare-host --dry-run # read it, then run without --dry-run
sudo /opt/incus-gha-runners/bin/build-template
On your computer:
gh api -X POST repos/OWNER/REPO/actions/runners/registration-token --jq .token \
| ssh HOST sudo /opt/incus-gha-runners/bin/add-runner --token-stdin --all
ssh HOST sudo /opt/incus-gha-runners/bin/doctor
Then use the label in a workflow: runs-on: [self-hosted, ci].
Documentation
- Setup: from a bare host to working runners
- Operations: more or fewer runners, a new template, hooks, migrating, several pools
- Troubleshooting
- Design: every configuration key, script, option and exit code
Tested on
Debian 13 (trixie) host, Docker 29.9, Incus 6.0.4 (Debian's package), as a QEMU virtual machine with 12 cores
and 11G of memory, running 4 runners for a pnpm monorepo CI (type checks, Playwright e2e on desktop and
mobile, Docker image builds with Compose), with NETWORK_PRESET=cn. The images.linuxcontainers.org path
(BASE_IMAGE=images) is untested. Other hosts with systemd and Incus 6.0+ should work (the containers are
Debian either way); see the host requirements.
Security
Jobs run the repository's code. The containers are unprivileged and cannot reach the host's files, the host's Docker or each other, but they can reach the host's network, and a container keeps its state between jobs. Use these runners only for repositories you trust, and do not let pull requests from forks run on them.