Most apps do not need a volume. Use
[`/shared.db` and `/shared/`](/docs/shared-data) for state shared across
instances and regions.

A persistent volume is a private filesystem for software that expects local
disk, such as Postgres, Redis, or a development environment. Attaching one
makes the service stable: its identity and disk remain together across deploys,
restarts, and host moves.

A workspace uses its persistent volume for the complete writable filesystem.
Package installs and changes outside `/data` survive workspace restarts and
host moves. `/data` remains the conventional project path. See [coding agents
& workspaces](/docs/agents) for the workspace workflow.

## attach a volume

Attach a volume to an automatic source build:

```bash
kedge up --volume /var/lib/myapp --volume-size 20GiB
```

For a prebuilt image:

```bash
kedge publish --app my-database --image postgres:18 \
  --volume /var/lib/postgresql/data \
  --volume-size 20GiB
```

`size` is a sparse logical limit. The volume is seeded from the image path on
first boot.

## Dockerfiles and images

A Dockerfile `VOLUME` creates a one-machine app by default:

```Dockerfile
VOLUME ["/var/lib/postgresql/data"]
```

Use `--machines 0` when an image declares a volume the service does not need:

```bash
kedge publish --app myapp --image example/myapp:1.2 --machines 0
```

Several volume paths for one machine share its underlying store while remaining
separate directories. Dockerfile and image volume declarations seed those
paths unless `volume.nocopy` is set.

## Compose

Use a named volume for a service in a Compose app:

```yaml
services:
  db:
    image: postgres:18
    volumes: [data:/var/lib/postgresql/data]

volumes:
  data:
    x-kedge:
      shared: false
      size: 20GiB
```

`shared: false` gives each machine its own persistent volume. A
[`shared: true` volume](/docs/shared-data#mount-shared-files-elsewhere) mounts
the app's replicated `/shared/` tree instead.

With a persistent volume, `deploy.replicas` sets the machine count. Outside
Compose, use `--machines`:

```bash
kedge up --volume /data --machines 3
```

## machines

A service with a persistent volume runs on **machines**. Machines have ordinals
starting at zero:

- `<ordinal>.<service>.<app>.internal` reaches one machine.
- `<service>.<app>.internal` reaches the whole set.

Each machine has its own volume. Scaling down retains that volume; scaling up
reattaches the same ordinal.

A set runs one machine at minimum. Machines do not scale to zero, because a
machine keeps its address and volume for its lifetime. `min: 0` and
`scale.idle-cooldown` apply only to [pooled instances](/docs/runtime), which
have no persistent volume.

`idle-suspend` makes an idle machine cheap without removing it. Set a delay and
a machine with no inbound traffic pauses in place, keeping its address, volume,
and host; the next packet restarts it. A paused machine bills no CPU and no
memory.

```yaml
services:
  db:
    x-kedge:
      machines: 3
      idle-suspend: 10m
```

Delays under 2s are raised to 2s.

A machine is paused on inbound traffic and guest CPU alone, so a request that
goes quiet on both while it is still being served can be paused mid-flight. It
resumes within seconds and the request completes slowly rather than hanging. To
rule it out, hold a shared lock while you serve:

```bash
flock -s /run/kedge/awake ./handle-request
```

Shared locks do not serialize, so concurrent requests each hold one.

A paused machine wakes on traffic, so declare it only where traffic is what the
machine waits for. Nothing wakes one whose next action is its own timer. Kedge
wakes a paused machine periodically anyway so scheduled work still runs, and
steps its clock forward on each wake.

The same lock, taken exclusively, holds a machine awake through work that must
not be interrupted:

```bash
flock /run/kedge/awake ./nightly-compaction.sh
```

The machine stays up while the lock is held. The kernel releases it when the
process exits, so a job that crashes does not leave a machine awake forever.

List machines or move one to another region:

```bash
kedge machines my-database
kedge machines my-database migrate 0 nrt
```

Kedge handles ordinary restarts and host recovery automatically.

## durability

Volume flushes use the host's local disk (NVMe on production hosts). `fsync`,
ext4 journal commits, and database checkpoints do not wait for object storage.

Kedge publishes local writes to object storage in the background after 1s or
16 MiB. A controlled stop, machine migration, or fork waits until every accepted
write is published before it proceeds. If object storage is unavailable, that
operation fails and leaves the machine in place.

The local writeback limit is 4 GiB per volume by default. If publication cannot
keep up and the limit fills, new writes fail instead of consuming unbounded
host memory. Restarting the daemon on the same host replays the local journal.
After complete host loss, recovery uses the last published remote state.

Check publication state:

```bash
kedge volumes status my-database-0
```

`clean` means every write accepted by the volume backend is present in the
remote manifest. It does not include data still held in the guest filesystem's
page cache. Other states report pending writes, object-store errors, lease
fencing, or a full local journal.

## retention and deletion

Machine volumes are named `<app>-<ordinal>` and never disappear as a side
effect. App deletion, service removal, or scale-down leaves them retained.

List and explicitly delete retained volumes:

```bash
kedge volumes
kedge volumes rm my-database-0
```

Attached volumes and ancestors still used by forks refuse deletion. The
generated [REST volume operations](/docs/api#data) expose the same operations.
