Field Notes


3 min read

Self-Hosted GitHub Runner in Kubernetes

GitHub’s hosted runners are convenient, but they bill by the minute, can’t reach anything inside your network and start from a clean machine every time. If you already run a Kubernetes cluster, you can run the jobs there instead. Actions Runner Controller (ARC) starts a fresh runner pod for each job and removes it afterwards.

This note uses the current setup GitHub maintains: the gha-runner-scale-set charts. Older guides use the community summerwind controller with RunnerDeployment resources. Don’t mix the two.

What you need

  • A Kubernetes cluster and kubectl access to it. A bare-metal cluster works fine.
  • Helm 3.
  • Owner rights in the GitHub organisation that should get the runners.

1. Create a GitHub App

ARC needs credentials to register runners. A GitHub App is better than a personal access token: it isn’t tied to a person and its permissions are narrow.

In the organisation settings, under Developer settings → GitHub Apps → New GitHub App:

  • Give it a name and any homepage URL. Turn off the webhook.
  • Under Organization permissions, set Self-hosted runners to Read and write.
  • Create the app, then generate a private key. A .pem file is downloaded.
  • Install the app on your organisation.

Note three values:

  • the App ID, shown on the app’s settings page,
  • the installation ID, the number at the end of the URL after you open the installed app under the organisation’s GitHub Apps settings,
  • the path to the private key file.

Keep the key file out of any repository.

2. Install the controller

The controller gets its own namespace:

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

Without --version, Helm installs the latest chart. Pin a version once it works, so an upgrade happens when you decide.

3. Store the app credentials

The runners get a second namespace. The secret has to live in the same namespace as the runner scale set:

kubectl create namespace arc-runners

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

The key names (github_app_id, github_app_installation_id, github_app_private_key) are fixed. ARC looks for exactly these.

4. Install a runner scale set

A runner scale set is a pool of runners for one organisation. Its installation name becomes the label you use in workflows:

helm install arc-runner-set \
  --namespace arc-runners \
  --set githubConfigUrl="https://github.com/<ORGANISATION>" \
  --set githubConfigSecret=arc-github-app \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set

5. Check that it runs

helm list -A
kubectl get pods -n arc-systems

Both the controller and a listener pod should be Running. The scale set also appears in the organisation settings under Actions → Runners.

There are no runner pods yet. They start when a job arrives.

6. Use it in a workflow

Set runs-on to the installation name:

name: Runner test
on:
  workflow_dispatch:

jobs:
  hello:
    runs-on: arc-runner-set
    steps:
      - run: echo "Running on $(hostname)"

Start the workflow from the Actions tab and watch the pods:

kubectl get pods -n arc-runners -w

A runner pod appears, runs the job and disappears again.

Things to know

  • Each job gets a fresh pod. Nothing survives between jobs, which is good for security and bad for caches. Use actions/cache or a registry cache for build layers.
  • The default runner image has no Docker daemon. Jobs that build images need the scale set’s containerMode set to dind (Docker in Docker) or kubernetes. Read the chart’s values.yaml before you choose.
  • Self-hosted runners and public repositories don’t mix. A pull request from a fork could run code on your cluster. Use them for private repositories only.

More in Field Notes

Share