was the painful platform - macOS. This one is sideways: we moved a chunk of the CI workload off Jenkins entirely, onto GitHub Actions, using EC2 spot instances as the runner fleet.
This isn't me saying "Jenkins is dead, use GitHub Actions". The Jenkins setup is still the workhorse for the big builds - macOS, Windows, anything that needs custom orchestration or runs for hours. GitHub Actions runs alongside it for a specific class of workload where it fits better.
This post is about the self-hosted spot runner pattern - how to point GitHub Actions at your own ephemeral EC2 fleet instead of GitHub's managed runners, and the things that bite once you do.
What "self-hosted spot runner" actually means
A self-hosted GitHub Actions runner is a small agent that registers itself with a GitHub repo or org, polls for jobs matching its labels, runs them, and reports results back. The agent doesn't care where it lives - bare-metal, VM, container, anything that can run the runner binary.
The runner can be:
- Persistent: registered once, sits there forever, picks up jobs as they come in.
- Ephemeral: registered with a single-use token, picks up exactly one job, then de-registers and shuts down.
We picked ephemeral, for the same reasons as in Part 1. A long-lived self-hosted runner means you're babysitting a host, eating build pollution from a shared agent, and carrying a security blast radius that never closes.
So every GitHub Actions job gets its own EC2 spot instance, freshly launched from a Packer-baked AMI. The job runs, the instance is terminated. It's the same one-build-per-worker idea as the Jenkins fleet, just driven from a different control plane.
The architecture, end to end
There's no single "self-hosted spot runner" plugin or service. You stitch the flow yourself, or you grab one of a few open-source modules that stitch it for you. I went with - the Terraform module that wires up the whole self-hosted spot runner architecture. (Formerly hosted at philips-labs/terraform-aws-github-runner; moved to its own org.)
- bakes the runner AMIs.
- the cheap, interruptible compute the runners live on.
- the queueing and orchestration glue (managed for you by the module above).
This is Part 3 of My CI/CD Odyssey. Thanks for sticking with the whole series. If you're doing self-hosted CI differently - Jenkins, GitHub Actions, or something else - I'd love to hear about it in the comments.
SOCIAL SHARE CARD GENERATOR