6769d79e08
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.