quickstart →

Custom runners & images

Define custom runner shapes and custom AMIs for RunsOn — per job in Flex (.github/runs-on.yml) or in the Terraform catalog for Fleet.

The built-in runners and images cover most workloads. When you need a specific instance shape, or your own AMI with internal SDKs, slow-changing toolchains, or security agents baked in, define a custom runner and/or a custom image.

The model is the same across products — only where you declare it and who selects it differ: in Flex workflow authors define and pick per job in .github/runs-on.yml; in Fleet the platform team curates a Terraform-owned catalog.

Custom runners#

A custom runner is a named runner shape (CPU/RAM/family/image/storage/extras/…) you reference instead of spelling everything out in the runs-on label. A sticky disk is separate from the runner root volume and gives cache directories persistent EBS storage.

Flex

Define runners in .github/runs-on.yml, then reference them with runner=<name>:

.github/runs-on.yml
runners:
gpu-type1:
family: ["g4dn", "g5"]
image: ubuntu24-gpu-x64
cpu: [4, 16]
spot: price-capacity-optimized
sticky: build-cache:30gb
extras: ["s3-cache"]
preinstall: |
apt-get update && apt-get install -y my-tool
.github/workflows/ci.yml
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=gpu-type1

Common fields: cpu, ram, volume, sticky, family, spot, image, ssh, extras, private, nested-virt, retry, preinstall, prerun, tags. See Repo-level config for the full reference.

Fleet

Fleet defines runner shapes in the Terraform runners map, exposes them through fleets, and workflows target the fleet label:

main.tf
runners = {
gpu = {
family = ["g5.xlarge"]
image = "ubuntu24-gpu-x64"
extras = ["s3-cache"]
sticky = "build-cache:30gb"
}
}
fleets = {
gpu = {
runner = "gpu"
runner_group = "platform"
}
}

See Runner fleets for the full catalog reference.

Use runs-on/action@v2 to place cache paths on a configured sticky disk. See Sticky disks for the label syntax, cache modes, and lifecycle.

Instance type priority#

List exact instance types in family from most to least preferred. For example, ["c8a.large", "c8i.large"] puts AMD ahead of Intel. How AWS uses that order depends on the purchasing option:

ConfigurationHow AWS selects an instance
On-demand (spot: false)RunsOn automatically enables prioritized allocation. AWS tries the first type before using the next to fulfill capacity.
Spot (spot: capacity-optimized-prioritized or spot: cop)AWS honors the order on a best-effort basis, but optimizes for capacity first.
Other Spot settings, including the default spot: trueList order has no effect. AWS uses the selected price/capacity strategy.

Spot prioritization requires v3.3.0 or later.

Flex

Define reusable runners in .github/runs-on.yml:

.github/runs-on.yml
runners:
amd-on-demand:
family: [c8a.large, c8i.large]
image: ubuntu24-full-x64
spot: false
amd-spot:
family: [c8a.large, c8i.large]
image: ubuntu24-full-x64
spot: capacity-optimized-prioritized

Select either runner with runner=amd-on-demand or runner=amd-spot. The equivalent per-job labels are:

.github/workflows/ci.yml
jobs:
on-demand:
runs-on: runs-on=${{ github.run_id }}/family=c8a.large+c8i.large/image=ubuntu24-full-x64/spot=false
spot:
runs-on: runs-on=${{ github.run_id }}/family=c8a.large+c8i.large/image=ubuntu24-full-x64/spot=cop
Fleet

Set the same ordered family list and spot value in the Terraform runners map. Workflow labels still select a fleet; they do not override its instance preferences.

Use exact types throughout the list for predictable ordering. On-demand chooses its allocation strategy after compatibility filtering: if every remaining entry is exact, it uses prioritized allocation; otherwise, it uses lowest-price allocation.

With spot=cop, every entry must be an exact type before filtering. Family names such as c8a, wildcards such as c8a.*, missing selectors, and unrecognized types cause a launch validation error, even when mixed with valid exact types.

Exact types ignore cpu and ram ranges, including values inherited from a runner preset. Choose sizes explicitly and use types compatible with the selected image. An x64 image cannot provide an ARM fallback.

With spot=cop, c8i.large may still launch while c8a.large has capacity: this is a preference, not a strict fallback chain or a fixed distribution. If Spot cannot fulfill the request, RunsOn falls back to on-demand and uses the exact-type order. That fallback may cost more; priority does not mean lowest price. See the AWS allocation strategy documentation ↗.

Custom images#

Every runner launches from an EC2 AMI. Default runners use a public AMI managed by RunsOn; a custom image points RunsOn at your own AMI instead.

Flex

Define the image once in .github/runs-on.yml, then select it from workflow YAML (or from a custom runner’s image):

.github/runs-on.yml
images:
ci-ubuntu24:
platform: linux
arch: x64
owner: "123456789012"
name: "ci-ubuntu24-*"
.github/workflows/ci.yml
jobs:
test:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/image=ci-ubuntu24
Fleet

The same decision belongs in the Terraform-owned catalog:

main.tf
images = {
ci-ubuntu24 = {
owner = "123456789012"
name = "ci-ubuntu24-*"
platform = "linux"
arch = "x64"
}
}
runners = {
linux-ci = {
family = ["c8i.large"]
image = "ci-ubuntu24"
}
}
fleets = {
linux-ci = { runner = "linux-ci", runner_group = "platform" }
}

Image model#

A runner image is an EC2 AMI plus metadata: platform, arch, owner, and either an exact ami ID or a name lookup pattern. The image must match the runner architecture and region — x64 runners need x64 AMIs, arm64 runners need arm64 AMIs, Windows runners need Windows AMIs.

The name pattern can include a wildcard (e.g. ci-ubuntu24-*); RunsOn then queries EC2 at launch time for the most recent matching AMI — the usual way to pick up nightly rebuilds without touching the workflow. Both Flex and Fleet images also support preinstall (runs before the runner agent: as root with bash on Linux, with PowerShell on Windows) and prerun (just-in-time setup, useful with warm pools).

Use custom images for dependencies that change slowly: SDKs, compilers, language runtimes, large base Docker layers, internal tooling, or security agents. Keep fast-changing build outputs in caches instead.

Docker images baked into the AMI are only available where Docker’s data stays on the root volume. On local NVMe and tmpfs runners, RunsOn moves Docker’s data directories to fresh storage at boot, so Docker starts empty. This has always applied to /var/lib/docker, and applies to Docker 29’s containerd image store (/var/lib/containerd) since v3.5.0.

Flex vs Fleet#

Same runner and image model, same metadata fields; only the place where they’re declared and selected changes.

AspectFlexFleet
Where defined.github/runs-on.ymlTerraform module (runners/images)
Who selectsWorkflow author, per jobPlatform team, in the catalog
Per-job ami= override✓—
Wildcard name lookup✓✓
Recommended forWorkflow-specific or experimental shapesShared standardized shapes for many repos