feat(betrieb): Abbild, Compose-Stapel und Betriebsskripte

Dreistufiges Dockerfile mit standalone-Ausgabe; im Laufzeitabbild liegen
weder Quelltext noch Bauwerkzeuge, und es läuft unter einem eigenen
Benutzer.

Die Migrationen erledigt ein eigener Einmaldienst aus der Bau-Stufe. Der
Versuch, nur die Prisma-CLI ins Laufzeitabbild zu kopieren, scheiterte an
ihren transitiven Modulen; sie alle mitzunehmen hätte das Abbild
aufgebläht. Der Dienst läuft vor der App und muss sauber durchlaufen.

Zwei Stellen mussten faul werden, weil Next beim Bauen Seitendaten
sammelt und dabei Module lädt, die es zur Bauzeit noch ohne
Umgebungsvariablen gibt:
- Der Prisma-Client entsteht erst beim ersten Zugriff, über einen Proxy.
- Die Auth.js-Konfiguration wird als Funktion übergeben.
Ebenso liest prisma.config.ts die URL über process.env statt über env(),
das beim Bauen ohne Datenbank abbricht.

Dazu backup.sh, update.sh mit Rückrollpfad und scripts/release.sh. Der
Dump wurde nachweislich in eine Wegwerf-Datenbank zurückgespielt und
stimmt zeilengenau mit dem Original überein. Alle Skripte sind
shellcheck-sauber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-10 14:08:04 +02:00
co-authored by Claude Opus 5
parent 126ad93301
commit f91d015cc4
9 changed files with 523 additions and 28 deletions
+22 -12
View File
@@ -20,15 +20,36 @@ services:
retries: 10
start_period: 20s
# Einmaldienst: spielt die Migrationen ein und beendet sich. Läuft aus der
# Bau-Stufe, weil die Prisma-CLI eine Reihe Module braucht, die im schlanken
# Laufzeitabbild nichts zu suchen haben.
migration:
build:
context: .
dockerfile: Dockerfile
target: bauen
container_name: planer-migration
restart: 'no'
depends_on:
db:
condition: service_healthy
environment:
TZ: ${TZ:-Europe/Berlin}
DATABASE_URL: postgresql://${POSTGRES_USER:-planer}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-planer}?schema=public
command: node node_modules/prisma/build/index.js migrate deploy
app:
build:
context: .
dockerfile: Dockerfile
target: betrieb
container_name: planer-app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
migration:
condition: service_completed_successfully
environment:
NODE_ENV: production
TZ: ${TZ:-Europe/Berlin}
@@ -45,21 +66,10 @@ services:
WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:?WEBAUTHN_RP_ID fehlt in .env}
WEBAUTHN_RP_NAME: ${WEBAUTHN_RP_NAME:-Planer}
WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:?WEBAUTHN_ORIGIN fehlt in .env}
# Der Demo-Modus ist im Betrieb nie erlaubt.
DEMO_MODUS: 'false'
ports:
- '${APP_PORT:-3000}:3000'
healthcheck:
test:
[
'CMD',
'node',
'-e',
"fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
]
interval: 30s
timeout: 5s
retries: 5
start_period: 40s
volumes:
db-daten: