# Coolify-Deployment (PREBUILT-Variante) — zieht fertige Images aus der Registry # statt auf dem Host zu bauen (Plan B: langsamer/timeoutender Host-Build umgehen). # Images vorher bauen+pushen: scripts/build-and-push-images.sh # Registry/Tag via Coolify-Env: REGISTRY (Default registry.example.com/craftvia), IMAGE_TAG (Default main). # Abgeleitet von docker-compose.coolify.yml. # Unterschiede zum lokalen Dev-Compose: # - keine host "ports": Coolify-Proxy routet die Domain intern auf app:3000 # - Konfiguration über Coolify-Env-Variablen statt env_file: .env # - 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) # - 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): # - F-11: migrate nutzt die schlanke "migrate"-Stage (kein Next-Build), Images gepinnt. # - F-18: Backend-Dienste (postgres/redis/garage/migrate) im internen Netz ohne Egress; # nur app zusätzlich im default-Netz (Coolify-Proxy). Redis mit Passwort. # Überall no-new-privileges, cap_drop ALL (+gezielte cap_add), Ressourcenlimits. services: # Einmaliger Migrations-Job. Nutzt die schlanke "migrate"-Stage (Prisma-CLI/tsx + src, # aber OHNE next build/.next) — kleinere Angriffsfläche als die frühere builder-Stage. # Nach `migrate deploy` läuft der Rollen-Rechte-Sync (scripts/sync-role-permissions.ts): # additiv + idempotent, zieht neu eingeführte Rechte für bestehende Mandanten nach # (F-06 × F-10). Bei 0 Änderungen ein No-op. Betroffene Nutzer danach neu einloggen. # Optionaler Demo-Seed nur, wenn RUN_DEMO_SEED=true (Testserver). Idempotent (upserts). # In Produktion die Variable NICHT setzen; stattdessen BOOTSTRAP_ADMIN. migrate: image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-migrate:${IMAGE_TAG:-main} command: sh -c "npx prisma migrate deploy && echo '>> Rollen-Rechte-Sync läuft…' && npx tsx scripts/sync-role-permissions.ts && if [ \"$$RUN_DEMO_SEED\" = \"true\" ]; then echo '>> Demo-Seed läuft…'; npx tsx prisma/seed.ts; fi && if [ \"$$BOOTSTRAP_ADMIN\" = \"true\" ]; then echo '>> Bootstrap-Admin läuft…'; npx tsx scripts/bootstrap-admin.ts; fi" environment: DATABASE_URL: ${DATABASE_URL} # Härtung §1: Passwort-Pepper (Argon2 `secret`). Seed/Bootstrap hashen Passwörter # → fail-secure, ohne gültigen 32-Byte-Hex wirft hashPassword und der Job scheitert. # Muss identisch zum app-Service sein (sonst verifizieren die Hashes später nicht). PASSWORD_PEPPER: ${PASSWORD_PEPPER} # Test: Demo-Seed. Produktiv: stattdessen BOOTSTRAP_ADMIN (Erst-Superadmin). RUN_DEMO_SEED: ${RUN_DEMO_SEED:-false} BOOTSTRAP_ADMIN: ${BOOTSTRAP_ADMIN:-false} BOOTSTRAP_ADMIN_EMAIL: ${BOOTSTRAP_ADMIN_EMAIL:-} BOOTSTRAP_ADMIN_PASSWORD: ${BOOTSTRAP_ADMIN_PASSWORD:-} BOOTSTRAP_ADMIN_NAME: ${BOOTSTRAP_ADMIN_NAME:-} BOOTSTRAP_TENANT_NAME: ${BOOTSTRAP_TENANT_NAME:-} BOOTSTRAP_TENANT_SLUG: ${BOOTSTRAP_TENANT_SLUG:-} BOOTSTRAP_TENANT_SHORT: ${BOOTSTRAP_TENANT_SHORT:-} BOOTSTRAP_TENANT_SECTOR: ${BOOTSTRAP_TENANT_SECTOR:-} networks: - backend security_opt: - "no-new-privileges:true" cap_drop: - ALL deploy: resources: limits: cpus: "1.0" memory: 1G depends_on: postgres: condition: service_healthy restart: "no" app: image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-app:${IMAGE_TAG:-main} environment: DATABASE_URL: ${DATABASE_URL} # F-04: Row Level Security scharf. Der migrate-Job oben läuft bewusst # weiter mit der Owner-DATABASE_URL (BYPASSRLS). Nur der App-Prozess # verbindet als eingeschränkte Rolle craftvia_app (NOBYPASSRLS) über # RLS_DATABASE_URL und setzt app.tenant_id pro Transaktion. RLS_ENFORCED: ${RLS_ENFORCED:-false} RLS_DATABASE_URL: ${RLS_DATABASE_URL} # F-18: REDIS_URL wird AUS REDIS_PASSWORD abgeleitet (nicht separat setzen!) — so # bleibt sie immer konsistent zum `--requirepass` des redis-Dienstes und Coolify # kann sie nicht als „managed" sperren. In Coolify NUR REDIS_PASSWORD setzen. REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 AUTH_SECRET: ${AUTH_SECRET} # Härtung §1: Passwort-Pepper (Argon2 `secret`). Fail-secure — ohne gültigen # 32-Byte-Hex wirft jeder Login/hash. Muss IDENTISCH zum migrate-Job sein und # ⚠ nach dem Setzen nicht mehr ändern (sonst verifizieren bestehende Hashes nicht). PASSWORD_PEPPER: ${PASSWORD_PEPPER} # SEC3-d: TOTP-Secrets werden „at rest" mit AES-256-GCM verschlüsselt # (src/server/secret-crypto.ts). Der Schlüssel leitet sich aus MFA_ENC_KEY # ab, sonst aus AUTH_SECRET — daher OPTIONAL (leer = Fallback AUTH_SECRET). # ⚠ Nach dem Setzen nicht mehr ändern: macht bereits verschlüsselte # TOTP-Secrets unlesbar (kein Umschlüssel-Tool). MFA_ENC_KEY: ${MFA_ENC_KEY:-} AUTH_URL: ${AUTH_URL} # Hinter dem Coolify/Traefik-Proxy zwingend für Auth.js v5 (sonst UntrustedHost). AUTH_TRUST_HOST: ${AUTH_TRUST_HOST:-true} S3_ENDPOINT: ${S3_ENDPOINT} S3_ACCESS_KEY: ${S3_ACCESS_KEY} S3_SECRET_KEY: ${S3_SECRET_KEY} S3_BUCKET: ${S3_BUCKET} # S1: echter S3-Adapter (src/server/storage/adapter.ts). Region ist für MinIO # beliebig (Default us-east-1); überschreibbar für echtes AWS-S3. S3_REGION: ${S3_REGION:-us-east-1} # Lokaler (persistenter) Backup-Zielspeicher, wenn der Betreiber im Portal „Lokal" # 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. BACKUP_LOCAL_DIR: ${BACKUP_LOCAL_DIR:-/app/.backups} 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:-} # 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). volumes: - backups:/app/.backups # app im internen Netz (Backend-Zugriff) UND im default-Netz (Coolify-Proxy-Routing). networks: - backend - default security_opt: - "no-new-privileges:true" cap_drop: - ALL deploy: resources: limits: cpus: "1.0" memory: 1G 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 healthcheck: # Dependency-frei (node ist im Image vorhanden); alles < 500 gilt als gesund test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/',r=>process.exit(r.statusCode<500?0:1)).on('error',()=>process.exit(1))"] interval: 15s timeout: 5s retries: 10 start_period: 30s restart: unless-stopped # SEC1: Mail-Worker. Eigener Langläufer-Prozess, der die BullMQ/Redis-Queue # abarbeitet (Zustellung mit Retry/DLQ + täglicher Fristen-/Reminder-Job). # Ist REDIS_URL gesetzt, reiht die App Jobs ein und DIESER Worker stellt zu; # ohne REDIS_URL versendet die App inline und der Worker wird nicht gebraucht. # Nutzt das schlanke "migrate"-Image (tsx + src + Mail-Deps + Prisma-Client). # Owner-DATABASE_URL (kein RLS): der Worker arbeitet mandantenübergreifend. # Im default-Netz (Egress), damit der externe SMTP-Server erreichbar ist. worker: image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-migrate:${IMAGE_TAG:-main} command: ["npx", "tsx", "scripts/mail-worker.ts"] environment: DATABASE_URL: ${DATABASE_URL} REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 # SMTP: fehlt die Konfiguration, bleibt der Worker aktiv und meldet den # Zustand (Jobs bleiben pending) — kein Neustart-Rennen beim Nachreichen. SMTP_HOST: ${SMTP_HOST} SMTP_PORT: ${SMTP_PORT} SMTP_SECURE: ${SMTP_SECURE:-} SMTP_USER: ${SMTP_USER} SMTP_PASSWORD: ${SMTP_PASSWORD} SMTP_FROM: ${SMTP_FROM} # Absolute Links in Mails (Reset etc.): APP_BASE_URL, sonst AUTH_URL. AUTH_URL: ${AUTH_URL} networks: - backend - default security_opt: - "no-new-privileges:true" cap_drop: - ALL deploy: resources: limits: cpus: "0.5" memory: 512M depends_on: postgres: condition: service_healthy redis: condition: service_started migrate: condition: service_completed_successfully restart: unless-stopped # Backup-Worker: arbeitet die BullMQ-Queue "backup-ops" ab (Portal-Restore, # "Export jetzt", DSGVO-Zustellung). OHNE diesen Dienst bleiben angestoßene Jobs # dauerhaft auf "queued" (die App reiht nur ein, führt NICHT inline aus). # Owner-DATABASE_URL (kein RLS): Restore/Export laufen mandantenübergreifend und # brauchen einen Superuser-Owner (session_replication_role beim Restore). # Braucht das S3/MinIO-Backup-Bucket (S3_*) und den Backup-Schlüssel (BACKUP_ENC_KEY, # Fallback AUTH_SECRET). concurrency ist im Worker seriell (destruktiv). backup-worker: image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-migrate:${IMAGE_TAG:-main} command: ["npx", "tsx", "scripts/backup-worker.ts"] environment: DATABASE_URL: ${DATABASE_URL} REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 AUTH_SECRET: ${AUTH_SECRET} # Pepper wird von der Fail-Secure-Startprüfung erwartet (assertSecureEnv). PASSWORD_PEPPER: ${PASSWORD_PEPPER} # Backup-Artefakt-Verschlüsselung (AES-256-GCM); ohne eigenen Key = AUTH_SECRET. BACKUP_ENC_KEY: ${BACKUP_ENC_KEY:-} # Objektspeicher für Backup-Artefakte + DSGVO-Pakete (S3/MinIO). Fehlt es, # nutzt der Store einen lokalen (ephemeren!) Ordner — für Prod S3_* setzen. S3_ENDPOINT: ${S3_ENDPOINT:-} S3_ACCESS_KEY: ${S3_ACCESS_KEY:-} S3_SECRET_KEY: ${S3_SECRET_KEY:-} S3_BUCKET: ${S3_BUCKET:-} S3_REGION: ${S3_REGION:-} # Lokaler (persistenter) Backup-Zielspeicher — identischer Mount wie beim app-Service, # damit „Lokal" für Worker (schreibt Artefakte) und app (liest im Portal) dasselbe # Volume sieht. DB-Config aus dem Betreiber-Portal hat Vorrang vor dieser Var. BACKUP_LOCAL_DIR: ${BACKUP_LOCAL_DIR:-/app/.backups} AUTH_URL: ${AUTH_URL} # Gleiches persistentes Volume wie app (gemeinsamer lokaler Zielspeicher). volumes: - backups:/app/.backups networks: - backend - default security_opt: - "no-new-privileges:true" cap_drop: - ALL deploy: resources: limits: cpus: "0.5" memory: 512M depends_on: postgres: condition: service_healthy redis: condition: service_started migrate: condition: service_completed_successfully 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: image: pgvector/pgvector:0.8.0-pg16 environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - pgdata:/var/lib/postgresql/data networks: - backend security_opt: - "no-new-privileges:true" cap_drop: - ALL # Postgres-Entrypoint chownt das Datadir und wechselt per gosu auf den postgres-User. cap_add: - CHOWN - DAC_OVERRIDE - FOWNER - SETGID - SETUID deploy: resources: limits: cpus: "1.0" memory: 1G healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] interval: 5s timeout: 5s retries: 10 restart: unless-stopped redis: image: redis:7.4.2-alpine # F-18: Redis mit Passwort. REDIS_URL (app) muss dieses Passwort tragen. command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] volumes: - redisdata:/data networks: - backend security_opt: - "no-new-privileges:true" cap_drop: - ALL # redis-alpine-Entrypoint chownt /data und wechselt per gosu auf den redis-User. cap_add: - CHOWN - SETGID - SETUID deploy: resources: limits: cpus: "0.5" memory: 256M restart: unless-stopped # Objektspeicher: Garage (S3-kompatibel) — ersetzt den früheren minio-Service # (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 # "garage-provision" (Admin-API). Nichts nach außen (kein Traefik/ports:) — # rein clusterintern, wie minio zuvor. Version gepinnt (kein latest). garage: # Image mit eingebackener deploy/garage.toml (Dockerfile-Stage „garage"). KEIN # Bind-Mount der Config: Coolify legt relative Bind-Quellen sonst als Verzeichnis an # → Garage bekäme /etc/garage.toml als Ordner („IO error: Is a directory"). image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-garage:${IMAGE_TAG:-main} # Secrets stehen NICHT in der eingebackenen deploy/garage.toml — der Daemon liest # rpc_secret/admin_token aus der Env (in Coolify literal setzen, nicht via ${…}). environment: GARAGE_RPC_SECRET: ${GARAGE_RPC_SECRET} GARAGE_ADMIN_TOKEN: ${GARAGE_ADMIN_TOKEN} volumes: # KRITISCH: meta enthält Bucket-/Key-/Layout-Definitionen → MUSS ins Host-Backup # (analog pgdata). Ohne meta sind die Objektdaten nicht adressierbar. - garage_meta:/var/lib/garage/meta - garage_data:/var/lib/garage/data networks: - backend security_opt: - "no-new-privileges:true" cap_drop: - ALL healthcheck: # Distroless-Image (nur /garage): der CLI-Status dient als Health-Signal. # Verbindet auf rpc_public_addr (loopback) → unabhängig von Compose-DNS. test: ["CMD", "/garage", "status"] interval: 10s timeout: 5s retries: 12 start_period: 10s deploy: resources: limits: cpus: "1.0" memory: 1G restart: unless-stopped # Idempotenter Provisioning-Job (analog "migrate", restart:no): stellt aus einer # leeren Garage Layout + Bucket + Access-Key + Rechte her (Garage-Admin-API, HTTP). # Läuft bei JEDEM Deploy; "already exists" = Erfolg. Ohne GARAGE_ADMIN_TOKEN No-op. # Nutzt das schlanke "migrate"-Image (Node/tsx) — kein Garage-Binary nötig. garage-provision: image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-migrate:${IMAGE_TAG:-main} command: ["npx", "tsx", "scripts/garage-provision.ts"] environment: GARAGE_ADMIN_URL: http://garage:3903 GARAGE_ADMIN_TOKEN: ${GARAGE_ADMIN_TOKEN} # Der zu importierende Garage-Key = App-Key (App-Env und Garage synchron). S3_ACCESS_KEY: ${S3_ACCESS_KEY} S3_SECRET_KEY: ${S3_SECRET_KEY} S3_BUCKET: ${S3_BUCKET:-craftvia-documents} # Optionaler separater Backup-Bucket (nur falls Backup-Store auf S3 statt lokal). BACKUP_S3_BUCKET: ${BACKUP_S3_BUCKET:-} # Single-Node-Layout (nominale Kapazität/Zone; für Prod ggf. anheben). GARAGE_ZONE: ${GARAGE_ZONE:-dc1} GARAGE_CAPACITY_BYTES: ${GARAGE_CAPACITY_BYTES:-100000000000} networks: - backend security_opt: - "no-new-privileges:true" cap_drop: - ALL deploy: resources: limits: cpus: "0.5" memory: 256M depends_on: garage: condition: service_healthy restart: "no" # ── ROLLBACK-NETZ (bis Garage abgenommen ist; danach entfernen) ─────────────── # Der alte minio-Service bleibt vorübergehend auskommentiert stehen, damit ein # Rollback ohne Repo-Archäologie möglich ist. NACH erfolgreicher Garage-Abnahme # diesen Block UND das Volume "miniodata" (unten) entfernen. # minio: # image: minio/minio:RELEASE.2025-04-22T22-12-26Z # command: server /data --console-address ":9001" # environment: # MINIO_ROOT_USER: ${MINIO_ROOT_USER} # MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} # volumes: # - miniodata:/data # networks: # - backend # security_opt: # - "no-new-privileges:true" # cap_drop: # - ALL # deploy: # resources: # limits: # cpus: "0.5" # memory: 512M # restart: unless-stopped networks: # Internes Netz ohne Egress/Host-Exposure für die Backend-Dienste. backend: internal: true # default: von Coolify für das Proxy-Routing zur app genutzt (mit Egress). default: volumes: pgdata: redisdata: # Garage-Objektspeicher. garage_meta ist KRITISCH (Bucket-/Key-/Layout-Definitionen) # und MUSS in die Host-Backup-Strategie (analog pgdata) — ohne meta sind die Daten # in garage_data nicht adressierbar. garage_meta: garage_data: # ROLLBACK: altes MinIO-Datenvolume. Nach abgenommener Garage-Migration entfernen. miniodata: # Persistenter lokaler Backup-Zielspeicher (Lane „Konfigurierbarer Backup-Zielspeicher"). # Von app + backup-worker unter /app/.backups gemountet, damit „Lokal" Redeploys überlebt. backups: