# runs-on in GitHub Actions: labels, examples, and the one line that cuts your bill

> runs-on picks the machine a job runs on, and with it what GitHub bills. ubuntu-latest bills every minute; a self-hosted label bills nothing. runs-on: skipbox moves a job to a self-hosted runner without the upkeep: 4 vCPU for 20,000 minutes a month is $39 flat instead of $222.

The `runs-on` line decides two things: the machine your job gets, and what GitHub charges for it. `runs-on: ubuntu-latest` bills every minute on a private repository, and a team running 20,000 minutes a month on GitHub's 4-core runner pays $222. A self-hosted label bills nothing.

Skipbox is a self-hosted runner without the upkeep. Write `runs-on: skipbox` and the job runs on your account's own machine for a flat $39 a month, unlimited minutes.

- `runs-on` picks the runner, and the runner sets the bill: hosted labels per minute, self-hosted labels $0 on GitHub
- 4 vCPU for 20,000 minutes a month: $222 on GitHub, $39 with `runs-on: skipbox`
- Docker layers and package caches stay on the machine between jobs, so builds skip the downloads

**GitHub Actions at a flat price, unlimited minutes** Skipbox runs your GitHub Actions jobs on your own dedicated machine: $39 a month, however many minutes you run. Docker layers and dependency caches stay between jobs, so the next job starts right away.

A 4 vCPU runner, 20,000 minutes a month: GitHub $222 → Skipbox $39. You save $183 every month.

Moving takes one line: `runs-on: skipbox`.

[Start my 7-day free trial](https://app.skipbox.dev/sign-in?ref=g-github-actions-runs-on-en)

## What runs-on takes

`runs-on` accepts a single label, an expression, a list of labels, or a runner group:

```yaml
jobs:
  hosted:
    runs-on: ubuntu-latest
  self-hosted:
    runs-on: [self-hosted, linux, x64]
  from-a-group:
    runs-on:
      group: build-runners
      labels: ubuntu-24.04-16core
```

A list is AND, not OR: the runner must carry every label. A self-hosted runner gets `self-hosted`, its OS (`linux`, `windows`, `macOS`) and its architecture (`x64`, `ARM`, `ARM64`) by default, plus custom labels you add. A larger runner is reached by the name its organization owner gave it. If no online runner matches, the job waits in the queue, and GitHub fails it after 24 hours.

## What your runs-on line costs you

On a private repository, `ubuntu-latest` is 2 CPUs and 8 GB at $0.006 a minute, and GitHub's 4-core runner is $0.012 a minute. Here is GitHub's 4-core runner against Skipbox's 4 vCPU plan:

| Minutes a month (4 vCPU) | GitHub | Skipbox S | You save each month |
|---|---|---|---|
| 10,000 | $102 | $39 | **$63** |
| 20,000 | $222 | $39 | **$183** |
| 30,000 | $342 | $39 | **$303** |
| Always on | $500.40 | $39 | **$461.40** |

GitHub's figures are on GitHub Team, after its included compute. Try your own minutes in the [calculator on the home page](https://skipbox.dev/?ref=g-github-actions-runs-on-en#cost).

The minutes keep climbing. Coding agents push more often than people, each new test suite adds jobs to every push, and a budget that caps the bill stops your jobs mid-month. Every line that says `ubuntu-latest` is a meter running.

## The self-hosted line, and the work behind it

GitHub bills nothing for self-hosted runner minutes; the charge it announced in December 2025 was shelved the next day. So the cheapest `runs-on` line is a self-hosted one. Behind it, if you run it yourself, sits a machine you own:

- servers sized for your builds, and more of them when jobs queue up
- the OS, Docker and runner application to keep current
- cleanup between jobs, so one job's credentials and containers never reach the next
- someone to fix it when it stops taking jobs

Skipbox is that machine, run for you.

## Skipbox: one label, no machine to run

### A flat price for every size

| `runs-on` | Machine | Plan, per month |
|---|---|---|
| `skipbox` | 4 vCPU, 16 GB | S, $39 |
| `skipbox-8x16` | 8 vCPU, 16 GB | M, $79 |
| `skipbox-16x32` | 16 vCPU, 32 GB | L, $149 |

Minutes are unlimited. Skipbox registers as a self-hosted runner, so its jobs use none of your GitHub minutes and keep running when an Actions budget runs out. Jobs beyond your plan's machines wait their turn.

### Faster, because the machine stays

Each plan is a VM kept for your account. Docker images, layers and build cache, package caches and the tool cache stay on its disk between jobs, so the next job starts with them. That is at least 7× the cache room GitHub gives a repository, with no cache keys to maintain.

### Nothing to manage

Skipbox updates the image, wipes each job's leftovers, health-checks the machine after every job and rebuilds one that fails. The VMs run Ubuntu 24.04 with Docker, Node.js and Python.

## Moving is one line

```yaml
jobs:
  test:
    runs-on: skipbox # was ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test
```

1. Sign in to Skipbox with GitHub.
2. Install the Skipbox GitHub App and pick the repositories.
3. Change `runs-on` and push.

In a matrix, give the Linux leg `skipbox` and leave Windows and macOS legs on their GitHub labels. To move a job back, change the line back.

## Security

- Each account gets its own machine, for public and private repositories.
- The runner's credentials, processes and containers are wiped after every job; from 13 November 2026 the checkout and Cargo's `target/` stay, so the next build is incremental.
- Code from people without write access (such as pull requests from forks) runs on a fresh VM that is deleted afterwards.

## Try it on your own jobs

The trial is free for 7 days or 3,000 job-minutes, whichever comes first, with no card. Change one `runs-on` line to `skipbox`, push twice, and compare the second run with what `ubuntu-latest` gave you.

## FAQ

### What does runs-on mean in GitHub Actions?

It names the machine a job runs on: a GitHub-hosted label such as ubuntu-latest, the labels of a self-hosted runner, the name of a larger runner, or a runner group. It also decides the bill, because hosted runners bill per minute and self-hosted runners don't. runs-on: skipbox puts a job on Skipbox, a self-hosted runner at a flat monthly price.

### Can runs-on have multiple labels?

Yes, as a list, and a runner must carry all of them: runs-on: [self-hosted, linux, x64] matches only a runner with all three. Skipbox needs one label per job, such as skipbox or skipbox-8x16.

### How do I use a self-hosted runner in runs-on?

Give the runner's labels, starting with self-hosted, for example runs-on: [self-hosted, linux, x64]. A runner service gives its machines its own labels: on Skipbox the line is runs-on: skipbox, and Skipbox runs the machine behind it.

### What happens if no runner matches runs-on?

The job waits in the queue, and GitHub fails it after 24 hours. A typo in a label queues the job instead of failing it at once, so check the spelling first. On Skipbox, a job beyond your plan's machines also waits its turn and starts as soon as one is free.

### How do I use runs-on with a matrix?

Put the labels in a matrix list, such as os: [skipbox, windows-latest, macos-latest], and set runs-on to the matrix's os value. GitHub starts one job per value. A Linux leg can use skipbox while Windows and macOS legs keep their GitHub labels.

Updated 2026-11-11 · https://skipbox.dev/guides/github-actions-runs-on
