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 psThen add the account running the GitHub Actions daemon to the Docker group:
sudo usermod -aG docker $USERStart 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.