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
+------------------------------------------+| 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:
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:
## Download runner packagemkdir actions-runner && cd actions-runnercurl -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.gztar 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 servicesudo ./svc.sh installsudo ./svc.sh start ## GitLab runner registrationgitlab-runner register \ --url https://gitlab.com \ --token REGISTRATION_TOKEN \ --executor docker \ --docker-image alpine:latest \ --description payment-api-runnerPractical Commands
## GitHub CLI -- list runners for a repositorygh api repos/OWNER/REPO/actions/runners ## List runner groupsgh api orgs/ORG/actions/runner-groups ## GitLab -- check runner statusgitlab-runner statusgitlab-runner list ## Jenkins -- check node statusjava -jar jenkins-cli.jar -s http://jenkins.internal list-nodesTroubleshooting
| 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 |
SecurityNever 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.
RememberGitHub-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 MistakeInstalling 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.
TipUse 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 onruns-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.