self-host →

Self-hosted GitHub Actions runners, done properly.

Ephemeral EC2 runners in your own AWS account: any instance per job, picked by label, gone when the job ends. One CloudFormation stack to deploy, one line to migrate. Then it just runs.

15-day free trial · free for non-commercial use

the label is a query — pick constraints, resolved at launch

.github/workflows/ci.yml yaml
# the label is a query — each line one constraint
jobs:
  build:
    runs-on:
      - runs-on=${{ github.run_id }}
      - cpu=16
      - family=c7i
      - image=ubuntu24-full-x64
      - extras=s3-cache
    steps: [...]
cpu
family
image
extras
see all job labels →
→ resolves to c7i.4xlarge · spot · ~$0.23/hr github-hosted 16cpu: $2.52/hr ≈ 11× cheaper

Queued to terminated in one loop.

A job is queued, an instance boots in your account, the job runs, the instance is terminated. About 30 seconds from queued to running, and nothing waits around in between. If GitHub must not call into your account at all, Fleet runs the same loop pull-based: no inbound webhook, no public endpoint.

Job lifecycle · one fresh box per job
// github // your aws account workflow_job queued · webhook label = query → c7i.4xlarge · spot ec2 boots fresh vm · ~20s job runs whole box, yours terminated nothing left idle job
01/05 workflow_job runs-on queued · labels parsed
712×

cheaper than GitHub-hosted runners — you pay AWS directly, no per-minute markup.

do the math on your bill →
1.5M+

jobs every day — ephemeral runners launched and destroyed, one box per job.

8M+

vCPUs daily — from 1 to 896 per runner, across x64 · arm64 · gpu.

running in production, every single day — ~1.5% of all github actions runs

Everything you were working around. Built in.

Instance choice, caching, nested virtualization: the parts a runner needs around it. They come with the stack, already wired, and you turn them on from the label.

01

Dynamic instance selection

The instance is the job. The label is a query — family, cpu, ram, image, volume — resolved against live spot capacity at launch. No pools to size, no scale sets to babysit.

1–96cpu x64 · arm64 · gpu spot → on-demand
02

Caching that's actually fast

Flip extras=s3-cache and actions/cache is backed by an S3 bucket in your account — same region, same VPC, no 10GB ceiling. Nothing else in the workflow changes.

no 10GB cap in-VPC restores
03

Nested virt, both OSes

KVM for Linux, Hyper-V for Windows — nested virtualization is just a label. Android emulators, VM-based e2e suites, Windows containers: they run.

KVM Hyper-V m8i · c8i · r8i

+ docker layer caching · per-job cost reporting · spot→on-demand retry · static egress IPs · warm pools · byo AMIs

Same job. Without the Kubernetes cluster.

The same workflow on Actions Runner Controller, then on RunsOn. One needs a cluster, a controller and an autoscaler. The other needs a label.

What changes when you switch.

From GitHub-hosted runners, you keep the workflow and change the bill. From Actions Runner Controller, you keep self-hosting and drop the cluster.

coming from

GitHub-hosted runners

dimension GitHub-hosted RunsOn
price per minute, at GitHub's rate AWS spot at cost, 7–12× cheaper
machines a fixed menu of sizes any EC2 instance, x64 · arm64 · gpu
cache 10GB per repo S3 in your VPC, no ceiling
code runs on GitHub's infrastructure your AWS account, fresh VM per job
setup none one CloudFormation stack, ~10 min
coming from

Actions Runner Controller

dimension ARC RunsOn
prerequisite a Kubernetes cluster, plus Helm one CloudFormation stack, no cluster
scaling scale sets on nodes you size one EC2 per job, scales to zero
images containers you build and patch AMIs from GitHub's templates, kept current
windows & gpu extra node groups and images a label
idle cost node pools sized for peak none — nothing runs between jobs

“Saved around 75% of our costs, and tests now run 5× faster on gigantic spot instances.”

