GitHub-hosted runners are convenient, but they come with limitations — fixed hardware specs, no access to private networks, limited concurrency on free tiers, and costs that scale with usage. Running self-hosted runners on Azure Kubernetes Service (AKS) solves all of these: you get autoscaling, private network access, custom tooling pre-installed, and you only pay for the compute you use.

In this guide we use GitHub’s official ARC scale sets — the newer, fully supported controller published by GitHub under ghcr.io/actions/actions-runner-controller-charts. This replaces the older community-maintained summerwind/actions-runner-controller. Key differences: no cert-manager required, built-in autoscaling, OCI Helm charts, and runners identified by scale set name rather than labels.

Prerequisites

Why GitHub App over PAT? GitHub Apps have higher API rate limits, more granular permissions, and short-lived tokens that rotate automatically — making them the right choice for production CI/CD.

Step 1 — Create a GitHub App

Go to your GitHub organisation → Settings → Developer settings → GitHub Apps → New GitHub App and configure it as follows:

Set these Repository permissions:

Set these Organisation permissions (if registering org-level runners):

Click Create GitHub App. On the next screen:

  1. Note the App ID
  2. Click Generate a private key — this downloads a .pem file
  3. Click Install App and install it on your target repository or organisation — note the Installation ID from the URL (github.com/organizations/<org>/settings/installations/INSTALLATION_ID)

Step 2 — Create the GitHub App Secret

Store the GitHub App credentials as a Kubernetes secret in the namespace where runners will run.

kubectl create namespace arc-runners

kubectl create secret generic arc-github-app-secret \
  --namespace arc-runners \
  --from-literal=github_app_id="<APP_ID>" \
  --from-literal=github_app_installation_id="<INSTALLATION_ID>" \
  --from-file=github_app_private_key=./your-app.pem

Step 3 — Install the ARC Controller

The GitHub ARC controller is published as an OCI Helm chart. No cert-manager required.

helm install arc \
  --namespace arc-systems \
  --create-namespace \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
# Verify controller is running
kubectl get pods -n arc-systems
# Expected: arc-gha-runner-scale-set-controller-... Running

Step 4 — Deploy a Runner Scale Set

A runner scale set is a group of identical runners registered to a specific repo or organisation.

helm install nerdsolv-runner-set \
  --namespace arc-runners \
  --set githubConfigUrl="https://github.com/your-org/your-repo" \
  --set githubConfigSecret="arc-github-app-secret" \
  --set minRunners=0 \
  --set maxRunners=10 \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set

Org-level runners: To register runners at the organisation level, set githubConfigUrl="https://github.com/your-org". The GitHub App must have Self-hosted runners: Read & write org permission.

Verify the scale set registered with GitHub:

kubectl get autoscalingrunnersets -n arc-runners
# GitHub repo → Settings → Actions → Runners → nerdsolv-runner-set should appear

Step 5 — Customise the Runner Pod (optional)

Use a values.yaml to set resource limits or configure Docker-in-Docker mode.

# runner-values.yaml
githubConfigUrl: "https://github.com/your-org/your-repo"
githubConfigSecret: "arc-github-app-secret"
minRunners: 0
maxRunners: 10

containerMode:
  type: "dind"   # Docker-in-Docker; use "kubernetes" for rootless builds

template:
  spec:
    containers:
      - name: runner
        image: ghcr.io/actions/actions-runner:latest
        resources:
          requests:
            cpu: "500m"
            memory: "1Gi"
          limits:
            cpu: "2"
            memory: "4Gi"
helm upgrade nerdsolv-runner-set \
  --namespace arc-runners \
  -f runner-values.yaml \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set

Step 6 — Autoscaling (Built-in)

Unlike the old summerwind ARC which required a separate HorizontalRunnerAutoscaler CRD, GitHub ARC scale sets have autoscaling built in. The controller deploys an AutoscalingListener pod that maintains a persistent connection to GitHub and receives job events in real time — no polling delay, no extra manifests.

# Watch runner pods scale up as jobs are queued
kubectl get pods -n arc-runners -w

# Check the autoscaling listener
kubectl get autoscalinglisteners -n arc-runners

# Check ephemeral runner status
kubectl get ephemeralrunners -n arc-runners

With minRunners: 0 the scale set runs zero pods when idle. A runner pod is created when a job is queued and terminated once the job completes — clean environment for every build.

HPA vs ARC autoscaler: Kubernetes native HPA scales on CPU/memory — it has no awareness of job queues. The ARC listener-based autoscaler is the right mechanism here. The two layers are complementary: ARC controls runner pod count, while AKS cluster autoscaler controls node count.

Step 7 — Update Your Workflow

With the new ARC, runs-on targets the Helm release name of your scale set.

# .github/workflows/build.yml
jobs:
  build:
    runs-on: nerdsolv-runner-set   # matches the Helm release name
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: make build

Cost Considerations

With minRunners: 0 you pay for AKS compute only when builds are running. For further savings, add an Azure Spot node pool for runners — up to 90% discount.

az aks nodepool add \
  --resource-group <rg> \
  --cluster-name <cluster> \
  --name spotrunnerpool \
  --priority Spot \
  --eviction-policy Delete \
  --spot-max-price -1 \
  --node-count 0 \
  --min-count 0 \
  --max-count 10 \
  --enable-cluster-autoscaler \
  --node-vm-size Standard_D4s_v3

Summary

GitHub ARC scale sets are simpler and more production-ready than the older summerwind controller — no cert-manager, no separate HRA manifests, OCI Helm charts, and event-driven autoscaling built in. Key steps: create a GitHub App, store credentials as a Kubernetes secret, install the ARC controller, deploy a runner scale set, and point your workflows at the scale set name.

From here you can extend with custom runner images, Workload Identity for Azure RBAC, or separate scale sets per team or environment.

Was this article helpful?

Need help setting this up for your team?

nerdSolv designs and deploys cloud-native infrastructure on Azure, AWS, and GCP.

Talk to us →