Lesson 03 of 8 · Modules
Actions Runner Controller
Run GitHub Actions jobs on your own Kubernetes cluster with Actions Runner Controller (ARC): the scale-set architecture, installing the controller and a runner scale set with Helm, GitHub App authentication, ephemeral autoscaling runners, and how jobs that build containers work.
Why self-hosted runners?
GitHub-hosted runners are easy, but you may need private network access (internal registries, clusters, artifact stores), special hardware (ARM, GPUs, big memory), cost control at high volume, or data residency. Actions Runner Controller (ARC) runs runners as pods in your Kubernetes cluster and scales them with demand.
GitHub-hosted runners are taxis: always available, but they can't drive into your private garden. ARC is your own fleet of bikes parked inside the garden. When an order arrives, a fresh bike appears, does exactly one delivery, and disappears. When lots of orders come in, more bikes appear.
Architecture (scale-set mode)
GitHub ──(jobs for "arc-runner-set")──► listener pod (arc-systems)
│ scale up/down
▼
AutoscalingRunnerSet → EphemeralRunner pods (arc-runners)
│ one job each, then deleted
- The controller (in
arc-systems) manages ARC's custom resources. - Each runner scale set has a listener that holds a long-poll connection to GitHub and asks for more or fewer runners.
- Ephemeral runner pods run exactly one job and are removed. There's no state left over between jobs.
(ARC's older, community-maintained "legacy" mode used different CRDs; new installs should use the scale-set mode described here.)
Install
Create a GitHub App for your organisation with the permissions ARC's docs list for your scope (organisation or repository), install it, and store its ID, installation ID and private key as a Secret:
$ kubectl create namespace arc-runners
$ kubectl create secret generic arc-github-app -n arc-runners \
--from-literal=github_app_id=123456 \
--from-literal=github_app_installation_id=654321 \
--from-file=github_app_private_key=arc-app.private-key.pem
$ helm install arc -n arc-systems --create-namespace \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
# values.yaml for the runner scale set
githubConfigUrl: https://github.com/acme # org (or a single repo URL)
githubConfigSecret: arc-github-app
minRunners: 0
maxRunners: 20
runnerGroup: platform # optional: restrict which repos can use it
template:
spec:
containers:
- name: runner
image: ghcr.io/actions/actions-runner:latest # pin a version in production
command: ["/home/runner/run.sh"]
resources:
requests: { cpu: "1", memory: 2Gi }
limits: { memory: 4Gi }
$ helm install arc-runner-set -n arc-runners -f values.yaml \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
Workflows use the installation name as the label:
jobs:
build:
runs-on: arc-runner-set
Pin both chart versions, and keep the controller and scale-set chart versions the same.
Building containers on ARC
| Option | How | Trade-off |
|---|---|---|
containerMode: dind |
A privileged Docker daemon sidecar | Easiest; privileged pods |
containerMode: kubernetes |
Job containers run as separate pods via container hooks | No privileged daemon; needs a work volume, some actions behave differently |
| Rootless builders in the default mode (BuildKit rootless, Buildah) | Build images without a Docker daemon | Needs builder-specific setup |
Whatever you choose, run runner pods on dedicated nodes (taints/tolerations) and apply the hardening in lesson 06.
Try it: ARC in a lab cluster
- Create a GitHub App in a test organisation (or use a PAT only for the lab), and install the controller.
- Install a scale set with
minRunners: 0,maxRunners: 3for one test repository. - Push a workflow with
runs-on: arc-runner-setand watchkubectl get pods -n arc-runners -w: a pod appears, runs the job and is deleted. - Trigger five jobs at once (a matrix) and watch scaling stop at 3.
- Try a Docker build job with
containerMode: dind, then read the runner pod spec and find the privileged container.
Going deeper: operating runner fleets
- Use separate scale sets per trust level and workload type (untrusted PR builds, trusted main-branch builds, GPU jobs) with runner groups limiting which repos can use each.
- Keep
minRunnersabove 0 for busy scale sets to avoid cold starts; pre-pull the runner image on nodes. - Scrape ARC's metrics (enable them in the chart values) for queue time and runner counts; queue time is the user-facing SLO.
- Autoscale the nodes too (Cluster Autoscaler or Karpenter; see Amazon EKS in Production with Terraform).
Recap
- ARC runs GitHub Actions runners as pods: controller + per-scale-set listener + ephemeral runner pods.
- Install both Helm charts (same version), authenticate with a GitHub App, target jobs with
runs-on: <scale-set name>. - One job per pod, scaling between
minRunnersandmaxRunners. - Choose a container-build mode deliberately: dind (privileged), kubernetes mode, or rootless builders.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.