Files
picpeak/docs
Paul Nothaft 6769d79e08 feat(docker): official single-container (all-in-one) image (#1042)
One `docker run`, one volume, working PicPeak — for the #705 audience on a NAS
or small VPS, where the four-service compose stack is what loses against SaaS
onboarding. Second of the two PRs planned there; #1041/#1043 shipped the
upgrade path out of it first, so a small install is never a dead end.

One Node process: no supervisor, no nginx, no Postgres, no Redis. SQLite is the
explicit, documented default — a library in-process writing one file on the
volume, no second daemon, no credentials, no pg_upgrade dance on image bumps.
DB_* still points at an external Postgres; the engine is resolved and logged at
boot (#1038).

Everything under one mount: /data/db (database + SETUP_TOKEN), /data/storage,
/data/logs.

Four backend changes were needed to make this work, all of which also fix the
existing SERVE_FRONTEND path:

- wait-for-db.sh waited for PostgreSQL unconditionally, resolving the engine
  only afterwards. On SQLite that blocks forever on a host that will never
  answer. Engine resolution moves ahead of the wait and the wait is skipped for
  sqlite3. resolveBootEngine reads the filesystem and env, never a live
  connection, so it is safe to run first.
- The writable-root checks hard-coded /app/storage, /app/data, /app/logs, so a
  single /data volume could not be guarded. They now follow the same
  STORAGE_PATH / DATA_DIR / LOG_DIR the app reads.
- express.static sent no Cache-Control at all. nginx sets `immutable` on hashed
  assets and no-store on index.html; without the latter a stale index.html
  after an upgrade names chunks that no longer exist and the app won't boot.
- The SPA history fallback only listed /admin/* and /gallery/*. nginx does
  `try_files $uri $uri/ /index.html`, so behind compose every route survived a
  reload — but direct hits on /setup, /impressum, /quote/:id, /transfer/:id,
  /invite/:token, /customer, /contract/:id, /payment-check and CMS /:slug all
  404'd without it. /setup is the first URL a new install visits. The catch-all
  is registered after the API 404 handler, so /api/* still answers JSON.

Header parity with frontend/nginx.conf was verified against a running
container, not assumed: CSP, X-Frame-Options, X-Content-Type-Options,
Referrer-Policy and Permissions-Policy all match (helmet already emitted them —
nginx proxy_hide_header's its copies to avoid duplicates).

CI publishes ghcr.io/.../aio with the same per-arch build -> manifest merge and
Trivy-by-digest scanning as the other two images.

Verified by building and running the image: SQLite boot healthy in ~6s, full
setup wizard completed in a browser, admin dashboard live, data surviving
container replacement, and an external-Postgres run healthy against a real
Postgres 15.
2026-08-16 22:03:58 +02:00
..