All Articles

Build a Self-Hosted GitHub Actions Runner on Kubernetes (2026)

GitHub-hosted runners are slow and expensive at scale. Here's how to set up self-hosted GitHub Actions runners on Kubernetes with auto-scaling using Actions Runner Controller.

DevOpsBoysApr 4, 20263 min read
Share:Tweet

GitHub-hosted runners have 2 CPUs and 7GB RAM. They start cold. They can't access your private network. And at scale, they get expensive.

Self-hosted runners on Kubernetes solve all of this — and with Actions Runner Controller (ARC), they auto-scale based on your queue.


What You'll Build

  • Actions Runner Controller (ARC) installed via Helm
  • Runner scale set that auto-scales from 0 to N runners
  • GitHub Actions workflow using your self-hosted runners
  • Runners with access to your cluster's private network

Prerequisites

  • Kubernetes cluster (EKS, GKE, or local k3s)
  • kubectl and helm installed
  • GitHub repository or organization
  • GitHub Personal Access Token (PAT) with repo scope

Step 1: Install cert-manager

ARC requires cert-manager for webhook TLS:

bash
helm repo add jetstack https://charts.jetstack.io
helm repo update
 
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set installCRDs=true

Wait for cert-manager pods to be ready:

bash
kubectl get pods -n cert-manager

Step 2: Install Actions Runner Controller

bash
helm repo add actions-runner-controller \
  https://actions-runner-controller.github.io/actions-runner-controller
helm repo update
 
# Create namespace
kubectl create namespace arc-systems
 
# Install ARC
helm install arc \
  --namespace arc-systems \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller

Step 3: Create GitHub PAT Secret

bash
# Create a GitHub PAT with repo/admin:org scope
# Store it as a Kubernetes secret
 
kubectl create secret generic controller-manager \
  --namespace arc-systems \
  --from-literal=github_token=<YOUR_GITHUB_PAT>

Step 4: Deploy a Runner Scale Set

A RunnerScaleSet auto-scales runners based on job queue depth:

bash
# For a repository
helm install arc-runner-set \
  --namespace arc-runners \
  --create-namespace \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
  --set githubConfigUrl="https://github.com/YOUR_ORG/YOUR_REPO" \
  --set githubConfigSecret=controller-manager \
  --set minRunners=0 \
  --set maxRunners=5

Check runners registered:

bash
kubectl get pods -n arc-runners

In GitHub → Settings → Actions → Runners, you should see your runner set appear.


Step 5: Use Self-Hosted Runner in Workflow

yaml
# .github/workflows/build.yml
name: Build and Deploy
 
on:
  push:
    branches: [main]
 
jobs:
  build:
    runs-on: arc-runner-set   # name of your scale set
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Build Docker image
        run: |
          docker build -t myapp:${{ github.sha }} .
      
      - name: Run tests
        run: |
          docker run --rm myapp:${{ github.sha }} npm test
      
      - name: Deploy to cluster
        run: |
          kubectl set image deployment/myapp \
            myapp=myapp:${{ github.sha }} \
            -n production

Custom Runner Image

The default runner image has common tools. For specific needs, build your own:

dockerfile
FROM ghcr.io/actions/actions-runner:latest
 
# Install additional tools
USER root
RUN apt-get update && apt-get install -y \
    awscli \
    kubectl \
    helm \
    && rm -rf /var/lib/apt/lists/*
 
USER runner

Configure ARC to use your custom image:

bash
helm upgrade arc-runner-set \
  --namespace arc-runners \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
  --set template.spec.containers[0].image=your-registry/custom-runner:latest \
  --set githubConfigUrl="https://github.com/YOUR_ORG/YOUR_REPO" \
  --set githubConfigSecret=controller-manager

Why Self-Hosted Runners Win at Scale

GitHub-HostedSelf-Hosted on K8s
CPU/RAM2 vCPU, 7GBYou choose (4, 8, 16 vCPU)
Cold start30–60s10–15s
Cost$0.008/minYour cluster cost
Private networkNoYes
Custom toolsRe-install each runBaked into image
Auto-scalingFixed concurrency0 to N based on queue

At 1000+ runs/month, self-hosted runners on Kubernetes are 3–5x cheaper than GitHub-hosted.


Troubleshooting

Runners not appearing in GitHub:

bash
kubectl logs -n arc-systems \
  deployment/arc-gha-runner-scale-set-controller

Jobs not picked up:

bash
kubectl get runners -n arc-runners
kubectl describe runner <runner-pod> -n arc-runners

Permission issues: Make sure your PAT has the correct scope:

  • Repository: repo scope
  • Organization: admin:org scope

What You've Built

A Kubernetes-native CI/CD runner fleet that:

  • Scales to zero (no idle cost)
  • Scales up automatically when jobs are queued
  • Has direct access to your cluster's internal services
  • Runs on your hardware with your custom tools

This is the setup used by teams running hundreds of pipelines per day.


Resources

Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments