fix(docker): address external review of the all-in-one image (#1042)

Seven findings, all verified against the code before applying.

- The built-in backup system wrote nowhere. Migrations 029/030 seed
  /backup/picpeak and /backup/database as destinations, and this image neither
  created nor mounted /backup — so backups failed, and anything written there
  would have died with the container. /backup is now a symlink into
  /data/backup, so the seeded defaults work and the archives land on the volume
  like everything else.
- Database backups need the sqlite3 CLI. DatabaseBackupService spawns it for
  .backup and PRAGMA integrity_check; the npm module does not provide the
  binary. The compose image omits it because it always runs Postgres — this one
  defaults to SQLite, so it failed with ENOENT. Added.
- Every AIO install served literal ${BRAND_TITLE}. index.html carries
  placeholders that frontend/docker-entrypoint.sh substitutes at start, and
  this image runs no nginx and never invoked it. Rendered at build with the
  same defaults that entrypoint applies, and the build now fails if any
  ${BRAND_*} token survives. Browser testing missed this because the SPA
  rewrites document.title at runtime — the og:/twitter: cards and view-source
  still showed the raw token.
- The SPA fallback swallowed backend file-route 404s. nginx gives /api,
  /photos, /thumbnails and /health their own location blocks, so try_files
  never applies to them; excluding only /api/ made the fallback strictly
  broader than the behaviour it claimed parity with, turning a missing photo
  into a 200 HTML body under an image URL. Now excludes the same set.
- Docs claimed DB_* alone switches the engine. It does not: the image declares
  DATABASE_CLIENT=sqlite3 and the resolver treats a declared client as
  explicit, so DATABASE_CLIENT=pg is required. Corrected, and /data/backup
  added to the documented layout.
- The AIO Trivy upload reused the backend's SARIF category, so the two scans
  replaced each other's results instead of both being retained.
- The AIO build reused the backend's buildx cache scope, so two concurrent jobs
  wrote the same cache object from different Dockerfiles.

Verified on a rebuilt image: sqlite3 3.53.2 present, <title>PicPeak</title>,
/backup -> /data/backup with both seeded subdirectories, /photos + /thumbnails
+ /api back to 404 while /setup /impressum /gallery/x /admin/login stay 200.
This commit is contained in:
Paul Nothaft
2026-08-16 23:11:51 +02:00
parent 103006132b
commit 167eeaa271
4 changed files with 36 additions and 7 deletions
+3 -1
View File
@@ -62,6 +62,7 @@ Everything that must survive a container replacement lives under `/data`:
| `/data/db` | `picpeak.db` (+ `-wal`/`-shm`) and `SETUP_TOKEN` |
| `/data/storage` | originals, thumbnails, archives |
| `/data/logs` | application logs |
| `/data/backup` | built-in backup output (`/backup` is symlinked here) |
One mount point is the whole point. Back up `/data` and you have backed up the
install.
@@ -80,7 +81,8 @@ Only `JWT_SECRET` is required. Everything else has a working default.
| `PORT` | `3000` | Listen port inside the container. |
| `FRONTEND_URL` | — | Public URL. Set it once you are behind a domain, so emails and share links point at the right host. |
| `SMTP_*` | — | Outbound email. Without it, PicPeak runs fine but sends nothing. |
| `DB_HOST`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` | — | Point at an **external** PostgreSQL. Setting these switches the engine off SQLite. |
| `DATABASE_CLIENT` | `sqlite3` | Set to `pg` to use an external PostgreSQL. Required — the image declares `sqlite3`, and the boot resolver treats a declared client as an explicit instruction, so `DB_*` alone will **not** switch engines. |
| `DB_HOST`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` | — | Connection details, used when `DATABASE_CLIENT=pg`. |
### Using an external PostgreSQL