Runners / Agents
A CI runner is the machine that executes jobs. GitHub Actions, GitLab CI, Buildkite, CircleCI — each provides hosted runners; self-hosted runners give you bigger machines, private network access, or cost control.
Hosted, self-hosted, scaling, security
EXAMPLE
# === GitHub Actions runners ===
# 1) Hosted runners (free for public repos; minutes-based for private)
jobs:
build:
runs-on: ubuntu-latest # Ubuntu 22.04 (or 24.04 with -latest)
# runs-on: ubuntu-22.04
# runs-on: macos-latest # macOS 14 (Apple Silicon)
# runs-on: macos-13 # Intel
# runs-on: windows-latest # Windows Server 2022
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
# Specs (GitHub-hosted, free tier):
# ubuntu-latest: 2 vCPU, 7 GB RAM, 14 GB SSD
# macos-latest: 3 vCPU, 7 GB RAM, 14 GB SSD
# windows-latest: 2 vCPU, 7 GB RAM, 14 GB SSD
# Larger runners (paid):
# runs-on: ubuntu-latest-4-cores
# runs-on: ubuntu-latest-8-cores
# runs-on: ubuntu-latest-16-cores-x64
# GPU runners: T4 / A10G available
# 2) Self-hosted runners
jobs:
build:
runs-on: [self-hosted, linux, x64, gpu]
steps:
- uses: actions/checkout@v4
- run: npm ci
# Labels are tags. Match the specific runner by label combinations.
# Install:
# 1. Settings → Actions → Runners → New self-hosted runner
# 2. Run the install commands on the target machine:
# mkdir actions-runner && cd actions-runner
# curl -O -L https://github.com/actions/runner/releases/download/v2.X/actions-runner-linux-x64-2.X.tar.gz
# tar xzf ./actions-runner-linux-x64-2.X.tar.gz
# ./config.sh --url https://github.com/ORG/REPO --token TOKEN
# ./run.sh
# 3. Run as a service (Linux):
# sudo ./svc.sh install
# sudo ./svc.sh start
# 3) Autoscaling self-hosted with EC2 / Kubernetes
# - actions-runner-controller (Kubernetes operator) — scale on workflow demand
# - Phillips Lang's terraform-aws-github-runner — EC2 autoscaling
# - Hosted services: BuildJet, Runs-on, BlackSmith
# Kubernetes:
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata: { name: builder }
spec:
replicas: 0 # scale on demand via autoscaler
template:
spec:
repository: my-org/my-repo
labels: [self-hosted, linux, x64, builder]
---
apiVersion: actions.summerwind.dev/v1alpha1
kind: HorizontalRunnerAutoscaler
metadata: { name: builder-hra }
spec:
scaleTargetRef:
name: builder
minReplicas: 0
maxReplicas: 20
metrics:
- type: PercentageRunnersBusy
scaleUpThreshold: '0.75'
scaleDownThreshold: '0.25'
# 4) Group + targeting
jobs:
test:
runs-on: [self-hosted, linux, x64, '${{ matrix.os }}']
strategy:
matrix:
os: [ubuntu-22, ubuntu-24]
# === GitLab Runners ===
# 1) Shared runners (GitLab.com) — free for public projects
build:
image: node:20
script:
- npm ci
- npm test
# 2) Specific runner via tag
build:
tags: [docker, large]
script:
- npm ci
- npm test
# 3) Self-hosted Docker executor
# Install GitLab Runner on a VM
sudo apt install gitlab-runner
sudo gitlab-runner register
# Provide GitLab URL + registration token
# Choose 'docker' executor + base image
# /etc/gitlab-runner/config.toml
[[runners]]
name = "my-runner"
url = "https://gitlab.com"
token = "..."
executor = "docker"
[runners.docker]
image = "alpine:latest"
privileged = false # set true only if running docker-in-docker
# 4) Kubernetes executor — autoscaling
[[runners]]
executor = "kubernetes"
[runners.kubernetes]
namespace = "gitlab-runners"
cpu_request = "500m"
memory_request = "1Gi"
# === Why self-hosted? ===
# Hosted runner pros:
# ✅ Zero ops; just write the workflow
# ✅ Always patched, fresh OS
# ✅ Pre-installed tools (Node, Python, Docker, AWS CLI, ...)
# ✅ Pay-per-minute model is straightforward
# Self-hosted runner reasons:
# ✅ Bigger machines (more CPU/RAM/disk) — needed for big builds
# ✅ Access to private network (VPC services, internal registries)
# ✅ GPU access for ML pipelines
# ✅ Custom hardware (ARM, specific kernels)
# ✅ Faster repeat builds (warm cache)
# ✅ Lower cost at scale
# === Self-hosted runner security ===
# CRITICAL: Self-hosted runners run untrusted code from your repo.
# If your repo accepts PRs from forks, runner = pwned.
# Mitigations:
# ✅ Self-hosted runners ONLY for private repos, OR
# ✅ Block PR triggers from forks (default in GitHub Actions for self-hosted)
# ✅ Use ephemeral runners (one job per runner, then destroy)
# ✅ Isolate networks — runners in their own VPC / namespace
# ✅ Limit secret access to specific jobs
# ✅ Audit runner logs + workflow changes
# ✅ Don't give runners access to long-lived cloud creds — use OIDC
# ✅ Run runners as low-privilege users + read-only filesystems where possible
# Ephemeral runners (GitHub):
./config.sh --url ... --token ... --ephemeral
# Runs ONE job, then exits. Combine with autoscaling to spin up fresh.
# === Matrix + parallelism ===
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false
max-parallel: 6
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ matrix.node-version }} }
- run: npm ci
- run: npm test
# This creates 9 jobs (3 node × 3 os) running in parallel.
# max-parallel caps concurrency to control resource use.
# === Larger / GPU runners ===
jobs:
train:
runs-on: ubuntu-latest-4-cores # 4 vCPU, 16 GB RAM
steps:
- run: ./train.py
inference:
runs-on:
- self-hosted
- gpu # tagged runner
steps:
- run: ./infer.py
# Alternatives: BuildJet, Runs-on, BlackSmith offer larger runners at lower cost.
# === Cost optimization ===
# 1. Cancel in-progress on PR pushes (saves minutes)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# 2. Skip duplicate workflows
if: |
!contains(github.event.head_commit.message, '[skip ci]')
# 3. Reduce log verbosity — fewer log writes = faster
# 4. Use smaller runners when possible
# 5. Cache aggressively (npm, pip, gradle, docker layers)
# 6. Self-host for sustained workloads — cheaper than hosted at scale
# === Monitoring runners ===
# GitHub: Settings → Actions → Runners → see status, last job
# Logs: per-job logs in workflow UI; download as artifact
# Metrics: GitHub Actions API + custom dashboard
# Prometheus: actions-runner-controller exposes metrics endpoint
# === Common bugs ===
# ❌ Runner offline / not picked up → check connectivity, token, labels
# ❌ Job hangs forever → set timeout-minutes (default 360 = 6h!)
# ❌ Self-hosted runner exposes secrets across jobs → use ephemeral
# ❌ Privileged Docker on shared runner → escape risk
# ❌ Runner has long-lived AWS keys → use OIDC instead
# ❌ Forgetting fail-fast: false → one failed matrix cell kills all
# === Best practices ===
# ✅ Hosted runners for small / open-source projects
# ✅ Self-hosted with ephemeral mode for private CI at scale
# ✅ Autoscale on demand (Kubernetes, EC2 autoscaling)
# ✅ Pin runner versions in critical workflows
# ✅ Use OIDC instead of long-lived cloud secrets
# ✅ Set timeout-minutes on every job
# ✅ Concurrency + cancel-in-progress on PR branches
# ✅ Matrix builds for cross-platform / cross-version testing
# ✅ Monitor queue depth + average build time as core CI metrics
Why it matters
GitHub-hosted is the right default; self-hosted (ephemeral, autoscaled, in your VPC) wins when you need bigger machines, private network access, or cost control at scale. Use OIDC instead of long-lived cloud credentials for any runner.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
# GitHub: runs-on: ubuntu-latest / windows-latest / macos-latest / self-hosted # GitLab: tags: [docker] # Self-hosted runners run on YOUR machine — useful for GPU / restricted networks.Try it Yourself »
Discussion
Loading…