Infrastructure · January 11, 2023

Setting up a self-hosted GitHub Actions runner

A small, practical experiment with an Ubuntu VM, Multipass, Hyper-V, and Docker.

Background

I’ve led several teams where I was responsible for CI/CD systems, including CircleCI and GitHub Actions. Both support self-hosted runners: virtual or physical machines managed by you where workflow jobs execute. The SaaS platform still oversees workflow status and retries, but you own the execution environment.

Why would you do this?

Cost savings

If you have capable machines sitting idle, you can use that spare capacity for CI. In the cloud, spot instances can also be cheaper than on-demand capacity. Do the math against hosted per-minute rates and remember to account for free platform minutes.

Security

Self-hosting gives you direct control over the environment. That can be useful when workloads should not execute on infrastructure you do not control—but it also makes security your responsibility.

Do not associate an unprotected self-hosted runner with a public repository. A malicious pull request could potentially execute code on your machine.

Custom resource classes

Hosted platforms offer fixed combinations of CPU and memory. A self-hosted runner can provide a resource profile that better matches your workload or budget.

Creating the Ubuntu VM

For this experiment, I used a six-core Ryzen 5 5600H mini PC running Windows 11 Pro. Rather than modify the host directly, I created an Ubuntu 22.04 VM with two CPU cores, 2 GB of RAM, and 10 GB of storage. Canonical’s Multipass utility created the preconfigured VM through Hyper-V.

Installing the runner

The VM needs GitHub’s runner daemon and a registration with your repository or organization. GitHub provides setup commands under the repository’s Actions runner settings. Once complete, the runner appears online and ready to accept jobs.

Adding Docker

Containerized jobs work on a self-hosted runner just as they do on hosted infrastructure. After installing Docker, verify the daemon:

sudo docker ps

Then add the account running the GitHub Actions daemon to the Docker group:

sudo usermod -aG docker $USER

Start a fresh login session and verify that docker ps works without sudo.

Putting it together

Create a small workflow with one job running directly on the Linux VM and another in a Docker container. Push it, open the Actions tab, and confirm both jobs execute on the self-hosted runner.

Conclusion

This covers the fundamentals for a small project. A production-grade enterprise runner fleet needs considerably more work around isolation, autoscaling, patching, secrets, observability, and lifecycle management—but the basic mechanics remain the same.