Files
admin 122bbeea48
gates / gates (push) Successful in 1s
papra: mount the volume where the app actually writes (R-156, last leg)
papra mounted papra_data:/app/data while the application writes to
/app/app-data, so its database sat in the container's writable layer: lost on
redeploy, and tarred nightly as an empty directory while the healthcheck stayed
green. Last of the three apps R-156 convicted.

Decided from the IMAGE, not the README. docker inspect of
ghcr.io/papra-hq/papra:26.6.1-rootless gives WORKDIR=/app and all three data
paths under ./app-data (DATABASE_URL, DOCUMENT_STORAGE_FILESYSTEM_ROOT,
PAPRA_CONFIG_DIR) — and /app/data does not exist in the image at all.

Reconfiguring the app to write to /app/data was available and deliberately not
taken: it enumerates data paths, so a fourth added upstream would escape to the
writable layer again, silently — this defect re-armed. Mounting the app's own
data ROOT captures every current and future path by construction.

Precondition checked rather than inherited: docker ps -a (including stopped) on
BOTH demo guests, plus the hub fleet view (two enrolled hosts, zero papra) —
both boxes were wiped and rebuilt today, so the 2 August evidence was re-measured.

Proven by the runtime gate in both directions: CLEAN with the self-test passing,
and BROKEN when the mount is reverted, with the exact R-156 evidence. Full
catalog_gates.py papra: all three gates OK.
2026-08-03 11:29:51 +02:00

3.5 KiB

REPORT — papra volume persistence (R-156, last leg)

Date: 2026-08-03 · Repo: app-catalog-felhom.eu · No version (catalog templates are unversioned)

What changed

templates/papra/docker-compose.yml — the volume mount moved from /app/data to /app/app-data. One line, plus a Hungarian comment recording why, so the next reader does not "fix" it back.

Why

papra mounted papra_data:/app/data while the application writes to /app/app-data. Its database therefore lived in the container's writable layer: lost on redeploy, and tarred nightly as an empty directory while the healthcheck stayed green. Last of the three apps R-156 convicted.

Precondition — checked, not inherited

The register's "deployed nowhere" evidence was from 2026-08-02 and covered one guest; both demo boxes were wiped and rebuilt on 2026-08-03, so it was re-measured three ways:

  • docker ps -a (including stopped containers) on demo-hp guest 9201 → no papra
  • docker ps -a on demo-felhom guest 9201 → no papra
  • hub /hosts fleet view → exactly two enrolled hosts (demo-felhom-8363b5, demo-hp-bb76ea), zero papra references

Deployed nowhere ⇒ the template fix strands no live data.

How the fix was chosen

From the image, not the README:

WORKDIR=/app
DATABASE_URL=file:./app-data/db/db.sqlite
DOCUMENT_STORAGE_FILESYSTEM_ROOT=./app-data/documents
PAPRA_CONFIG_DIR=./app-data

and /app/data does not exist in the image at all — the old mount pointed at a path nothing could ever write.

Reconfiguring the app to write to /app/data was available and was deliberately not taken. All three paths are env-settable, so option (1) of the task's preference order was open. It enumerates data paths: a fourth added upstream would escape to the writable layer again, silently — this exact defect, re-armed and invisible. Mounting the app's own data ROOT captures every current and future path by construction, in one line rather than three env vars coupled to upstream.

Proof — the runtime gate, in both directions

Run Verdict
check-volume-persistence.py papra (fixed) CLEAN, self-test passed: "prober flags the R-156 signature and clears a correct template — trustworthy"
same gate, mount reverted to /app/data (red-proof) BROKENmount /app/data is NOT writable by the app's own uid=999; DATA in the writable layer at /app/app-data/db (db_signature=True, e.g. ['db.sqlite']); declared volume /app/data is EMPTY
catalog_gates.py papra (full, not --fast) rc=0 — image-pins OK (53 templates, 0 unpinned) · image-resolvable OK · volume-persistence OK

The red-proof was run against the real template, not only the built-in canary, so the gate is shown to discriminate on the artifact actually being shipped.

Two operational notes for the next run of that gate

  • It needs root. It reads volume contents under /var/lib/docker/volumes (mode drwx--x---). As a normal user its own canary self-test fails UNDETERMINED and it correctly refuses to report — the fail-closed behaviour worked exactly as designed.
  • Scope it to the app you touched. Unscoped it deploys all 53 templates; that run exceeded ten minutes and was aborted. Its scratch containers were cleaned up afterwards (volgate-* projects).
  • It hardcodes a scratch path /srv/felhom-gate, which had to be created on DooPlex.

Teardown

volgate-* scratch compose projects removed with their volumes. The pre-existing jarr-* containers (9 days old, unrelated) were left untouched.