A data volume is persistent storage scoped to a project, not to any single box. Its identity is the pair (project, name), and it is backed by a btrfs subvolume stored separately from box roots. Because a volume lives outside the box filesystem tree, it survives box deletion: you can delete every box in a project and the volume remains, ready to attach to the next one. This is the right tool when you want state that outlives an individual box: a database directory, a model cache, a workspace you re-mount across experiments. For capturing and restoring the state of a box’s own root filesystem, see snapshots and bases instead.
The CLI uses “fork” in argument help text, while the API and error messages use “box”. They refer to the same thing. This page uses “box”.

Mental model

  • A volume is identified by (project, name) and exists independently of any box.
  • You manage volumes with 4kr data subcommands.
  • You make a volume’s contents visible inside a box by attaching it at a mount path.
  • 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 (4kr snapshot), which capture the box root, not data volumes.

Naming rules

Data volume names, snapshot names, and backup names all match the regex ^[a-zA-Z0-9_.-]+$ — letters, digits, ., _, and -, and they must be non-empty. This is more permissive than box and project names, which must be DNS labels (1–63 characters, lowercase a-z0-9-, starting and ending alphanumeric; projects additionally disallow --). You can target a volume in another project with project:name syntax. The project part is validated as a project name and the name part as a data name. When you do not specify a project, resolution order is --projectFORKR_PROJECT → git repo name → default. (FORKR_PROFILE only selects the API endpoint and credentials, not the project.)

Create, list, delete

Create

4kr 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 <P>
  • -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.

Cloning from a snapshot

To create a new volume from an existing volume’s local snapshot, pass --from and --from-snapshot together:
4kr 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.
The source volume and the new volume must be in the same project, otherwise the command fails with Error: source data volume and new volume must be in the same project. 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 can only be restored in place onto the same volume (covered later).

List

4kr data list
Flags:
  • -a, --all — walk every project (the default project plus every project that has a box). Adds a PROJECT column.
  • -q, --quiet — print one name per line, or project:name with --all.
  • -p, --project <P>
  • -j, --json — print {"data":[...]}.
The default table has columns NAME SIZE CREATED; with --all it is NAME PROJECT SIZE CREATED. The JSON DataVolumeInfo records are {name, project, created_at, size_bytes} — there is no attachments field.

Delete

4kr data delete postgres-data
delete takes a name positional and only the -p, --project flag.
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.
On success it prints OK: (<project>:<name>) deleted; on failure it prints FAIL: (<project>:<name>) <msg> and exits 1.

Attach and detach

Attaching makes a volume’s contents appear inside a box at a mount path.
4kr data attach postgres-data webapp --path /var/lib/postgresql
attach takes two positionals — data (the volume) and fork (the box) — and these flags:
  • --path <PATH> (required) — mount path inside the box; must be absolute.
  • --read-only — mount read-only. The default is read-write.
  • -p, --project <P>
  • -j, --json
The volume and the box must share a project, otherwise the command fails with Error: data volume and fork must be in the same project. On success it prints OK (or JSON). A read-only attach is enforced as a read-only bind mount inside the runner. Mounts are applied live only when the box is not stopped; detach removes the mount only when the box is running.

Detach

4kr data detach postgres-data webapp
detach takes data and fork positionals plus -p, --project and -j, --json. It enforces the same-project check, 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
/etc
/volumes
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.

The 4kr cp gotcha

4kr cp does not route into custom data-volume mount paths. A copy to a data-volume mount path lands in the box root subvolume on the host, not in the bind-mounted volume.
4kr cp reads and writes through the host file API, operating on host paths under the box’s root subvolume. The server only redirects copies into its built-in system volumes, whose mount paths are /box/bin, /box/all, and /box/proj. Custom data-volume mount paths are not in that redirect list, so a cp targeting one writes to the box root rather than the volume. To put files into a data volume, write to the mount path from inside the box (for example via 4kr exec or 4kr console), where the bind mount is live.

Local snapshots

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

Create a snapshot

4kr 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.
  • -p, --project <P>
  • -j, --json
When you omit the 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

4kr data snapshot list postgres-data
Positional data, plus -p, --project and -j, --json. The table columns are NAME SIZE CREATED DESCRIPTION; JSON is {"snapshots":[...]} with records {name, created_at, size_bytes, description?}.

Restore a snapshot

4kr data snapshot restore postgres-data nightly
Positionals data and snapshot, plus -p, --project only. Restore deletes the live subvolume and snapshots the named snapshot back into its place.
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: 4kr 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. 4kr data snapshot only touches data volumes; 4kr 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 4kr data backup. There is no delete subcommand.

The backup target

Backups and restores require a target configured by the deployment through the FORKR_BACKUP_TARGET environment variable on the API. If it is unset or empty, 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 GOOGLE_APPLICATION_CREDENTIALS to point 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. These values are set per environment by the deployment, not by the CLI or API:
EnvironmentSchemeValue
devfile://file:///var/lib/forks/data/remote-backups
staginggs://gs://forkr-staging-backups/volume-backups
prodgs://gs://forkr-dev-7015b7-backups/volume-backups

Create a backup

4kr 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.
  • -p, --project <P>
  • -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

4kr data backup list postgres-data
4kr 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 accept -p, --project and -j, --json.

Wait on a backup

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

Restore from a backup

4kr data restore has two forms.

Start a restore

4kr 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).
  • -p, --project <P>
  • -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 deletes the current volume subvolume and snapshots the received data into place — so the restore lands on the same named 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 (4kr data create --from … --from-snapshot …).

Query restore status

4kr data restore status postgres-data restore-20260623120000-a1b2c3
The status form is 4kr 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. The table columns are OPERATION STATUS DATA BACKUP UPDATED ERROR, and a restore moves through runningsucceeded or failed.
Passing a third positional to the start form (4kr 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.