What is a Runner?
runs-on: ubuntu-latest has appeared in every workflow so far without much explanation — just “the machine that runs the job.” It’s time to actually unpack what that machine is, what you get for free, and where it quietly changes shape under your feet.
What A Runner Actually Is
A runner is a real virtual machine — not a container, not a shared box (however Single-CPU runners are hosted in a container) — that GitHub provisions the moment your job is ready to start, and destroys the moment it finishes. Nothing persists between runs unless you explicitly save it (which is a topic for later). Every job gets a completely clean machine: fresh OS, fresh disk, nothing left behind by any previous run — including previous jobs in the same workflow, a detail that becomes important once you start using multiple jobs. The runs-on key is simply a label telling GitHub which flavor of machine to hand your job.
The Menu Of Standard Runners
These are the labels available on GitHub-hosted runners today (may change, see docs), and what each one actually gives you on a public repository:
| Label | OS / Architecture | CPU | RAM | Disk |
|---|---|---|---|---|
ubuntu-latest, ubuntu-24.04, ubuntu-22.04 | Linux x64 | 4 vCPU | 16 GB | 14 GB SSD |
ubuntu-24.04-arm, ubuntu-22.04-arm | Linux arm64 | 4 vCPU | 16 GB | 14 GB SSD |
windows-latest, windows-2025, windows-2022 | Windows x64 | 4 vCPU | 16 GB | 14 GB SSD |
macos-latest | macOS arm64 (Apple Silicon) | 3 vCPU | 7 GB | 14 GB SSD |
ubuntu-slim | Linux x64, container-based | 1 vCPU | 5 GB | 14 GB SSD |
ubuntu-latest is the workhorse you’ll use for nearly everything in this course — it’s fast to provision, has by far the widest tool support, and (as covered in the previous chapter’s setup) is free and unlimited on public repositories.
Warning
ubuntu-slim has a hard 15-minute job timeout, and runs your steps inside a lightweight container rather than a full VM — some operations that need elevated privileges (mounting filesystems, Docker-in-Docker) don’t work there. It’s meant for short, lightweight automation tasks, not the kind of build-and-test jobs you’re setting up in this section.
Note
Private repositories get a smaller ubuntu-latest. As mentioned when you set up your practice repository, the exact same ubuntu-latest label resolves to a 2 vCPU / 8 GB machine on a private repo instead of the 4 vCPU / 16 GB you get on a public one. The label doesn’t change — the hardware behind it does, depending on repository visibility.
Runners Come With Sudo, Not Root
GitHub-hosted Linux and macOS runners run as a regular user with passwordless sudo — you can install packages or run privileged commands without ever being prompted for a password. Windows runners run as an administrator instead, with User Account Control disabled. You won’t need this for anything in this chapter, but it’s useful to know it’s there once you start needing tools that aren’t preinstalled.
Try Switching Runners
Open hello.yml and change runs-on from ubuntu-latest to windows-latest:
jobs:
greet:
runs-on: windows-latest
steps:
- name: Say hello
run: |
echo "Hello from GitHub Actions!"
echo "This job was triggered by: $GITHUB_ACTOR"Push that and watch it run. It fails — or produces garbled output, depending on the exact syntax. The reason is the default shell changes with the operating system: Linux and macOS runners default to bash, but Windows runners default to PowerShell (pwsh). Your $GITHUB_ACTOR syntax is bash-specific; PowerShell doesn’t understand it the same way.
You can force a specific shell per step with a shell: key:
- name: Say hello
shell: bash
run: |
echo "Hello from GitHub Actions!"
echo "This job was triggered by: $GITHUB_ACTOR"Adding shell: bash makes this work on windows-latest too — GitHub-hosted Windows runners ship with Git Bash preinstalled specifically so this is possible. But this is a good moment to internalize the underlying lesson rather than just patch around it: the same run step can behave completely differently depending on which runner executes it, and that’s a gotcha worth remembering long after this example.
Revert back to ubuntu-latest before moving on — everything from here forward in this section assumes Linux:
jobs:
greet:
runs-on: ubuntu-latest
steps:
- name: Say hello
run: |
echo "Hello from GitHub Actions!"
echo "This job was triggered by: $GITHUB_ACTOR"
echo "Repository: $GITHUB_REPOSITORY"
echo "Current time on the runner: $(date)"Commit and push that revert before continuing:
git add .github/workflows/hello.yml
git commit -m "Revert to ubuntu-latest runner"
git push