Opsio - Cloud and AI Solutions
DevOpsCI/CDCloud7 min readΒ· 1,301 words

Self-Hosted GitHub Actions Runners: Setup, Scaling, and Security on AWS, Azure, and GCP

Published: Β·Updated: Β·Reviewed by Opsio Engineering Team
Johan Carlsson

Country Manager, Sweden

AI, DevOps, Security, and Cloud Solutioning. 12+ years leading enterprise cloud transformation across Scandinavia

Self-Hosted GitHub Actions Runners: Setup, Scaling, and Security on AWS, Azure, and GCP

GitHub-managed runners are excellent for short jobs but break down on three workloads: long-running integration tests, GPU-bound builds, and anything that needs to reach into a private VPC. The fix is self-hosted runners β€” runner agents you operate on your own AWS, Azure, or GCP infrastructure. Done well, self-hosted runners cut CI bills by 60-80% on heavy workloads and unlock cloud-resource access that managed runners cannot reach. Done badly, they become a security liability and a perpetual maintenance burden.

This guide walks through the runner setup that we deploy in customer engagements: ephemeral, autoscaled, OIDC-authenticated runners with the Actions Runner Controller (ARC) on Kubernetes, and the equivalent patterns for AWS, Azure, and GCP without Kubernetes. It assumes you already have a workflow library you want to migrate or extend, and that you have read our pillar on github actions implementation services.

Why Self-Host Instead of Paying for Managed Minutes?

The pricing math is straightforward. GitHub-hosted Linux Standard runners list at roughly $0.008/minute (April 2026 prices), with larger runners scaling up to $0.064/minute on the 64-vCPU tier. Run a single 30-minute test suite on a 16-vCPU GitHub-hosted runner across 200 PRs/week and you are paying ~$2,000/month for that one workflow. The same workload on a Spot-priced m6i.4xlarge in eu-west-1 costs ~$300/month. The crossover is at roughly 25 active CI hours per week per concurrency slot β€” below that, managed runners win on operational simplicity; above it, self-hosted wins on cost.

Cost is not the only driver. Three other reasons surface in every customer engagement:

  • VPC access β€” runners that need to talk to private RDS, internal APIs, or non-public artifact registries
  • Compliance β€” regulated estates where build artefacts must never leave a specific region or VPC
  • Hardware shape β€” GPU-backed builds, Apple-silicon runners for iOS, FPGA-attached runners for hardware testing

The Right Architecture: Ephemeral, Autoscaled, OIDC-Federated

A long-lived runner registered to a repo is the worst-case design: it accumulates state across jobs, the registration token sits on disk, and one compromised job leaks the runner's credentials to the next. The right design is the opposite β€” runners are ephemeral (one job, then destroyed), autoscaled to zero, and authenticate via short-lived OIDC tokens rather than long-lived PATs.

The Actions Runner Controller (ARC) on Kubernetes is the reference implementation in 2026 and our default recommendation. Its sister project for non-K8s estates β€” terraform-aws-github-runner, the Philips Labs module β€” is the equivalent on AWS without Kubernetes.

Free Expert Consultation

Need expert help with self-hosted github actions runners?

Our cloud architects can help you with self-hosted github actions runners β€” from strategy to implementation. Book a free 30-minute advisory call with no obligation.

Solution ArchitectAI ExpertSecurity SpecialistDevOps Engineer
50+ certified engineersAWS Advanced Partner24/7 support
Completely free β€” no obligationResponse within 24h

AWS: Actions Runner Controller on EKS or Fargate-Backed terraform-aws-github-runner

The two viable patterns on AWS:

  1. ARC on EKS β€” runners are pods, scale via the AutoscalingRunnerSet CRD, scheduled onto Karpenter-managed Spot nodes. Runner pods are ephemeral, deleted after each job.
  2. terraform-aws-github-runner (Philips Labs) β€” runners are EC2 instances or Lambda-launched containers, lifecycle-managed by Lambda functions reacting to the GitHub workflow_job webhook. No Kubernetes required.

For ARC on EKS, the runner-set definition looks like this:

apiVersion: actions.github.com/v1alpha1
kind: AutoscalingRunnerSet
metadata:
  name: opsio-eks-runners
  namespace: arc-runners
spec:
  githubConfigUrl: https://github.com/opsio/platform
  githubConfigSecret: gh-app-secret
  minRunners: 0
  maxRunners: 50
  runnerScaleSetName: linux-x64-large
  template:
    spec:
      nodeSelector:
        karpenter.sh/capacity-type: spot
        kubernetes.io/arch: amd64
      containers:
        - name: runner
          image: ghcr.io/actions/actions-runner:2.319.1
          resources:
            requests: { cpu: 4, memory: 8Gi }
            limits:   { cpu: 4, memory: 8Gi }

