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
+3 -3
View File
@@ -40,15 +40,15 @@ const config: VitestUserConfig = {
host: true,
proxy: {
'/api': {
target: 'http://localhost:7101',
target: 'http://localhost:3001',
changeOrigin: true,
},
'/photos': {
target: 'http://localhost:7101',
target: 'http://localhost:3001',
changeOrigin: true,
},
'/uploads': {
target: 'http://localhost:7101',
target: 'http://localhost:3001',
changeOrigin: true,
},
},