Volumes
What a volume starts with, where volumes live, and how a backup is cut.
Every app has one volume: the writable top of its root, where every write lands and the only
thing backed up. The document names it in volumes, and an instance points at it by
volumeId.
"volumes": [
{ "volumeId": "vol-1", "appId": "app-1", "sizeBytes": 8589934592, "desiredState": "present" }
]Starting from an archive
A volume is formatted empty the first time the document names it. To start it with content, name an archive in the artifact store and where to unpack it:
"volumes": [
{
"volumeId": "vol-1", "appId": "app-1", "sizeBytes": 8589934592, "desiredState": "present",
"initialContents": {
"digest": "<sha256 of the archive, lowercase hex>",
"objectKey": "seeds/app-1",
"destinationPath": "/app/data"
}
}
]- The archive is a tar, gzipped or not, or a zip — what
tar -czorzip -rwrites. - Entries land under
destinationPathas the app sees them:nested/hello.txtbecomes/app/data/nested/hello.txt. The directory and everything in it are given to uid 65534. - What the archiver added on its own —
.DS_Store,__MACOSX,Thumbs.db, editor swap files — is skipped. - An entry that reaches outside
destinationPath, a symlink in a zip, or an archive that does not fit fails the volume by name.
Good to know
The copy happens once, as the volume is formatted. Changing or dropping initialContents on a
volume that exists changes nothing about it.
Where volumes live
volumes.backend in config.toml chooses between two backends.
The reference is every key of both.
On this disk
[volumes]
backend = "local-file"
storage_prefix = "host-1"Volumes are sparse files under paths.state_dir. Nothing else to configure, and
[volumes.zerofs] is refused rather than ignored. This is what the installer writes.
What it costs is what the other backend exists for: a volume that outlives the machine. Checkpoints and exports are refused on this backend, because they are cut in the object store.
In an object store
[volumes]
backend = "zerofs"
storage_prefix = "host-1"
[volumes.zerofs]
binary = "/opt/nibrunner/bin/zerofs"
config_file = "/etc/zerofs/config.toml"
mount_path = "/mnt/zerofs"
nbd_socket_path = "/run/zerofs/nbd.sock"
ninep_socket_path = "/run/zerofs/9p.sock"
rpc_socket_path = "/run/zerofs/rpc.sock"
storage_url = "s3://my-bucket/host-1"
cache_dir = "/data/zerofs"
cache_disk_gib = 200
cache_memory_gib = 2
checkpoint_runtime_dir = "/run/zerofs-checkpoint"
checkpoint_config_file = "/etc/zerofs/checkpoint.toml"
checkpoint_cache_dir = "/data/zerofs-checkpoint"Blocks live in the object store and reach the guest over NBD, through
ZeroFS. install renders ZeroFS's configuration files and
the units that supervise it from these keys, so this is the only place any of it is said. The
object-store credentials and the encryption password go in host.env.
- Size
cache_disk_gibagainst the disk it is on. It shares that disk with the snapshots inpaths.snapshot_dir. A snapshot lost costs one cold boot; a full disk breaks the filesystem every app on the host runs from. - Whole gibibytes. The daemon holds
cache_memory_gibback from what a guest may be promised, and reads it back from the file it rendered, which truncates. - Slot N takes
/dev/nbdN.installloads the nbd module withmax_apps + 1minors. A module already loaded with fewer is not reloaded — that would drop every device under every guest — so raisingmax_appson this backend asks for a reboot.
Good to know
volumes.storage_prefix is one host, not one app. Every volume placed here shares it, and
deleting it destroys all of them.
Checkpoints and exports
On the zerofs backend, the document can cut a volume at a point in time, and bundle it:
"checkpoints": [
{ "checkpointId": "ckpt-1", "volumeId": "vol-1", "desiredState": "present" }
],
"exports": [
{
"exportId": "exp-1", "appId": "app-1", "volumeId": "vol-1",
"objectKey": "exports/app-1/bundle.tar.gz", "desiredState": "present",
"environment": { "DATABASE_URL": "sqlite:///app/data.db" }
}
]- A checkpoint is the volume as it was when the checkpoint was cut, kept in the object store
until the document says
absent. - An export freezes the guest's filesystem, cuts a checkpoint of its own, and writes a
gzipped tar — the volume's contents under
data/,environmentas.env— toobjectKeyinexports.store_url. To start another volume from it, giveinitialContentsan archive of what is underdata/, withdestinationPath/.
reported.json says what became of each, and why.