Runners authenticate to GitHub via a GitHub App installation, not a personal access token. Per-job AWS access uses OIDC federation: the runner's GITHUB_TOKEN exchange via aws-actions/configure-aws-credentials@v4 assumes an IAM role scoped to that workflow. No long-lived AWS credentials live on the runner.

Azure: ARC on AKS or VMSS-Backed Runners

On Azure the equivalent patterns are ARC on AKS (functionally identical to the EKS configuration above) or Azure VM Scale Sets driven by an Azure Function listening to the workflow_job webhook. AKS with Karpenter (now generally available on Azure) is our default for customers already operating AKS clusters; VMSS is the lightest-weight option for organisations that want self-hosted runners without operating Kubernetes.

For Azure-specific OIDC, the runner uses azure/login@v2 with workload identity federation:

- uses: azure/login@v2
  with:
    client-id: ${{ vars.AZURE_CLIENT_ID }}
    tenant-id: ${{ vars.AZURE_TENANT_ID }}
    subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

The trust relationship on the Entra ID app registration must allow the GitHub OIDC issuer (https://token.actions.githubusercontent.com) and scope the federated identity credential to the specific repository and branch or environment. Wildcards on this trust ("any workflow on any branch") are the most common Azure misconfiguration we see β€” restrict the audience and subject claims explicitly.

Customers running broader Azure DevOps estates frequently combine self-hosted runners with our azure devops services for the workflow design and migration work.

GCP: ARC on GKE or Cloud Run Jobs

On GCP, ARC on GKE follows the same shape as EKS or AKS. The newer alternative is Cloud Run Jobs running runner containers on demand β€” simpler to operate than GKE for organisations that don't already run Kubernetes, with cold-start times of 5-15 seconds for a fresh runner.

OIDC federation uses Workload Identity Federation: the runner exchanges its GitHub OIDC token for a short-lived GCP access token via a Workload Identity Pool. Configuration:

- uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: projects/123/locations/global/workloadIdentityPools/gh/providers/gh
    service_account: gha-deploy@my-project.iam.gserviceaccount.com

Security: The Five Controls Every Self-Hosted Runner Needs

Self-hosted runners are arbitrary code-execution environments. The five controls below are non-negotiable in any production deployment:

  1. Ephemeral runners only β€” never persist state between jobs. ARC and the terraform-aws-github-runner module both default to ephemeral; do not turn this off
  2. Network egress restrictions β€” runners should only reach the registries, package mirrors, and cloud APIs they need. Blanket internet egress is how supply-chain attacks reach you
  3. Short-lived credentials only β€” OIDC federation, never PATs or long-lived service-account keys on the runner filesystem
  4. Repository allowlist β€” runner groups should only accept jobs from explicitly named repositories, not "any repo in the org"
  5. No public-fork acceptance β€” never run unreviewed PRs from public-fork contributors on self-hosted runners. Use the workflow_run pattern to separate untrusted execution from trusted deploy

The fifth control is the one most teams skip β€” and it is the one that has caused most of the publicly disclosed self-hosted-runner compromises (including the well-known 2023 incident on a major OSS project).

Operational Costs at Steady State

A typical mid-market customer (50 engineers, 200 PRs/week, mixed test suites) running ARC on EKS-Karpenter-Spot in eu-west-1 spends roughly:

  • Compute: $400-900/month (vs. $2,500-4,500 on managed runners)
  • EKS control plane: $73/month per cluster
  • Karpenter, ARC, observability stack: bundled into existing platform overhead

The break-even on the migration effort is typically 2-4 months. Beyond that, self-hosted is pure savings, and you also gain the VPC and compliance properties as side effects.

How Opsio Helps

Opsio's managed github actions service deploys self-hosted runners with the security and autoscaling configuration above as a managed offering on AWS, Azure, or GCP. Most engagements run 6-10 weeks: discovery and workload classification, runner topology design, ARC deployment, OIDC federation rollout, and a documented operating model the customer's platform team owns afterwards. For customers running broader cloud platforms underneath the runner fleet, we also operate aws cloud delivery and devops services on top of which the runner infrastructure runs.

For hands-on delivery, see cloud solutions services.

About the Author

Johan Carlsson
Johan Carlsson

Country Manager, Sweden at Opsio

AI, DevOps, Security, and Cloud Solutioning. 12+ years leading enterprise cloud transformation across Scandinavia

Editorial standards: This article was written by a certified practitioner and peer-reviewed by our engineering team. We update content quarterly to ensure technical accuracy. Opsio maintains editorial independence β€” we recommend solutions based on technical merit, not commercial relationships.