122bbeea48
gates / gates (push) Successful in 1s
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.
71 lines
3.5 KiB
Markdown
71 lines
3.5 KiB
Markdown
# 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) | **BROKEN** — `mount /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.
|