Files
app-catalog-felhom.eu/audits/persistence-sweep-2026-08-02/state/gate.log
T
admin 2b22a23d60 persistence sweep: 53 templates measured; gramps-web + wishlist fixed; runtime gate added
Campaign 10's R-156 found papra writing its database into the container's writable layer while the
volume the template preserves stayed empty — a backup that completes, verifies, and contains
nothing. papra was never the point: nothing anywhere checked that the folder a template preserves
is the folder the app writes to. All 53 templates have now been measured live.

43 CLEAN / 3 BROKEN / 7 UNDETERMINED. UNDETERMINED is counted separately, each with its reason,
and never folded into CLEAN.

FIXED (neither app is deployed anywhere, so nothing was stranded):
- gramps-web mounted /app/data, /app/media, /tmp — and /app/data is a path the application never
  writes. Its accounts database and ITS FAMILY TREE both landed in the writable layer while
  gramps_data was tarred nightly as an empty directory. Now persists the eight paths the image's
  own environment names, matching upstream's reference compose. Proven: users.sqlite and the
  family-tree files survive a redeploy byte-identical, same inode.
- wishlist mounted wishlist_data:/data, another path the app never writes; prod.db landed in the
  ANONYMOUS volume from the image's VOLUME directive — absent from ResolveDockerVolumeNames, so
  never backed up, and orphaned by a redeploy. Now mounts /usr/src/app/data + /usr/src/app/uploads.
  Proven: prod.db byte-identical, same inode, across a redeploy.

Every corrected path confirmed by two independent sources — the shipped image's own
environment/Config.Volumes and upstream's reference compose — never inferred from a directory name.

papra is NOT fixed. It is live on one box, and changing the mount target makes the next compose up
recreate the container and destroy the writable layer its documents live in. The fix is prepared
and proven in the scratch guest (current: db.sqlite differs after a redeploy, so a real account
created via the API is lost; fixed: byte-identical, it survives). Referred to the operator with the
two options; no migration written.

NEW GATE scripts/check-volume-persistence.py — the third catalog gate and the only RUNTIME one.
This class is invisible to static analysis, measured not assumed: a static audit of all 53 composes
reports the catalog clean AND reports papra clean. Exit 0 clean / 1 REFUSED / 2 undecided. It
refuses to report at all unless it has just re-proven itself in both directions against two canary
templates that differ only in which path the volume mounts at, so every run carries a live
demonstration of R-156 and of its fix. No docker exec anywhere (Campaign 7 §1.1). 44 fixture tests
driving check(), the function __main__ calls; every rule red-proofed.

Enforcement is convention, not CI — this repo has no CI. Stated plainly in the report; raising it
is proposed as R-160.

Report, per-app evidence, proofs and proposed register entries (R-158..R-161, NOT filed — felhom.eu
is fenced this session): audits/persistence-sweep-2026-08-02/
2026-08-02 12:21:30 +02:00

91 lines
8.4 KiB
Plaintext

