GitHub Actions Runners

Bare-metal Linux, Windows, and macOS runners for GitHub Actions. Swap one label, keep your workflow.


One-line adoption#

Sandal runners are drop-in replacements for GitHub-hosted runners. Change runs-on, nothing else.

.github/workflows/ci.yml
jobs:
  test:
    runs-on: sandal-ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - run: pnpm install
      - run: pnpm test

That's it. Your existing steps, actions, and secrets work unmodified, only the executor underneath changes.

JIT ephemeral, single-tenant#

Every job gets its own microVM, Firecracker on Linux/Windows, a dedicated Mac mini for macOS, provisioned just-in-time when the job is queued and destroyed the moment it finishes. No shared kernel, no leftover state, no noisy neighbors.

  • Runner registration is JIT: Sandal registers a fresh, single-use runner token with GitHub for each job, so nothing sits idle in your runner pool between builds.
  • Cold create <1s for Linux/Windows microVMs; macOS jobs claim from a warm pool of physical minis.
  • Kernel-level isolation means untrusted forks, third-party actions, and monorepo matrices never share a blast radius.

Warm pools#

Cold starts are fine for most CI, but if you're running hundreds of jobs an hour, keep instances pre-booted:

import { Sandal } from "@sandal/sdk";

const sandal = new Sandal({ apiKey: process.env.SANDAL_API_KEY });

await sandal.runners.pools.create({
  os: "macos",
  labels: ["sandal-macos-m4"],
  minWarm: 2,
});

minWarm keeps N runners booted and idle-registered so jobs claim instantly instead of waiting on a cold boot. Pools scale down to zero automatically when idle, you only pay active-CPU while a job is actually running.

Cache and sticky disks#

Attach a persistent disk to a label so dependency caches, build artifacts, and toolchain installs survive across jobs without re-downloading:

runs-on: sandal-ubuntu-24.04
env:
  SANDAL_STICKY_DISK: pnpm-store-v1

Sticky disks are content-addressed and mounted read-write per job, then snapshotted incrementally at the end of the run, no full re-upload, just the delta. This is the same per-minute incremental snapshot engine (V4/RAFS) that powers sandbox pause/resume, so restores are lazy and prefetch-driven instead of blocking on a full disk pull.

Pricing#

Billed per second, active-CPU only. Paused or queued time costs nothing.

RunnervCPU / RAMPrice
sandal-ubuntu-24.042 vCPU / 4GB$0.003/min
sandal-ubuntu-24.04-4x4 vCPU / 8GB$0.006/min
sandal-ubuntu-24.04-8x8 vCPU / 16GB$0.012/min
sandal-ubuntu-24.04-16x16 vCPU / 32GB$0.024/min
sandal-windows-20222 vCPU / 4GB$0.006/min
sandal-windows-2022-4x4 vCPU / 8GB$0.012/min
sandal-macos-m4dedicated M4 mini$0.06/min

Windows runners are priced at 2x the equivalent Linux shape (Windows Server microVM overhead). macOS runs on dedicated Mac minis, not shared virtualization, capacity is metered per-minute, not per-second-fractional, but billing still stops the instant the job ends.

Migration wizard#

Point the CLI at an existing workflow file and it rewrites runs-on labels, flags actions that assume GitHub-hosted-only behavior (like actions/cache paths tied to /opt/hostedtoolcache), and opens a PR:

$ npx sandal migrate --repo your-org/your-repo --workflow ci.yml

The wizard is conservative, it never touches job logic, only the runs-on line and any pool/label wiring it can infer from your matrix strategy. Review the diff before merging.

Pools API#

For programmatic control, scaling pools ahead of a release branch cut, or tearing them down after a migration freeze, use the SDK directly:

import { Sandal } from "@sandal/sdk";

const sandal = new Sandal({ apiKey: process.env.SANDAL_API_KEY });

// Create a warm pool
const pool = await sandal.runners.pools.create({
  os: "linux",
  labels: ["sandal-ubuntu-24.04-4x"],
  minWarm: 5,
});

// Scale it
await sandal.runners.pools.update(pool.id, { minWarm: 10 });

// List active pools
const pools = await sandal.runners.pools.list();

// Tear down
await sandal.runners.pools.delete(pool.id);

Every pool emits per-second-billed metrics, active jobs, warm idle count, and claim latency, queryable from the same evidence-gated capability matrix that backs the rest of Sandal's numbers.