Volumes & data
An instance's filesystem has three write tiers:
- Rootfs — read-only squashfs by construction; nothing can modify it
- Scratch — the overlay upper layer; writable,
noexec,nosuid, gone when the instance goes - Named volumes — declared in the manifest, survive everything
#Declaring volumes
[volumes]
data = { path = "/var/lib/myapp" } # per-instance (default)
shared = { path = "/srv/uploads", scope = "shared" } # opt-in shared
cache = { path = "/var/cache/myapp", ephemeral = true } # GC-ableVolumes declare what, not where — ply materializes them at
/var/lib/ply/volumes/<app>/<name>.<n>/ and bind-mounts them over the
declared path.
#Per-instance by default
Scaling to three instances gives each its own data volume. This is
deliberate: single-writer state (SQLite files, Postgres data dirs) can
never be silently corrupted by --scale. Sharing is an explicit opt-in
(scope = "shared") and is surfaced by ply audit as risk surface.
Instance slots are stable: instance 2 of an app gets volume .2 today,
after a restart, and after an upgrade — state follows the slot.
#Lifecycle
- Volumes survive stop, start, crash, upgrade, and
ply rm ply rm myapp --volumesdestroys them — data deletion is always a separate, explicit actephemeral = truemarks a volume as a cache: still persistent across restarts, butply gcmay reclaim it
#Ownership
If your app drops privileges, declare the runtime user and ply will create the passwd entry and chown volumes to match:
[package]
user = "postgres:70:70" # name:uid:gid#What ply doesn't do
No volume drivers, no NFS/cloud volumes, no snapshots, no cross-host replication. The host manages storage (mount your NFS/EBS wherever you like); ply bind-mounts paths. Snapshots are the filesystem's job.
#Dev mode is the same mechanism
ply run --link ./src:/opt/myapp myapp.img--link is a bind-mount like any volume — about fifty lines of shared code,
which is the point.