Ephemeral CI runners
A clean, isolated VM per build, with snapshots to skip the slow parts
CI runners have a well-known tension: you want a completely clean environment per build (no state leaking between runs, no “works on this runner but not that one”), but a clean environment means reinstalling dependencies every single time. A microVM plus a snapshot gives you both: full isolation per build, without paying the setup cost on every run.
The pattern
Describe the build environment in a Fusefile, bring it up, install dependencies once, and snapshot that state as your baseline:
version: 1
image: ci-base # a rootfs baked on the host, not an OCI ref
resources:
cpus: 4
memory: 8GB
storage: 20GB
setup:
- npm ci
- bun install --frozen-lockfile
The baseline only installs dependencies, there is deliberately no run: here. A fork
cold-boots the snapshot’s disk, so neither setup nor run executes again on it,
which is why the test command comes from fuse environment exec further down instead.
fuse up # build the "warm" environment once
fuse snapshot create <id> --comment "warm deps, main branch" --mode manual
For every actual CI run after that, fork from the snapshot instead of rebuilding from scratch:
fuse environment fork <id> --reuse-snapshot <snapshot-id>
- Forking from a snapshot skips
setupentirely, dependencies are already installed on disk. Only your actual test command runs fresh. - Each fork is a genuinely separate environment: no filesystem or process state is shared between builds, so one flaky build can’t corrupt another’s working tree the way it can on a shared CI runner. Placement is a separate question, see the bullet below.
- Every fork lands on the host its source lives on, and it never goes through the scheduler, so parallel build capacity is bounded by that one host rather than by the fleet. There is no capacity check on the fork path either, so enough concurrent forks will overcommit that host rather than being refused.
- The baseline environment has to stay up. Fork snapshots the source’s host-local state and requires the source to be in the running state, so destroying the warm VM (or losing its host) invalidates every snapshot you were forking from and you have to re-cut the baseline.
--commentis ignored when you pass--reuse-snapshot, it only lands on the seed snapshot that a plainforkcreates, so keep build labels in the task id or in your CI system instead.- Re-cut the baseline snapshot whenever your lockfile changes (a new dependency install step in your pipeline that only runs on a cache miss), everything else keeps forking from the same warm state.
Running the actual test command
Once the fork is up, run your test suite the same way you’d run any command in a guest:
fuse environment exec <forked-id> -- npm test
exec exits with the guest command’s exit code, so it composes with your
CI system’s normal pass/fail detection, no extra parsing needed. See
fuse environment exec.
Why not just restore instead of fork
Restore is in-place only: it overwrites the original VM the snapshot came from, so every CI run would fight over the same environment instead of running in parallel. Fork is the right primitive here because it creates an independent new environment from the snapshot each time, which is exactly what concurrent CI builds need.