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.
Backend use case Backend use case: this grounds the legacy summerwind runner deployments, the newer scale-set charts, dind runner config, and dynamic `runs-on` selection via `runners.js`.
Common mistake Common mistake: thinking `runs-on` is a fixed label in this repo when it is often resolved dynamically from runner-mapping code.
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

In plain English Plain English: ARC turns GitHub Actions runners into autoscaling Kubernetes pods so CI jobs can run on infrastructure you control.

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.