Tim Dumol — Founding Engineer, Expedock

“After benchmarking a lot of tools, it's the best. Costs divided by 4 — thousands of jobs per day.”

Corentin Smith — CTO, Dashdoc

“Costs down 70%, CI runtime improved up to 80%. A clear win with virtually no downside.”

Théophile D. de Segonzac — Lead DevOps, Lingoda

Everything else you'd want to know.

The questions your security review, your finance team and your on-call rotation will ask. The long versions are in the docs.

01 Will my existing workflows break? +
No. The runner images are ported from GitHub's own templates, so actions, cache steps and secrets keep working as they are. The migration is the one-line label change at the bottom of this page. Linux x64 and arm64, Windows and GPU are all included, and GitHub Enterprise Server is supported.
02 Can I switch one job at a time? +
Yes. The label is per job, so anything still on ubuntu-latest keeps running on GitHub. Most teams move the slow, expensive jobs first and the rest when convenient. macOS jobs stay where they are: RunsOn is Linux and Windows on EC2, and the two coexist in the same workflow.
03 How fast does a job start? +
About 30 seconds from queued to running on a fresh instance. Warm pools bring that under 6 seconds where it matters. And since the instance is whatever size you asked for, the job itself finishes sooner.
04 Does Docker work? +
Yes. Each job gets the whole VM, so Docker, docker-in-docker and compose behave as they do on any EC2 instance. Docker layer caching is built in.
05 Is it safe to run inside my AWS account? +
That's the point of it. Every job gets a fresh EC2 instance that is destroyed when the job ends — nothing shared, nothing persistent. Code, secrets, caches, logs and the GitHub App credentials never leave your account; GitHub sends in the job request and nothing else. The control plane runs in your account on Fargate under a limited IAM role, and with Fleet there is no inbound endpoint at all. The full model is on the security page.
06 What does leave my account? +
Two small flows to runs-on.com, both over TLS: a license check (org name, region, license key, app version) and a per-launch telemetry event (region, instance type, boot timings, enabled features). Neither carries repository contents, secrets, or anything from the runner filesystem, and neither sits in the job's execution path.
07 What does it need from my AWS account? +
An account and a region. The CloudFormation stack creates the rest: VPC, cache bucket, IAM roles and the scheduler, which runs on Fargate. It can reuse an existing VPC instead, and one license covers as many stacks as you want — production, staging, another region.
08 What happens when spot capacity dries up? +
RunsOn tries spot first and falls back to on-demand when capacity is unavailable, at a 2–3s launch penalty. A job interrupted mid-run is retried once on an on-demand instance by default. You can pin any job to on-demand from the label, and a circuit breaker moves the whole stack to on-demand when interruptions spike.
09 What does it cost beyond the license? +
Your AWS bill: spot instances for the minutes your jobs actually run, plus a control plane that costs a few dollars a month. There is no markup on compute, and your existing AWS credits and commitments apply. Per-job cost reporting is built in, and the calculator does the math against your current GitHub bill.
10 How locked in am I? +
As little as it gets. Leaving is the same one-line label change in reverse; then you delete the CloudFormation stack and nothing remains. The license is one flat annual fee per legal entity — no per-minute meter, no seats — and the 15-day trial runs the real product in your account, not a sandbox.

Three steps. One line to migrate.

About ten minutes from an empty AWS account to the first job landing on a spot instance you own.

01

Deploy the stack

One CloudFormation template — or the official Terraform module. VPC, cache bucket, IAM, and the scheduler come up together.

02

Connect the repo

Install the GitHub App on your org. RunsOn registers ephemeral runners on demand — nothing stays running idle.

03

Change one label

Swap a single line in your workflow. Every existing action, cache step, and secret keeps working untouched.

# the entire migration, in one diff
runs-on: ubuntu-latest
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64

15-day free trial · one flat annual license, see pricing

runs on your account · runs on spot · runs on arm64, windows, gpu · runs on one label