Persistence
How Composery keeps your changes across restarts with the persistence daemon.
Composery is a container, but it should feel like a machine whose state survives
restarts. Persistence keeps only your changes to the image - your delta - on the /data
volume, and mounting one durable volume there is the only hard requirement for
self-hosting. Two engines implement that, and which
one a box runs changes nothing about the promise.
The delta model is what makes image upgrades safe: a new image ships new base contents, your changes stay on top, and every file you never touched moves to the new version. See Updating for the procedure.
/data is owned by the normal user account and is the direct durable disk. Put
databases, object stores, large build artifacts, and other high-volume state there. Files
under /data occupy the volume once, under either engine. Files elsewhere behave like
ordinary machine files; under the copy engine a changed one occupies both the container's
writable layer and its delta copy, so a 10 GB changed file outside /data costs roughly
20 GB of host disk (the overlay engine stores it once). df -h /data shows the durable
disk's ordinary filesystem usage.
If you run Docker inside your box, point its data-root at /data — add
{"data-root": "/data/docker"} to /etc/docker/daemon.json and restart it. Left at
/var/lib/docker, the daemon sits on Composery's own root filesystem: under the overlay
engine it cannot nest overlay2 and silently falls back to the slower fuse-overlayfs,
and under the copy engine its layers are churning, regenerable files that the delta has no
reason to carry. On /data it gets native overlay2 and stores its layers once.
The volume must be a normal block-backed Linux filesystem (ext4, xfs, btrfs, or a Docker named volume). Network filesystems such as NFS are not supported: persistence relies on file locking, atomic renames, and xattrs that NFS does not reliably provide.
Engines
Persistence has two engines behind one contract, selected by COMPOSERY_PERSISTENCE
(auto by default):
- copy — the universal engine, and the only one that works on managed PaaS that
cannot grant container privileges (Render, Railway, Koyeb, and similar). A userspace
daemon compares the live root filesystem against the image baseline and writes only your
deltas to
/data/persistence. Its observation cost is bounded by construction (see Bounded observation). - overlay — a kernel-maintained engine that makes the root filesystem an overlayfs
whose upper layer lives on the
/datavolume, so the kernel records the delta directly. It needs a privileged container (CAP_SYS_ADMIN), which means Composery Cloud, thesystemdcompose recipes, and any host you own; a managed platform that cannot grant privileges gets the copy engine. Because the kernel maintains the delta, there is no restore pass at boot and nothing to watch while you work.
What is identical across engines: one durable volume at /data, the same delta model, the
same image-upgrade promise (a new image ships a new baseline/lower, your changes stay on
top, every file you never touched moves to the new version), and the same integrity
boundary — the paths under What persists are never captured either way.
auto probes by attempting a real overlay mount on the /data volume and falls back to
copy on any failure; an explicit COMPOSERY_PERSISTENCE=overlay that cannot be satisfied
stops the container with the probe's own error rather than quietly running copy, and
composery persistence status always reports the selected engine and the reason.
What honestly differs:
| copy | overlay | |
|---|---|---|
| Boot | replays the delta before the editor starts, so a large delta costs boot time | the kernel mounts it; nothing to replay |
| While running | a daemon watches and copies changes; a change is durable within one audit interval at worst | every write is already in the upper layer |
| Disk | a changed file outside /data occupies both the container layer and its delta copy | one copy |
| Deletions | recorded as tombstones | recorded as overlayfs whiteouts |
One consequence is worth knowing about. Under either engine a file you delete stays deleted across an upgrade, which is what you want — but an upgrade that reintroduces content at a path you had deleted, or adds files inside a directory you had replaced wholesale, has to decide between your change and the image's. Overlay reconciles this at boot: a deletion whose file the new image genuinely changed is dropped so the new version arrives, a deletion the image left alone is kept, and a directory you replaced is left exactly as you left it and reported rather than silently reopened. The container log lists what it reconciled on every boot.
What persists
Persisted:
- regular files and directories;
- symlinks;
- hardlinks when the volume supports them;
- mode bits, ownership, and mtimes;
- xattrs, ACLs, and file capabilities, when supported by the kernel, mounted filesystem, and container privileges;
- FIFOs and device-node metadata, when supported by the mounted filesystem and container privileges;
- package manager state under paths such as
/usr,/etc, and/var/lib/dpkg.
Always excluded because they are runtime state, container-managed files, or the update-owned Composery implementation:
/data;/run,/var/run,/proc,/sys,/dev, and/tmp;/opt/persistenceand/opt/composery;- resolver and hostname files:
/etc/hosts,/etc/hostname,/etc/resolv.conf.
/var/cache is also excluded by default because it holds regenerable caches such as
downloaded apt archives. Unlike the integrity exclusions above, this is a default the image
ships rather than an unremovable rule: list it under persist in config.json to keep it,
and a future image can change the default set without rewriting your volume.
For the systemd profile, keep /etc/systemd, /var/lib/systemd, and /etc/machine-id
persisted. Those paths store enabled units, service state, and machine identity; excluding
them would make Composery feel less like a VPS after restart.
Unix sockets are runtime endpoints and are ignored even outside excluded directories. The owning process must recreate them after restart.
When a regular file still has the same bytes as the image but only metadata has changed -
mode, owner, mtime, or xattrs - Composery stores the metadata delta without copying the
full file into changed/. This keeps a touched large image file from ballooning the
/data volume.
The active config lives at /data/persistence/config.json and records only your intent,
never the built-in policy. Two symmetric arrays: exclude adds paths to the boundary, and
persist keeps a path the image excludes by default (it can never override an integrity
exclusion). A new config contains neither — the image owns the default set, so a future
image can change it — alongside an audit block and a maxWatches cap. The effective
exclusion set is the integrity set, plus the image defaults you did not persist, plus
whatever you exclude. A config written by an older build, with a single exclusions
array, keeps working unchanged: exclusions is read as exclude, so the paths you named
go on excluding what they always did. If the file is malformed or
contains an invalid value, it is preserved beside the original as config.invalid-N.json
and replaced with safe defaults, so a configuration typo cannot prevent the IDE from
starting. Storage or filesystem-integrity failures still stop boot rather than pretending
the durable state was applied.
Bounded observation
The live inotify watcher is a latency optimisation, not the source of truth: the rolling
audit is the recovery floor. So the watcher's cost is capped by construction rather than
allowed to scale with the workload. It watches at most maxWatches directories (8192 by
default, tunable in config.json) and evicts the least-recently-active watch when full;
the dirty-change queue between the watcher and the writer is likewise bounded and sheds
under sustained pressure. Neither /var/lib/docker overlay churn nor a node_modules
storm can grow the daemon's memory. Anything not currently watched - trees past the budget,
or under a saturated queue - is still recovered by the next audit pass, at most one audit
interval later. Both the watcher and the audit also stop at mount boundaries: a bind-mounted
volume or a container runtime's overlay tree is runtime-managed, not part of the image, so
it is neither watched nor audited. composery persistence status reports the active watch
count, the budget, cumulative evictions, and whether the watcher has shed to audit-only.
Commands
Inside the container:
sudo composery persistence status
sudo composery persistence status --json
sudo composery persistence doctor
sudo composery persistence pruneReadiness
Readiness is exposed through /run/persistence/ready and /_composery/healthz.
If composery persistence apply fails during boot, the IDE does not become ready.
Hostname
The hostname is set by your container runtime, not the image - it is one of the
Docker-managed files Composery cannot persist. Set a stable one through your orchestrator:
docker run --hostname ..., Compose hostname:, or the equivalent.