Firecracker Sandboxes
Firecracker runs each Temps sandbox as its own microVM with its own guest kernel under KVM, instead of as a container sharing the host kernel. It is opt-in, Linux-only, and has to be provisioned on the host before it can be selected.
What the Firecracker backend is
Temps has two sandbox backends:
| Backend | Isolation | Availability |
|---|---|---|
docker | Container. Shares the host kernel. | Default, always available when Docker is reachable. |
firecracker | microVM. Own guest kernel, KVM-enforced boundary. | Only after temps firecracker setup completes on a Linux host with /dev/kvm. |
When a sandbox runs on Firecracker, Temps boots a VM named with the temps-fcsandbox- prefix from a pinned guest kernel (vmlinux-6.1.141) and a per-VM ext4 root disk. The guest boot arguments are console=ttyS0 reboot=k panic=1 pci=off init=/sbin/temps-vm-agent, plus an ip= argument when the VM is networked.
There is no SSH and no shared filesystem into the guest. Every operation (exec, file read/write, shutdown) travels over vsock to a guest agent, temps-vm-agent, which runs as PID 1:
- Guest CID
3, agent port52, host socketv.sockinside the VM directory - Maximum frame size 64 MiB
- Working directory inside the guest is
/workspace - Timeouts: 15s to wait for the agent to come up, 300s per RPC, 8s shutdown grace before the VM process is killed
Firecracker pins its own component versions: Firecracker and jailer v1.16.1, downloaded and verified against the release .sha256.txt, and the guest kernel verified against a per-architecture sha256 digest baked into the binary.
Host requirements
Setup runs a preflight stage that checks all of these and refuses to continue if any of them fails.
| Requirement | Detail |
|---|---|
| Linux | Any non-Linux host is a hard preflight failure. macOS development machines cannot enable the backend. |
| x86_64 or aarch64 | Both have pinned guest-kernel digests. Other architectures are rejected. |
| Host kernel 4.14 or newer | Read from /proc/sys/kernel/osrelease. |
| CPU virtualization (x86_64) | vmx or svm must appear in /proc/cpuinfo flags. |
/dev/kvm | Must exist and be openable read and write by the user the Temps server runs as. |
mkfs.ext4 | Debian and Ubuntu: apt install e2fsprogs. |
ip | Debian and Ubuntu: apt install iproute2. |
| A reachable Docker daemon | Used to build every rootfs. See Docker is still required. |
temps-vm-agent | The static musl guest binary, installed to <data_dir>/firecracker/bin/temps-vm-agent. Without it there is no way into a VM. |
| Root, for the network stage only | Bridge, NAT, sysctl, and the TAP pool. Everything else runs unprivileged. |
bridge (from iproute2) is optional. It is used for best-effort per-TAP port isolation and is silently skipped when the tool is absent.
On x86_64 the virtualization check is the one that most often fails on rented infrastructure. A cloud VM without nested virtualization cannot run Firecracker; the failure message points at bare metal instead (AWS *.metal instance types, Hetzner dedicated servers).
Setup gives targeted remediation for /dev/kvm problems rather than a generic error: sudo modprobe kvm_intel or kvm_amd when the device is missing, sudo usermod -aG kvm <user> plus a re-login when the open is denied. It also detects the stale-session case, where the user is already in the kvm group but the current login predates the change, and suggests sg kvm -c '...'.
Once provisioned, the runtime availability check requires all five of the following to hold, on every check:
state.jsonrecordssmoke_ok: truebin/firecrackerexistsbin/temps-vm-agentexists- A pinned guest kernel resolves
/dev/kvmopens read and write
Enable it with temps firecracker setup
temps firecracker setup
setup is the only subcommand under temps firecracker. It runs its stages in order:
- Preflight. Everything in Host requirements.
- Binaries. Downloads
firecrackerandjailerat the pinned version and verifies both against the release.sha256.txt. - Guest agent. Installs the
temps-vm-agentbinary into the data directory. - Guest kernel. Downloads
vmlinux-6.1.141and verifies its sha256 digest. - Network (requires root). Creates the
temps-fc-br0bridge, runsip addr replace <subnet>, enablesnet.ipv4.ip_forwardand persists it to/etc/sysctl.d/99-temps-firecracker.conf, installs iptables MASQUERADE and FORWARD rules, and pre-creates the TAP pool (temps-fc-tapprefix). - Jailer uid range check. Aborts with
jailer uid range collisionif the reserved uid range overlaps an existing/etc/passwdentry. - Smoke test. Boots a real VM from
busybox:stablewith 1 vCPU and 128 MiB, with a 60s timeout.
Only a passing smoke test writes smoke_ok: true to <data_dir>/firecracker/state.json, and only that flag makes the backend selectable. If you run setup as a non-root user, the network stage is deferred and setup prints the command to finish it:
sudo temps firecracker setup --network-only
To validate a host without changing anything, use --check. It runs the preflight probes plus a provisioned-state report (binaries, guest kernel, bridge, smoke-test state) and exits non-zero if any check failed, which makes it usable in a provisioning script:
temps firecracker setup --check
To remove the bridge, NAT rules, binaries, kernel, and cached state:
sudo temps firecracker setup --uninstall
Setup flags
- Name
--data-dir- Type
- path
- Description
Data directory for configuration and runtime files. Also readable from the
TEMPS_DATA_DIRenvironment variable. Optional; resolved the same way the server resolves it.
- Name
--check- Type
- flag
- Description
Run every probe without mutating the host, print a provisioned-state report, then exit non-zero if anything failed.
- Name
--network-only- Type
- flag
- Description
Run only the network stage (bridge, NAT, TAP pool). Requires root.
- Name
--uninstall- Type
- flag
- Description
Remove the bridge, NAT rules, binaries, kernel, and cached state.
- Name
--skip-smoke- Type
- flag
- Description
Skip the smoke-test VM boot. The backend is explicitly left disabled (
smoke_ok: false), so Firecracker will not be selectable afterwards.
- Name
--subnet- Type
- CIDR
- Description
Gateway address and subnet for the sandbox bridge, in CIDR notation. Default
192.168.222.1/24.
- Name
--uid-base- Type
- u32
- Description
First uid/gid of the jailer's unprivileged identity range. Default
52000.
- Name
--uid-count- Type
- u32
- Description
Number of uids/gids reserved for jailed VMs. Default
1000.
- Name
--tap-count- Type
- u32
- Description
Number of persistent TAP devices to pre-create, one per concurrent networked VM. Default
32. They are created root-owned but user-attachable so the unprivileged server can wire up VMs without privileges.
- Name
--tap-user- Type
- string
- Description
User allowed to attach the TAP devices. Defaults to
$SUDO_USER, then$USER, thenroot.
- Name
--vm-agent-bin- Type
- path
- Description
Path to the static musl
temps-vm-agentguest binary to install. Defaults to atemps-vm-agentfile next to the running executable.
Guest addressing follows from --subnet and the TAP pool: each VM gets the gateway network with 10 + tap_index in the host part, so allocation starts at .10 and leaves room for the gateway. The guest MAC is derived from the guest IP octets as 06:fc:<a>:<b>:<c>:<d>.
Select the backend per sandbox
Send backend: "firecracker" when creating a sandbox. Omit the field to get the host default.
curl https://your-temps-instance.com/api/v1/sandboxes \
-H "Authorization: Bearer your-temps-api-key" \
-H "Content-Type: application/json" \
-d '{
"backend": "firecracker",
"image": "alpine:3.20",
"cpu_limit": 2,
"memory_limit_mb": 1024,
"disk_size_mb": 4096,
"timeout_secs": 1800,
"ports": [3000]
}'
A successful create returns 201 Created with a sandbox response body. Creating a sandbox requires an authenticated request with either the sandbox-write or the project-write permission.
- Name
backend- Type
- string
- Description
"docker"(default) or"firecracker". Any other value is rejected. Omit it to use the host default.
- Name
image- Type
- string
- Description
Container image used to build the root filesystem. This is the field that picks the image; the
@vercel/sandboxruntimefield is accepted for compatibility and then ignored. Defaults toalpine:3.20.
- Name
cpu_limit- Type
- number
- Description
Cores. On Firecracker this becomes the vCPU count, rounded up and floored at 1. Default 1 vCPU.
- Name
memory_limit_mb- Type
- number
- Description
Guest memory. Firecracker default is 512 MiB.
- Name
disk_size_mb- Type
- number
- Description
Root disk size. Firecracker only; the Docker backend ignores it. Default 1024 MiB. Grow-only, and clamped up to the image's content size when that is larger.
- Name
timeout_secs- Type
- number
- Description
Idle timeout in seconds. Clamped to the range 60 to 86400; default 3600. The
@vercel/sandboxtimeoutfield (milliseconds) is used only whentimeout_secsis absent.
- Name
env- Type
- object
- Description
Extra environment variables baked in at create time. Empty by default.
- Name
ports- Type
- array
- Description
Ports to expose. Each one becomes a route entry in the create and get responses.
- Name
source- Type
- object
- Description
Optional code to seed the sandbox with, tagged by
type: either{"type":"git", ...}or{"type":"tarball", "url": "..."}. For git,git_connection_idis mutually exclusive withusernameandpassword, and credentials embedded in the URL are rejected.
- Name
preview_password- Type
- string
- Description
8 to 256 characters. Puts every preview URL behind a login form. Never returned; responses only expose the last four characters as a hint.
The host default backend itself is a global setting (sandbox_backend, either "docker" or "firecracker") that is only consulted when the Firecracker backend probes as available. Otherwise Docker is used regardless of the setting.
Three related routes matter on Firecracker specifically:
| Route | Purpose |
|---|---|
POST /api/v1/sandboxes/{id}/resize | Grow the root disk, 256 MB to 65536 MB. Firecracker only; a non-Firecracker sandbox is rejected with 400. |
GET /api/v1/sandboxes/rootfs | Inventory of the cached Firecracker root filesystems. |
POST /api/v1/sandboxes/rootfs/gc | Garbage-collect that cache. |
To find out whether a host can serve Firecracker before you try, GET /api/settings/sandbox-status returns a firecracker_available boolean (with a per-project variant under the project's agents routes).
Use the global GET /api/settings/sandbox-status. The project-scoped variant, GET /api/projects/{project_id}/agents/sandbox-status, currently returns a hardcoded firecracker_available: false, so polling it on a fully provisioned host always reports the backend as unavailable.
When the host cannot run Firecracker
Asking for "firecracker" on a host that has not been provisioned fails the create request. It does not silently fall back to Docker.
The response is 400 Bad Request with content type application/problem+json:
{
"title": "Validation Error",
"detail": "backend 'firecracker' is not available on this host"
}
A half-provisioned host fails differently, and less clearly. The availability check does not inspect the bridge or the TAP pool. If the smoke test passed but the root network stage never ran, the backend reports itself available, the 400 guard passes, and creation instead fails inside the provider with "sandbox requested network access but the host network stage has not run". That surfaces as a 500 Internal Server Error titled "Sandbox Creation Failed", not the 400 above. If you see that, run sudo temps firecracker setup --network-only.
An unrecognized backend string produces the same status with a different detail:
{
"title": "Validation Error",
"detail": "unknown backend 'local' (expected \"docker\" or \"firecracker\")"
}
Failing closed is deliberate. Isolation level is a security property, and quietly downgrading a request for a microVM into a shared-kernel container would be worse than a clear error.
Docker is still required
Firecracker replaces the container runtime, not the image toolchain. In v1 every Firecracker root filesystem is built from a container image using Docker, which is why a dead Docker daemon is a hard preflight failure even on a fully provisioned host.
The pipeline for an image is:
- Pull the image and create a container from it.
- Export the container filesystem to a temporary tar stream.
- Extract it unprivileged. Only regular files, directories, symlinks, and hardlinks are extracted; device nodes are skipped.
- Scrub
/.dockerenv,/etc/resolv.conf,/etc/hostname, and/etc/hosts. - Inject the guest agent at
/sbin/temps-vm-agent. - Build the image with
mkfs.ext4 -b 4096 -E lazy_itable_init=0,lazy_journal_init=0 -dover the extracted tree.
The result is cached per image digest and sized to the content size times 1.5 plus 64 MiB. Per-VM copies are made with reflink or sparse copies and grown from there. They are never shrunk.
On-disk layout
Everything lives under <data_dir>/firecracker/:
| Path | Contents |
|---|---|
bin/firecracker, bin/jailer, bin/temps-vm-agent | Pinned binaries plus the guest agent. |
kernel/vmlinux-<version> | Digest-verified guest kernel. |
state.json | Provisioning state, including the smoke_ok flag that gates availability. |
rootfs-cache/<image-digest>.ext4 | Digest-keyed rootfs cache. |
vms/<name>/ | Per-VM rootfs.ext4, vm.json, fc.pid, v.sock, console.log, env.json. |
taps.json | TAP pool allocation. |
Current limitations
This is v1. The following are deliberately deferred, and it is better to plan around them than to discover them at runtime.
The jailer is not used. Setup downloads the jailer binary and validates a uid range for it, but the VMM is launched directly as the server's own user. Isolation is what KVM provides, not a jailer chroot with a dropped uid.
Pause and resume are not snapshots. POST /api/v1/sandboxes/{id}/pause sends a shutdown RPC to the guest agent, waits 8 seconds, then kills the VM process. resume cold-boots a fresh VM from the persisted vm.json and rootfs.ext4. Filesystem state survives; memory and process state do not. No Firecracker snapshot API is involved.
restart is a full guest reboot (stop, then start), not a supervisor-style process restart.
Disk resize is offline. Growing the root disk stops the VM, grows the image, and starts it again, so the guest reboots. Shrinking is rejected with an explicit error.
Network modes. Only "no network" and full network work. Full network means NAT egress that is open to the internet; the only controls in place are iptables DROP rules for the cloud metadata address (169.254.169.254/32), the rest of link-local (169.254.0.0/16), and guest-to-host traffic arriving from the bridge, plus best-effort per-TAP bridge isolation. The "restricted" mode is accepted but fails closed to no network at all, with a warning in the logs, because the egress proxy it depends on has not shipped. It does not silently grant the egress it was asked to restrict.
The global network_mode setting does not apply to sandboxes created through this API. It is read only on the agent-run path. POST /api/v1/sandboxes creates sandboxes with no network mode set, and the Firecracker provider treats that as fully networked. Setting the global to none or restricted to lock down egress therefore does not restrict API-created microVMs, which still get full NAT egress. Do not rely on that setting as an egress control for this endpoint.
No mid-run output streaming. Output is delivered after the command finishes. Streaming exec falls back to wrapping the non-streaming path, so every line arrives tagged as stdout and stderr never arrives as a separate stream.
No user switching inside the VM. Running a command as root or as a specific user falls through to a plain exec; there is no uid concept in the microVM path.
One CLI subcommand. temps firecracker setup is all there is. There is no temps firecracker status; --check fills that role.
Partial CLI parity for the backend field. The @temps-sdk/cli package has no --backend flag today, so scripting against the API means setting the field directly. The server binary's own CLI does support it: temps sandbox create --backend firecracker.