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.