self-host →

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
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:

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.

For Flex, define reusable runners in .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:

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

For 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-*"
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:

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 as root before the runner agent) 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.

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