Lesson 7 · Build, deploy & trunk-based delivery
Self-hosted ARC runners
Where the jobs actually run — autoscaling runners as Kubernetes pods, sized per job.
Your win: explain why a team runs self-hosted runners, what Actions Runner Controller (ARC) is, and how this repo picks a right-sized runner for each job — including the Docker-in-runner trick that makes E2E possible.
GitHub-hosted vs self-hosted
Every job runs on a runner (Lesson 2). GitHub offers hosted runners (ephemeral, managed, metered), but teams self-host when they need more: bigger machines, private-network access, custom tools, cost control at scale, or the ability to run their own Kubernetes tooling.1 This repo self-hosts.
ARC — runners as Kubernetes pods
Actions Runner Controller (ARC) runs self-hosted runners as pods in a Kubernetes cluster, autoscaling the pool up and down with the job queue.2 It's a lovely closing of the loop: the CI that deploys to Kubernetes runs on Kubernetes.
- Legacy ARC (summerwind):
deployments/runner/runners/*.yaml — a
RunnerDeployment+ aHorizontalRunnerAutoscalerthat scales onTotalNumberOfQueuedAndInProgressWorkflowRuns(more queued jobs → more runner pods). - New ARC scale sets (official):
deployments/helm/platforms/actions-runner-controller/ —
installed via Helm (Course 3),
gha-runner-scale-set.
dockerdWithinRunnerContainer: true
(deployments/runner/runners/2-8-runnerdeploy.yaml:15) — a full
Docker daemon inside the runner pod (docker-in-docker). That's what
lets a job docker build the image (Lesson 5) and spin up a whole
kind cluster for E2E tests (Course 1) — inside a pod that is itself running
in Kubernetes. Kubernetes, all the way down.
Right-sizing: dynamic runs-on
runs-on:
isn't hard-coded — the runners composite action runs
.github/actions/runners/runners.js, which maps each job to a
runner-size label: e.g. unit-test/lint → a
4-16-large-runner, run-e2e →
8-32-non-persistent-large-runner (runners.js:46,56).
dynamicRunners() prefers the self-hosted HCM scale set when available. So each
job lands on a right-sized runner, chosen at run time.
non-persistent label for E2E: those runners are thrown away after the
job, so a heavy, stateful E2E run never leaks state into the next one. Ephemeral runners are
a security and hygiene win — no build can poison a later build's environment.
GitHub — Self-hosted runners + Actions Runner Controller
Why/when to self-host, and how ARC autoscales runners on Kubernetes.
→ docs.github.com — Self-hosted runners
→ docs.github.com — Actions Runner Controller
Check yourself (from memory)
Q1. Actions Runner Controller (ARC) runs runners as…
Q2. dockerdWithinRunnerContainer: true (dind) is needed to…
docker build and spin up a kind cluster for E2E.
Q3. In this repo, a job's runner size is…
runners.js maps job → label (lint → 4-16, e2e →
8-32), so runs-on is computed at run time.
TotalNumberOfQueuedAndInProgressWorkflowRuns)
— the CI runs on the same K8s it deploys to. Repo: legacy summerwind
(deployments/runner/runners/, RunnerDeployment+
HorizontalRunnerAutoscaler) + new scale sets
(helm/platforms/actions-runner-controller/). DIND:
dockerdWithinRunnerContainer: true = a Docker daemon inside the runner pod → lets
jobs docker build + run a KIND cluster for E2E. DYNAMIC runs-on:
runners.js maps job→label (lint/unit → 4-16-large, e2e →
8-32-non-persistent-large); non-persistent = ephemeral for isolation.