Persistent volumes
By default a sandbox is ephemeral: its writable rootfs is a per-VM clone that vanishes when the VM goes away. A volume is the opposite — a named, fsync-honest block device that outlives the sandbox or app it attaches to. Mount one at a database's data directory, a browser's profile, or an upload folder, and the data survives destroy/re-create, a hard VM kill, an app redeploy, sleep, and a daemon restart.
crucible volume create pgdata --size 5G
crucible run postgres:16 --volume pgdata:/var/lib/postgresql/data \
--memory 2048 -e POSTGRES_PASSWORD=secretVolumes require the daemon to have a volume directory: start it with
--volume-dir <dir> (see Storage & the daemon).
The one rule: single-writer
A volume is backed by an ext4 filesystem, which allows exactly one writer. Everything else about volumes follows from that:
- A volume may be attached to at most one live sandbox at a time. A second concurrent attach is refused.
- A volume-backed app cannot scale out (
--max-scale/--min-scale > 1) — N replicas would be N writers. - A volume-backed app redeploys via destroy-then-boot (a brief blip), not the zero-downtime flip, because the flip runs two instances at once.
- A volume-backed app snapshot-sleeps and wakes in place (~170 ms on reflink), just like a stateless app: sleep snapshots the instance and stops the VMM (RAM freed, the volume host-fsync'd), wake restores it with the volume re-attached — no cold boot, no DB recovery (v0.6.2).
This is the honest trade: stateless stays magic; stateful gets durable.
Sandbox volumes
Attach a volume to any sandbox with --volume NAME:/absolute/path (repeatable).
The volume is created and formatted on first use, then reattached by name:
# write into a fresh volume
SBX=$(crucible run alpine --volume work:/data)
crucible sandbox exec $SBX -- sh -c 'echo hello > /data/file'
crucible rm $SBX # the sandbox is gone…
# …the volume persists — re-attach and the data is still there
SBX=$(crucible run alpine --volume work:/data)
crucible sandbox exec $SBX -- cat /data/file # → hello--volume works on crucible run and crucible sandbox create.
Managing volumes
crucible volume create <name> [--size 5G] # explicit create + size
crucible volume ls # NAME SIZE ATTACHED HOST AGE
crucible volume rm <name> # delete the volume and its datacreatepre-provisions at an explicit size. Without it,run --volumestill auto-creates a volume on first use at the daemon's--volume-default-size(docker--vergonomics) — but an explicitcreate --sizeis honored by a laterrun --volume(it uses the recorded size).rmis refused while the volume is attached to a live sandbox; remove or stop the sandbox first. It deletes the backing file, so the data is gone.- Records are durable:
volume lssurvives a daemon restart.
The same operations are available over the REST API (/volumes) and to agents
via the MCP tools volume_create, list_volumes, delete_volume.
App volumes
Give a durable app a volume with app create --volume NAME:/path. The data
survives redeploys, sleep, and daemon restarts (the daemon re-creates the app
from spec and re-attaches the volume):
crucible app create db --image postgres:16 \
--volume pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret --port 5432Because a volume app is single-writer, its lifecycle differs from a stateless app:
| Stateless app | Volume app | |
|---|---|---|
Redeploy (app update) |
zero-downtime flip | destroy-then-boot (brief blip) |
| Sleep / wake | snapshot, ~125 ms wake | snapshot, ~170 ms wake (reflink) |
Scale out (--max-scale) |
yes | no (single writer) |
Idle-sleep without --port |
n/a | rejected (needs the proxy to wake) |
An HTTP volume app (--port) can still scale to zero and wake on request —
the wake is a cold-create instead of a snapshot restore. A TCP-only volume app
(no --port) can't idle-sleep yet (wake-on-connection arrives in a later
release), so it runs always-on.
Durability guarantees
- fsync is honest. Volume drives attach to Firecracker with
cache_type=Writeback, so a guestfsyncbecomes a hostfdatasyncon the backing file. Committed data survives a hard kill of the VM (verified byscripts/smoke_volumes.sh). - Sleep flushes first. Before a volume app's instance is stopped, the guest
is
sync'd so un-fsync'd writes reach the volume. - Crash-consistent, then clean. An unclean stop leaves an ext4 journal that recovers on the next mount; a database's own WAL layers on top of that.
The floor is the host filesystem under --volume-dir: crucible can't be more
durable than the disk you point it at.
Storage & the daemon
--volume-dir <dir>enables volumes and holds the backing files (<name>.img) plus a small bbolt index. It must be on the same filesystem as--chroot-base— volumes hardlink into the jail (a cross-filesystem volume dir is rejected rather than silently copied, which would break persistence). Put it on the disk you trust.--volume-default-size(default 2 GiB) sizes a volume created implicitly byrun --volumewith no priorvolume create.- The daemon host needs
mkfs.ext4(e2fsprogs) — preflighted at startup.
What's next
- Serverless over TCP (shipped v0.6.1) — a wake-on-connection forwarder wakes a scaled-to-zero postgres (or any TCP service) on the incoming connection, not just an HTTP request, so a volume app can be TCP-only and idle-sleeping. See serverless.md.
- Instant wake (shipped v0.6.2) — a volume app snapshot-sleeps and wakes in place in ~170 ms (reflink), no cold boot or DB recovery. See benchmarks.md.
- Volume backups (shipped v0.6.3) — a consistent point-in-time copy of a volume for backup, clone, and off-host durability, including a no-downtime live backup via fsfreeze. See backups.md.
See the ROADMAP for sequencing.