Runners & Execution Environment
Runners & Execution Environment
In Lesson 1 you learned that a workflow is a YAML file composed of jobs and steps triggered by events. But who actually runs those steps? The answer is a runner — a server (physical, virtual, or container) that listens for work from GitHub Actions, executes each step inside your job, and reports the result back. Every decision you make about runners — which type, which OS, how many, how they are networked — has direct consequences for your pipeline's speed, cost, security posture, and compliance story.
GitHub-Hosted Runners
GitHub maintains a global fleet of ephemeral virtual machines. Every job that runs on a GitHub-hosted runner starts from a freshly imaged VM — there is no state left over from a previous job, by any team or any repository. This is the primary security property of hosted runners: each run is a clean slate.
The three major families and their current specs (billed to your Actions minutes quota or paid per-minute for larger runners):
ubuntu-24.04/ubuntu-22.04— Linux (x64). 4 vCPU, 16 GB RAM, 14 GB SSD. The workhorse for most CI workloads. Fastest boot, cheapest per-minute, best tooling preinstalled.windows-2022/windows-2025— Windows Server (x64). 4 vCPU, 16 GB RAM. Required for .NET Framework targets, MSBuild, and Windows-specific tests. Billed at 2× the Linux rate.macos-15/macos-14— macOS (arm64 on M1 hardware). Required for iOS and macOS app builds (Xcode). Billed at 10× the Linux rate — the most expensive hosted option by far.
GitHub also offers larger runners — 8, 16, 32, or 64 vCPU — for organizations that need them. These require an organization or enterprise plan and are configured under Settings > Actions > Runner groups.
ubuntu-latest is a floating pointer that GitHub advances when a new LTS is ready. In the past, GitHub migrated ubuntu-latest from 20.04 to 22.04 and broke pipelines that relied on system package versions. Always pin to an explicit version (ubuntu-24.04) in production workflows so upgrades are deliberate, not accidental.The runner environment ships with a large software manifest — Node.js, Python, Java, Docker, kubectl, Terraform, and dozens more — but the installed versions change with each new image release. For any tool your pipeline depends on, install it explicitly in a step (using an actions/setup-* action) rather than relying on whatever happens to be preinstalled.
Self-Hosted Runners
A self-hosted runner is any machine you provision, register with GitHub, and maintain yourself. The GitHub Actions runner agent is a small open-source Go binary (github.com/actions/runner) that polls the GitHub API for jobs assigned to it. It runs on Linux, Windows, macOS, and ARM.
The three canonical reasons teams move to self-hosted runners:
- Network access. Your integration tests, deployment scripts, or security scans must reach private resources — an on-premise database, an internal artifact registry, a VPC-hosted Kubernetes cluster — that are not reachable from GitHub's public IP ranges.
- Cost at scale. At high volume (thousands of CI minutes per day), the per-minute cost of GitHub-hosted runners exceeds the amortized cost of owning or renting dedicated compute. Many large engineering organizations run their runners on spot/preemptible VMs to cut cost further.
- Custom hardware. GPU-accelerated ML training, Apple Silicon for iOS native builds, FPGA targets, or ARM hardware-in-the-loop testing all require machines you bring yourself.
Registering a self-hosted runner takes under five minutes:
In production, never register a single runner. Register a fleet behind a label and let GitHub distribute jobs across the fleet. Use runner groups to control which repositories can access which runners — a critical isolation boundary in multi-team organizations.
Runner Groups: Access Control at Scale
Runner groups are an organization-level (or enterprise-level) construct that lets you assign runners to groups and then grant specific repositories access to those groups. This is how big-tech engineering organizations prevent a team's repository from accidentally (or intentionally) scheduling jobs on another team's expensive GPU runners or, worse, on runners that have production credentials loaded.
--ephemeral (available since runner v2.293). In ephemeral mode, the runner deregisters itself after completing a single job, and your infrastructure-as-code (Terraform, Pulumi, or a custom controller) provisions a fresh VM for the next job. This gives you the security property of GitHub-hosted runners (no cross-job state) while keeping the benefits of self-hosted (network access, custom hardware). Tools like actions-runner-controller (ARC) for Kubernetes automate this pattern at scale.Containers as Jobs: The container Key
GitHub Actions supports running an entire job inside a Docker container, rather than directly on the runner host. This is different from a step that happens to run a Docker command — the entire job executes inside the specified image, giving you a fully reproducible environment that matches your production container exactly.
The container key at the job level wraps every step in a Docker container. Steps do not run on the host OS — they run inside the container, with the workspace mounted in. This means if you use a python:3.12-slim image, every step has exactly that Python version, regardless of what the underlying runner OS has installed. No actions/setup-python needed.
Service containers spin up alongside the job container and are reachable by hostname (postgres, redis) via Docker's internal bridge network. The --health-* options ensure Actions waits for the service to be ready before running your steps — a critical detail that prevents flaky tests caused by a race between your app code and the database not yet accepting connections.
Runner Selection Decision Framework
In production, runner selection follows a clear decision tree:
- Start with GitHub-hosted (
ubuntu-24.04) for everything. This is the right default. The cost is predictable, the maintenance burden is zero, and the security model is strong. - Move to self-hosted only when you have a concrete requirement that hosted runners cannot satisfy: private network access, custom hardware, or volume-driven cost savings. Use ephemeral runners (ARC or a custom controller) to preserve the security properties of hosted runners.
- Add
container:when your test environment must match a specific runtime exactly, or when you need sidecar services (databases, message brokers, SMTP servers) that are costly to mock correctly. - Use runner groups as soon as you have more than one team or more than one sensitivity tier of runner (e.g., runners with production cloud credentials vs. runners with no credentials at all).
Understanding runners deeply is foundational for everything that follows in this tutorial — caching strategies, matrix builds, OIDC-based cloud auth, and secrets scoping all interact directly with where and how your jobs execute. In Lesson 3 we move to expressions and contexts — the templating language that lets your workflow logic adapt dynamically to the event, branch, and runner environment at runtime.