feat(deploy): make the all-in-one image installable without a shell (#1124)

The all-in-one image could not be installed from a GUI at all — the deployment it
exists for. validateEnv treats a missing JWT_SECRET as critical and exits, and the
documented run command supplies it with `openssl rand`, a shell command a Synology
Container Manager or QNAP Container Station form cannot run.

wait-for-db.sh now generates one on first start and persists it next to the database,
extending the existing /run/secrets hydration rather than adding a second mechanism.
Explicit env still wins, then /run/secrets, then the generated file. The write is
load-bearing: JWT_SECRET is exported only when the file actually persisted, because an
unpersisted secret would mint a new one every restart and sign every session out.

Creation writes to a private temp file and hard-links it into place — atomic, fails with
EEXIST when another container won, and the loser adopts the winner's value. Non-regular
paths are rejected before the link, since POSIX ln links INTO a directory rather than
failing, which would make a mistyped -v target unrecoverable.

Also repairs the onboarding paths a new install actually walks: the installer no longer
rotates the secrets of a running install on re-run, deprecates the dead scripts/install.sh
in place, corrects the CONTRIBUTING dev loop, and fixes the vite proxy target that had
been pointing at a stray local port since 0da45e69.

Reviewed over three rounds. Co-authored by @Luca-Timo.
This commit is contained in:
Luca
2026-08-22 18:37:12 +02:00
committed by GitHub
parent 8f23118782
commit 7223118b89
9 changed files with 475 additions and 45 deletions
+15 -4
View File
@@ -110,9 +110,20 @@ services:
- FACE_ML_TOKEN=${FACE_ML_TOKEN:-}
- FACE_PROCESSOR_CONCURRENCY=${FACE_PROCESSOR_CONCURRENCY:-}
volumes:
- ${APP_STORAGE}:/app/storage
- ${LOGS}:/app/logs
- ${APP_DATA}:/app/data
# Defaulted so a .env that simply does not set these still works. Without
# them compose aborts with "invalid spec: :/app/storage: empty section
# between colons", which reads like a broken compose file rather than a
# missing variable (#705).
#
# Note this does NOT make `config` work with no .env at all: `env_file`
# above still requires the file. Making it optional needs
# `required: false`, which is Compose 2.24+ syntax that OLDER Compose
# rejects as a schema error — taking the whole stack down rather than
# just losing a default. Not worth it for a case the onboarding flow
# never hits, since it copies .env.example first.
- ${APP_STORAGE:-./storage}:/app/storage
- ${LOGS:-./logs}:/app/logs
- ${APP_DATA:-./data}:/app/data
- picpeak-secrets:/run/secrets:ro
ports:
- "${BACKEND_PORT:-3001}:3000"
@@ -182,7 +193,7 @@ services:
# Uses same channel as backend for consistency
image: ghcr.io/picpeak/picpeak/frontend:${PICPEAK_CHANNEL:-stable}
container_name: picpeak-frontend
# Note: Pre-built frontend uses Nginx to proxy /api to backend:3001.
# Note: Pre-built frontend uses Nginx to proxy /api to backend:3000.
# Prefer keeping API base as '/api' in builds to avoid CORS.
environment:
# Substituted into index.html at container start (see frontend/