- docker-compose.yml: explizite Build-Targets (runner/migrate) — Ursache für "npx not found" bei garage-provision war das Default-Target (letzte Stage = Garage-Image ohne Node); worker startet npm run worker:mail; Defaults craftvia - Coolify-Compose: incident-inbound-worker und sync-policy-templates entfernt, Rollen-/Bucket-/Image-Namen auf craftvia - Dockerfile: seed/ und docs/wizard-uebergabe entfernt, messages/ im Runner - npm: Name craftvia, Scripts test (scripts/run-tests.ts) und gate, ISMS-Pakete entfernt (handlebars, marked, sanitize-html, @xyflow/react, @dagrejs/dagre, html-to-image, imapflow, mailparser, exceljs, @dnd-kit/core) - .env-Beispiele, launch.json (craftvia-dev), CI-Kommentare umbenannt Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
420 lines
17 KiB
YAML
420 lines
17 KiB
YAML
# 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)
|
||
# In Coolify als "Docker Compose Location" -> docker-compose.coolify.yml setzen.
|
||
#
|
||
# 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}
|
||
AI_PROVIDER: ${AI_PROVIDER}
|
||
AI_API_KEY: ${AI_API_KEY}
|
||
SMTP_HOST: ${SMTP_HOST}
|
||
SMTP_PORT: ${SMTP_PORT}
|
||
SMTP_USER: ${SMTP_USER}
|
||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||
SMTP_FROM: ${SMTP_FROM}
|
||
# 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
|
||
|
||
|
||
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/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:
|