Self-hosted GitHub Actions runners
Your CI, on your cloud. Without running a single runner yourself.
Commit an airship.yaml, point runs-on at an airship- label, and every job gets its own machine in your cloud account. Airship launches it, watches it and tears it down.
AWS · Google Cloud · Azure
airship-buildAWSc7i.xlargespotus-east-1busy2m 41sairship-gpuGCPg2-standard-8spotus-central1-blaunching0m 12sairship-buildAWSm7i.xlargeus-east-1running0m 38sairship-windowsAzureStandard_D4s_v5eastusbusy6m 02sairship-smallAWSt4g.mediumspotus-west-2pending—airship-armAWSc7g.2xlargespoteu-west-1finished4m 19sjobs:
test:
runs-on: airship-build--${{ github.run_id }}
steps:
- uses: actions/checkout@v5
- run: make testYour account, your bill
Machines launch in your AWS, Google Cloud or Azure account. You pay the cloud, at your rates.
One file per repo
airship.yaml says what a runner is: machine type, image, region, spot, GPU. runs-on picks it.
Fresh machine per job
Every job gets a new machine that registers for that one job and is gone when it ends.
Spot, then fall back
List instance types in order. Spot capacity first, on-demand when the cloud says no.
Features
Everything a runner needs, in one file
Each repository describes its runners in airship.yaml. Airship turns that into machines.
runners:
- name: build
cloud: aws # or gcp · azure
instance_type: c7i.xlarge
region: us-east-1
labels: [airship-build]
- name: windows
cloud: azure
instance_type: Standard_D4s_v5
machine_image:
publisher: MicrosoftWindowsServer
offer: WindowsServer
sku: 2022-datacenter-g2
labels: [airship-windows]Launching airship-build
- 1c7i.xlargespotInsufficientInstanceCapacity×
- 2m7i.xlargespotMaxSpotInstanceCountExceeded×
- 3c7a.xlargeon-demandi-0a3f…e21 running✓
Idle runners · mon-fri 9-17 → 4, otherwise 1
A job takes a warm runner; a new one boots behind it.
runners:
- name: gpu
cloud: gcp
instance_type: n1-standard-8
region: us-central1 # any zone with capacity
gpu: nvidia-tesla-t4
preemptible: true
labels: [airship-gpu]$ nvidia-smi --query-gpu=name,memory.total
name, memory.total [MiB]
Tesla T4, 15360 MiB
machine_image:
ubuntu2404Canonical, newestami-0123…cdef0your AMIWindows Server 2022Windows boot scriptc7g · t4g · AmpereARM64galleries/ci/images/buildAzure galleryprojects/acme/global/images/ciGCE image
- uses: actions/cache@v4 # unchanged
with:
path: ~/.cargo
key: cargo-${{ hashFiles('Cargo.lock') }}$ curl -H "Authorization: Bearer $AIRSHIP_TOKEN" \
https://api.airshipci.com/api/v1/repo \
-d '{"repository": "acme/web", "active": true}'
{"fullName": "acme/web", "activated": true, ...}How it works
We run the fleet. You own the machines.
Airship's control plane holds your sealed cloud key, listens to GitHub, and launches, watches and terminates machines in your account. Nothing long-lived runs on your side.
Connect a cloudA job asks. A machine answers.
GitHub tells Airship a job wants an airship- label. Airship finds the runner in that repository's airship.yaml and launches it in your account, registered for exactly that job.
Labels without the airship- prefix cost no API calls.
RunInstances→ droppedYour key, sealed at rest
The access key you connect is encrypted, tied to its own database row, and opened only for the length of one cloud call. It is never logged and never sent back.
Give it a role scoped to what Airship launches.
pendinglaunchingrunningbusyfinished
finished, and why
completedcancelledidlelaunch_failedlostmax_lifetime
Nothing left running
Every runner moves through one lifecycle, and every ending is recorded. A reconciler compares the cloud with the database and removes machines nobody is using, so a lost answer from the cloud doesn't become a bill.
Idle 10 minutes, or 8 hours old: the machine goes.
- AWSEC2 · S3 cacheconnected
- Google CloudCompute Engine · GPUsconnected
- AzureVirtual Machinesconnected
Runs where your account already is
Connect one cloud or all three. Each runner names its cloud, and machines launch with your credentials, in your regions, on your bill.
Change cloud: in airship.yaml and nothing else.
Security
Your jobs run on machines you own
Airship reads airship.yaml and job events from GitHub and makes cloud calls with your key. Your code, secrets and build output stay between GitHub and your machines.
- SEALED KEYSEncrypted at rest, opened per call
- YOUR NETWORKYour network, your subnets
- ONE JOBOne machine per job, then gone
- SCOPED CACHEShort-lived grants to one bucket
- SELF-HOSTRun the control plane yourself
Runners register with a just-in-time config for a single job, so a machine can't pick up work it wasn't launched for. Only labels starting with airship- are looked up, and only in repositories someone activated.
Get started
Your first runner in four steps.
- 1Install the GitHub App
- 2Connect a cloud
- 3Activate a repository
- 4Commit airship.yaml
runners:
- name: build
cloud: aws
instance_type: [c7i.xlarge, m7i.xlarge]
preemptible: [true, false]
labels: [airship-build]
extra_config:
cache: true