L10b Betrieb & Aufräumen: Deploy – craftvia-worker, CI-Testjob, DEPLOY.md, Certvia-Doku archiviert
- 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>
This commit is contained in:
+46
-8
@@ -1,7 +1,8 @@
|
|||||||
# Referenz für die Coolify-Environment-Variablen (Testserver, intern).
|
# Referenz für die Coolify-Environment-Variablen (Testserver, intern).
|
||||||
# ECHTE Secrets NUR in Coolify eintragen – diese Datei enthält nur Platzhalter.
|
# ECHTE Secrets NUR in Coolify eintragen – diese Datei enthält nur Platzhalter.
|
||||||
# In Coolify: Ressource -> Environment Variables (Bulk-Paste möglich).
|
# In Coolify: Ressource -> Environment Variables (Bulk-Paste möglich).
|
||||||
# Hostnamen sind die Compose-Service-Namen (postgres/redis/garage), NICHT localhost.
|
# Hostnamen sind die Compose-Service-Namen (postgres/redis/garage), NICHT localhost.
|
||||||
|
# Betriebsdoku: docs/craftvia/DEPLOY.md
|
||||||
|
|
||||||
# --- Datenbank (Service "postgres") ---
|
# --- Datenbank (Service "postgres") ---
|
||||||
POSTGRES_USER=craftvia
|
POSTGRES_USER=craftvia
|
||||||
@@ -9,6 +10,12 @@ POSTGRES_PASSWORD=CHANGE_ME_db_password
|
|||||||
POSTGRES_DB=craftvia
|
POSTGRES_DB=craftvia
|
||||||
DATABASE_URL=postgresql://craftvia:CHANGE_ME_db_password@postgres:5432/craftvia?schema=public
|
DATABASE_URL=postgresql://craftvia:CHANGE_ME_db_password@postgres:5432/craftvia?schema=public
|
||||||
|
|
||||||
|
# --- Row Level Security (F-04) ---
|
||||||
|
# Auf dem Testserver zunächst false (Owner-Betrieb). Zum Scharfschalten craftvia_app mit
|
||||||
|
# LOGIN + Passwort versehen (ALTER ROLE craftvia_app WITH LOGIN PASSWORD '<pw>';), dann:
|
||||||
|
RLS_ENFORCED=false
|
||||||
|
# RLS_DATABASE_URL=postgresql://craftvia_app:CHANGE_ME_app_password@postgres:5432/craftvia?schema=public
|
||||||
|
|
||||||
# --- Redis (Service "redis") ---
|
# --- Redis (Service "redis") ---
|
||||||
# F-18: Redis läuft mit requirepass. NUR REDIS_PASSWORD setzen — REDIS_URL wird in der
|
# F-18: Redis läuft mit requirepass. NUR REDIS_PASSWORD setzen — REDIS_URL wird in der
|
||||||
# docker-compose.coolify.yml daraus abgeleitet (redis://:${REDIS_PASSWORD}@redis:6379)
|
# docker-compose.coolify.yml daraus abgeleitet (redis://:${REDIS_PASSWORD}@redis:6379)
|
||||||
@@ -44,29 +51,60 @@ GARAGE_ADMIN_TOKEN=CHANGE_ME_openssl_rand_hex_32
|
|||||||
# `backups` auf /app/.backups (app + backup-worker) — Pfad hier NICHT aendern, ausser
|
# `backups` auf /app/.backups (app + backup-worker) — Pfad hier NICHT aendern, ausser
|
||||||
# der Mount wird angepasst.
|
# der Mount wird angepasst.
|
||||||
BACKUP_LOCAL_DIR=/app/.backups
|
BACKUP_LOCAL_DIR=/app/.backups
|
||||||
|
# BACKUP_ENC_KEY= (leer = AUTH_SECRET)
|
||||||
|
|
||||||
# --- Auth (NextAuth) ---
|
# --- Auth (Auth.js v5) ---
|
||||||
# AUTH_SECRET: openssl rand -base64 32
|
# AUTH_SECRET: openssl rand -base64 32
|
||||||
# AUTH_URL: exakt die Coolify-Domain des app-Service (http:// für intern)
|
# AUTH_URL: exakt die Coolify-Domain des app-Service (http:// für intern)
|
||||||
AUTH_SECRET=CHANGE_ME_openssl_rand_base64_32
|
AUTH_SECRET=CHANGE_ME_openssl_rand_base64_32
|
||||||
# PASSWORD_PEPPER (Härtung §1): openssl rand -hex 32 — frisch je Umgebung, NICHT rotierbar, nie ins Artefakt.
|
# PASSWORD_PEPPER (Härtung §1): openssl rand -hex 32 — frisch je Umgebung, NICHT rotierbar, nie ins Artefakt.
|
||||||
PASSWORD_PEPPER=CHANGE_ME_openssl_rand_hex_32
|
PASSWORD_PEPPER=CHANGE_ME_openssl_rand_hex_32
|
||||||
|
# MFA_ENC_KEY= (leer = aus AUTH_SECRET abgeleitet; nach dem Setzen nicht mehr ändern)
|
||||||
AUTH_URL=http://REPLACE-WITH-COOLIFY-SSLIP-DOMAIN
|
AUTH_URL=http://REPLACE-WITH-COOLIFY-SSLIP-DOMAIN
|
||||||
# Hinter Reverse-Proxy (Coolify/Traefik) für Auth.js v5 zwingend, sonst UntrustedHost:
|
# Hinter Reverse-Proxy (Coolify/Traefik) für Auth.js v5 zwingend, sonst UntrustedHost:
|
||||||
AUTH_TRUST_HOST=true
|
AUTH_TRUST_HOST=true
|
||||||
|
|
||||||
# --- Demo-Seed (NUR Testserver!) ---
|
# --- Demo-Seed (NUR Testserver!) ---
|
||||||
# true => migrate-Job legt nach der Migration den Demo-Mandanten + Nutzer an
|
# true => migrate-Job legt nach der Migration die Demo-Mandanten + Nutzer an
|
||||||
# (admin@demo.example / Demo1234!). In Produktion NICHT setzen / auf false lassen.
|
# (admin@demo.example / Demo1234!). In Produktion NICHT setzen / auf false lassen.
|
||||||
RUN_DEMO_SEED=true
|
RUN_DEMO_SEED=true
|
||||||
|
|
||||||
# --- KI-Provider (optional, aktuell ungenutzt) ---
|
# --- E-Mail (optional; ohne SMTP bleiben Mails "pending") ---
|
||||||
AI_PROVIDER=anthropic
|
|
||||||
AI_API_KEY=
|
|
||||||
|
|
||||||
# --- E-Mail (optional, im Test ungenutzt) ---
|
|
||||||
SMTP_HOST=
|
SMTP_HOST=
|
||||||
SMTP_PORT=1025
|
SMTP_PORT=1025
|
||||||
|
SMTP_SECURE=
|
||||||
SMTP_USER=
|
SMTP_USER=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
SMTP_FROM=craftvia@example.com
|
SMTP_FROM=craftvia@example.com
|
||||||
|
MAIL_FROM_NAME=Craftvia
|
||||||
|
MAIL_REPLY_TO=
|
||||||
|
# Basis für absolute Links in Mails/PDFs; leer = AUTH_URL.
|
||||||
|
APP_BASE_URL=
|
||||||
|
|
||||||
|
# --- KI: Auftragsimport-Extraktion & Lotse (Anthropic, optional) ---
|
||||||
|
# Ohne ANTHROPIC_API_KEY: manuelle Erfassung, kein Lotse-Entwurf.
|
||||||
|
AI_EXTRACTION_PROVIDER=anthropic
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
ANTHROPIC_MODEL=
|
||||||
|
|
||||||
|
# --- KI: Transkription (Whisper-kompatibel, optional) ---
|
||||||
|
TRANSCRIPTION_PROVIDER=openai-compatible
|
||||||
|
TRANSCRIPTION_API_URL=https://api.openai.com/v1/audio/transcriptions
|
||||||
|
TRANSCRIPTION_API_KEY=
|
||||||
|
TRANSCRIPTION_MODEL=whisper-1
|
||||||
|
|
||||||
|
# --- KI: Kostenbremse & Aufbewahrung KI-Protokoll ---
|
||||||
|
# Tokens je Mandant je Kalendermonat (ein+aus), 0 = unbegrenzt.
|
||||||
|
AI_MONTHLY_TOKEN_LIMIT=0
|
||||||
|
# Ein-/Ausgaben im KI-Protokoll (AiGeneration) nach N Tagen leeren/pseudonymisieren.
|
||||||
|
AI_GENERATION_RETENTION_DAYS=180
|
||||||
|
|
||||||
|
# --- Craftvia: API-Rate-Limits (je Nutzer/Minute) ---
|
||||||
|
API_RATE_LIMIT_PER_MINUTE=300
|
||||||
|
# /api/v1/sync, /api/v1/uploads, /api/v1/field/**
|
||||||
|
API_FIELD_RATE_LIMIT_PER_MINUTE=1200
|
||||||
|
|
||||||
|
# --- Craftvia: Offline/PWA & Malware-Scan ---
|
||||||
|
OFFLINE_MAX_DAYS=7
|
||||||
|
CLAMAV_HOST=
|
||||||
|
CLAMAV_PORT=3310
|
||||||
|
|||||||
@@ -101,5 +101,44 @@ TRANSCRIPTION_API_KEY=
|
|||||||
TRANSCRIPTION_MODEL=whisper-1
|
TRANSCRIPTION_MODEL=whisper-1
|
||||||
|
|
||||||
# --- Craftvia: optionaler Malware-Scan für Uploads (ClamAV clamd) ---
|
# --- Craftvia: optionaler Malware-Scan für Uploads (ClamAV clamd) ---
|
||||||
|
# Leer = nur Allowlist/Magic-Byte-Prüfung; gesetzt = zusätzlich clamd INSTREAM.
|
||||||
CLAMAV_HOST=
|
CLAMAV_HOST=
|
||||||
CLAMAV_PORT=3310
|
CLAMAV_PORT=3310
|
||||||
|
|
||||||
|
# --- Craftvia: Berichts-PDF (Worker, playwright-core + Chromium) ---
|
||||||
|
# Leer = Playwright-Chromium bzw. lokal installiertes Google Chrome (Entwicklerrechner).
|
||||||
|
# Im Docker-Worker-Image fest /usr/bin/chromium.
|
||||||
|
PDF_CHROMIUM_PATH=
|
||||||
|
|
||||||
|
# --- Craftvia: Offline/PWA ---
|
||||||
|
# Ab wie vielen Tagen ein lokal gespeichertes Auftragsbundle als veraltet gilt (1–365).
|
||||||
|
OFFLINE_MAX_DAYS=7
|
||||||
|
|
||||||
|
# --- Craftvia: Rate Limits der REST-API (je Nutzer, Anfragen pro Minute) ---
|
||||||
|
# Allgemein für /api/v1/**.
|
||||||
|
API_RATE_LIMIT_PER_MINUTE=300
|
||||||
|
# Einsatz-/Sync-Endpunkte (/api/v1/sync, /api/v1/uploads, /api/v1/field/**) – höher, weil
|
||||||
|
# die PWA nach Offline-Phasen Outbox und Fotos in Schüben nachsendet.
|
||||||
|
API_FIELD_RATE_LIMIT_PER_MINUTE=1200
|
||||||
|
|
||||||
|
# --- Craftvia: KI-Protokoll & Kostenbremse ---
|
||||||
|
# Nach N Tagen leert/pseudonymisiert ein Worker-Job Ein-/Ausgaben im KI-Protokoll
|
||||||
|
# (AiGeneration); Metadaten (Art, Modell, Tokens, Zeitpunkt) bleiben erhalten.
|
||||||
|
AI_GENERATION_RETENTION_DAYS=180
|
||||||
|
# Tokens (ein + aus) je Mandant je Kalendermonat; darüber lehnen Lotse und
|
||||||
|
# Import-Extraktion ab. 0 = unbegrenzt.
|
||||||
|
AI_MONTHLY_TOKEN_LIMIT=0
|
||||||
|
|
||||||
|
# --- Optionale Fundament-Variablen (Default leer) ---
|
||||||
|
# MFA_ENC_KEY: Schlüssel für TOTP-Secrets at-rest (leer = aus AUTH_SECRET abgeleitet).
|
||||||
|
# ⚠ Nach dem Setzen nicht mehr ändern.
|
||||||
|
# MFA_ENC_KEY=
|
||||||
|
# BACKUP_ENC_KEY: Verschlüsselung der Backup-Artefakte (leer = AUTH_SECRET).
|
||||||
|
# BACKUP_ENC_KEY=
|
||||||
|
# WebAuthn/Passkeys: Origin und RP-ID (leer = aus AUTH_URL abgeleitet).
|
||||||
|
# WEBAUTHN_ORIGIN=http://localhost:3000
|
||||||
|
# WEBAUTHN_RP_ID=localhost
|
||||||
|
# Demo-Seed-Passwort (Default Demo1234!).
|
||||||
|
# SEED_PASSWORD=
|
||||||
|
# RLS-Test hart statt Skip, wenn craftvia_app kein LOGIN hat (CI).
|
||||||
|
# RLS_TEST_REQUIRED=true
|
||||||
|
|||||||
+95
-38
@@ -1,6 +1,7 @@
|
|||||||
# Referenz für die PRODUKTIV-Env-Variablen (Contabo-VPS + Coolify).
|
# Referenz für die PRODUKTIV-Env-Variablen (Coolify, docker-compose.coolify[.prebuilt].yml).
|
||||||
# ECHTE Secrets NUR in Coolify eintragen – diese Datei enthält nur Platzhalter.
|
# ECHTE Secrets NUR in Coolify eintragen – diese Datei enthält nur Platzhalter.
|
||||||
# Unterschiede zum Testserver: HTTPS-AUTH_URL, KEIN Demo-Seed, stattdessen Bootstrap-Admin.
|
# Unterschiede zum Testserver: HTTPS-AUTH_URL, KEIN Demo-Seed, stattdessen Bootstrap-Admin,
|
||||||
|
# RLS scharf. Betriebsdoku: docs/craftvia/DEPLOY.md
|
||||||
|
|
||||||
# --- Datenbank (Service "postgres") ---
|
# --- Datenbank (Service "postgres") ---
|
||||||
POSTGRES_USER=craftvia
|
POSTGRES_USER=craftvia
|
||||||
@@ -9,68 +10,124 @@ POSTGRES_DB=craftvia
|
|||||||
DATABASE_URL=postgresql://craftvia:CHANGE_ME_starkes_db_passwort@postgres:5432/craftvia?schema=public
|
DATABASE_URL=postgresql://craftvia:CHANGE_ME_starkes_db_passwort@postgres:5432/craftvia?schema=public
|
||||||
|
|
||||||
# --- Row Level Security scharfschalten (F-04) ---
|
# --- Row Level Security scharfschalten (F-04) ---
|
||||||
# RLS_ENFORCED=true – die App verbindet sich als eingeschränkte Rolle craftvia_app
|
# RLS_ENFORCED=true – app und craftvia-worker verbinden sich als eingeschränkte Rolle
|
||||||
# (NOBYPASSRLS) und setzt app.tenant_id pro Transaktion; FORCE ROW LEVEL SECURITY
|
# craftvia_app (NOBYPASSRLS) und setzen app.tenant_id pro Transaktion; FORCE ROW LEVEL
|
||||||
# macht die Policies dann scharf. Ist der Kontext nicht gesetzt, sieht craftvia_app
|
# SECURITY macht die Policies dann scharf. Ist der Kontext nicht gesetzt, sieht craftvia_app
|
||||||
# NULL Zeilen – daher NUR mit korrekt gesetztem RLS_DATABASE_URL einschalten.
|
# NULL Zeilen – daher NUR mit korrekt gesetztem RLS_DATABASE_URL einschalten (sonst
|
||||||
|
# bricht die App beim Start bewusst ab).
|
||||||
# WICHTIG: Die Owner-/Migrate-Rolle in DATABASE_URL MUSS BYPASSRLS/Superuser sein
|
# WICHTIG: Die Owner-/Migrate-Rolle in DATABASE_URL MUSS BYPASSRLS/Superuser sein
|
||||||
# (Migrationen, Seed und der mandantenübergreifende Login-Lookup laufen darüber),
|
# (Migrationen, Seed, Mail-/Backup-Worker und der mandantenübergreifende Login-Lookup
|
||||||
# sonst sähe der Login keine Nutzer. craftvia_app in Prod EINMALIG mit LOGIN + starkem
|
# laufen darüber). craftvia_app wird von der Baseline-Migration NOLOGIN angelegt und in
|
||||||
# Passwort versehen: ALTER ROLE craftvia_app WITH LOGIN PASSWORD '<stark>';
|
# Prod EINMALIG mit LOGIN + starkem Passwort versehen:
|
||||||
|
# ALTER ROLE craftvia_app WITH LOGIN PASSWORD '<stark>';
|
||||||
RLS_ENFORCED=true
|
RLS_ENFORCED=true
|
||||||
RLS_DATABASE_URL=postgresql://craftvia_app:CHANGE_ME_starkes_craftvia_app_passwort@postgres:5432/craftvia?schema=public
|
RLS_DATABASE_URL=postgresql://craftvia_app:CHANGE_ME_starkes_craftvia_app_passwort@postgres:5432/craftvia?schema=public
|
||||||
|
|
||||||
# --- Redis ---
|
# --- Redis (Service "redis") ---
|
||||||
# F-18: Redis läuft mit requirepass. REDIS_PASSWORD setzen (stark!) und identisch
|
# F-18: Redis läuft mit requirepass. NUR REDIS_PASSWORD setzen – REDIS_URL wird in den
|
||||||
# in die REDIS_URL einsetzen (redis://:<pw>@redis:6379).
|
# Coolify-Compose-Dateien daraus abgeleitet (redis://:${REDIS_PASSWORD}@redis:6379).
|
||||||
REDIS_PASSWORD=CHANGE_ME_starkes_redis_passwort
|
REDIS_PASSWORD=CHANGE_ME_starkes_redis_passwort
|
||||||
REDIS_URL=redis://:CHANGE_ME_starkes_redis_passwort@redis:6379
|
|
||||||
|
|
||||||
# --- Objektspeicher (MinIO) – S3_* muss zu MINIO_ROOT_* passen ---
|
# --- Objektspeicher (Service "garage", S3-kompatibel) ---
|
||||||
S3_ENDPOINT=http://minio:9000
|
# Bucket/Key legt der Init-Job "garage-provision" an (Admin-API). Format erzwungen:
|
||||||
S3_ACCESS_KEY=CHANGE_ME_GK_plus_24_hex
|
# S3_ACCESS_KEY = "GK" + 24 Hex -> echo "GK$(openssl rand -hex 12)"
|
||||||
S3_SECRET_KEY=CHANGE_ME_starkes_minio_passwort
|
# S3_SECRET_KEY = 64 Hex -> openssl rand -hex 32
|
||||||
|
S3_ENDPOINT=http://garage:3900
|
||||||
|
S3_ACCESS_KEY=GK000000000000000000000000
|
||||||
|
S3_SECRET_KEY=CHANGE_ME_openssl_rand_hex_32
|
||||||
S3_BUCKET=craftvia-documents
|
S3_BUCKET=craftvia-documents
|
||||||
MINIO_ROOT_USER=craftvia
|
S3_REGION=us-east-1
|
||||||
MINIO_ROOT_PASSWORD=CHANGE_ME_starkes_minio_passwort
|
# Garage-Daemon-Secrets (LITERAL in Coolify setzen, nicht via ${...}).
|
||||||
|
GARAGE_RPC_SECRET=CHANGE_ME_openssl_rand_hex_32
|
||||||
|
GARAGE_ADMIN_TOKEN=CHANGE_ME_openssl_rand_hex_32
|
||||||
|
# GARAGE_ZONE=dc1
|
||||||
|
# GARAGE_CAPACITY_BYTES=100000000000
|
||||||
|
|
||||||
# --- Backup-Zielspeicher (optional) ---
|
# --- Backup-Zielspeicher (optional) ---
|
||||||
# Ziel der Backup-/DSGVO-Artefakte ist im Betreiber-Portal (/admin/backup) waehlbar
|
# Ziel der Backup-/DSGVO-Artefakte ist im Betreiber-Portal (/admin/backup) waehlbar
|
||||||
# (Lokal/S3) und wird verschluesselt in der DB gehalten. Praezedenz: DB-Config →
|
# (Lokal/S3) und wird verschluesselt in der DB gehalten. Praezedenz: DB-Config →
|
||||||
# Env (S3_*/BACKUP_LOCAL_DIR) → lokaler Default. Sobald im Portal gespeichert, hat
|
# Env (S3_*/BACKUP_LOCAL_DIR) → lokaler Default. Fuer „Lokal" mountet die Compose-Datei
|
||||||
# die DB-Config Vorrang. Fuer „Lokal" auf ein gemountetes, persistentes Volume zeigen.
|
# das persistente Volume `backups` auf /app/.backups (app + backup-worker).
|
||||||
BACKUP_LOCAL_DIR=/app/.backups
|
BACKUP_LOCAL_DIR=/app/.backups
|
||||||
|
# Optionaler eigener Backup-Bucket (nur wenn der Backup-Store auf S3 laeuft).
|
||||||
|
# BACKUP_S3_BUCKET=
|
||||||
|
# Verschluesselung der Backup-Artefakte (AES-256-GCM); leer = AUTH_SECRET. Je Umgebung
|
||||||
|
# eigener Wert, alte Keys bis Retention-Ende aufbewahren.
|
||||||
|
BACKUP_ENC_KEY=CHANGE_ME_openssl_rand_hex_32
|
||||||
|
|
||||||
# --- Auth (NextAuth) – Produktiv über HTTPS ---
|
# --- Auth (Auth.js v5) – Produktiv über HTTPS ---
|
||||||
# AUTH_SECRET: openssl rand -base64 32 (frisch, NICHT der Testwert)
|
# AUTH_SECRET: openssl rand -base64 32 (frisch, NICHT der Testwert)
|
||||||
AUTH_SECRET=CHANGE_ME_openssl_rand_base64_32
|
AUTH_SECRET=CHANGE_ME_openssl_rand_base64_32
|
||||||
# PASSWORD_PEPPER (Härtung §1): openssl rand -hex 32 — frisch je Umgebung, NICHT rotierbar, nie ins Artefakt.
|
# PASSWORD_PEPPER (Härtung §1): openssl rand -hex 32 — frisch je Umgebung, NICHT rotierbar, nie ins Artefakt.
|
||||||
PASSWORD_PEPPER=CHANGE_ME_openssl_rand_hex_32
|
PASSWORD_PEPPER=CHANGE_ME_openssl_rand_hex_32
|
||||||
AUTH_URL=https://app.craftvia.de
|
# MFA_ENC_KEY: TOTP-Secrets at-rest (leer = aus AUTH_SECRET). ⚠ Nach dem Setzen nicht mehr ändern.
|
||||||
# AUTH_TRUST_HOST ist im Compose fest auf true (hinter dem Coolify-Proxy) – nicht nötig.
|
MFA_ENC_KEY=CHANGE_ME_openssl_rand_hex_32
|
||||||
|
AUTH_URL=https://app.craftvia.example
|
||||||
|
# AUTH_TRUST_HOST ist im Compose fest auf true (hinter dem Coolify-Proxy) – nicht nötig.
|
||||||
|
# Passkeys/WebAuthn: leer = aus AUTH_URL abgeleitet.
|
||||||
|
# WEBAUTHN_ORIGIN=https://app.craftvia.example
|
||||||
|
# WEBAUTHN_RP_ID=app.craftvia.example
|
||||||
|
|
||||||
# --- KI-Provider (optional) ---
|
# --- E-Mail (produktives SMTP-Relay; SPF/DKIM/DMARC der Absenderdomain vorher einrichten) ---
|
||||||
AI_PROVIDER=anthropic
|
|
||||||
AI_API_KEY=
|
|
||||||
|
|
||||||
# --- E-Mail (produktives SMTP-Relay, sobald Einladungs-/Mailflow aktiv) ---
|
|
||||||
SMTP_HOST=
|
SMTP_HOST=
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
|
# true = implizites TLS (465), false = STARTTLS (587); leer = aus Port abgeleitet.
|
||||||
|
SMTP_SECURE=
|
||||||
SMTP_USER=
|
SMTP_USER=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
SMTP_FROM=noreply@craftvia.de
|
SMTP_FROM=no-reply@craftvia.example
|
||||||
|
MAIL_FROM_NAME=Craftvia
|
||||||
|
MAIL_REPLY_TO=
|
||||||
|
# Basis für absolute Links in Mails/PDFs; leer = AUTH_URL.
|
||||||
|
APP_BASE_URL=https://app.craftvia.example
|
||||||
|
|
||||||
|
# --- KI: Auftragsimport-Extraktion & Lotse (Anthropic) ---
|
||||||
|
# Ohne ANTHROPIC_API_KEY: graceful degradation (manuelle Erfassung, kein Lotse-Entwurf).
|
||||||
|
AI_EXTRACTION_PROVIDER=anthropic
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
# Leer = Default-Modell aus src/server/ai/client.ts
|
||||||
|
ANTHROPIC_MODEL=
|
||||||
|
|
||||||
|
# --- KI: Transkription von Sprachnotizen (Whisper-kompatible API) ---
|
||||||
|
# Ohne TRANSCRIPTION_API_KEY bleibt die Transkription deaktiviert (Status "disabled").
|
||||||
|
TRANSCRIPTION_PROVIDER=openai-compatible
|
||||||
|
TRANSCRIPTION_API_URL=https://api.openai.com/v1/audio/transcriptions
|
||||||
|
TRANSCRIPTION_API_KEY=
|
||||||
|
TRANSCRIPTION_MODEL=whisper-1
|
||||||
|
|
||||||
|
# --- KI: Kostenbremse & Aufbewahrung ---
|
||||||
|
# Tokens (ein + aus) je Mandant je Kalendermonat; darüber lehnen Lotse und
|
||||||
|
# Import-Extraktion ab. 0 = unbegrenzt.
|
||||||
|
AI_MONTHLY_TOKEN_LIMIT=0
|
||||||
|
# Nach N Tagen leert/pseudonymisiert ein Worker-Job Ein-/Ausgaben im KI-Protokoll
|
||||||
|
# (AiGeneration). Frist mit dem DSB abstimmen.
|
||||||
|
AI_GENERATION_RETENTION_DAYS=180
|
||||||
|
|
||||||
|
# --- Craftvia: API-Rate-Limits (je Nutzer, Anfragen pro Minute) ---
|
||||||
|
API_RATE_LIMIT_PER_MINUTE=300
|
||||||
|
# /api/v1/sync, /api/v1/uploads, /api/v1/field/**
|
||||||
|
API_FIELD_RATE_LIMIT_PER_MINUTE=1200
|
||||||
|
|
||||||
|
# --- Craftvia: Offline/PWA ---
|
||||||
|
OFFLINE_MAX_DAYS=7
|
||||||
|
|
||||||
|
# --- Craftvia: optionaler Malware-Scan (ClamAV clamd) ---
|
||||||
|
CLAMAV_HOST=
|
||||||
|
CLAMAV_PORT=3310
|
||||||
|
|
||||||
|
# PDF_CHROMIUM_PATH ist im Worker-Image/Compose fest /usr/bin/chromium – nicht setzen.
|
||||||
|
|
||||||
# --- Demo-Seed: in PROD AUS lassen! ---
|
# --- Demo-Seed: in PROD AUS lassen! ---
|
||||||
RUN_DEMO_SEED=false
|
RUN_DEMO_SEED=false
|
||||||
|
|
||||||
# --- Erst-Superadmin-Bootstrap (statt Demo-Seed) ---
|
# --- Erst-Admin-Bootstrap (statt Demo-Seed) ---
|
||||||
# Beim ersten Deploy true setzen -> migrate-Job legt Admin + Mandant an (idempotent).
|
# Beim ersten Deploy true setzen -> migrate-Job legt Plattform-Admin + ersten Mandanten an
|
||||||
# Danach kann true bleiben (tut nichts, wenn der Admin existiert) oder auf false.
|
# (idempotent). Danach auf false setzen oder stehen lassen (No-op, wenn vorhanden).
|
||||||
BOOTSTRAP_ADMIN=true
|
BOOTSTRAP_ADMIN=true
|
||||||
BOOTSTRAP_ADMIN_EMAIL=admin@craftvia.de
|
BOOTSTRAP_ADMIN_EMAIL=admin@craftvia.example
|
||||||
BOOTSTRAP_ADMIN_PASSWORD=CHANGE_ME_initiales_admin_passwort
|
BOOTSTRAP_ADMIN_PASSWORD=CHANGE_ME_initiales_admin_passwort
|
||||||
BOOTSTRAP_ADMIN_NAME=Craftvia Admin
|
BOOTSTRAP_ADMIN_NAME=Craftvia Admin
|
||||||
BOOTSTRAP_TENANT_NAME=Craftvia
|
BOOTSTRAP_TENANT_NAME=Musterbetrieb GmbH
|
||||||
BOOTSTRAP_TENANT_SLUG=craftvia
|
BOOTSTRAP_TENANT_SLUG=musterbetrieb
|
||||||
BOOTSTRAP_TENANT_SHORT=Craftvia
|
BOOTSTRAP_TENANT_SHORT=Musterbetrieb
|
||||||
BOOTSTRAP_TENANT_SECTOR=
|
BOOTSTRAP_TENANT_SECTOR=
|
||||||
|
|||||||
+59
-7
@@ -13,8 +13,50 @@ on:
|
|||||||
branches: ["main", "dev", "dev-*"]
|
branches: ["main", "dev", "dev-*"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-check:
|
# Vollständiges Qualitäts-Gate (= `npm run gate`) gegen echte Infrastruktur:
|
||||||
|
# Postgres 16 mit pgvector + Redis als Service-Container. Garage/S3 wird bewusst
|
||||||
|
# weggelassen: der Storage-Adapter fällt ohne S3_* auf den Stub zurück, S3-abhängige
|
||||||
|
# Prüfungen (test-garage-storage, Byte-Abruf in test-einsatz-sync/test-berichte-pdf)
|
||||||
|
# überspringen sich. Ohne SMTP_HOST überspringt test-mail den echten Versand, ohne
|
||||||
|
# ANTHROPIC_API_KEY/TRANSCRIPTION_API_KEY die Live-KI-Tests. test-berichte-pdf
|
||||||
|
# überspringt sich, wenn im Runner-Image kein Chromium/Chrome startbar ist.
|
||||||
|
gate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: pgvector/pgvector:0.8.0-pg16
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: craftvia
|
||||||
|
POSTGRES_PASSWORD: craftvia
|
||||||
|
POSTGRES_DB: craftvia
|
||||||
|
options: >-
|
||||||
|
--health-cmd "pg_isready -U craftvia"
|
||||||
|
--health-interval 5s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 20
|
||||||
|
redis:
|
||||||
|
image: redis:7.4.2-alpine
|
||||||
|
options: >-
|
||||||
|
--health-cmd "redis-cli ping"
|
||||||
|
--health-interval 5s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 20
|
||||||
|
# Nur CI-Dummywerte (keine echten Secrets). Gitea act_runner führt den Job in einem
|
||||||
|
# Container im selben Netz wie die Services aus → Hostnamen = Service-Namen
|
||||||
|
# (postgres/redis), NICHT localhost. Läuft der Runner im Host-Modus
|
||||||
|
# (Label ubuntu-latest:host), Hosts auf localhost umstellen und ports: ergänzen.
|
||||||
|
env:
|
||||||
|
DATABASE_URL: "postgresql://craftvia:craftvia@postgres:5432/craftvia?schema=public"
|
||||||
|
# RLS-Test (scripts/test-rls-enforcement.ts): RLS_ENFORCED bleibt aus, der Test schaltet
|
||||||
|
# selbst scharf. RLS_TEST_REQUIRED=true macht aus dem Skip einen harten Fehler.
|
||||||
|
RLS_DATABASE_URL: "postgresql://craftvia_app:craftvia_app_ci@postgres:5432/craftvia?schema=public"
|
||||||
|
RLS_TEST_REQUIRED: "true"
|
||||||
|
REDIS_URL: "redis://redis:6379"
|
||||||
|
AUTH_SECRET: "ci-dummy-auth-secret-0000000000000000"
|
||||||
|
AUTH_URL: "http://localhost:3000"
|
||||||
|
APP_BASE_URL: "http://localhost:3000"
|
||||||
|
PASSWORD_PEPPER: "0000000000000000000000000000000000000000000000000000000000000abc"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -34,11 +76,20 @@ jobs:
|
|||||||
run: npm ci --include=optional --no-audit --no-fund
|
run: npm ci --include=optional --no-audit --no-fund
|
||||||
|
|
||||||
- name: Prisma Client generieren
|
- name: Prisma Client generieren
|
||||||
# Platzhalter-URL nur fürs Laden von prisma.config.ts — keine echte DB-Verbindung.
|
|
||||||
env:
|
|
||||||
DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public"
|
|
||||||
run: npx prisma generate
|
run: npx prisma generate
|
||||||
|
|
||||||
|
- name: Migrationen anwenden
|
||||||
|
run: npx prisma migrate deploy
|
||||||
|
|
||||||
|
- name: Demo-Seed
|
||||||
|
run: npx prisma db seed
|
||||||
|
|
||||||
|
# Die Baseline-Migration legt craftvia_app NOLOGIN an; das Passwort ist ein Betriebs-
|
||||||
|
# Secret und wird nie migriert. Für den RLS-Test hier ein CI-Dummy-Passwort setzen
|
||||||
|
# (über die Prisma-Config-Datasource, damit kein psql-Client im Runner nötig ist).
|
||||||
|
- name: RLS-Rolle craftvia_app mit LOGIN versehen
|
||||||
|
run: echo "ALTER ROLE craftvia_app WITH LOGIN PASSWORD 'craftvia_app_ci';" | npx prisma db execute --stdin
|
||||||
|
|
||||||
- name: Typprüfung (tsc --noEmit)
|
- name: Typprüfung (tsc --noEmit)
|
||||||
run: npx tsc --noEmit
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
@@ -46,10 +97,11 @@ jobs:
|
|||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
|
||||||
DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public"
|
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Tests (scripts/test-*.ts)
|
||||||
|
run: npm run test
|
||||||
|
|
||||||
audit:
|
audit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -103,7 +155,7 @@ jobs:
|
|||||||
# Lockfile die @swc/helpers-Inkonsistenz trägt (siehe Dockerfile / Folgeänderung
|
# Lockfile die @swc/helpers-Inkonsistenz trägt (siehe Dockerfile / Folgeänderung
|
||||||
# der Dependency-Lane), bricht der Schritt mit ESBOMPROBLEMS ab — daher
|
# der Dependency-Lane), bricht der Schritt mit ESBOMPROBLEMS ab — daher
|
||||||
# continue-on-error. Alternative ohne npm-Baum-Validierung: Syft gegen das
|
# continue-on-error. Alternative ohne npm-Baum-Validierung: Syft gegen das
|
||||||
# gebaute Image (siehe docs/DEPLOY-PROD-CONTABO.md, Abschnitt SBOM).
|
# gebaute Image (siehe docs/_certvia-archiv/DEPLOY-PROD-CONTABO.md, Abschnitt SBOM).
|
||||||
- name: SBOM erzeugen (CycloneDX)
|
- name: SBOM erzeugen (CycloneDX)
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: npm sbom --sbom-format cyclonedx --omit dev > sbom.cyclonedx.json
|
run: npm sbom --sbom-format cyclonedx --omit dev > sbom.cyclonedx.json
|
||||||
|
|||||||
@@ -13,8 +13,51 @@ on:
|
|||||||
branches: ["main", "dev", "dev-*"]
|
branches: ["main", "dev", "dev-*"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-check:
|
# Vollständiges Qualitäts-Gate (= `npm run gate`) gegen echte Infrastruktur:
|
||||||
|
# Postgres 16 mit pgvector + Redis als Service-Container. Garage/S3 wird bewusst
|
||||||
|
# weggelassen: der Storage-Adapter fällt ohne S3_* auf den Stub zurück, S3-abhängige
|
||||||
|
# Prüfungen (test-garage-storage, Byte-Abruf in test-einsatz-sync/test-berichte-pdf)
|
||||||
|
# überspringen sich. Ohne SMTP_HOST überspringt test-mail den echten Versand, ohne
|
||||||
|
# ANTHROPIC_API_KEY/TRANSCRIPTION_API_KEY die Live-KI-Tests. test-berichte-pdf nutzt
|
||||||
|
# Google Chrome des Runners (channel "chrome") oder überspringt sich ohne Browser.
|
||||||
|
gate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: pgvector/pgvector:0.8.0-pg16
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: craftvia
|
||||||
|
POSTGRES_PASSWORD: craftvia
|
||||||
|
POSTGRES_DB: craftvia
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd "pg_isready -U craftvia"
|
||||||
|
--health-interval 5s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 20
|
||||||
|
redis:
|
||||||
|
image: redis:7.4.2-alpine
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
options: >-
|
||||||
|
--health-cmd "redis-cli ping"
|
||||||
|
--health-interval 5s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 20
|
||||||
|
# Nur CI-Dummywerte (keine echten Secrets). GitHub-Hosted-Runner: Service-Ports auf localhost.
|
||||||
|
env:
|
||||||
|
DATABASE_URL: "postgresql://craftvia:craftvia@localhost:5432/craftvia?schema=public"
|
||||||
|
# RLS-Test (scripts/test-rls-enforcement.ts): RLS_ENFORCED bleibt aus, der Test schaltet
|
||||||
|
# selbst scharf. RLS_TEST_REQUIRED=true macht aus dem Skip einen harten Fehler.
|
||||||
|
RLS_DATABASE_URL: "postgresql://craftvia_app:craftvia_app_ci@localhost:5432/craftvia?schema=public"
|
||||||
|
RLS_TEST_REQUIRED: "true"
|
||||||
|
REDIS_URL: "redis://localhost:6379"
|
||||||
|
AUTH_SECRET: "ci-dummy-auth-secret-0000000000000000"
|
||||||
|
AUTH_URL: "http://localhost:3000"
|
||||||
|
APP_BASE_URL: "http://localhost:3000"
|
||||||
|
PASSWORD_PEPPER: "0000000000000000000000000000000000000000000000000000000000000abc"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -34,10 +77,20 @@ jobs:
|
|||||||
run: npm ci --include=optional --no-audit --no-fund
|
run: npm ci --include=optional --no-audit --no-fund
|
||||||
|
|
||||||
- name: Prisma Client generieren
|
- name: Prisma Client generieren
|
||||||
env:
|
|
||||||
DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public"
|
|
||||||
run: npx prisma generate
|
run: npx prisma generate
|
||||||
|
|
||||||
|
- name: Migrationen anwenden
|
||||||
|
run: npx prisma migrate deploy
|
||||||
|
|
||||||
|
- name: Demo-Seed
|
||||||
|
run: npx prisma db seed
|
||||||
|
|
||||||
|
# Die Baseline-Migration legt craftvia_app NOLOGIN an; das Passwort ist ein Betriebs-
|
||||||
|
# Secret und wird nie migriert. Für den RLS-Test hier ein CI-Dummy-Passwort setzen
|
||||||
|
# (über die Prisma-Config-Datasource, damit kein psql-Client nötig ist).
|
||||||
|
- name: RLS-Rolle craftvia_app mit LOGIN versehen
|
||||||
|
run: echo "ALTER ROLE craftvia_app WITH LOGIN PASSWORD 'craftvia_app_ci';" | npx prisma db execute --stdin
|
||||||
|
|
||||||
- name: Typprüfung (tsc --noEmit)
|
- name: Typprüfung (tsc --noEmit)
|
||||||
run: npx tsc --noEmit
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
@@ -45,10 +98,11 @@ jobs:
|
|||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
|
||||||
DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public"
|
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Tests (scripts/test-*.ts)
|
||||||
|
run: npm run test
|
||||||
|
|
||||||
audit:
|
audit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -100,7 +154,7 @@ jobs:
|
|||||||
# Lockfile die @swc/helpers-Inkonsistenz trägt (siehe Dockerfile / Folgeänderung
|
# Lockfile die @swc/helpers-Inkonsistenz trägt (siehe Dockerfile / Folgeänderung
|
||||||
# der Dependency-Lane), bricht der Schritt mit ESBOMPROBLEMS ab — daher
|
# der Dependency-Lane), bricht der Schritt mit ESBOMPROBLEMS ab — daher
|
||||||
# continue-on-error. Alternative ohne npm-Baum-Validierung: Syft gegen das
|
# continue-on-error. Alternative ohne npm-Baum-Validierung: Syft gegen das
|
||||||
# gebaute Image (siehe docs/DEPLOY-PROD-CONTABO.md, Abschnitt SBOM).
|
# gebaute Image (siehe docs/_certvia-archiv/DEPLOY-PROD-CONTABO.md, Abschnitt SBOM).
|
||||||
- name: SBOM erzeugen (CycloneDX)
|
- name: SBOM erzeugen (CycloneDX)
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: npm sbom --sbom-format cyclonedx --omit dev > sbom.cyclonedx.json
|
run: npm sbom --sbom-format cyclonedx --omit dev > sbom.cyclonedx.json
|
||||||
|
|||||||
+5
-1
@@ -123,7 +123,11 @@ COPY src ./src
|
|||||||
COPY messages ./messages
|
COPY messages ./messages
|
||||||
ENV DATABASE_URL="postgresql://build:build@localhost:5432/build?schema=public"
|
ENV DATABASE_URL="postgresql://build:build@localhost:5432/build?schema=public"
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
|
# Chromium legt beim Start ein Profil-/Crashpad-Verzeichnis unter $HOME an. /app gehört root
|
||||||
|
# → als User "app" bricht der Start mit "Failed to create headless user data directory" ab.
|
||||||
|
# Daher eigenes, beschreibbares Home-Verzeichnis für den non-root-User.
|
||||||
RUN groupadd --system --gid 1001 app \
|
RUN groupadd --system --gid 1001 app \
|
||||||
&& useradd --system --uid 1001 --gid app --home-dir /app app
|
&& useradd --system --uid 1001 --gid app --home-dir /home/app --create-home app
|
||||||
|
ENV HOME=/home/app
|
||||||
USER app
|
USER app
|
||||||
CMD ["npx", "tsx", "scripts/craftvia-worker.ts"]
|
CMD ["npx", "tsx", "scripts/craftvia-worker.ts"]
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ Login: `admin@demo.example` / `Demo1234!` (weitere Demo-Nutzer siehe AGENTS.md).
|
|||||||
|
|
||||||
## Betrieb
|
## Betrieb
|
||||||
|
|
||||||
Container-Build über das Multi-Stage-`Dockerfile` (Targets `runner`, `migrate`, `garage`),
|
Container-Build über das Multi-Stage-`Dockerfile` (Targets `runner`, `migrate`, `garage`, `worker`),
|
||||||
Deployment mit `docker-compose.coolify.yml` bzw. `docker-compose.coolify.prebuilt.yml`.
|
Deployment mit `docker-compose.coolify.yml` bzw. `docker-compose.coolify.prebuilt.yml`.
|
||||||
Hinweise: `docs/DEPLOY-COOLIFY.md`, `docs/SECRETS-REGISTER.md` (aus dem Fundament übernommen,
|
Hinweise: [`docs/craftvia/DEPLOY.md`](docs/craftvia/DEPLOY.md) (Betrieb, Secrets, Worker, RLS, Backup),
|
||||||
Namen teils noch Certvia).
|
[`docs/craftvia/API.md`](docs/craftvia/API.md) (`/api/v1`). Übernommene Certvia-Dokumente liegen in
|
||||||
|
`docs/_certvia-archiv/`.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# Garage-Objektspeicher — Basiskonfiguration (Single-Node pro Environment).
|
# Garage-Objektspeicher — Basiskonfiguration (Single-Node pro Environment).
|
||||||
# Gehört zur MinIO→Garage-Migration, siehe docs/KONZEPT-garage-migration.md (§5).
|
# Gehört zur MinIO→Garage-Migration, siehe docs/_certvia-archiv/KONZEPT-garage-migration.md (§5).
|
||||||
#
|
#
|
||||||
# WICHTIG — KEINE Secrets in dieser Datei (sie ist im Repo eingecheckt):
|
# WICHTIG — KEINE Secrets in dieser Datei (sie ist im Repo eingecheckt):
|
||||||
# rpc_secret ← wird zur Laufzeit aus GARAGE_RPC_SECRET gelesen
|
# rpc_secret ← wird zur Laufzeit aus GARAGE_RPC_SECRET gelesen
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
# - Konfiguration über Coolify-Env-Variablen statt env_file: .env
|
# - Konfiguration über Coolify-Env-Variablen statt env_file: .env
|
||||||
# - Service "migrate": Init-Job (prisma migrate deploy + Rollen-Rechte-Sync), läuft einmalig VOR app
|
# - Service "migrate": Init-Job (prisma migrate deploy + Rollen-Rechte-Sync), läuft einmalig VOR app
|
||||||
# - kein mailhog (Dev); Service "worker" = SEC1 Mail-Worker (BullMQ/Redis-Queue)
|
# - kein mailhog (Dev); Service "worker" = SEC1 Mail-Worker (BullMQ/Redis-Queue)
|
||||||
# In Coolify als "Docker Compose Location" -> docker-compose.coolify.yml setzen.
|
# - Service "craftvia-worker" = Craftvia-Job-Worker (Image craftvia-worker, Stage "worker")
|
||||||
|
# In Coolify als "Docker Compose Location" -> docker-compose.coolify.prebuilt.yml setzen.
|
||||||
|
# Betriebsdoku: docs/craftvia/DEPLOY.md
|
||||||
#
|
#
|
||||||
# Härtung (F-11/F-18):
|
# Härtung (F-11/F-18):
|
||||||
# - F-11: migrate nutzt die schlanke "migrate"-Stage (kein Next-Build), Images gepinnt.
|
# - F-11: migrate nutzt die schlanke "migrate"-Stage (kein Next-Build), Images gepinnt.
|
||||||
@@ -97,13 +99,36 @@ services:
|
|||||||
# wählt bzw. als Env-Fallback. Muss auf das gemountete `backups`-Volume zeigen,
|
# wählt bzw. als Env-Fallback. Muss auf das gemountete `backups`-Volume zeigen,
|
||||||
# sonst sind Sicherungen beim Redeploy flüchtig. DB-Config hat Vorrang vor dieser Var.
|
# sonst sind Sicherungen beim Redeploy flüchtig. DB-Config hat Vorrang vor dieser Var.
|
||||||
BACKUP_LOCAL_DIR: ${BACKUP_LOCAL_DIR:-/app/.backups}
|
BACKUP_LOCAL_DIR: ${BACKUP_LOCAL_DIR:-/app/.backups}
|
||||||
AI_PROVIDER: ${AI_PROVIDER}
|
|
||||||
AI_API_KEY: ${AI_API_KEY}
|
|
||||||
SMTP_HOST: ${SMTP_HOST}
|
SMTP_HOST: ${SMTP_HOST}
|
||||||
SMTP_PORT: ${SMTP_PORT}
|
SMTP_PORT: ${SMTP_PORT}
|
||||||
|
SMTP_SECURE: ${SMTP_SECURE:-}
|
||||||
SMTP_USER: ${SMTP_USER}
|
SMTP_USER: ${SMTP_USER}
|
||||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||||
SMTP_FROM: ${SMTP_FROM}
|
SMTP_FROM: ${SMTP_FROM}
|
||||||
|
MAIL_FROM_NAME: ${MAIL_FROM_NAME:-}
|
||||||
|
MAIL_REPLY_TO: ${MAIL_REPLY_TO:-}
|
||||||
|
# Basis für absolute Links (Mails, PDFs); leer = Fallback AUTH_URL.
|
||||||
|
APP_BASE_URL: ${APP_BASE_URL:-}
|
||||||
|
# Craftvia-KI (ARCHITEKTUR §4.5). Ohne ANTHROPIC_API_KEY bzw. TRANSCRIPTION_API_KEY
|
||||||
|
# graceful degradation (Status "disabled", manuelle Eingabe).
|
||||||
|
AI_EXTRACTION_PROVIDER: ${AI_EXTRACTION_PROVIDER:-anthropic}
|
||||||
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||||
|
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-}
|
||||||
|
TRANSCRIPTION_PROVIDER: ${TRANSCRIPTION_PROVIDER:-openai-compatible}
|
||||||
|
TRANSCRIPTION_API_URL: ${TRANSCRIPTION_API_URL:-}
|
||||||
|
TRANSCRIPTION_API_KEY: ${TRANSCRIPTION_API_KEY:-}
|
||||||
|
TRANSCRIPTION_MODEL: ${TRANSCRIPTION_MODEL:-}
|
||||||
|
# Tokenbudget je Mandant je Kalendermonat (ein+aus); 0 = unbegrenzt.
|
||||||
|
AI_MONTHLY_TOKEN_LIMIT: ${AI_MONTHLY_TOKEN_LIMIT:-0}
|
||||||
|
# Optionaler Malware-Scan der Uploads (clamd INSTREAM); leer = nur Typ-/Magic-Byte-Prüfung.
|
||||||
|
CLAMAV_HOST: ${CLAMAV_HOST:-}
|
||||||
|
CLAMAV_PORT: ${CLAMAV_PORT:-3310}
|
||||||
|
# PWA: ab wie vielen Tagen ein Offline-Bundle als veraltet gilt (1–365, Default 7).
|
||||||
|
OFFLINE_MAX_DAYS: ${OFFLINE_MAX_DAYS:-7}
|
||||||
|
# Rate Limits je Nutzer/Minute: /api/v1/** allgemein bzw. Einsatz-/Sync-Endpunkte
|
||||||
|
# (/api/v1/sync, /api/v1/uploads, /api/v1/field/**).
|
||||||
|
API_RATE_LIMIT_PER_MINUTE: ${API_RATE_LIMIT_PER_MINUTE:-300}
|
||||||
|
API_FIELD_RATE_LIMIT_PER_MINUTE: ${API_FIELD_RATE_LIMIT_PER_MINUTE:-1200}
|
||||||
# Persistenter lokaler Backup-Zielspeicher (überlebt Redeploys).
|
# Persistenter lokaler Backup-Zielspeicher (überlebt Redeploys).
|
||||||
volumes:
|
volumes:
|
||||||
- backups:/app/.backups
|
- backups:/app/.backups
|
||||||
@@ -238,6 +263,81 @@ services:
|
|||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Craftvia-Job-Worker (ARCHITEKTUR §4.4, scripts/craftvia-worker.ts): je BullMQ-Queue ein
|
||||||
|
# Worker für import-extraction, transcription, report-pdf, image-derivatives.
|
||||||
|
# OHNE diesen Dienst bleiben Import-Extraktion, Transkription, Berichts-PDFs und
|
||||||
|
# Bild-Derivate in der Queue liegen (die App reiht bei gesetztem REDIS_URL nur ein).
|
||||||
|
# Image craftvia-worker = Dockerfile-Stage "worker" (tsx + src + Prisma-Client +
|
||||||
|
# Debian-Chromium + Schriften) — HTML→PDF läuft NUR hier, nie in app.
|
||||||
|
# Processors greifen über dbForTenant zu → bei RLS_ENFORCED=true wie app über
|
||||||
|
# RLS_DATABASE_URL (Rolle craftvia_app). Egress (default-Netz) für Anthropic-/
|
||||||
|
# Transkriptions-API und SMTP. Kein Port, kein Traefik.
|
||||||
|
craftvia-worker:
|
||||||
|
image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-worker:${IMAGE_TAG:-main}
|
||||||
|
command: ["npx", "tsx", "scripts/craftvia-worker.ts"]
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
RLS_ENFORCED: ${RLS_ENFORCED:-false}
|
||||||
|
RLS_DATABASE_URL: ${RLS_DATABASE_URL}
|
||||||
|
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
|
||||||
|
AUTH_SECRET: ${AUTH_SECRET}
|
||||||
|
MFA_ENC_KEY: ${MFA_ENC_KEY:-}
|
||||||
|
AUTH_URL: ${AUTH_URL}
|
||||||
|
APP_BASE_URL: ${APP_BASE_URL:-}
|
||||||
|
# Objektspeicher (Garage): Import-PDFs/Sprachnotizen lesen, PDFs/Derivate schreiben.
|
||||||
|
S3_ENDPOINT: ${S3_ENDPOINT}
|
||||||
|
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
||||||
|
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
||||||
|
S3_BUCKET: ${S3_BUCKET}
|
||||||
|
S3_REGION: ${S3_REGION:-us-east-1}
|
||||||
|
# KI-Provider (siehe app). Ohne Key: Jobs enden mit Status "disabled".
|
||||||
|
AI_EXTRACTION_PROVIDER: ${AI_EXTRACTION_PROVIDER:-anthropic}
|
||||||
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||||
|
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-}
|
||||||
|
TRANSCRIPTION_PROVIDER: ${TRANSCRIPTION_PROVIDER:-openai-compatible}
|
||||||
|
TRANSCRIPTION_API_URL: ${TRANSCRIPTION_API_URL:-}
|
||||||
|
TRANSCRIPTION_API_KEY: ${TRANSCRIPTION_API_KEY:-}
|
||||||
|
TRANSCRIPTION_MODEL: ${TRANSCRIPTION_MODEL:-}
|
||||||
|
AI_MONTHLY_TOKEN_LIMIT: ${AI_MONTHLY_TOKEN_LIMIT:-0}
|
||||||
|
# Aufbewahrung KI-Protokoll (AiGeneration): Ein-/Ausgaben älter als N Tage leeren.
|
||||||
|
AI_GENERATION_RETENTION_DAYS: ${AI_GENERATION_RETENTION_DAYS:-180}
|
||||||
|
# Chromium aus dem Debian-Paket (im Image bereits gesetzt, hier explizit).
|
||||||
|
PDF_CHROMIUM_PATH: /usr/bin/chromium
|
||||||
|
SMTP_HOST: ${SMTP_HOST}
|
||||||
|
SMTP_PORT: ${SMTP_PORT}
|
||||||
|
SMTP_SECURE: ${SMTP_SECURE:-}
|
||||||
|
SMTP_USER: ${SMTP_USER}
|
||||||
|
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||||
|
SMTP_FROM: ${SMTP_FROM}
|
||||||
|
MAIL_FROM_NAME: ${MAIL_FROM_NAME:-}
|
||||||
|
MAIL_REPLY_TO: ${MAIL_REPLY_TO:-}
|
||||||
|
# Chromium nutzt /dev/shm für Renderer-Speicher; render.ts setzt zusätzlich
|
||||||
|
# --disable-dev-shm-usage, 1 GB schützt dennoch vor Abstürzen bei Fotoberichten.
|
||||||
|
shm_size: "1gb"
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
- default
|
||||||
|
security_opt:
|
||||||
|
- "no-new-privileges:true"
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "1.0"
|
||||||
|
memory: 1536M
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
garage:
|
||||||
|
condition: service_healthy
|
||||||
|
garage-provision:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: pgvector/pgvector:0.8.0-pg16
|
image: pgvector/pgvector:0.8.0-pg16
|
||||||
@@ -297,7 +397,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# Objektspeicher: Garage (S3-kompatibel) — ersetzt den früheren minio-Service
|
# Objektspeicher: Garage (S3-kompatibel) — ersetzt den früheren minio-Service
|
||||||
# (MinIO Community EOL/Maintenance-Mode). Konzept: docs/KONZEPT-garage-migration.md.
|
# (MinIO Community EOL/Maintenance-Mode). Konzept: docs/_certvia-archiv/KONZEPT-garage-migration.md.
|
||||||
# Buckets/Keys werden NICHT über die S3-API angelegt, sondern vom Init-Job
|
# Buckets/Keys werden NICHT über die S3-API angelegt, sondern vom Init-Job
|
||||||
# "garage-provision" (Admin-API). Nichts nach außen (kein Traefik/ports:) —
|
# "garage-provision" (Admin-API). Nichts nach außen (kein Traefik/ports:) —
|
||||||
# rein clusterintern, wie minio zuvor. Version gepinnt (kein latest).
|
# rein clusterintern, wie minio zuvor. Version gepinnt (kein latest).
|
||||||
|
|||||||
+106
-3
@@ -4,7 +4,10 @@
|
|||||||
# - Konfiguration über Coolify-Env-Variablen statt env_file: .env
|
# - Konfiguration über Coolify-Env-Variablen statt env_file: .env
|
||||||
# - Service "migrate": Init-Job (prisma migrate deploy + Rollen-Rechte-Sync), läuft einmalig VOR app
|
# - Service "migrate": Init-Job (prisma migrate deploy + Rollen-Rechte-Sync), läuft einmalig VOR app
|
||||||
# - kein mailhog (Dev); Service "worker" = SEC1 Mail-Worker (BullMQ/Redis-Queue)
|
# - kein mailhog (Dev); Service "worker" = SEC1 Mail-Worker (BullMQ/Redis-Queue)
|
||||||
|
# - Service "craftvia-worker" = Craftvia-Job-Worker (Import-Extraktion, Transkription,
|
||||||
|
# Berichts-PDF mit Chromium, Bild-Derivate) aus der Dockerfile-Stage "worker"
|
||||||
# In Coolify als "Docker Compose Location" -> docker-compose.coolify.yml setzen.
|
# In Coolify als "Docker Compose Location" -> docker-compose.coolify.yml setzen.
|
||||||
|
# Betriebsdoku: docs/craftvia/DEPLOY.md
|
||||||
#
|
#
|
||||||
# Härtung (F-11/F-18):
|
# Härtung (F-11/F-18):
|
||||||
# - F-11: migrate nutzt die schlanke "migrate"-Stage (kein Next-Build), Images gepinnt.
|
# - F-11: migrate nutzt die schlanke "migrate"-Stage (kein Next-Build), Images gepinnt.
|
||||||
@@ -97,13 +100,36 @@ services:
|
|||||||
# wählt bzw. als Env-Fallback. Muss auf das gemountete `backups`-Volume zeigen,
|
# wählt bzw. als Env-Fallback. Muss auf das gemountete `backups`-Volume zeigen,
|
||||||
# sonst sind Sicherungen beim Redeploy flüchtig. DB-Config hat Vorrang vor dieser Var.
|
# sonst sind Sicherungen beim Redeploy flüchtig. DB-Config hat Vorrang vor dieser Var.
|
||||||
BACKUP_LOCAL_DIR: ${BACKUP_LOCAL_DIR:-/app/.backups}
|
BACKUP_LOCAL_DIR: ${BACKUP_LOCAL_DIR:-/app/.backups}
|
||||||
AI_PROVIDER: ${AI_PROVIDER}
|
|
||||||
AI_API_KEY: ${AI_API_KEY}
|
|
||||||
SMTP_HOST: ${SMTP_HOST}
|
SMTP_HOST: ${SMTP_HOST}
|
||||||
SMTP_PORT: ${SMTP_PORT}
|
SMTP_PORT: ${SMTP_PORT}
|
||||||
|
SMTP_SECURE: ${SMTP_SECURE:-}
|
||||||
SMTP_USER: ${SMTP_USER}
|
SMTP_USER: ${SMTP_USER}
|
||||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||||
SMTP_FROM: ${SMTP_FROM}
|
SMTP_FROM: ${SMTP_FROM}
|
||||||
|
MAIL_FROM_NAME: ${MAIL_FROM_NAME:-}
|
||||||
|
MAIL_REPLY_TO: ${MAIL_REPLY_TO:-}
|
||||||
|
# Basis für absolute Links (Mails, PDFs); leer = Fallback AUTH_URL.
|
||||||
|
APP_BASE_URL: ${APP_BASE_URL:-}
|
||||||
|
# Craftvia-KI (ARCHITEKTUR §4.5). Ohne ANTHROPIC_API_KEY bzw. TRANSCRIPTION_API_KEY
|
||||||
|
# graceful degradation (Status "disabled", manuelle Eingabe).
|
||||||
|
AI_EXTRACTION_PROVIDER: ${AI_EXTRACTION_PROVIDER:-anthropic}
|
||||||
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||||
|
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-}
|
||||||
|
TRANSCRIPTION_PROVIDER: ${TRANSCRIPTION_PROVIDER:-openai-compatible}
|
||||||
|
TRANSCRIPTION_API_URL: ${TRANSCRIPTION_API_URL:-}
|
||||||
|
TRANSCRIPTION_API_KEY: ${TRANSCRIPTION_API_KEY:-}
|
||||||
|
TRANSCRIPTION_MODEL: ${TRANSCRIPTION_MODEL:-}
|
||||||
|
# Tokenbudget je Mandant je Kalendermonat (ein+aus); 0 = unbegrenzt.
|
||||||
|
AI_MONTHLY_TOKEN_LIMIT: ${AI_MONTHLY_TOKEN_LIMIT:-0}
|
||||||
|
# Optionaler Malware-Scan der Uploads (clamd INSTREAM); leer = nur Typ-/Magic-Byte-Prüfung.
|
||||||
|
CLAMAV_HOST: ${CLAMAV_HOST:-}
|
||||||
|
CLAMAV_PORT: ${CLAMAV_PORT:-3310}
|
||||||
|
# PWA: ab wie vielen Tagen ein Offline-Bundle als veraltet gilt (1–365, Default 7).
|
||||||
|
OFFLINE_MAX_DAYS: ${OFFLINE_MAX_DAYS:-7}
|
||||||
|
# Rate Limits je Nutzer/Minute: /api/v1/** allgemein bzw. Einsatz-/Sync-Endpunkte
|
||||||
|
# (/api/v1/sync, /api/v1/uploads, /api/v1/field/**).
|
||||||
|
API_RATE_LIMIT_PER_MINUTE: ${API_RATE_LIMIT_PER_MINUTE:-300}
|
||||||
|
API_FIELD_RATE_LIMIT_PER_MINUTE: ${API_FIELD_RATE_LIMIT_PER_MINUTE:-1200}
|
||||||
# Persistenter lokaler Backup-Zielspeicher (überlebt Redeploys).
|
# Persistenter lokaler Backup-Zielspeicher (überlebt Redeploys).
|
||||||
volumes:
|
volumes:
|
||||||
- backups:/app/.backups
|
- backups:/app/.backups
|
||||||
@@ -242,6 +268,83 @@ services:
|
|||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Craftvia-Job-Worker (ARCHITEKTUR §4.4, scripts/craftvia-worker.ts): je BullMQ-Queue ein
|
||||||
|
# Worker für import-extraction, transcription, report-pdf, image-derivatives.
|
||||||
|
# OHNE diesen Dienst bleiben Import-Extraktion, Transkription, Berichts-PDFs und
|
||||||
|
# Bild-Derivate in der Queue liegen (die App reiht bei gesetztem REDIS_URL nur ein).
|
||||||
|
# Eigene Dockerfile-Stage "worker": tsx + src + Prisma-Client + Debian-Chromium und
|
||||||
|
# Schriften (fonts-dejavu-core, fonts-liberation) — HTML→PDF läuft NUR hier, nie in app.
|
||||||
|
# Processors greifen über dbForTenant zu → bei RLS_ENFORCED=true wie app über
|
||||||
|
# RLS_DATABASE_URL (Rolle craftvia_app). Egress (default-Netz) für Anthropic-/
|
||||||
|
# Transkriptions-API und SMTP. Kein Port, kein Traefik.
|
||||||
|
craftvia-worker:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: worker
|
||||||
|
command: ["npx", "tsx", "scripts/craftvia-worker.ts"]
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
RLS_ENFORCED: ${RLS_ENFORCED:-false}
|
||||||
|
RLS_DATABASE_URL: ${RLS_DATABASE_URL}
|
||||||
|
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
|
||||||
|
AUTH_SECRET: ${AUTH_SECRET}
|
||||||
|
MFA_ENC_KEY: ${MFA_ENC_KEY:-}
|
||||||
|
AUTH_URL: ${AUTH_URL}
|
||||||
|
APP_BASE_URL: ${APP_BASE_URL:-}
|
||||||
|
# Objektspeicher (Garage): Import-PDFs/Sprachnotizen lesen, PDFs/Derivate schreiben.
|
||||||
|
S3_ENDPOINT: ${S3_ENDPOINT}
|
||||||
|
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
||||||
|
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
||||||
|
S3_BUCKET: ${S3_BUCKET}
|
||||||
|
S3_REGION: ${S3_REGION:-us-east-1}
|
||||||
|
# KI-Provider (siehe app). Ohne Key: Jobs enden mit Status "disabled".
|
||||||
|
AI_EXTRACTION_PROVIDER: ${AI_EXTRACTION_PROVIDER:-anthropic}
|
||||||
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||||
|
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-}
|
||||||
|
TRANSCRIPTION_PROVIDER: ${TRANSCRIPTION_PROVIDER:-openai-compatible}
|
||||||
|
TRANSCRIPTION_API_URL: ${TRANSCRIPTION_API_URL:-}
|
||||||
|
TRANSCRIPTION_API_KEY: ${TRANSCRIPTION_API_KEY:-}
|
||||||
|
TRANSCRIPTION_MODEL: ${TRANSCRIPTION_MODEL:-}
|
||||||
|
AI_MONTHLY_TOKEN_LIMIT: ${AI_MONTHLY_TOKEN_LIMIT:-0}
|
||||||
|
# Aufbewahrung KI-Protokoll (AiGeneration): Ein-/Ausgaben älter als N Tage leeren.
|
||||||
|
AI_GENERATION_RETENTION_DAYS: ${AI_GENERATION_RETENTION_DAYS:-180}
|
||||||
|
# Chromium aus dem Debian-Paket (im Image bereits gesetzt, hier explizit).
|
||||||
|
PDF_CHROMIUM_PATH: /usr/bin/chromium
|
||||||
|
SMTP_HOST: ${SMTP_HOST}
|
||||||
|
SMTP_PORT: ${SMTP_PORT}
|
||||||
|
SMTP_SECURE: ${SMTP_SECURE:-}
|
||||||
|
SMTP_USER: ${SMTP_USER}
|
||||||
|
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||||
|
SMTP_FROM: ${SMTP_FROM}
|
||||||
|
MAIL_FROM_NAME: ${MAIL_FROM_NAME:-}
|
||||||
|
MAIL_REPLY_TO: ${MAIL_REPLY_TO:-}
|
||||||
|
# Chromium nutzt /dev/shm für Renderer-Speicher; render.ts setzt zusätzlich
|
||||||
|
# --disable-dev-shm-usage, 1 GB schützt dennoch vor Abstürzen bei Fotoberichten.
|
||||||
|
shm_size: "1gb"
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
- default
|
||||||
|
security_opt:
|
||||||
|
- "no-new-privileges:true"
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "1.0"
|
||||||
|
memory: 1536M
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
garage:
|
||||||
|
condition: service_healthy
|
||||||
|
garage-provision:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: pgvector/pgvector:0.8.0-pg16
|
image: pgvector/pgvector:0.8.0-pg16
|
||||||
@@ -301,7 +404,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# Objektspeicher: Garage (S3-kompatibel) — ersetzt den früheren minio-Service
|
# Objektspeicher: Garage (S3-kompatibel) — ersetzt den früheren minio-Service
|
||||||
# (MinIO Community EOL/Maintenance-Mode). Konzept: docs/KONZEPT-garage-migration.md.
|
# (MinIO Community EOL/Maintenance-Mode). Konzept: docs/_certvia-archiv/KONZEPT-garage-migration.md.
|
||||||
# Buckets/Keys werden NICHT über die S3-API angelegt, sondern vom Init-Job
|
# Buckets/Keys werden NICHT über die S3-API angelegt, sondern vom Init-Job
|
||||||
# "garage-provision" (Admin-API). Nichts nach außen (kein Traefik/ports:) —
|
# "garage-provision" (Admin-API). Nichts nach außen (kein Traefik/ports:) —
|
||||||
# rein clusterintern, wie minio zuvor. Version gepinnt (kein latest).
|
# rein clusterintern, wie minio zuvor. Version gepinnt (kein latest).
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Certvia-Archiv
|
||||||
|
|
||||||
|
Craftvia ist aus dem Fundament des ISMS-Produkts **Certvia** hervorgegangen (Auth.js mit
|
||||||
|
Identity/Mitgliedschaften, RLS, Mail-/Backup-Worker, Garage, Härtung). Dieser Ordner hält
|
||||||
|
die dabei übernommenen Dokumente **unverändert** vor. Sie sind **nicht maßgeblich** für Craftvia.
|
||||||
|
|
||||||
|
## Warum archiviert
|
||||||
|
|
||||||
|
- Produkt-, Domain- und Personenbezug auf Certvia/ISMS (`app.certvia.de`, Gitea-/Coolify-Hosts,
|
||||||
|
Rollen ISB/DSB, Vorfall-Mail-Eingang, Risiko-Backfill), veraltete Namen (`isms_app`,
|
||||||
|
`isms-documents`, MinIO).
|
||||||
|
- Konzepte und Umsetzungs-Prompts sind umgesetzt. Der Ist-Stand steht im Code und in der
|
||||||
|
Craftvia-Doku.
|
||||||
|
- Der betriebsrelevante Inhalt (Coolify-Deploy, Prebuilt-Images, RLS-Aktivierung, Secrets,
|
||||||
|
Backup/Restore, Garage) ist in **[docs/craftvia/DEPLOY.md](../craftvia/DEPLOY.md)**
|
||||||
|
zusammengeführt und auf Craftvia umgeschrieben.
|
||||||
|
|
||||||
|
Maßgeblich sind [AGENTS.md](../../AGENTS.md), [docs/craftvia/SPEC-CRAFTVIA.md](../craftvia/SPEC-CRAFTVIA.md),
|
||||||
|
[docs/craftvia/ARCHITEKTUR.md](../craftvia/ARCHITEKTUR.md) und [docs/craftvia/DEPLOY.md](../craftvia/DEPLOY.md).
|
||||||
|
|
||||||
|
## Inhalt
|
||||||
|
|
||||||
|
| Datei | Thema | Noch als Hintergrund nützlich für |
|
||||||
|
|---|---|---|
|
||||||
|
| `DEPLOY-COOLIFY.md` | Testserver via Coolify (Certvia) | – (ersetzt durch DEPLOY.md) |
|
||||||
|
| `DEPLOY-PROD-CONTABO.md` | Prod-VPS, LUKS, pgBackRest/age/restic, PITR, Vorfall-Mail-Eingang | Host-Encryption- und PITR-Details |
|
||||||
|
| `DEPLOY-PROD-PREBUILT.md` | Prebuilt-Images über die Registry | – (ersetzt durch DEPLOY.md) |
|
||||||
|
| `HANDOVER-DEVOPS.md` | frühe DevOps-Übergabe (Stand Juli 2026) | – |
|
||||||
|
| `DEVOPS-INTEGRATION-RUNBOOK.md` | Branch-Integration im Certvia-Team | – |
|
||||||
|
| `SECRETS-REGISTER.md` | Secrets-Register (Certvia) | Rotationsregeln (in DEPLOY.md übernommen) |
|
||||||
|
| `KONZEPT-backup-restore.md` | Backup-/Restore-/DSGVO-Engine | Designbegründung von `src/server/backup/**` |
|
||||||
|
| `KONZEPT-backup-target.md` | konfigurierbarer Backup-Zielspeicher | Designbegründung `/admin/backup` |
|
||||||
|
| `KONZEPT-garage-migration.md` | MinIO → Garage | Designbegründung Garage/`garage-provision` |
|
||||||
|
| `KONZEPT-haertung.md` | Pepper, Host-Encryption, Secrets | Designbegründung `PASSWORD_PEPPER` |
|
||||||
|
| `KONZEPT-identity-mandanten.md`, `FEINDESIGN-identity-mandanten.md`, `UEBERGABE-identity-mandanten.md` | zentrale Identity + Mandanten-Mitgliedschaften | Designbegründung Two-Step-Login/Mandantenwechsel |
|
||||||
|
| `KONZEPT-ui-i18n.md` | Betreiber-Konsole-UX, i18n | – |
|
||||||
|
| `SEC1-MAIL.md`, `SEC2-AUTH-SELFSERVICE.md` | Mail-Fundament, Passwort-Self-Service | Hintergrund zu `src/server/mail/**`, `scripts/test-mail.ts`, `scripts/test-auth-selfservice.ts` |
|
||||||
|
| `sicherheit/` | PO-Konzept und Claude-Code-Prompts SEC1–SEC6 (Certvia) | – |
|
||||||
|
|
||||||
|
Die Querverweise **innerhalb** dieser Dokumente (`docs/…`) zeigen noch auf die alten Pfade.
|
||||||
|
Sie werden bewusst nicht nachgezogen.
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
# 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 **`app`** die Domain setzen, z. B. `https://app.craftvia.example:3000`
|
||||||
|
(Port 3000 = Container-Port, Schema `https`). Coolify/Traefik stellt das Let's-Encrypt-Zertifikat aus.
|
||||||
|
Kein Host-Port wird exponiert. `garage`, `postgres`, `redis` und 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_URL` für absolute Links in Mails/PDFs (leer = `AUTH_URL`).
|
||||||
|
- Passkeys: `WEBAUTHN_ORIGIN`/`WEBAUTHN_RP_ID` nur setzen, wenn sie von `AUTH_URL` abweichen.
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
1. **Ressource:** Git-Repo (Deploy-Key, nur lesend), Build Pack **Docker Compose**, Compose Location
|
||||||
|
`docker-compose.coolify.yml` (oder `…prebuilt.yml`, siehe §9).
|
||||||
|
2. **Environment-Variablen** aus `.env.prod.example` bzw. `.env.coolify.example` eintragen (§4). Secrets
|
||||||
|
**literal** setzen, nicht über `${…}` referenzieren (Coolify-Interpolation).
|
||||||
|
3. **Persistent Storage prüfen:** `pgdata`, `garage_meta`, `garage_data`, `redisdata`, `backups`.
|
||||||
|
4. **Erster Deploy Produktion:** `RUN_DEMO_SEED=false`, `BOOTSTRAP_ADMIN=true` + `BOOTSTRAP_ADMIN_*` +
|
||||||
|
`BOOTSTRAP_TENANT_*`. Der `migrate`-Job legt über `scripts/bootstrap-admin.ts` den 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 im `migrate`-Container prüfen, danach Passwort ändern und
|
||||||
|
`BOOTSTRAP_ADMIN=false`.
|
||||||
|
5. **Testserver:** `RUN_DEMO_SEED=true` legt die Demo-Mandanten an (Logins siehe `AGENTS.md`). Nie in Produktion.
|
||||||
|
6. **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** Deploy `npx prisma migrate deploy` aus. 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](MIGRATIONS.md).
|
||||||
|
- Manuell (Coolify-Terminal des `migrate`-Containers oder `docker 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`.
|
||||||
|
|
||||||
|
1. Passwort für die App-Rolle setzen (einmalig je Umgebung, Wert in den Passwortmanager):
|
||||||
|
```bash
|
||||||
|
docker exec -it <postgres-container> psql -U craftvia -d craftvia \
|
||||||
|
-c "ALTER ROLE craftvia_app WITH LOGIN PASSWORD '<STARKES_PASSWORT>';"
|
||||||
|
```
|
||||||
|
2. Env setzen:
|
||||||
|
`RLS_ENFORCED=true`,
|
||||||
|
`RLS_DATABASE_URL=postgresql://craftvia_app:<STARKES_PASSWORT>@postgres:5432/craftvia?schema=public`
|
||||||
|
3. Neu deployen. Fehlt `RLS_DATABASE_URL` bei aktivem Flag, bricht der Prozess beim Start ab
|
||||||
|
(fail secure).
|
||||||
|
4. Nachweis: `npx tsx scripts/test-rls-enforcement.ts`. Der Test prüft Owner-Sicht, Isolation A/B, 0 Zeilen
|
||||||
|
ohne Kontext, `WITH CHECK` und `dbForTenant` end-to-end. Er gehört auch zum CI-Gate.
|
||||||
|
|
||||||
|
**Warnungen:**
|
||||||
|
- Die Owner-Rolle in `DATABASE_URL` **muss** Superuser oder BYPASSRLS sein. Sonst sieht der Login keine
|
||||||
|
Nutzer, und Restore (`session_replication_role`) scheitert.
|
||||||
|
- Mehrschritt-Schreibvorgänge nur über `inTransaction(ctx, fn)`, direktes `ctx.db.$transaction` ist
|
||||||
|
unter `RLS_ENFORCED=true` nicht atomar (ARCHITEKTUR §4.8).
|
||||||
|
- Zeilen mit `tenant_id = NULL` (Plattform-Audit, Mail-Logs, Auth-Tokens) sind für `craftvia_app`
|
||||||
|
unsichtbar. 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-Pakete `chromium`, `fonts-dejavu-core`,
|
||||||
|
`fonts-liberation`; `node_modules` inkl. `tsx`, generierter Prisma-Client, `scripts/`, `src/`,
|
||||||
|
`messages/`, `prisma/`. Start `npx tsx scripts/craftvia-worker.ts`.
|
||||||
|
- PDF-Rendering (`src/server/pdf/render.ts`): `playwright-core` startet `PDF_CHROMIUM_PATH` mit
|
||||||
|
`--no-sandbox --disable-dev-shm-usage`. Der Container braucht deshalb keine zusätzlichen Capabilities.
|
||||||
|
`shm_size: 1gb` ist als Reserve für fotoreiche Berichte gesetzt. Seiten laden keine Netzressourcen, alle Assets sind als `data:`
|
||||||
|
eingebettet.
|
||||||
|
- Concurrency: `report-pdf` 2, übrige Queues 4 je Worker-Prozess. Horizontal skalieren = weitere
|
||||||
|
`craftvia-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 `AiGeneration` protokolliert (Art, Provider, Modell, Bezug, Tokens ein/aus,
|
||||||
|
auslösender Nutzer, Ein-/Ausgabe).
|
||||||
|
- **Kostenbremse:** `AI_MONTHLY_TOKEN_LIMIT` ist die Plattform-Vorgabe für Tokens (ein + aus) je Mandant je
|
||||||
|
Kalendermonat (UTC), `0` = unbegrenzt. Mandantenadministratoren können unter `/settings/lotse` einen
|
||||||
|
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 |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
1. `migrate` und `garage-provision` mit Exit 0 beendet. `app` ist healthy, `craftvia-worker`, `worker` und
|
||||||
|
`backup-worker` laufen, im Log steht `[worker] listening on report-pdf` usw.
|
||||||
|
2. `https://app.craftvia.example/login` lädt mit gültigem Zertifikat. `/sw.js` und `/site.webmanifest` sind ohne
|
||||||
|
Session erreichbar (PWA).
|
||||||
|
3. Login Backoffice → `/dashboard`, `/work-orders`, `/customers`, `/reports`. Login Monteur → `/m`.
|
||||||
|
4. Datei-Upload an einem Auftrag und Download über `/files/<documentId>` (prüft Garage + S3-Keys).
|
||||||
|
5. Bericht freigeben → PDF erscheint am Auftrag (prüft Queue, craftvia-worker, Chromium).
|
||||||
|
6. Optional: Import-PDF hochladen → Extraktion (bei gesetztem API-Key). Sprachnotiz → Transkription.
|
||||||
|
7. Test-Mail (z. B. Passwort-Reset) kommt an bzw. steht nachvollziehbar auf `pending`.
|
||||||
|
8. Plattform-Login `/platform/login` → `/admin`, `/admin/backup` zeigt das Backup-Ziel.
|
||||||
|
9. 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 und `repo-cipher-type=aes-256-cbc`.
|
||||||
|
Deckt auch die globalen Tabellen (Identity, Plattform-Admins, Kataloge) ab.
|
||||||
|
```bash
|
||||||
|
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**) und `garage_data`
|
||||||
|
sichern, z. B. mit restic (eigenes Repo, eigenes Passwort) oder als Volume-Snapshot bei gestopptem
|
||||||
|
`garage`. Ohne `garage_meta` sind 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/backup` wählbar (Lokal = Volume `/app/.backups` oder 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
|
||||||
|
vom `backup-worker`. Die Artefakte sind mit `BACKUP_ENC_KEY` (AES-256-GCM) verschlüsselt, der Restore arbeitet
|
||||||
|
nur innerhalb von `tenant_id` und betrifft keine anderen Mandanten. `TENANT_MODELS` in `src/server/db.ts` und
|
||||||
|
`src/server/backup/topology.ts` mü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):**
|
||||||
|
1. CI grün (Gate-Job: migrate, seed, tsc, lint, build, Tests).
|
||||||
|
2. Migrationen der Release sichten. Bei destruktiven Änderungen vorher `pg_dump` (§11.1).
|
||||||
|
3. Coolify-Redeploy (bzw. Images pushen, dann Redeploy). `migrate` läuft vor app und Workern.
|
||||||
|
4. Smoke (§10). Neue Env-Variablen aus den `.env.*.example`-Dateien vorher eintragen.
|
||||||
|
|
||||||
|
**Rollback:**
|
||||||
|
- **Ohne Schemaänderung:** vorheriges Image-Tag als `:main` retaggen 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 deploy` toleriert 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_URL` korrekt
|
||||||
|
- [ ] `RLS_ENFORCED=true` + `RLS_DATABASE_URL`, Smoke mit aktiver RLS bestanden
|
||||||
|
- [ ] `craftvia-worker` läuft, Test-PDF erzeugt
|
||||||
|
- [ ] SMTP mit SPF/DKIM/DMARC der Absenderdomain
|
||||||
|
- [ ] KI: AVV geklärt, `AI_MONTHLY_TOKEN_LIMIT` und `AI_GENERATION_RETENTION_DAYS` festgelegt
|
||||||
|
- [ ] 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
|
||||||
@@ -15,7 +15,7 @@ import { provisionTenant } from "@/server/provision";
|
|||||||
* Beide mit denselben Zugangsdaten (BOOTSTRAP_ADMIN_*), analog zum Seed.
|
* Beide mit denselben Zugangsdaten (BOOTSTRAP_ADMIN_*), analog zum Seed.
|
||||||
*
|
*
|
||||||
* Ausführung: im migrate-Job nach `prisma migrate deploy`, gesteuert per
|
* Ausführung: im migrate-Job nach `prisma migrate deploy`, gesteuert per
|
||||||
* BOOTSTRAP_ADMIN=true (siehe docker-compose.coolify.yml, docs/DEPLOY-PROD-CONTABO.md).
|
* BOOTSTRAP_ADMIN=true (siehe docker-compose.coolify.yml, docs/_certvia-archiv/DEPLOY-PROD-CONTABO.md).
|
||||||
*
|
*
|
||||||
* Idempotent: provisionTenant und der platformAdmin.upsert nutzen upserts; ein bereits
|
* Idempotent: provisionTenant und der platformAdmin.upsert nutzen upserts; ein bereits
|
||||||
* gesetztes Passwort wird beim erneuten Lauf NICHT überschrieben.
|
* gesetztes Passwort wird beim erneuten Lauf NICHT überschrieben.
|
||||||
|
|||||||
@@ -46,10 +46,12 @@ build_one() {
|
|||||||
build_one runner craftvia-app
|
build_one runner craftvia-app
|
||||||
build_one migrate craftvia-migrate
|
build_one migrate craftvia-migrate
|
||||||
build_one garage craftvia-garage
|
build_one garage craftvia-garage
|
||||||
|
# L10b: Hintergrund-Worker (BullMQ: Import-Extraktion, Transkription, Berichts-PDF mit Chromium, KI-Aufbewahrung)
|
||||||
|
build_one worker craftvia-worker
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo ">> Push ..."
|
echo ">> Push ..."
|
||||||
for name in craftvia-app craftvia-migrate craftvia-garage; do
|
for name in craftvia-app craftvia-migrate craftvia-garage craftvia-worker; do
|
||||||
docker push "$REGISTRY/$name:$TAG"
|
docker push "$REGISTRY/$name:$TAG"
|
||||||
[ "$ALSO_MAIN" = "true" ] && docker push "$REGISTRY/$name:main" || true
|
[ "$ALSO_MAIN" = "true" ] && docker push "$REGISTRY/$name:main" || true
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import "dotenv/config";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* IM/Garage-Migration — idempotentes Provisioning eines Single-Node-Garage
|
* IM/Garage-Migration — idempotentes Provisioning eines Single-Node-Garage
|
||||||
* (docs/KONZEPT-garage-migration.md §6 Lane B, §7 Runbook).
|
* (docs/_certvia-archiv/KONZEPT-garage-migration.md §6 Lane B, §7 Runbook).
|
||||||
*
|
*
|
||||||
* Garage verwaltet Buckets/Keys/Rechte NICHT über die S3-API (`CreateBucket` gibt es
|
* Garage verwaltet Buckets/Keys/Rechte NICHT über die S3-API (`CreateBucket` gibt es
|
||||||
* dort nicht), sondern out-of-band. Dieser Init-Job (Compose-Service „garage-provision",
|
* dort nicht), sondern out-of-band. Dieser Init-Job (Compose-Service „garage-provision",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { closeMailProvider } from "../src/server/mail/provider-smtp";
|
|||||||
* 9. Enumeration: unbekannte Adresse liefert kein Konto.
|
* 9. Enumeration: unbekannte Adresse liefert kein Konto.
|
||||||
*
|
*
|
||||||
* Die Browser-Abläufe (Reset-Mail → Link → neues Passwort) stehen im Testplan
|
* Die Browser-Abläufe (Reset-Mail → Link → neues Passwort) stehen im Testplan
|
||||||
* von docs/SEC2-AUTH-SELFSERVICE.md.
|
* von docs/_certvia-archiv/SEC2-AUTH-SELFSERVICE.md.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let failures = 0;
|
let failures = 0;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Smoke-Test des S3-Objektspeichers gegen einen echten Garage-Node (Phase C/D der
|
// Smoke-Test des S3-Objektspeichers gegen einen echten Garage-Node (Phase C/D der
|
||||||
// MinIO→Garage-Migration, docs/KONZEPT-garage-migration.md §9).
|
// MinIO→Garage-Migration, docs/_certvia-archiv/KONZEPT-garage-migration.md §9).
|
||||||
//
|
//
|
||||||
// Prüft den realen S3-Pfad (AWS SDK v3, path-style) über die exportierte
|
// Prüft den realen S3-Pfad (AWS SDK v3, path-style) über die exportierte
|
||||||
// `resolveBackupStore()`:
|
// `resolveBackupStore()`:
|
||||||
|
|||||||
Reference in New Issue
Block a user