AC

Actions Runner Controller

Run self-hosted GitHub Actions runners on Kubernetes

Automation & Workflows ★ 6.5k stars Hard setup Apache-2.0

Actions Runner Controller manages self-hosted GitHub Actions runners as scalable workloads on Kubernetes, creating and destroying runners on demand. It gives teams elastic, self-hosted CI capacity.

Key features

  • Autoscaling runners
  • Kubernetes-native
  • Ephemeral runner pods
  • Webhook-driven scaling

Pros & cons

Strengths

  • Elastic CI capacity
  • Keeps builds in your cluster

Trade-offs

  • Kubernetes expertise required
  • Complex configuration

Actions Runner Controller replaces

Last reviewed Sep 13, 2026 · 884 words

GitHub-hosted Linux runners cost about $0.008 per minute on private repositories at last check, after the free allowance, and a team with 30 minutes of CI per push burns through that faster than anyone budgets. Actions Runner Controller (ARC) puts those runners on a Kubernetes cluster you already pay for, scales them from 0 to whatever the queue needs, and destroys each pod after one job. It is the officially maintained way to do this, it works, and it is more machinery than most small teams need. Below is what the install looks like, where it hurts, and the two cheaper options I would try first.

Two modes, and only one is worth learning

ARC has a legacy mode (RunnerDeployment resources, cert-manager, a webhook or polling-based autoscaler) and the newer runner scale set mode, which GitHub adopted when it took over maintenance. The scale set mode uses a long-lived listener that holds a connection to GitHub's job queue and asks the controller for pods as jobs arrive, so scaling is driven by demand rather than polling. It needs no cert-manager and no webhook. Every new install should use it; the legacy documentation still ranks well in search and will lead you astray. The whole thing is 2 Helm charts:

helm install arc \
  --namespace arc-systems --create-namespace \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller

helm install arc-runner-set \
  --namespace arc-runners --create-namespace \
  --set githubConfigUrl="https://github.com/your-org" \
  --set githubConfigSecret.github_token="ghp_..." \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set

The release name of the second chart, arc-runner-set, becomes the label your workflows target with runs-on: arc-runner-set. Use a GitHub App rather than a personal token for anything beyond a trial; the App's credentials go in the same secret with github_app_id, github_app_installation_id and github_app_private_key. The controller itself idles in a couple hundred megabytes; the 512 MB figure covers controller plus listener, not the runner pods, which are sized per job.

The Docker-in-Docker decision

The default runner pod cannot build container images, because it has no Docker daemon. You pick one of 2 container modes. containerMode.type=dind adds a privileged sidecar running dockerd, which gives you docker build exactly as on a GitHub-hosted runner and makes your security team unhappy, because a privileged pod on a shared cluster is a node escape waiting for a CVE. containerMode.type=kubernetes runs each job step in its own pod through the runner container hooks, which avoids privilege but breaks any action that assumes a local Docker socket, and needs a ReadWriteMany volume for the workspace. Most teams choose dind on a dedicated node pool with a taint, which is the pragmatic answer, and a dedicated node pool means the cost saving is smaller than the headline.

Where it hurts in practice

Caching. Every pod is fresh, so actions/cache round-trips to GitHub's cache service over the internet, and a 2 GB Docker layer cache pulls every job. You end up running a registry mirror and a persistent cache volume anyway. Image pulls: the runner image is over 1 GB and each scale-up on a new node pays for it. Public repositories: GitHub's own guidance is not to use self-hosted runners on public repos, because a pull request from a stranger runs code on your cluster. Debugging: a failed job's pod is gone before you can kubectl exec into it, so logs and artefacts have to go out during the run. None of these are blockers; all of them are afternoons.

The cheaper options to try first

Your situationDo this
Under about 3,000 CI minutes a monthStay on GitHub-hosted runners
One team, steady load, no KubernetesA single VM with the runner installed as a service
Kubernetes already in production, bursty loadARC in scale set mode
You want CI off GitHub entirelyForgejo or Gitea Actions with their own runner

The single-VM runner is underrated. A 4-core VM running the runner binary as a systemd service, with Docker installed and a cron to prune images, handles a small team's CI with persistent caches and no orchestration. It does not scale to zero and it does not isolate jobs, and for one team on private repos both are acceptable. If your objection to GitHub-hosted runners is GitHub rather than the price, the CI/CD category has the full set of self-hosted forges and runners, and Forgejo's runner speaks the same workflow syntax closely enough that most actions port unchanged.

What I'd do

If you already run Kubernetes for real workloads and your Actions bill has 4 digits, install ARC in scale set mode with a GitHub App, dind on a tainted node pool, a registry mirror in-cluster, and a minimum of 0 runners so idle costs nothing. If you do not already run Kubernetes, do not adopt it for CI; put the runner on a VM and spend the saved week on tests. And if the goal is independence from GitHub, ARC is the wrong project: it makes GitHub cheaper, not optional.

Similar automation & workflows apps