🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
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.

DevOpsBoys3 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

🔧

Today I Fixed

Short real fixes from production — posted daily

Browse fixes
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