Github Actions runner

This guide brings up a self-hosted GitHub Actions runner for lxc/incus-compose inside a privileged Incus container that runs its own (nested) Incus daemon, so the test suite can create instances, networks and volumes.

The steps move between three shells. Each section says which one it runs in:

  • Host — your workstation/server running Incus.
  • Container (root) — a root shell inside the runner-local container.
  • Runner user — an unprivileged runner login shell inside the container.

Placeholders to replace as you go: example.com (your OCI registry mirror domain), <ip-from-above> (the container's bridge IP), and the --token registration token from GitHub.

1. Create the runner container — host

incus --project=ic-github-runner launch images:debian/trixie runner-local -c security.privileged=true -c security.nesting=true
incus --project=ic-github-runner exec runner-local /bin/bash

The exec drops you into a root shell inside the container; the next steps run there.

2. Install base packages — container (root)

apt install sudo sudo-rs vim golang git shellcheck
ln -s /usr/sbin/sudo-rs /usr/local/sbin/sudo

3. Install Incus from the Zabbly repository — container (root)

curl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc
sh -c 'cat <<EOF > /etc/apt/sources.list.d/zabbly-incus-stable.sources
Enabled: yes
Types: deb
URIs: https://pkgs.zabbly.com/incus/stable
Suites: $(. /etc/os-release && echo ${VERSION_CODENAME})
Components: main
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/zabbly.asc

EOF'

apt update; apt install incus podman skopeo xdelta3 umoci jq nano

4. Create the runner user — container (root)

The incus-admin group lets the runner talk to the nested Incus daemon without sudo.

adduser --disabled-password --shell /usr/bin/bash runner
usermod -aG incus-admin runner

5. Install golangci-lint — runner user

The install script drops the binary in ~/.local/bin. Create that directory before logging in: Debian's ~/.profile only adds ~/.local/bin to PATH if it exists at login, so log out and back in afterwards to pick it up.

sudo -u runner -iH
mkdir -p ~/.local/bin
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b ~/.local/bin
exit

sudo -u runner -iH
which golangci-lint

6. Install other tools and configure podman - runner user

go install gotest.tools/gotestsum@latest
mkdir -p ~/.config/containers/
echo -e '[engine]\ncgroup_manager = "cgroupfs"' > ~/.config/containers/containers.conf
loginctl enable-linger runner

restart the container/vm.

7. Initialise the nested Incus daemon — runner user

Accept the defaults unless you have a reason not to.

incus admin init

8. Add OCI registry remotes — runner user

These point at your registry mirrors so images can be pulled by short name.

export DOMAIN=example.com
incus remote add --protocol=oci docker.io https://docker-registry.$DOMAIN
incus remote add --protocol=oci ghcr.io https://ghcr-registry.$DOMAIN
incus remote add --protocol=oci registry.gitlab.com https://gitlab-registry.$DOMAIN

9. Enable HTTPS access to the local daemon — runner user

Generate a client certificate, trust it, find the bridge IP, expose the daemon over HTTPS, and add a remote pointing at it.

incus remote generate-certificate
incus config trust add-certificate ~/.config/incus/client.crt
ip a show dev incusbr0
export IP=<ip-from-above>
incus config set core.https_address=:8443
incus remote add local-https $IP --accept-certificate

10. Download the GitHub Actions runner — runner user

mkdir actions-runner; cd actions-runner
curl -o actions-runner.tar.gz -L https://github.com/actions/runner/releases/download/v2.335.1/actions-runner-linux-x64-2.335.1.tar.gz
tar xf actions-runner.tar.gz; rm -f actions-runner.tar.gz
exit

11. Install runner dependencies — container (root)

The dependency installer needs root, so run it after the exit above.

/home/runner/actions-runner/bin/installdependencies.sh

12. Register the runner — runner user

Get a registration token from the repository's Settings → Actions → Runners → New self-hosted runner, then register:

sudo -u runner -iH
cd actions-runner
./config.sh --url https://github.com/lxc/incus-compose --token XXX
echo "HOME=/home/runner" >> ~/actions-runner/.env

The interactive prompts look like this (the values shown are the ones used here):

--------------------------------------------------------------------------------
|        ____ _ _   _   _       _          _        _   _                      |
|       / ___(_) |_| | | |_   _| |__      / \   ___| |_(_) ___  _ __  ___      |
|      | |  _| | __| |_| | | | | '_ \    / _ \ / __| __| |/ _ \| '_ \/ __|     |
|      | |_| | | |_|  _  | |_| | |_) |  / ___ \ (__| |_| | (_) | | | \__ \     |
|       \____|_|\__|_| |_|\__,_|_.__/  /_/   \_\___|\__|_|\___/|_| |_|___/     |
|                                                                              |
|                       Self-hosted runner registration                        |
|                                                                              |
--------------------------------------------------------------------------------

# Authentication


√ Connected to GitHub

# Runner Registration

Enter the name of the runner group to add this runner to: [press Enter for Default]

Enter the name of runner: [press Enter for runner-local] server01-runner-local

This runner will have the following labels: 'self-hosted', 'Linux', 'X64'
Enter any additional labels (ex. label-1,label-2): [press Enter to skip] incus-compose-local

√ Runner successfully added

# Runner settings

Enter name of work folder: [press Enter for _work]

√ Settings Saved.

13. Run the runner as a service — container (root)

exit
pushd /home/runner/actions-runner
./svc.sh install runner
./svc.sh start

The runner is now registered and starts automatically with the container.