Run microVMs in a KVM sandbox
Give the agent a sandbox with /dev/kvm, to run a hypervisor of its own such as Firecracker and the microVMs it starts.
Recipe: Nested virtualization
An agent develops or tests something that starts virtual machines: a microVM runtime, a
Firecracker setup, a test suite that boots guests. The sandbox
has no /dev/kvm. The kvm-sandbox recipe is the same sandbox with it, and with Firecracker in
/usr/local/bin. First, connect your agent.
Check that this sandbox can run Firecracker, then run the project's tests in it.
briq_run({ recipe: "kvm-sandbox" })
→ bq_3m8w1x5k7qzd sandbox (ghcr.io/briq-cloud/sandbox:kvm) running · cost so far … · sleeps at 18:42 UTC unless used
outputs:
WORKDIR=/work
briq_exec({ briq: "bq_3m8w1x5k7qzd", cmd: ["ls", "-l", "/dev/kvm"] })
→ crw… /dev/kvm
briq_exec({ briq: "bq_3m8w1x5k7qzd", cmd: ["firecracker", "--version"] })
→ Firecracker v…briq_runstarts alargemicroVM (4 CPUs, 8 GB) with a 40 GB volume at/work.- The boot runs
kvm-readybefore thesteps: udev, the ZFS module, the cgroup controllers. It logs one line,kvm-ready: /dev/kvm, kernel …, zfs 2.4.1, cgroup controllers: …. If it fails, the start fails andbriq_logssays why. briq_execruns commands as in the sandbox./dev/kvmandfirecrackerare there for them.
Boot a microVM
A guest needs a kernel and a root filesystem. The sandbox brings neither: use your own, or follow
Firecracker's
getting started,
which starts a guest from /usr/local/bin/firecracker. Put the files on /work: the volume is kept
while the sandbox sleeps.
Run a test suite
A suite that boots guests takes minutes. Run it with async: true and read the result when it is
done (Run a long command):
briq_exec({ briq: "bq_…", cmd: ["sh", "-c", "cd /work/app && cargo test"], async: true })
→ ex_5t2n8k1w9qrd running since 10:02 UTC
briq_exec_result({ briq: "bq_…", exec: "ex_5t2n8k1w9qrd" })
→ ex_5t2n8k1w9qrd exited 0 after 14mA running async command counts as use, so the sandbox does not sleep under it. Work briq cannot
see (a daemon you started, a guest left running) needs a longer sleep, "3h" or "never":
briq_run({ recipe: "kvm-sandbox", sleep: "3h" })Set up at boot
Clone and build in steps, as in the sandbox. They run at every start, and every start is a cold boot:
briq_run({
recipe: "kvm-sandbox",
steps: [{ run: "git clone --depth 1 https://github.com/acme/vmm ." }]
})Limits
- x86_64 only.
- Not everywhere.
briq_cataloglists the recipe only where/dev/kvmis available. - The agent's policy has to allow the size. Otherwise
policy_violation: seeallowedSizesin Keys and policy. - Every start is a cold boot. No memory is kept while the sandbox sleeps. A guest running
inside is gone after a sleep, and so is every process: start them again. Files on
/workstay. - A long run needs
sleep. A longer value, or"never", keeps the sandbox up. - Pro and Team plans. A Hobby team gets a
plan_limiterror. largeby default.xlargefor a full test suite or many VMs at once; see Cost.- Kernel modules come from the host. The ZFS module matches the host's kernel, not the image; another module the kernel does not have cannot be loaded.
- A fork copies the disk only. The guests it runs are not in it.
Run real systemd units
systemd is in the image, but it is not the init. For code that needs real units, start it as PID 1
of namespaces of its own, with / shared as on a server:
briq_exec({ briq: "bq_…", cmd: ["unshare", "--pid", "--fork", "--mount", "--cgroup", "--mount-proc", "sh", "-c",
"mount -t cgroup2 none /sys/fs/cgroup; mount --make-rshared /; exec env container=lxc /usr/lib/systemd/systemd"], async: true })Commands enter them with nsenter -t <pid of systemd> -a. Without the shared /, a sandboxed unit
keeps copies of mounts made after it starts, and their unmount answers busy. A complete setup also
gives systemd its own /run and masks the units a container has no use for.
Example: a microVM runtime's test suites
A Rust runtime that runs microVMs on Firecracker. Its suites need /dev/kvm, ZFS, nftables, systemd
units and real guests. All of them ran in one sandbox, with a long sleep because they ran detached,
where the briq cannot see them.
Measured on one xlarge, AMD Ryzen host, 2026-11-05, stage times to within 15 s:
| Stage | Result | Time |
|---|---|---|
| Release build, from nothing | 95 s | |
| Install: systemd, then the daemon under its unit | 30 s | |
| Start a guest inside, image already pulled | 1.02 s, 0.83 s | |
cargo test --workspace | 1434 passed | 197 s |
| Fleet smoke | 33 passed | 61 s |
| End-to-end | 294 passed | 561 s |
| Stress, with crashes | 635 checks passed | 501 s |
| Crashes, isolation, a full disk | 134 checks passed | 611 s |
| Alerts fire and resolve | 45 checks passed | 880 s |