Actions Runner Controller

Actions Runner Controller is the Kubernetes controller that registers self-hosted GitHub Actions runners as pods and scales the fleet with the job queue.

Actions Runner Controller (ARC) is a Kubernetes controller, published by GitHub at github.com/actions/actions-runner-controller under the Apache License 2.0, that registers self-hosted GitHub Actions runners as pods in a cluster you operate and scales their count with the job queue. A workflow reaches those runners the ordinary way, by naming the runner scale set in runs-on, and ARC creates one ephemeral runner pod for each assigned job.

The word controller is doing the work in that name. ARC supplies no machines of its own. It reconciles Kubernetes objects against the queue GitHub reports, and every node those pods land on, every image they boot from, and every upgrade of the controller itself belongs to whoever owns the cluster.

Definition

ARC installs from two Helm charts. The gha-runner-scale-set-controller chart deploys the controller manager once per cluster. The gha-runner-scale-set chart is installed once per runner scale set, and a scale set is the unit a workflow targets. GitHub's documentation describes the project as one it adopted to release as a new GitHub product, with support covering the latest Autoscaling Runner Sets version.

A running deployment has three moving parts. Component behavior below is from GitHub's ARC concept documentation, and the resource kinds live in the actions.github.com/v1alpha1 API group, both checked on 2026-08-13.

PieceKubernetes shapeWhat it does
Controller managerDeployment from the gha-runner-scale-set-controller chartWatches the custom resources in the actions.github.com group and reconciles the desired runner count
ListenerOne pod per scale set, backed by AutoscalingListenerHolds an outbound HTTPS long poll to the GitHub Actions Service and stays idle until a Job Available message arrives
Runner podsEphemeralRunnerSet and EphemeralRunnerOne pod per assigned job, registered with a just-in-time configuration and removed when the job ends

Two properties follow from that shape. The cluster needs no inbound network path, because the listener dials out and waits. And each job starts on a pod that was created for it, so nothing survives from the previous job on the same runner.

How a job reaches a pod

Routing is a label match, the same rule any self-hosted runner follows. The Helm installation name of a scale set becomes the label it registers with, so a release installed as arc-runner-set answers runs-on: arc-runner-set. The scaleSetLabels chart key, rendered into the custom resource as runnerScaleSetLabels, adds further labels for jobs that want to select on several at once.

What the chart exposes

The scale set chart takes githubConfigUrl for the repository, organization, or enterprise scope, and githubConfigSecret for the GitHub App or personal access token that authenticates the listener. minRunners and maxRunners bound the fleet. containerMode.type decides how container jobs and service containers work, with dind running a Docker daemon beside the runner and kubernetes using the runner container hooks and a work volume claim. The runner pod template is a full PodSpec, so image, resources, security context, volumes, and node selection are all yours to set.

Two generations

Older material describes a different ARC. The community generation used the actions.summerwind.net API group, RunnerDeployment plus HorizontalRunnerAutoscaler, and an inbound webhook server for queue-driven scaling. The current generation uses autoscaling runner sets, the actions.github.com group, and the outbound listener described above, with no webhook to expose. Chart keys and troubleshooting advice do not transfer between the two, so the first thing to check about any ARC instruction is which generation it was written for.

Example

The prerequisites are a Kubernetes cluster and Helm 3. Installing the controller and one scale set is two Helm releases:

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/my-org" \
  --set githubConfigSecret.github_token="$GITHUB_PAT" \
  --set minRunners=0 \
  --set maxRunners=20 \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set

The second release is named arc-runner-set, and that name is the label a workflow asks for:

name: build
on:
  push:
    branches: [main]

jobs:
  package:
    runs-on: arc-runner-set
    steps:
      - uses: actions/checkout@v4
      - run: make package

When that push lands, GitHub queues the package job and sends a Job Available message down the listener's open connection. The controller creates an EphemeralRunner, Kubernetes schedules a pod on a node with room for it, the runner agent inside registers with a just-in-time configuration, claims the job, runs actions/checkout and make package, and the pod is deleted. With minRunners at zero the scale set holds no pods between jobs, so the next push pays a pod start before the first step runs.

What the cluster then owns

Those two Helm commands leave four standing jobs with the team that ran them.

Autoscaling. minRunners and maxRunners are static Helm values, so a change in scale bounds is a redeploy. GitHub's scale set documentation states that ARC does not support scheduled maximum and minimum configurations and suggests a cron job or another scheduling solution to update the configuration on a schedule. Warm capacity is bought by raising minRunners, and those idle pods hold cluster nodes around the clock.

Images. The shipped runner image carries the minimum packages needed for the runner binaries, so a toolchain of your own means an image of your own, kept structurally valid: the agent under /home/runner/, the container named runner, and the container hooks present when Kubernetes mode is used. Rebuilds are on a clock, because GitHub documents that a self-hosted runner which goes 30 days without a software update stops being queued jobs.

Storage. Kubernetes mode needs a work volume claim served by a dynamic provisioner, and the volume is per runner pod. In dind mode the Docker layers a build writes live inside the pod and disappear with it, so a layer cache that survives across jobs is a separate service somebody sets up and runs.

Upgrades. GitHub documents that Helm cannot upgrade or delete CRDs, so upgrading ARC is a five step procedure: uninstall every scale set release, wait for cleanup, uninstall the controller, remove the actions.github.com CRDs when they changed, then reinstall. The documented way to keep runners available through that window is a second Kubernetes cluster in another region. The gha-runner-scale-set chart shipped five releases in the twelve months to 2026-08-13, the latest being 0.14.2 on 2026-05-22 (releases, read on 2026-08-13).

FAQ

What does Actions Runner Controller actually do?

It runs inside a Kubernetes cluster, watches custom resources in the actions.github.com API group, and keeps a set of self-hosted GitHub Actions runners registered as pods. A listener pod per runner scale set holds an outbound long poll to the GitHub Actions Service, and each assigned job gets one ephemeral runner pod that is deleted when the job finishes.

How does a workflow target an ARC runner scale set?

By the name the scale set was installed under. The Helm installation name becomes the label the scale set registers with, so a release installed as arc-runner-set is reached by runs-on: arc-runner-set. Setting runnerScaleSetLabels adds further labels, and a job then names them the way it names any other self-hosted runner labels.

How does ARC decide how many runners to run?

The listener creates one ephemeral runner per assigned job, bounded by the minRunners and maxRunners values in the scale set chart. Both are static Helm values, so changing them is a redeploy, and GitHub's documentation states that ARC does not support scheduled maximum and minimum configurations and suggests a cron job to update the configuration on a schedule.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.