self-testing the prober (both directions)…
prober flags the R-156 signature and clears a correct template — trustworthy
actualbudget CLEAN
adventurelog CLEAN
audiobookshelf CLEAN
bentopdf UNDETERMINED nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is U
bookstack CLEAN
calcom CLEAN
calibre-web CLEAN calibre-web: /app/calibre-web-automated/empty_library — database file(s) touched but byte-identical to the image; a chown sweep, not a write; calibre-
claper UNDETERMINED claper: EVERY declared mount is empty, yet the container created entries in 2 director(y/ies) outside all of them (e.g. ['/app/priv', '/app/priv/stati
code-server CLEAN
crafty-controller CLEAN crafty-controller: declared volume /crafty/servers is EMPTY; crafty-controller: declared volume /crafty/import is EMPTY; crafty-controller: declared v
docmost UNDETERMINED docmost: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/app/apps/client/dist']) —
emby CLEAN
ghost CLEAN
gitea CLEAN
glance UNDETERMINED glance: not running (status=restarting exit=1 restarts=None)
gokapi CLEAN
grafana CLEAN
gramps-web BROKEN gramps-web: DATA in the writable layer at /app/users (db_signature=True, e.g. ['users.sqlite']); gramps-web: declared volume /app/data is EMPTY; gramp
home-assistant CLEAN
homebox CLEAN
homepage CLEAN
immich UNDETERMINED immich-machine-learning: EVERY declared mount is empty, yet the container created entries in 3 director(y/ies) outside all of them (e.g. ['/', '/root'
jellyfin CLEAN
kimai CLEAN
komga CLEAN
mealie CLEAN
n8n CLEAN
navidrome CLEAN
nextcloud CLEAN
onlyoffice CLEAN onlyoffice: writable-layer writes at /var/www/onlyoffice/documentserver/sdkjs-plugins/{07FD8DFA-DFE0-4089-AL24-0730933CC80A}/resources/store (path sug
opengist CLEAN
outline CLEAN outline: declared volume /var/lib/outline/data is EMPTY; outline-redis: declared volume /data is EMPTY
paperless-ngx CLEAN
papra BROKEN papra: mount /app/data is NOT writable by the app's own uid=999 gid=999; papra: DATA in the writable layer at /app/app-data/db (db_signature=True, e.g
plant-it UNDETERMINED no containers created (compose up rc=1: msdeluise/plant-it:0.10.0 Pulling
Image msdeluise/plant-it:0.10.0 Error pull access denied for msdeluise/pla
plex CLEAN plex: declared volume /transcode is EMPTY
privatebin BROKEN privatebin: 14 file(s) in an ANONYMOUS volume at /run — not in the compose `volumes:` block, so it is never backed up and a redeploy orphans it; priva
radarr CLEAN radarr: writable-layer writes at /media (path suggests state, no database signature — judgement needed): ['movies']
rallly CLEAN
recipe-importer UNDETERMINED recipe-importer: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/']) — nothing this
romm CLEAN
seerr CLEAN
sonarr CLEAN sonarr: writable-layer writes at /media (path suggests state, no database signature — judgement needed): ['tv']
sparkyfitness UNDETERMINED sparkyfitness-server: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/app/SparkyFit
tandoor CLEAN tandoor: declared volume /opt/recipes/mediafiles is EMPTY
termix CLEAN
uptime-kuma UNDETERMINED nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is U
vaultwarden CLEAN
vikunja CLEAN vikunja: declared volume /app/vikunja/files is EMPTY
wanderer UNDETERMINED wanderer: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/root']) — nothing this ap
wger CLEAN wger: declared volume /home/wger/media is EMPTY
wishlist BROKEN wishlist: 1 file(s) in an ANONYMOUS volume at /usr/src/app/data — not in the compose `volumes:` block, so it is never backed up and a redeploy orphans
zipline UNDETERMINED zipline: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/zipline']) — nothing this
BROKEN — the app's data does NOT land where the template preserves it:
gramps-web
gramps-web: DATA in the writable layer at /app/users (db_signature=True, e.g. ['users.sqlite'])
gramps-web: declared volume /app/data is EMPTY
gramps-web: declared volume /app/media is EMPTY
gramps-web: declared volume /tmp is EMPTY
papra
papra: mount /app/data is NOT writable by the app's own uid=999 gid=999
papra: DATA in the writable layer at /app/app-data/db (db_signature=True, e.g. ['db.sqlite'])
papra: declared volume /app/data is EMPTY
privatebin
privatebin: 14 file(s) in an ANONYMOUS volume at /run — not in the compose `volumes:` block, so it is never backed up and a redeploy orphans it
privatebin: declared volume /srv/data is EMPTY
wishlist
wishlist: 1 file(s) in an ANONYMOUS volume at /usr/src/app/data — not in the compose `volumes:` block, so it is never backed up and a redeploy orphans it
wishlist: declared volume /data is EMPTY
UNDETERMINED — NOT a pass. The question was not answered for these:
bentopdf: nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is UNDETERMINED, not CLEAN
claper: claper: EVERY declared mount is empty, yet the container created entries in 2 director(y/ies) outside all of them (e.g. ['/app/priv', '/app/priv/static']) — nothing this app wrote landed anywhere the
docmost: docmost: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/app/apps/client/dist']) — nothing this app wrote landed anywhere the templat
glance: glance: not running (status=restarting exit=1 restarts=None)
immich: immich-machine-learning: EVERY declared mount is empty, yet the container created entries in 3 director(y/ies) outside all of them (e.g. ['/', '/root', '/root/.config']) — nothing this app wrote lande
plant-it: no containers created (compose up rc=1: msdeluise/plant-it:0.10.0 Pulling
Image msdeluise/plant-it:0.10.0 Error pull access denied for msdeluise/plant-it, repository does not exist or may require 'd
recipe-importer: recipe-importer: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/']) — nothing this app wrote landed anywhere the template preserves.
sparkyfitness: sparkyfitness-server: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/app/SparkyFitnessServer']) — nothing this app wrote landed anyw
uptime-kuma: nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is UNDETERMINED, not CLEAN; uptime-kuma: declared volu
wanderer: wanderer: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/root']) — nothing this app wrote landed anywhere the template preserves. Ne
zipline: zipline: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/zipline']) — nothing this app wrote landed anywhere the template preserves.
38 clean · 4 broken · 11 undetermined (of 53 in scope)
volume-persistence gate REFUSED