Skip to content

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:

LabelOS / ArchitectureCPURAMDisk
ubuntu-latest, ubuntu-24.04, ubuntu-22.04Linux x644 vCPU16 GB14 GB SSD
ubuntu-24.04-arm, ubuntu-22.04-armLinux arm644 vCPU16 GB14 GB SSD
windows-latest, windows-2025, windows-2022Windows x644 vCPU16 GB14 GB SSD
macos-latestmacOS arm64 (Apple Silicon)3 vCPU7 GB14 GB SSD
ubuntu-slimLinux x64, container-based1 vCPU5 GB14 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
Last updated on