A data volume is persistent storage that belongs to the whole deployment, not to any single box or project. Its identity is a single global name, backed by a btrfs subvolume stored separately from box roots. Persistent volumes survive box deletion: delete every box that used a volume and the volume stays, ready to attach to the next one — in any project. Box-owned volumes are the exception; Zomg creates them for zomg create --fork data clones and deletes them with the owning box after box deletion fully completes. Reach for a data volume when you want state that outlives a box: a database directory, a model cache, a workspace you re-mount across experiments. To capture and restore a box’s own root filesystem, see snapshots and bases instead.

Mental model

  • A volume is identified by a single global name and exists independently of any box or project.
  • You manage volumes with zomg data subcommands.
  • You make a volume’s contents visible inside a box by attaching it at a mount path — the box can live in any project.
  • project on a volume is an optional display label with no scoping behavior. It never affects which box can attach the volume.
  • Normal data volumes have persistent ownership and outlive boxes. Generated box-owned volumes have box_owned ownership and record their owning box; Zomg reaps them with that box.
  • A volume can be snapshotted locally (fast btrfs snapshots) and backed up remotely (to a file:// or gs:// target).
  • All of this is disjoint from box-root snapshots (zomg snapshot), which capture the box root, not data volumes.

Naming rules

Data volume names, snapshot names, and backup names use the same safe component format as boxes: 1-63 characters, lowercase a-z0-9-, starting and ending alphanumeric, and no --. Names are global, so a name is unique across the whole deployment. The optional project label follows the same DNS-label shape. It is only a label — there is no project:name data syntax and no project resolution for data volumes. A plain name is the whole reference everywhere zomg data takes a <data> argument. The names box-bin, box-all, box-home, and every box-proj-<project> are reserved for platform-owned system volumes. Creating or deleting one of them is rejected with reserved_data_name (HTTP 400).

Create, list, delete

Create

zomg data create postgres-data
create takes a required name positional. Flags:
  • --from <SOURCE> and --from-snapshot <SNAP> — clone from an existing volume’s snapshot (see below).
  • -p, --project <LABEL> — sets the optional display label only. It does not scope the volume and has no environment default.
  • -j, --json
On success it prints OK (or JSON with --json). A plain create runs btrfs subvolume create; if a volume with that name already exists, it fails with data already exists. Creating a reserved name (box-bin, box-all, box-home, box-proj-<project>) is rejected with reserved_data_name (400).

Cloning from a snapshot

To create a new volume from an existing volume’s local snapshot, pass --from and --from-snapshot together:
zomg data create postgres-copy --from postgres-data --from-snapshot v3
--from and --from-snapshot must be used together. If you pass only one, the command fails with Error: --from and --from-snapshot must be used together and exits 1.
Both the source and the new volume are plain global names. The clone is a btrfs snapshot of the named snapshot, and the snapshot must already exist. This is the only way to produce a new volume from existing data. Remote backups only restore in place onto the same volume (covered later).

List

zomg data list
zomg data list lists every volume in the deployment — there is no --all flag, because volumes are already global. Flags:
  • -p, --project <LABEL> — filter to volumes whose optional project label matches. It is a filter, not a scope.
  • -q, --quiet — print one name per line.
  • -j, --json — print {"data":[...]}.
The table has columns NAME PROJECT SIZE CREATED, where PROJECT shows the optional label (empty when unset). The JSON DataVolumeInfo records are {name, project, created_at, size_bytes, ownership} where project is the optional label; there is no attachments field. ownership is a typed sum. Normal user-managed volumes report {"kind":"persistent"}. Generated box-owned clone artifacts report {"kind":"box_owned","project":"<owner-project>","box":"<owner-box>"}; they may appear in zomg data list --json, but Zomg attaches them to the owning clone and cleans them up with that box.
User zomg data attach, zomg data detach, and zomg data delete reject box-owned volumes with data is box-owned. Delete the owning box to clean them up.

Delete

zomg data delete postgres-data
delete takes a single name positional. Because names are global there is no project qualifier.
Delete refuses if the volume is attached to any box, returning data is attached (HTTP 409). Detach it from every box first. Deleting a missing volume returns data not found, and deleting a reserved system name (box-bin, box-all, box-home, box-proj-<project>) is rejected with reserved_data_name (400).
On success it prints OK: (<name>) deleted; on failure it prints FAIL: (<name>) <msg> and exits 1.

Attach and detach

Attaching makes a volume’s contents appear inside a box at a mount path.
zomg data attach postgres-data webapp --path /var/lib/postgresql
attach takes two positionals — data (the global volume name) and box (the box) — and these flags:
  • --path <PATH> (required) — mount path inside the box; must be absolute and outside reserved roots.
  • --read-only — mount read-only. The default is read-write.
  • -j, --json
The volume is global, so it does not have to share the box’s project — you can attach any volume to a box in any project. The data argument is a plain name; the box argument is a box ref that follows normal box resolution (a bare name in the current project, or a project-qualified ref for a box elsewhere). On success it prints OK (or JSON). A read-only attach becomes a read-only bind mount inside the runner. Mounts apply live only when the box is not stopped; detach removes the mount only when the box is running.

Detach

zomg data detach postgres-data webapp
detach takes data and box positionals plus -j, --json. The data argument is a plain global name and the box is a box ref (project-qualified if the box is in another project). It removes the mount and is idempotent — detaching a volume that is not attached succeeds. It prints OK.

Mount-path restrictions

Mount paths are validated both client-side and server-side. A path is rejected if it is empty, not absolute, or normalizes to /, and if it equals or falls under any of these blocked prefixes:
/proc
/sys
/dev
/run
/tmp
/etc
/volumes
/home
/box
Other attach conflicts the server rejects:
ConditionError
Same volume already attached to that boxdata already attached (409)
A different volume already mounted at that path on the boxmount path already in use (409)
Volume does not existdata not found
To move a volume to a different path on the same box, detach it first, then attach at the new path.

zomg cp and mount paths

zomg cp reads and writes through the host file API, and the server resolves box paths to the same storage the running box sees through its mounts: paths under a data-volume mount path go to the backing volume, system-volume paths (/box/bin, /box/all, /box/proj, /home) go to those shared volumes, and /tmp//run go to the box’s ephemeral storage. Everything else operates on the box root subvolume. So zomg cp ./file my-box:/data/file writes into the mounted volume itself: the running box sees the file immediately, and the bytes live (and persist) with the volume, not the box. Copying into a mount attached --read-only is rejected with a 400: the box was granted read access only, and the file API does not bypass that. Copy to a writable path instead, or attach the volume read-write.

Local snapshots

Local snapshots are fast, read-only btrfs snapshots of a volume, kept on the same VM. Manage them with zomg data snapshot (alias zomg data snap).

Create a snapshot

zomg data snapshot create postgres-data nightly -m "before migration"
Positionals: data, and an optional name (auto-generated if omitted). Flags:
  • --name <NAME> — overrides the positional name if both are given.
  • -m, --message <DESC> — stored as the snapshot description.
  • -j, --json
The data positional is a plain global name. When you omit the snapshot name, it is auto-generated as v0, v1, and so on. The snapshot is a read-only btrfs snapshot. Creating a snapshot whose name is already taken fails with snapshot already exists; snapshotting a missing volume fails with data not found. On success it prints OK.

List snapshots

zomg data snapshot list postgres-data
Positional data (a plain global name), plus -j, --json. The table columns are NAME SIZE CREATED DESCRIPTION; JSON is {"snapshots":[...]} with records {name, created_at, size_bytes, description?}.

Restore a snapshot

zomg data snapshot restore postgres-data nightly
Positionals data (a plain global name) and snapshot. Restore swaps the named snapshot into the same volume. Zomg refuses attached volumes before starting, then may clean up the replaced subvolume after the committed restore.
Restore refuses if the volume is attached to any box, returning data is attached (409). Detach it first.

Contrast: box-root snapshots

Box-root snapshots are a separate feature: zomg snapshot (create/list/restore) captures a box’s root subvolume and stores it under the checkpoints directory. The two are completely disjoint — different commands, different storage. zomg data snapshot only touches data volumes; zomg snapshot only touches box roots. See snapshots and bases.

Remote backups

Remote backups send a volume’s data off the VM to a configured target using btrfs send. Manage them with zomg data backup. There is no delete subcommand.

The backup target

Whether remote backups are available depends on the deployment’s configured backup target (boxes_backup_target, surfaced to the API as the ZOMG_BACKUP_TARGET environment variable). The target is fixed per deployment — the CLI never sets it. If the deployment has no target configured, backups and restores fail with backup target is not configured. Accepted schemes:
  • file://<absolute-path> — the path must be absolute, otherwise file backup target must be absolute.
  • gs://<bucket>[/<prefix>] — the bucket is required.
Any other value fails with backup target must start with gs:// or file://. A gs:// target also requires the deployment to provide GOOGLE_APPLICATION_CREDENTIALS pointing at a service-account key JSON; if it is missing, backups fail with GOOGLE_APPLICATION_CREDENTIALS not set — GCS backup requires a service account key.

Create a backup

zomg data backup create postgres-data nightly -m "weekly full"
Positionals: data, and an optional name. Flags:
  • --name <NAME> — overrides the positional name.
  • -m, --message <DESC> — backup description.
  • --no-wait — return immediately instead of blocking.
  • -j, --json
By default create blocks until the backup reaches a terminal state, with a hard-coded 900-second (15-minute) timeout that you cannot override on create. After waiting it prints OK if the status is ready, otherwise FAIL: backup status <status> and exits 1. With --no-wait it returns right away (printing OK or JSON). When you omit the name, it is auto-generated as bkp-<YYYYMMDDHHMMSS>-<6 hex>. A backup is full or incremental depending on whether a prior ready backup snapshot exists to use as a parent — the first backup is full, later ones are incremental. Concurrent backup or restore on the same volume is rejected with backup/restore already running for data volume (409).

List and show backups

zomg data backup list postgres-data
zomg data backup show postgres-data nightly
list takes data; its table columns are NAME STATUS MODE PARENT UPDATED DESCRIPTION, and JSON is {"backups":[...]} with records {name, created_at, updated_at, status, mode?, parent?, description?, error?} where mode is full or incremental. show takes data and backup and displays the fields NAME, STATUS, MODE, PARENT, CREATED, UPDATED, DESCRIPTION, ERROR. Both take a plain data name and accept -j, --json.

Wait on a backup

zomg data backup wait postgres-data nightly --timeout 1200
wait takes data (a plain global name) and backup positionals, plus --timeout (default 900 seconds) and -j, --json. It prints OK if the backup is ready, otherwise FAIL: backup status <status> and exits 1.

Restore from a backup

zomg data restore has two forms.

Start a restore

zomg data restore postgres-data nightly
This starts a restore of the named backup. Flags:
  • --no-wait — return the operation id immediately instead of blocking.
  • --timeout <SECS> — wait timeout (default 900).
  • -j, --json
By default it waits, polling until the operation is succeeded or failed, then prints OK on success or FAIL: restore status <status> and exits 1. With --no-wait it returns the operation id right away. A restore downloads the backup chain and receives it, then swaps the received data into the same named volume transactionally: it stages a fresh copy into a hidden sibling and swaps it into place, so the volume is never left absent if the restore fails partway. The displaced subvolume is cleaned up after the swap commits. Just before swapping, Zomg re-checks that the volume still exists and is unattached, so a delete or attach that raced the download fails the restore instead of clobbering a live volume.
Restore refuses if the volume is attached to any box, returning data is attached (409). Detach it first. The backup must also be ready.
There is no API to restore a backup into a different volume name. To get data into a new volume, use a local snapshot clone (zomg data create --from … --from-snapshot …).

Query restore status

zomg data restore status postgres-data restore-20260623120000-a1b2c3
The status form is zomg data restore status <data> <operation-id>. Operation ids have the form restore-<YYYYMMDDHHMMSS>-<6 hex>. Operation records persist, so status keeps working after the restore completes. It prints a FIELD/VALUE table with the fields OPERATION, STATUS, DATA, BACKUP, UPDATED, and ERROR, and a restore moves through runningsucceeded or failed.
Passing a third positional to the start form (zomg data restore <data> <backup> <extra>) is an error. Use the explicit status form to query an operation.

Boxes

What a box is and how it runs.

Snapshots and bases

Capture and restore box-root state.

Environment

The runtime inside a box.

API reference

The data, backup, and restore routes.