- docker-compose.coolify(.prebuilt).yml: Service craftvia-worker (Target worker, Chromium, shm_size 1gb, gleiche Härtung), Craftvia-Variablen für app und worker. - Dockerfile: worker-Stage mit HOME=/home/app (Chromium-Profil für non-root); lokaler docker build der Targets runner und worker erfolgreich, PDF-Erzeugung im Image geprüft. - .env.example/.env.prod.example/.env.coolify.example: alle Craftvia-Variablen inkl. RLS, KI-Provider, PDF_CHROMIUM_PATH, OFFLINE_MAX_DAYS, API_RATE_LIMIT_*, AI_GENERATION_RETENTION_DAYS, AI_MONTHLY_TOKEN_LIMIT. - CI (.github, .gitea): Job gate mit Postgres (pgvector) und Redis als Service: migrate deploy, seed, Passwort für craftvia_app, tsc, lint, build, npm run test. - docs/craftvia/DEPLOY.md (aus den Certvia-Deploy-Docs abgeleitet): Architektur, Domains, Secrets, Worker, Migrationen, RLS-Aktivierung, Backup/Restore, KI, Rate Limits, Aufbewahrung, Smoke, Update/Rollback. build-and-push-images.sh baut craftvia-worker. - Certvia-/ISMS-Dokumente aus docs/ nach docs/_certvia-archiv/ (mit README); Verweise in README.md und Skript-Kommentaren angepasst. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
23 KiB
Craftvia – Betrieb & Deployment
Maßgeblich für Test- und Produktivbetrieb. Abgeleitet aus den Fundament-Runbooks (archiviert unter
docs/_certvia-archiv/) und auf Craftvia umgeschrieben. Domains in diesem Dokument sind Platzhalter (app.craftvia.example). Deploy-Dateien:docker-compose.coolify.yml(Build auf dem Host) bzw.docker-compose.coolify.prebuilt.yml(fertige Images aus der Registry). Env-Referenzen:.env.coolify.example(Testserver),.env.prod.example(Produktion),.env.example(lokal).
1. Architekturüberblick
Internet ──► Coolify-Proxy (Traefik, TLS) ──► app:3000
│
┌─────────────── Netz "backend" (internal: true, kein Egress) ─┼──────────────────────────┐
│ postgres (pgvector/pg16, RLS) redis (requirepass) garage (S3 :3900, Admin :3903) │
│ ▲ ▲ ▲ ▲ ▲ ▲ ▲ │
│ migrate app craftvia-worker worker backup-worker garage-provision │
└──────────────────────────────────────────────────────────────────────────────────────────┘
app, craftvia-worker, worker, backup-worker zusätzlich im Netz "default" (Egress/Proxy)
| Dienst | Dockerfile-Target / Image | Aufgabe | Lebensdauer |
|---|---|---|---|
migrate |
migrate / craftvia-migrate |
prisma migrate deploy → Rollen-Rechte-Sync (scripts/sync-role-permissions.ts) → optional Demo-Seed (RUN_DEMO_SEED) bzw. Erst-Admin (BOOTSTRAP_ADMIN) |
Init-Job, restart: "no" |
app |
runner / craftvia-app |
Next.js standalone (Backoffice, PWA /m, REST /api/v1/**, Betreiber-Portal /admin) |
dauerhaft, Healthcheck auf / |
craftvia-worker |
worker / craftvia-worker |
BullMQ-Queues import-extraction, transcription, report-pdf, image-derivatives (scripts/craftvia-worker.ts); enthält Chromium + Schriften |
dauerhaft |
worker |
migrate / craftvia-migrate |
Mail-Worker (scripts/mail-worker.ts): Zustellung mit Retry/DLQ, täglicher Erinnerungslauf |
dauerhaft |
backup-worker |
migrate / craftvia-migrate |
Queue backup-ops (scripts/backup-worker.ts): Mandanten-Export/-Restore, DSGVO-Export, seriell |
dauerhaft |
postgres |
pgvector/pgvector:0.8.0-pg16 |
Primärdatenbank, RLS-Policies tenant_isolation |
Volume pgdata |
redis |
redis:7.4.2-alpine |
Queues (BullMQ) | Volume redisdata |
garage |
garage / craftvia-garage |
Objektspeicher (Dokumente, Fotos, PDFs, Backups), Config eingebacken aus deploy/garage.toml |
Volumes garage_meta, garage_data |
garage-provision |
migrate / craftvia-migrate |
Layout, Bucket, Access-Key, Rechte über die Admin-API (idempotent) | Init-Job |
Startreihenfolge: postgres (healthy) → migrate; garage (healthy) → garage-provision; danach
app, craftvia-worker, worker, backup-worker.
Ohne laufende Worker reiht die App bei gesetztem REDIS_URL Jobs nur ein: Import-Extraktion,
Transkription, Berichts-PDFs und Bild-Derivate bleiben dann liegen (craftvia-worker), Mails bleiben
pending (worker), Restore/Export bleiben queued (backup-worker). Ohne Redis laufen die
Craftvia-Processors inline in der App. Das ist nur für Dev/Demo gedacht: der PDF-Processor ist absichtlich nicht im
App-Bundle, Freigaben bleiben gültig, „PDF erzeugen" auf /reports/[id] stößt den Job erneut an.
Härtung (alle Dienste): no-new-privileges, cap_drop: ALL (gezielte cap_add nur für die
Entrypoints von postgres/redis), CPU-/RAM-Limits, gepinnte Image-Tags, non-root-User app (UID
1001) in allen Node-Images, Redis mit Passwort.
2. Domains, Proxy, TLS
- In Coolify beim Service
appdie Domain setzen, z. B.https://app.craftvia.example:3000(Port 3000 = Container-Port, Schemahttps). Coolify/Traefik stellt das Let's-Encrypt-Zertifikat aus. Kein Host-Port wird exponiert.garage,postgres,redisund die Worker erhalten keine Domain. AUTH_URL= exakt die öffentliche App-URL (https://app.craftvia.example),AUTH_TRUST_HOST=true(im Compose Default).APP_BASE_URLfür absolute Links in Mails/PDFs (leer =AUTH_URL).- Passkeys:
WEBAUTHN_ORIGIN/WEBAUTHN_RP_IDnur setzen, wenn sie vonAUTH_URLabweichen. - Uploads bis 25 MB laufen über den Proxy (
experimental.proxyClientMaxBodySize = 26mb). Vorgelagerte Proxies dürfen kein niedrigeres Body-Limit haben. - DNS: A/AAAA-Record
app.craftvia.example→ Server-IP; Firewall nur 80/443 + SSH.
3. Ersteinrichtung (Coolify)
- Ressource: Git-Repo (Deploy-Key, nur lesend), Build Pack Docker Compose, Compose Location
docker-compose.coolify.yml(oder…prebuilt.yml, siehe §9). - Environment-Variablen aus
.env.prod.examplebzw..env.coolify.exampleeintragen (§4). Secrets literal setzen, nicht über${…}referenzieren (Coolify-Interpolation). - Persistent Storage prüfen:
pgdata,garage_meta,garage_data,redisdata,backups. - Erster Deploy Produktion:
RUN_DEMO_SEED=false,BOOTSTRAP_ADMIN=true+BOOTSTRAP_ADMIN_*+BOOTSTRAP_TENANT_*. Dermigrate-Job legt überscripts/bootstrap-admin.tsden ersten Mandanten mit Mandanten-Admin (/login) und einen Plattform-Admin (/platform/login, MFA-Einrichtung beim ersten Login) an. Das Skript ist idempotent und überschreibt kein Passwort. Log immigrate-Container prüfen, danach Passwort ändern undBOOTSTRAP_ADMIN=false. - Testserver:
RUN_DEMO_SEED=truelegt die Demo-Mandanten an (Logins sieheAGENTS.md). Nie in Produktion. - RLS scharfschalten (§6), Smoke (§10).
4. Konfiguration & Secrets
4.1 Variablen
| Variable | Dienste | Pflicht | Bedeutung |
|---|---|---|---|
POSTGRES_USER/PASSWORD/DB, DATABASE_URL |
postgres, alle Node-Dienste | ja | Owner-Verbindung (Superuser/BYPASSRLS), Host postgres |
RLS_ENFORCED, RLS_DATABASE_URL |
app, craftvia-worker | Prod ja | scharfe RLS über Rolle craftvia_app (§6) |
REDIS_PASSWORD |
redis + alle Queue-Nutzer | ja | REDIS_URL wird im Compose daraus gebildet, nicht separat setzen |
S3_ENDPOINT/ACCESS_KEY/SECRET_KEY/BUCKET/REGION |
app, craftvia-worker, backup-worker, garage-provision | ja | Garage: http://garage:3900, Key GK+24 Hex, Secret 64 Hex, Region us-east-1. Ohne S3 speichert der Adapter nur Metadaten (Stub), in Prod also unbrauchbar |
GARAGE_RPC_SECRET, GARAGE_ADMIN_TOKEN |
garage, garage-provision | ja | je openssl rand -hex 32 |
AUTH_SECRET |
app, Worker | ja | ≥ 32 Zeichen, sonst Fail-Secure-Abbruch (src/server/env.ts) |
PASSWORD_PEPPER |
app, migrate, backup-worker | ja | 64 Hex, nicht rotierbar |
MFA_ENC_KEY |
app, craftvia-worker | empfohlen | TOTP-Secrets at-rest (leer = aus AUTH_SECRET), nach dem Setzen nicht ändern |
BACKUP_ENC_KEY |
backup-worker | empfohlen | AES-256-GCM der Backup-Artefakte (leer = AUTH_SECRET) |
AUTH_URL, APP_BASE_URL |
app, Worker | ja | öffentliche URL |
SMTP_HOST/PORT/SECURE/USER/PASSWORD/FROM, MAIL_FROM_NAME, MAIL_REPLY_TO |
app, worker, craftvia-worker | für Mailversand | ohne vollständige Konfiguration bleiben Mails pending mit Begründung |
AI_EXTRACTION_PROVIDER, ANTHROPIC_API_KEY, ANTHROPIC_MODEL |
app, craftvia-worker | optional | §7 |
TRANSCRIPTION_PROVIDER/API_URL/API_KEY/MODEL |
app, craftvia-worker | optional | §7 |
AI_MONTHLY_TOKEN_LIMIT |
app, craftvia-worker | optional | §7.2, Default 0 = unbegrenzt |
AI_GENERATION_RETENTION_DAYS |
craftvia-worker | optional | §7.3, Default 180 |
API_RATE_LIMIT_PER_MINUTE, API_FIELD_RATE_LIMIT_PER_MINUTE |
app | optional | §8, Default 300 / 1200 |
OFFLINE_MAX_DAYS |
app | optional | Offline-Bundle gilt nach N Tagen als veraltet (1–365, Default 7) |
CLAMAV_HOST, CLAMAV_PORT |
app | optional | zusätzlicher Malware-Scan per clamd INSTREAM (sonst Allowlist + Magic Bytes) |
PDF_CHROMIUM_PATH |
craftvia-worker | – | im Image/Compose fest /usr/bin/chromium |
BACKUP_LOCAL_DIR, BACKUP_S3_BUCKET |
app, backup-worker, garage-provision | optional | §11 |
RUN_DEMO_SEED, BOOTSTRAP_ADMIN*, BOOTSTRAP_TENANT_* |
migrate | – | §3 |
4.2 Secrets-Register (Grundregeln)
Werte liegen ausschließlich im Passwortmanager (je Umgebung eigener Ordner) plus versiegelter Offline-Kopie und werden nur als Coolify-Env verteilt, nie im Repo, Image oder Backup-Bucket. Test, Staging und Prod haben unterschiedliche Werte.
| Secret | Rotierbar? | Folge einer Rotation |
|---|---|---|
AUTH_SECRET |
ja | alle Sessions ungültig, Nutzer loggen neu ein |
PASSWORD_PEPPER |
nein | erzwungener Passwort-Reset aller Konten |
MFA_ENC_KEY |
nein | alle Nutzer müssen MFA neu einrichten |
BACKUP_ENC_KEY |
bedingt | gilt nur für neue Artefakte, Altschlüssel bis Retention-Ende aufbewahren |
craftvia_app-Passwort (RLS_DATABASE_URL) |
ja | ALTER ROLE … PASSWORD, danach Env setzen und app + craftvia-worker neu starten |
POSTGRES_PASSWORD, REDIS_PASSWORD |
ja | koordiniert mit allen Diensten neu deployen |
GARAGE_*, S3_ACCESS_KEY/SECRET_KEY |
ja | neuen Key provisionieren (garage-provision), Env tauschen, alten Key entfernen |
ANTHROPIC_API_KEY, TRANSCRIPTION_API_KEY, SMTP_PASSWORD |
ja | Env tauschen, Dienste neu starten |
5. Migrationen
- Der
migrate-Job führt bei jedem Deploynpx prisma migrate deployaus. Das ist idempotent: nur neue Migrationen werden angewandt. app und Worker starten erst nach erfolgreichem Abschluss (service_completed_successfully). Der App-Container migriert nie selbst. - Danach läuft
scripts/sync-role-permissions.ts: additiv, zieht neu eingeführte Rechte für bestehende Mandanten nach. Betroffene Nutzer sehen neue Rechte nach erneutem Login (JWT). - Regeln für neue Migrationen (RLS für Tenant-Tabellen usw.): MIGRATIONS.md.
- Manuell (Coolify-Terminal des
migrate-Containers oderdocker exec):npx prisma migrate status/npx prisma migrate deploy. - Vor Migrationen mit Datenumbau: Cluster-Backup ziehen (§11.1).
6. Row Level Security aktivieren
Die Baseline-Migration legt die Rolle craftvia_app NOLOGIN NOBYPASSRLS an, vergibt die
Tabellenrechte und aktiviert je Tenant-Tabelle ENABLE + FORCE ROW LEVEL SECURITY mit Policy
tenant_isolation (USING + WITH CHECK auf current_setting('app.tenant_id', true)).
Mit RLS_ENFORCED=true verbinden sich app und craftvia-worker über RLS_DATABASE_URL als
craftvia_app und setzen app.tenant_id transaktionslokal (src/server/db.ts, dbForTenant,
tenantTransaction). Migrationen, Seed/Bootstrap, Login-Lookup, Mail- und Backup-Worker laufen weiter über die
Owner-DATABASE_URL.
- Passwort für die App-Rolle setzen (einmalig je Umgebung, Wert in den Passwortmanager):
docker exec -it <postgres-container> psql -U craftvia -d craftvia \ -c "ALTER ROLE craftvia_app WITH LOGIN PASSWORD '<STARKES_PASSWORT>';" - Env setzen:
RLS_ENFORCED=true,RLS_DATABASE_URL=postgresql://craftvia_app:<STARKES_PASSWORT>@postgres:5432/craftvia?schema=public - Neu deployen. Fehlt
RLS_DATABASE_URLbei aktivem Flag, bricht der Prozess beim Start ab (fail secure). - Nachweis:
npx tsx scripts/test-rls-enforcement.ts. Der Test prüft Owner-Sicht, Isolation A/B, 0 Zeilen ohne Kontext,WITH CHECKunddbForTenantend-to-end. Er gehört auch zum CI-Gate.
Warnungen:
- Die Owner-Rolle in
DATABASE_URLmuss Superuser oder BYPASSRLS sein. Sonst sieht der Login keine Nutzer, und Restore (session_replication_role) scheitert. - Mehrschritt-Schreibvorgänge nur über
inTransaction(ctx, fn), direktesctx.db.$transactionist unterRLS_ENFORCED=truenicht atomar (ARCHITEKTUR §4.8). - Zeilen mit
tenant_id = NULL(Plattform-Audit, Mail-Logs, Auth-Tokens) sind fürcraftvia_appunsichtbar. Sie werden nur über den Owner-Client geschrieben.
7. Worker, Chromium und KI-Provider
7.1 craftvia-worker und Chromium
- Image-Stage
worker(Dockerfile):node:22.14.0-slim+ Debian-Paketechromium,fonts-dejavu-core,fonts-liberation;node_modulesinkl.tsx, generierter Prisma-Client,scripts/,src/,messages/,prisma/. Startnpx tsx scripts/craftvia-worker.ts. - PDF-Rendering (
src/server/pdf/render.ts):playwright-corestartetPDF_CHROMIUM_PATHmit--no-sandbox --disable-dev-shm-usage. Der Container braucht deshalb keine zusätzlichen Capabilities.shm_size: 1gbist als Reserve für fotoreiche Berichte gesetzt. Seiten laden keine Netzressourcen, alle Assets sind alsdata:eingebettet. - Concurrency:
report-pdf2, übrige Queues 4 je Worker-Prozess. Horizontal skalieren = weiterecraftvia-worker-Replicas (BullMQ verteilt). RAM-Limit im Compose 1,5 GB. - Diagnose im Container:
chromium --version; Logs zeigen[worker] listening on <queue>bzw.[worker] <queue> job <id> failed: ….
7.2 KI-Provider
| Zweck | Env | Verhalten ohne Konfiguration |
|---|---|---|
| Auftragsimport-Extraktion (PDF/Bild) | AI_EXTRACTION_PROVIDER=anthropic, ANTHROPIC_API_KEY, ANTHROPIC_MODEL (leer = claude-opus-5) |
Import bleibt manuell erfassbar |
| Lotse (Berichtsentwurf, Vollständigkeitsprüfung) | ANTHROPIC_API_KEY, ANTHROPIC_MODEL |
kein Entwurf, UI funktioniert weiter |
| Transkription von Sprachnotizen | TRANSCRIPTION_PROVIDER=openai-compatible, TRANSCRIPTION_API_URL (Default OpenAI /v1/audio/transcriptions), TRANSCRIPTION_API_KEY, TRANSCRIPTION_MODEL (Default whisper-1) |
Status disabled |
- Jede KI-Nutzung wird in
AiGenerationprotokolliert (Art, Provider, Modell, Bezug, Tokens ein/aus, auslösender Nutzer, Ein-/Ausgabe). - Kostenbremse:
AI_MONTHLY_TOKEN_LIMITist die Plattform-Vorgabe für Tokens (ein + aus) je Mandant je Kalendermonat (UTC),0= unbegrenzt. Mandantenadministratoren können unter/settings/lotseeinen eigenen Wert setzen (TenantSettings.aiMonthlyTokenLimit; leer = Plattform-Vorgabe,0= unbegrenzt). Ist das Kontingent aufgebraucht, lehnt der Lotse neue Entwürfe/Zusammenfassungen ab („Kontingent aufgebraucht“), die Import-Extraktion fällt auf manuelle Erfassung zurück. Geprüft wird vor jedem Aufruf – ein laufender Aufruf kann das Limit einmalig überschreiten. Transkription (Audio) liefert keine Tokens und wird nicht gezählt. - Datenschutz: Anbieter (Anthropic, Transkriptions-API) sind Auftragsverarbeiter, daher AVV und
Drittlandbewertung vor Aktivierung klären. Die Worker brauchen Egress (Netz
default).
7.3 Aufbewahrung KI-Protokoll
AI_GENERATION_RETENTION_DAYS (Default 180): Der Job ai-retention (Queue gleichen Namens) läuft
täglich im craftvia-worker (BullMQ-Job-Scheduler ai-retention-daily, beim Worker-Start registriert,
idempotent auch bei mehreren Replikas). Er leert Ein- und Ausgaben (input/output) von
AiGeneration-Einträgen, die älter als die Frist sind, und entfernt den Personenbezug (createdById).
Metadaten (Art, Modell, Tokens, Zeitpunkt, Bezug) bleiben für Kosten- und Nachvollziehbarkeit erhalten;
je Mandant wird ein Audit-Eintrag ai_generation_retention geschrieben. Die Frist mit dem DSB abstimmen.
Die Variable muss im craftvia-worker gesetzt sein.
8. Rate Limits
| Bereich | Env | Default | Zählung |
|---|---|---|---|
REST-API /api/v1/** |
API_RATE_LIMIT_PER_MINUTE |
300 | je Nutzer pro Minute |
Einsatz/Sync: /api/v1/sync, /api/v1/uploads, /api/v1/field/** |
API_FIELD_RATE_LIMIT_PER_MINUTE |
1200 | je Nutzer pro Minute |
| Passwort-Reset, Alt-Passwort-Prüfung, E-Mail-Änderung | fest (src/server/rate-limit.ts) |
5–10 je Fenster | je IP und je Konto |
Das Field-Limit ist höher, weil die PWA nach Offline-Phasen Outbox-Batches (≤ 50 Ops) und Fotos in Schüben nachsendet. Wird es zu knapp gewählt, laufen die Clients in Retry/Backoff, und die Sync-Seite zeigt Fehler. Limits je Prozess gelten pro App-Instanz. Bei mehreren Replicas multipliziert sich das effektive Limit.
9. Prebuilt-Images (Registry)
Wenn der Host-Build in Coolify zu lange dauert, die Images auf einem Build-Host (amd64) bauen, in die
Registry pushen und in Coolify docker-compose.coolify.prebuilt.yml verwenden.
| Image | Target | Dienste |
|---|---|---|
${REGISTRY}/craftvia-app:${IMAGE_TAG} |
runner |
app |
${REGISTRY}/craftvia-migrate:${IMAGE_TAG} |
migrate |
migrate, worker, backup-worker, garage-provision |
${REGISTRY}/craftvia-worker:${IMAGE_TAG} |
worker |
craftvia-worker |
${REGISTRY}/craftvia-garage:${IMAGE_TAG} |
garage |
garage |
docker login <registry-host>
REGISTRY=registry.example.com/craftvia ALSO_MAIN=true ./scripts/build-and-push-images.sh
# worker-Image (bis das Skript es mitbaut):
docker build --platform linux/amd64 --target worker -t registry.example.com/craftvia/craftvia-worker:main .
docker push registry.example.com/craftvia/craftvia-worker:main
Gotchas: Coolify reicht IMAGE_TAG nicht zuverlässig in die Compose-Interpolation, daher immer auch
:main pushen. Coolify entfernt alte Container vor dem Pull: erst alle Images pushen, dann
Redeploy, sonst ist die Umgebung unten. Registry-Token mit Minimalrechten (read/write:package)
verwenden und nach Klartext-Nutzung widerrufen.
10. Smoke nach Deploy
migrateundgarage-provisionmit Exit 0 beendet.appist healthy,craftvia-worker,workerundbackup-workerlaufen, im Log steht[worker] listening on report-pdfusw.https://app.craftvia.example/loginlädt mit gültigem Zertifikat./sw.jsund/site.webmanifestsind ohne Session erreichbar (PWA).- Login Backoffice →
/dashboard,/work-orders,/customers,/reports. Login Monteur →/m. - Datei-Upload an einem Auftrag und Download über
/files/<documentId>(prüft Garage + S3-Keys). - Bericht freigeben → PDF erscheint am Auftrag (prüft Queue, craftvia-worker, Chromium).
- Optional: Import-PDF hochladen → Extraktion (bei gesetztem API-Key). Sprachnotiz → Transkription.
- Test-Mail (z. B. Passwort-Reset) kommt an bzw. steht nachvollziehbar auf
pending. - Plattform-Login
/platform/login→/admin,/admin/backupzeigt das Backup-Ziel. - Bei aktiver RLS: Login + Auftragsliste funktionieren (sonst prüfen:
RLS_DATABASE_URL, Rolle hat LOGIN).
Automatisierter HTTP-Smoke mit Session-Cookie (ohne Passworteingabe, braucht DB-Zugriff und
AUTH_SECRET): BASE=https://app.craftvia.example npx tsx scripts/smoke-auth.ts (z. B. im
migrate-Container oder von einem Admin-Host mit Tunnel zur DB).
11. Backup & Restore
Zwei Ebenen, beide sind nötig: DB und Objektspeicher.
11.1 Ebene A: Cluster (gesamte Datenbank + Volumes)
- Postgres: mindestens täglich
pg_dump -Fc(Coolify Scheduled Task) in einen separaten, verschlüsselten Speicher. Für PITR pgBackRest/wal-g mit WAL-Archiving undrepo-cipher-type=aes-256-cbc. Deckt auch die globalen Tabellen (Identity, Plattform-Admins, Kataloge) ab.docker exec <postgres-container> pg_dump -U craftvia -d craftvia -Fc > craftvia-$(date +%F).dump # Restore in leere DB (App + Worker gestoppt): docker exec -i <postgres-container> pg_restore -U craftvia -d craftvia --clean --if-exists < craftvia-YYYY-MM-DD.dump - Garage:
garage_meta(Bucket-/Key-/Layout-Definitionen, kritisch) undgarage_datasichern, z. B. mit restic (eigenes Repo, eigenes Passwort) oder als Volume-Snapshot bei gestopptemgarage. Ohnegarage_metasind die Objektdaten nicht adressierbar. - Volume
backups: enthält lokale App-Backup-Artefakte (Ebene B), mitsichern. - Host-Encryption: Daten-Volumes auf LUKS bzw. provider-verschlüsseltem Block-Storage. Das Boot-Unlock-Verfahren dokumentieren.
- Restore-Test mindestens quartalsweise in eine Wegwerf-Umgebung, Ergebnis protokollieren.
11.2 Ebene B: Mandanten-Export/-Restore und DSGVO (Betreiber-Portal)
- Ziel der Artefakte in
/admin/backupwählbar (Lokal = Volume/app/.backupsoder S3). Die Konfiguration liegt verschlüsselt in der DB. Präzedenz: DB-Config →S3_*/BACKUP_LOCAL_DIR→ lokaler Default. - Export, Restore und DSGVO-Export je Mandant unter
/admin/[id](Plattform-Full-Admin + MFA-Step-up), ausgeführt vombackup-worker. Die Artefakte sind mitBACKUP_ENC_KEY(AES-256-GCM) verschlüsselt, der Restore arbeitet nur innerhalb vontenant_idund betrifft keine anderen Mandanten.TENANT_MODELSinsrc/server/db.tsundsrc/server/backup/topology.tsmüssen jede Tenant-Tabelle enthalten.
11.3 Restore-Kohärenz (Vorbedingung)
PASSWORD_PEPPER, MFA_ENC_KEY und BACKUP_ENC_KEY stehen nicht im Backup. Ein Restore in eine
Umgebung mit anderen Werten macht Logins (Pepper), MFA (MFA_ENC_KEY) bzw. das Entschlüsseln
der Artefakte (BACKUP_ENC_KEY) unmöglich. Vor jedem Restore die Secrets der Quellumgebung
bereitstellen oder einen Passwort-/MFA-Reset einplanen. Cross-Environment-Restores (prod → staging)
sind nur so lauffähig.
12. Update & Rollback
Update (Standard):
- CI grün (Gate-Job: migrate, seed, tsc, lint, build, Tests).
- Migrationen der Release sichten. Bei destruktiven Änderungen vorher
pg_dump(§11.1). - Coolify-Redeploy (bzw. Images pushen, dann Redeploy).
migrateläuft vor app und Workern. - Smoke (§10). Neue Env-Variablen aus den
.env.*.example-Dateien vorher eintragen.
Rollback:
- Ohne Schemaänderung: vorheriges Image-Tag als
:mainretaggen und pushen (Prebuilt) bzw. vorherigen Commit deployen. Die Worker ziehen dasselbe Tag mit. - Mit Schemaänderung: Prisma-Migrationen haben kein automatisches Down. Entweder Vorwärts-Fix
(neue Migration), oder App + Worker stoppen, DB aus dem Pre-Deploy-Dump wiederherstellen (§11.1),
dann den alten Stand deployen.
prisma migrate deploytoleriert in der DB angewandte Migrationen, die im alten Code fehlen. - Queues: Beim Rollback können Jobs eines neueren Payload-Formats in Redis liegen. Vor dem Rollback Worker-Logs prüfen, fehlgeschlagene Jobs nach dem Fix erneut anstoßen (z. B. „PDF erzeugen").
13. Go-Live-Checkliste
- Frische, starke Secrets je Umgebung, im Passwortmanager + versiegelte Offline-Kopie
RUN_DEMO_SEED=false, Bootstrap-Admin-Passwort geändert,BOOTSTRAP_ADMIN=false- HTTPS aktiv,
AUTH_URL/APP_BASE_URLkorrekt RLS_ENFORCED=true+RLS_DATABASE_URL, Smoke mit aktiver RLS bestandencraftvia-workerläuft, Test-PDF erzeugt- SMTP mit SPF/DKIM/DMARC der Absenderdomain
- KI: AVV geklärt,
AI_MONTHLY_TOKEN_LIMITundAI_GENERATION_RETENTION_DAYSfestgelegt - Backups Ebene A (Postgres +
garage_meta/garage_data+backups) eingerichtet, Restore-Test dokumentiert - Monitoring: Uptime-Check auf die App-URL, Log-Aggregation, Alarm bei Worker-Neustarts
- Firewall (80/443/SSH), SSH-Key-Login, unattended-upgrades