Skip to main content

Runner

A CI/CD runner is the machine or container that executes jobs in a pipeline. GitHub-hosted runners are managed ephemeral VMs. Self-hosted runners run on your own infrastructure — giving control over hardware, networking, installed tools, and access to private resources.

Understanding CI/CD Runners

What Is a Runner in Simple Terms

A runner is the worker that actually runs your pipeline jobs. When GitHub Actions starts a job, it needs somewhere to execute the commands — that somewhere is a runner. Think of runners as the factory workers on your assembly line: the pipeline is the process, the runner is the person (or machine) doing the work.

GitHub-hosted runners are virtual machines that GitHub spins up for each job and terminates when the job completes. Self-hosted runners are machines you manage — physical servers, VMs, or Kubernetes pods — that stay registered with your CI platform and pick up jobs from a queue.

How It Works

◈ DIAGRAM
+------------------------------------------+
| GitHub Actions Workflow |
| job: build |
| runs-on: ubuntu-latest |
+------------------------------------------+
|
job queued
|
v
+------------------------------------------+
| GitHub picks an available runner |
| |
| GitHub-hosted: |
| Fresh Ubuntu VM, 2 vCPU, 7GB RAM |
| Pre-installed: Node, Python, Docker |
| Cost: billed per minute |
| Terminated after job completes |
+------------------------------------------+
OR
+------------------------------------------+
| Self-hosted: |
| Your server/VM/Kubernetes pod |
| Custom tools, faster network, |
| access to private resources |
| Cost: your infrastructure |
+------------------------------------------+

Runner selection by label:

YAML
jobs:
build:
## GitHub-hosted runner
runs-on: ubuntu-latest
deploy-prod:
## Self-hosted runner with specific label
## Only runs on your runner in the production VPC
runs-on: [self-hosted, production, linux]
build-arm:
## GitHub-hosted ARM runner
runs-on: ubuntu-latest
strategy:
matrix:
platform: [linux/amd64, linux/arm64]

Self-hosted runner setup:

Bash
## Download runner package
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.311.0.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz
tar xzf ./actions-runner-linux-x64-2.311.0.tar.gz
## Configure runner (get token from GitHub repo settings)
./config.sh \
--url https://github.com/razorpay/payment-api \
--token RUNNER_TOKEN \
--labels production,linux,payment-vpc
## Install as a service
sudo ./svc.sh install
sudo ./svc.sh start
## GitLab runner registration
gitlab-runner register \
--url https://gitlab.com \
--token REGISTRATION_TOKEN \
--executor docker \
--docker-image alpine:latest \
--description payment-api-runner

Practical Commands

Bash
## GitHub CLI -- list runners for a repository
gh api repos/OWNER/REPO/actions/runners
## List runner groups
gh api orgs/ORG/actions/runner-groups
## GitLab -- check runner status
gitlab-runner status
gitlab-runner list
## Jenkins -- check node status
java -jar jenkins-cli.jar -s http://jenkins.internal list-nodes

Troubleshooting

Symptom Check What to Look For
Job queued but not starting Runner availability All runners busy or offline
Self-hosted runner offline Runner service status ./svc.sh status on runner machine
Tool not found on runner Runner image/labels Use correct runner label for required tools
Runner picking wrong jobs Label configuration Labels must match exactly
Security

Never use self-hosted runners on public repositories unless you have implemented strict security controls. A malicious PR can execute arbitrary code on your self-hosted runner — which may have access to production credentials and infrastructure. Use GitHub-hosted runners for public repos and self-hosted only for private repos with restricted access.

Remember

GitHub-hosted runners start fresh every time — nothing persists between jobs. If Job A installs a tool, Job B does not have it. Use setup actions (actions/setup-node, actions/setup-python) at the start of each job, or build a custom runner image with your tools pre-installed.

Common Mistake

Installing tools directly on a self-hosted runner instead of using Docker containers or setup actions. A runner that has been manually configured over time becomes a snowflake — impossible to reproduce, impossible to scale horizontally, and a nightmare when the machine needs to be replaced. Keep runners stateless and install tools per-job using setup actions or Docker.

Tip

Use runner labels to target specific hardware for specific jobs. A Docker build job that benefits from a high-core-count machine can use runs-on: [self-hosted, high-cpu]. An ARM image build runs on runs-on: [self-hosted, arm64]. Label your runners by capability, not by machine name, so you can scale horizontally.

Frequently Asked Questions

When does it make sense to use self-hosted runners instead of hosted ones?

Self-hosted runners earn their operational overhead when jobs need access to private network resources (an internal database, an on-prem service), require specific hardware (GPUs for ML training, ARM architecture), need software licenses that can't be installed on ephemeral hosted VMs, or when hosted-runner minutes become a significant cost at scale. Hosted runners remain the better default otherwise — they're pre-provisioned, isolated per job, and require zero patching or capacity management from your team.

What's a security risk specific to self-hosted runners that hosted runners avoid?

A self-hosted runner attached to a public repository will happily execute code from any pull request, including forks from untrusted contributors, in an environment that persists between jobs (unlike hosted runners' fresh VM per job) — letting an attacker's PR potentially exfiltrate secrets or leave persistent malware on your infrastructure. This is why self-hosted runners should generally be restricted to private repos, or gated behind approval for workflows triggered by external contributors.