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.

Anchor — two generations coexist Both autoscale the runner pool to match demand — so a busy morning spins up more runners, and a quiet night scales them down.
The dind trick — Docker inside the runner The legacy runners 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

Anchor — a job's machine is computed A one-line lint job doesn't need the same machine as an 8-core E2E run. So 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-e2e8-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" = ephemeral for isolation Note the 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.
Read this next

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…

ARC runs runners as pods, scaling the pool with the job queue — the CI runs on the same Kubernetes it deploys to.

Q2. dockerdWithinRunnerContainer: true (dind) is needed to…

A Docker daemon inside the runner pod lets jobs 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.
Why self-host, what ARC is, dind, and dynamic runner sizing.
recall, then click to reveal
SELF-HOSTED runners = you operate them (bigger machines, private network, custom tools, cost at scale). ARC (Actions Runner Controller) runs them as AUTOSCALING KUBERNETES PODS, scaling with the job queue (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.
Want to see how the autoscaler decides pod count, or why the org migrated to the new scale sets? Ask me.

1. GitHub — Self-hosted runners.

2. GitHub — Actions Runner Controller.