From f1710fdb9ef7e1cf83786ecea01911d2f0f2f73d Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar Date: Fri, 13 Mar 2026 10:45:35 +0000 Subject: [PATCH] fix: migrate deployment from Hetzner to Coolify (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add Coolify deployment configuration (docker-compose, healthchecks, network setup) - Replace deploy-hetzner CI job with Coolify webhook deploy - Externalize postgres, qdrant, S3 for Coolify environment ## All changes since branch creation - Coolify docker-compose with Traefik labels and healthchecks - CI pipeline: deploy-hetzner → deploy-coolify (simple webhook curl) - SQLAlchemy 2.x text() compatibility fixes - Alpine-compatible Dockerfile fixes Co-authored-by: Sharang Parnerkar Reviewed-on: https://gitea.meghsakha.com/Benjamin_Boenisch/breakpilot-compliance/pulls/1 --- .env.coolify.example | 61 ++++ .gitea/workflows/ci.yaml | 100 +------ admin-compliance/Dockerfile | 4 +- ai-compliance-sdk/Dockerfile | 2 +- .../compliance/api/compliance_scope_routes.py | 13 +- .../compliance/api/import_routes.py | 19 +- .../compliance/api/screening_routes.py | 21 +- developer-portal/Dockerfile | 6 +- docker-compose.coolify.yml | 272 ++++++++++++++++++ 9 files changed, 377 insertions(+), 121 deletions(-) create mode 100644 .env.coolify.example create mode 100644 docker-compose.coolify.yml diff --git a/.env.coolify.example b/.env.coolify.example new file mode 100644 index 0000000..f448ae8 --- /dev/null +++ b/.env.coolify.example @@ -0,0 +1,61 @@ +# ========================================================= +# BreakPilot Compliance — Coolify Environment Variables +# ========================================================= +# Copy these into Coolify's environment variable UI +# for the breakpilot-compliance Docker Compose resource. +# ========================================================= + +# --- External PostgreSQL (Coolify-managed, same as Core) --- +COMPLIANCE_DATABASE_URL=postgresql://breakpilot:CHANGE_ME@:5432/breakpilot_db + +# --- Security --- +JWT_SECRET=CHANGE_ME_SAME_AS_CORE + +# --- External S3 Storage (same as Core) --- +S3_ENDPOINT= +S3_ACCESS_KEY=CHANGE_ME_SAME_AS_CORE +S3_SECRET_KEY=CHANGE_ME_SAME_AS_CORE +S3_SECURE=true + +# --- External Qdrant --- +QDRANT_URL=https:// +QDRANT_API_KEY=CHANGE_ME_QDRANT_API_KEY + +# --- Session --- +SESSION_TTL_HOURS=24 + +# --- SMTP (Real mail server) --- +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USERNAME=compliance@breakpilot.ai +SMTP_PASSWORD=CHANGE_ME_SMTP_PASSWORD +SMTP_FROM_NAME=BreakPilot Compliance +SMTP_FROM_ADDR=compliance@breakpilot.ai + +# --- LLM Configuration --- +COMPLIANCE_LLM_PROVIDER=anthropic +SELF_HOSTED_LLM_URL= +SELF_HOSTED_LLM_MODEL= +COMPLIANCE_LLM_MAX_TOKENS=4096 +COMPLIANCE_LLM_TEMPERATURE=0.3 +COMPLIANCE_LLM_TIMEOUT=120 +ANTHROPIC_API_KEY=CHANGE_ME_ANTHROPIC_KEY +ANTHROPIC_DEFAULT_MODEL=claude-sonnet-4-5-20250929 + +# --- Ollama (optional) --- +OLLAMA_URL= +OLLAMA_DEFAULT_MODEL= +COMPLIANCE_LLM_MODEL= + +# --- LLM Fallback --- +LLM_FALLBACK_PROVIDER= + +# --- PII & Audit --- +PII_REDACTION_ENABLED=true +PII_REDACTION_LEVEL=standard +AUDIT_RETENTION_DAYS=365 +AUDIT_LOG_PROMPTS=true + +# --- Frontend URLs (build args) --- +NEXT_PUBLIC_API_URL=https://api-compliance.breakpilot.ai +NEXT_PUBLIC_SDK_URL=https://sdk.breakpilot.ai diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 9cc229d..d706806 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -7,7 +7,7 @@ # Node.js: admin-compliance, developer-portal # # Workflow: -# Push auf main → Tests → Build → Deploy (Hetzner) +# Push auf main → Tests → Deploy (Coolify) # Pull Request → Lint + Tests (kein Deploy) name: CI/CD @@ -186,10 +186,11 @@ jobs: python scripts/validate-controls.py # ======================================== - # Build & Deploy auf Hetzner (nur main, kein PR) + # Deploy via Coolify (nur main, kein PR) # ======================================== - deploy-hetzner: + deploy-coolify: + name: Deploy runs-on: docker if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: @@ -198,92 +199,11 @@ jobs: - test-python-document-crawler - test-python-dsms-gateway - validate-canonical-controls - container: docker:27-cli + container: + image: alpine:latest steps: - - name: Deploy + - name: Trigger Coolify deploy run: | - set -euo pipefail - DEPLOY_DIR="/opt/breakpilot-compliance" - COMPOSE_FILES="-f docker-compose.yml -f docker-compose.hetzner.yml" - COMMIT_SHA="${GITHUB_SHA:-unknown}" - SHORT_SHA="${COMMIT_SHA:0:8}" - REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" - - echo "=== BreakPilot Compliance Deploy ===" - echo "Commit: ${SHORT_SHA}" - echo "Deploy Dir: ${DEPLOY_DIR}" - echo "" - - # Der Runner laeuft in einem Container mit Docker-Socket-Zugriff, - # hat aber KEINEN direkten Zugriff auf das Host-Dateisystem. - # Loesung: Alpine-Helper-Container mit Host-Bind-Mount fuer Git-Ops. - - # 1. Repo auf dem Host erstellen/aktualisieren via Helper-Container - echo "=== Updating code on host ===" - docker run --rm \ - -v "${DEPLOY_DIR}:${DEPLOY_DIR}" \ - --entrypoint sh \ - alpine/git:latest \ - -c " - if [ ! -d '${DEPLOY_DIR}/.git' ]; then - echo 'Erstmaliges Klonen nach ${DEPLOY_DIR}...' - git clone '${REPO_URL}' '${DEPLOY_DIR}' - else - cd '${DEPLOY_DIR}' - git fetch origin main - git reset --hard origin/main - fi - " - echo "Code aktualisiert auf ${SHORT_SHA}" - - # 2. .env sicherstellen (muss einmalig manuell angelegt werden) - docker run --rm -v "${DEPLOY_DIR}:${DEPLOY_DIR}" alpine \ - sh -c " - if [ ! -f '${DEPLOY_DIR}/.env' ]; then - echo 'WARNUNG: ${DEPLOY_DIR}/.env fehlt!' - echo 'Bitte einmalig auf dem Host anlegen.' - echo 'Deploy wird fortgesetzt (Services starten ggf. mit Defaults).' - else - echo '.env vorhanden' - fi - " - - # 3. Build + Deploy via Helper-Container mit Docker-Socket + Deploy-Dir - # docker compose muss die YAML-Dateien lesen koennen, daher - # alles in einem Container mit beiden Mounts ausfuehren. - echo "" - echo "=== Building + Deploying ===" - docker run --rm \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v "${DEPLOY_DIR}:${DEPLOY_DIR}" \ - -w "${DEPLOY_DIR}" \ - docker:27-cli \ - sh -c " - COMPOSE_FILES='-f docker-compose.yml -f docker-compose.hetzner.yml' - - echo '=== Building Docker Images ===' - docker compose \${COMPOSE_FILES} build --parallel \ - admin-compliance \ - backend-compliance \ - ai-compliance-sdk \ - developer-portal - - echo '' - echo '=== Starting containers ===' - docker compose \${COMPOSE_FILES} up -d --remove-orphans \ - admin-compliance \ - backend-compliance \ - ai-compliance-sdk \ - developer-portal - - echo '' - echo '=== Health Checks ===' - sleep 10 - for svc in bp-compliance-admin bp-compliance-backend bp-compliance-ai-sdk bp-compliance-developer-portal; do - STATUS=\$(docker inspect --format='{{.State.Status}}' \"\${svc}\" 2>/dev/null || echo 'not found') - echo \"\${svc}: \${STATUS}\" - done - " - - echo "" - echo "=== Deploy abgeschlossen: ${SHORT_SHA} ===" + apk add --no-cache curl + curl -sf "${{ secrets.COOLIFY_WEBHOOK }}" \ + -H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}" diff --git a/admin-compliance/Dockerfile b/admin-compliance/Dockerfile index 61494a6..b98962a 100644 --- a/admin-compliance/Dockerfile +++ b/admin-compliance/Dockerfile @@ -37,8 +37,8 @@ WORKDIR /app ENV NODE_ENV=production # Create non-root user -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs +RUN addgroup -S -g 1001 nodejs +RUN adduser -S -u 1001 -G nodejs nextjs # Copy built assets COPY --from=builder /app/public ./public diff --git a/ai-compliance-sdk/Dockerfile b/ai-compliance-sdk/Dockerfile index ff9c684..4e27e62 100644 --- a/ai-compliance-sdk/Dockerfile +++ b/ai-compliance-sdk/Dockerfile @@ -17,7 +17,7 @@ COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /ai-compliance-sdk ./cmd/server # Runtime stage -FROM alpine:3.19 +FROM alpine:3.21 WORKDIR /app diff --git a/backend-compliance/compliance/api/compliance_scope_routes.py b/backend-compliance/compliance/api/compliance_scope_routes.py index 408ed3d..1b94173 100644 --- a/backend-compliance/compliance/api/compliance_scope_routes.py +++ b/backend-compliance/compliance/api/compliance_scope_routes.py @@ -15,6 +15,7 @@ from typing import Any, Optional from fastapi import APIRouter, HTTPException, Header from pydantic import BaseModel +from sqlalchemy import text from database import SessionLocal @@ -75,13 +76,13 @@ async def get_compliance_scope( db = SessionLocal() try: row = db.execute( - """SELECT tenant_id, + text("""SELECT tenant_id, state->'compliance_scope' AS scope, created_at, updated_at FROM sdk_states WHERE tenant_id = :tid - AND state ? 'compliance_scope'""", + AND state ? 'compliance_scope'"""), {"tid": tid}, ).fetchone() @@ -106,22 +107,22 @@ async def upsert_compliance_scope( db = SessionLocal() try: db.execute( - """INSERT INTO sdk_states (tenant_id, state) + text("""INSERT INTO sdk_states (tenant_id, state) VALUES (:tid, jsonb_build_object('compliance_scope', :scope::jsonb)) ON CONFLICT (tenant_id) DO UPDATE SET state = sdk_states.state || jsonb_build_object('compliance_scope', :scope::jsonb), - updated_at = NOW()""", + updated_at = NOW()"""), {"tid": tid, "scope": scope_json}, ) db.commit() row = db.execute( - """SELECT tenant_id, + text("""SELECT tenant_id, state->'compliance_scope' AS scope, created_at, updated_at FROM sdk_states - WHERE tenant_id = :tid""", + WHERE tenant_id = :tid"""), {"tid": tid}, ).fetchone() diff --git a/backend-compliance/compliance/api/import_routes.py b/backend-compliance/compliance/api/import_routes.py index 14d8044..25ab62d 100644 --- a/backend-compliance/compliance/api/import_routes.py +++ b/backend-compliance/compliance/api/import_routes.py @@ -15,6 +15,7 @@ from typing import Optional import httpx from fastapi import APIRouter, File, Form, Header, UploadFile, HTTPException from pydantic import BaseModel +from sqlalchemy import text from database import SessionLocal @@ -291,11 +292,11 @@ async def analyze_document( db = SessionLocal() try: db.execute( - """INSERT INTO compliance_imported_documents + text("""INSERT INTO compliance_imported_documents (id, tenant_id, filename, file_type, file_size, detected_type, detection_confidence, extracted_text, extracted_entities, recommendations, status, analyzed_at) VALUES (:id, :tenant_id, :filename, :file_type, :file_size, :detected_type, :confidence, - :text, :entities::jsonb, :recommendations::jsonb, 'analyzed', NOW())""", + :text, :entities::jsonb, :recommendations::jsonb, 'analyzed', NOW())"""), { "id": doc_id, "tenant_id": tenant_id, @@ -313,9 +314,9 @@ async def analyze_document( if total_gaps > 0: import json db.execute( - """INSERT INTO compliance_gap_analyses + text("""INSERT INTO compliance_gap_analyses (tenant_id, document_id, total_gaps, critical_gaps, high_gaps, medium_gaps, low_gaps, gaps, recommended_packages) - VALUES (:tenant_id, :document_id, :total, :critical, :high, :medium, :low, :gaps::jsonb, :packages::jsonb)""", + VALUES (:tenant_id, :document_id, :total, :critical, :high, :medium, :low, :gaps::jsonb, :packages::jsonb)"""), { "tenant_id": tenant_id, "document_id": doc_id, @@ -358,7 +359,7 @@ async def get_gap_analysis( db = SessionLocal() try: result = db.execute( - "SELECT * FROM compliance_gap_analyses WHERE document_id = :doc_id AND tenant_id = :tid", + text("SELECT * FROM compliance_gap_analyses WHERE document_id = :doc_id AND tenant_id = :tid"), {"doc_id": document_id, "tid": tid}, ).fetchone() if not result: @@ -374,11 +375,11 @@ async def list_documents(tenant_id: str = "default"): db = SessionLocal() try: result = db.execute( - """SELECT id, filename, file_type, file_size, detected_type, detection_confidence, + text("""SELECT id, filename, file_type, file_size, detected_type, detection_confidence, extracted_entities, recommendations, status, analyzed_at, created_at FROM compliance_imported_documents WHERE tenant_id = :tenant_id - ORDER BY created_at DESC""", + ORDER BY created_at DESC"""), {"tenant_id": tenant_id}, ) rows = result.fetchall() @@ -424,11 +425,11 @@ async def delete_document( try: # Delete gap analysis first (FK dependency) db.execute( - "DELETE FROM compliance_gap_analyses WHERE document_id = :doc_id AND tenant_id = :tid", + text("DELETE FROM compliance_gap_analyses WHERE document_id = :doc_id AND tenant_id = :tid"), {"doc_id": document_id, "tid": tid}, ) result = db.execute( - "DELETE FROM compliance_imported_documents WHERE id = :doc_id AND tenant_id = :tid", + text("DELETE FROM compliance_imported_documents WHERE id = :doc_id AND tenant_id = :tid"), {"doc_id": document_id, "tid": tid}, ) db.commit() diff --git a/backend-compliance/compliance/api/screening_routes.py b/backend-compliance/compliance/api/screening_routes.py index 307ca67..9b9ee16 100644 --- a/backend-compliance/compliance/api/screening_routes.py +++ b/backend-compliance/compliance/api/screening_routes.py @@ -17,6 +17,7 @@ from typing import Optional import httpx from fastapi import APIRouter, File, Form, UploadFile, HTTPException from pydantic import BaseModel +from sqlalchemy import text from database import SessionLocal @@ -366,13 +367,13 @@ async def scan_dependencies( db = SessionLocal() try: db.execute( - """INSERT INTO compliance_screenings + text("""INSERT INTO compliance_screenings (id, tenant_id, status, sbom_format, sbom_version, total_components, total_issues, critical_issues, high_issues, medium_issues, low_issues, sbom_data, started_at, completed_at) VALUES (:id, :tenant_id, 'completed', 'CycloneDX', '1.5', :total_components, :total_issues, :critical, :high, :medium, :low, - :sbom_data::jsonb, :started_at, :completed_at)""", + :sbom_data::jsonb, :started_at, :completed_at)"""), { "id": screening_id, "tenant_id": tenant_id, @@ -391,11 +392,11 @@ async def scan_dependencies( # Persist security issues for issue in issues: db.execute( - """INSERT INTO compliance_security_issues + text("""INSERT INTO compliance_security_issues (id, screening_id, severity, title, description, cve, cvss, affected_component, affected_version, fixed_in, remediation, status) VALUES (:id, :screening_id, :severity, :title, :description, :cve, :cvss, - :component, :version, :fixed_in, :remediation, :status)""", + :component, :version, :fixed_in, :remediation, :status)"""), { "id": issue["id"], "screening_id": screening_id, @@ -486,10 +487,10 @@ async def get_screening(screening_id: str): db = SessionLocal() try: result = db.execute( - """SELECT id, status, sbom_format, sbom_version, + text("""SELECT id, status, sbom_format, sbom_version, total_components, total_issues, critical_issues, high_issues, medium_issues, low_issues, sbom_data, started_at, completed_at - FROM compliance_screenings WHERE id = :id""", + FROM compliance_screenings WHERE id = :id"""), {"id": screening_id}, ) row = result.fetchone() @@ -498,9 +499,9 @@ async def get_screening(screening_id: str): # Fetch issues issues_result = db.execute( - """SELECT id, severity, title, description, cve, cvss, + text("""SELECT id, severity, title, description, cve, cvss, affected_component, affected_version, fixed_in, remediation, status - FROM compliance_security_issues WHERE screening_id = :id""", + FROM compliance_security_issues WHERE screening_id = :id"""), {"id": screening_id}, ) issues_rows = issues_result.fetchall() @@ -566,12 +567,12 @@ async def list_screenings(tenant_id: str = "default"): db = SessionLocal() try: result = db.execute( - """SELECT id, status, total_components, total_issues, + text("""SELECT id, status, total_components, total_issues, critical_issues, high_issues, medium_issues, low_issues, started_at, completed_at, created_at FROM compliance_screenings WHERE tenant_id = :tenant_id - ORDER BY created_at DESC""", + ORDER BY created_at DESC"""), {"tenant_id": tenant_id}, ) rows = result.fetchall() diff --git a/developer-portal/Dockerfile b/developer-portal/Dockerfile index 3dd000e..2dae055 100644 --- a/developer-portal/Dockerfile +++ b/developer-portal/Dockerfile @@ -12,7 +12,7 @@ RUN npm install # Copy source code COPY . . -# Ensure public directory exists +# Ensure public directory exists (may not have static assets) RUN mkdir -p public # Build the application @@ -27,8 +27,8 @@ WORKDIR /app ENV NODE_ENV=production # Create non-root user -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs +RUN addgroup -S -g 1001 nodejs +RUN adduser -S -u 1001 -G nodejs nextjs # Copy built assets COPY --from=builder /app/public ./public diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml new file mode 100644 index 0000000..8e1d0fb --- /dev/null +++ b/docker-compose.coolify.yml @@ -0,0 +1,272 @@ +# ========================================================= +# BreakPilot Compliance — Compliance SDK Platform (Coolify) +# ========================================================= +# Requires: breakpilot-core must be running +# Deployed via Coolify. SSL termination handled by Traefik. +# External services (managed separately in Coolify): +# - PostgreSQL, Qdrant, S3-compatible storage +# ========================================================= + +networks: + breakpilot-network: + external: true + name: breakpilot-network + coolify: + external: true + name: coolify + +volumes: + dsms_data: + +services: + + # ========================================================= + # FRONTEND + # ========================================================= + admin-compliance: + build: + context: ./admin-compliance + dockerfile: Dockerfile + args: + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-https://api-compliance.breakpilot.ai} + NEXT_PUBLIC_SDK_URL: ${NEXT_PUBLIC_SDK_URL:-https://sdk.breakpilot.ai} + container_name: bp-compliance-admin + labels: + - "traefik.docker.network=coolify" + expose: + - "3000" + environment: + NODE_ENV: production + DATABASE_URL: ${COMPLIANCE_DATABASE_URL} + BACKEND_URL: http://backend-compliance:8002 + CONSENT_SERVICE_URL: http://bp-core-consent-service:8081 + SDK_URL: http://ai-compliance-sdk:8090 + OLLAMA_URL: ${OLLAMA_URL:-} + COMPLIANCE_LLM_MODEL: ${COMPLIANCE_LLM_MODEL:-} + depends_on: + backend-compliance: + condition: service_started + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/"] + interval: 30s + timeout: 10s + start_period: 30s + retries: 3 + restart: unless-stopped + networks: + - breakpilot-network + - coolify + + developer-portal: + build: + context: ./developer-portal + dockerfile: Dockerfile + container_name: bp-compliance-developer-portal + labels: + - "traefik.docker.network=coolify" + expose: + - "3000" + environment: + NODE_ENV: production + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/"] + interval: 30s + timeout: 10s + start_period: 30s + retries: 3 + restart: unless-stopped + networks: + - breakpilot-network + - coolify + + # ========================================================= + # BACKEND + # ========================================================= + backend-compliance: + build: + context: ./backend-compliance + dockerfile: Dockerfile + container_name: bp-compliance-backend + labels: + - "traefik.docker.network=coolify" + expose: + - "8002" + environment: + PORT: 8002 + DATABASE_URL: ${COMPLIANCE_DATABASE_URL} + JWT_SECRET: ${JWT_SECRET} + ENVIRONMENT: production + CONSENT_SERVICE_URL: http://bp-core-consent-service:8081 + VALKEY_URL: redis://bp-core-valkey:6379/0 + SESSION_TTL_HOURS: ${SESSION_TTL_HOURS:-24} + COMPLIANCE_LLM_PROVIDER: ${COMPLIANCE_LLM_PROVIDER:-anthropic} + SELF_HOSTED_LLM_URL: ${SELF_HOSTED_LLM_URL:-} + SELF_HOSTED_LLM_MODEL: ${SELF_HOSTED_LLM_MODEL:-} + COMPLIANCE_LLM_MAX_TOKENS: ${COMPLIANCE_LLM_MAX_TOKENS:-4096} + COMPLIANCE_LLM_TEMPERATURE: ${COMPLIANCE_LLM_TEMPERATURE:-0.3} + COMPLIANCE_LLM_TIMEOUT: ${COMPLIANCE_LLM_TIMEOUT:-120} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + SMTP_HOST: ${SMTP_HOST} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USERNAME: ${SMTP_USERNAME} + SMTP_PASSWORD: ${SMTP_PASSWORD} + SMTP_FROM_NAME: ${SMTP_FROM_NAME:-BreakPilot Compliance} + SMTP_FROM_ADDR: ${SMTP_FROM_ADDR:-compliance@breakpilot.ai} + RAG_SERVICE_URL: http://bp-core-rag-service:8097 + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:8002/health"] + interval: 30s + timeout: 10s + start_period: 15s + retries: 3 + restart: unless-stopped + networks: + - breakpilot-network + - coolify + + # ========================================================= + # SDK SERVICES + # ========================================================= + ai-compliance-sdk: + build: + context: ./ai-compliance-sdk + dockerfile: Dockerfile + container_name: bp-compliance-ai-sdk + labels: + - "traefik.docker.network=coolify" + expose: + - "8090" + environment: + PORT: 8090 + ENVIRONMENT: production + DATABASE_URL: ${COMPLIANCE_DATABASE_URL} + JWT_SECRET: ${JWT_SECRET} + LLM_PROVIDER: ${COMPLIANCE_LLM_PROVIDER:-anthropic} + LLM_FALLBACK_PROVIDER: ${LLM_FALLBACK_PROVIDER:-} + OLLAMA_URL: ${OLLAMA_URL:-} + OLLAMA_DEFAULT_MODEL: ${OLLAMA_DEFAULT_MODEL:-} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + ANTHROPIC_DEFAULT_MODEL: ${ANTHROPIC_DEFAULT_MODEL:-claude-sonnet-4-5-20250929} + PII_REDACTION_ENABLED: ${PII_REDACTION_ENABLED:-true} + PII_REDACTION_LEVEL: ${PII_REDACTION_LEVEL:-standard} + AUDIT_RETENTION_DAYS: ${AUDIT_RETENTION_DAYS:-365} + AUDIT_LOG_PROMPTS: ${AUDIT_LOG_PROMPTS:-true} + ALLOWED_ORIGINS: "*" + TTS_SERVICE_URL: http://compliance-tts-service:8095 + QDRANT_URL: ${QDRANT_URL} + QDRANT_API_KEY: ${QDRANT_API_KEY:-} + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8090/health"] + interval: 30s + timeout: 3s + start_period: 10s + retries: 3 + restart: unless-stopped + networks: + - breakpilot-network + - coolify + + # ========================================================= + # TTS SERVICE (Piper TTS + FFmpeg) + # ========================================================= + compliance-tts-service: + build: + context: ./compliance-tts-service + dockerfile: Dockerfile + container_name: bp-compliance-tts + expose: + - "8095" + environment: + MINIO_ENDPOINT: ${S3_ENDPOINT} + MINIO_ACCESS_KEY: ${S3_ACCESS_KEY} + MINIO_SECRET_KEY: ${S3_SECRET_KEY} + MINIO_SECURE: ${S3_SECURE:-true} + PIPER_MODEL_PATH: /app/models/de_DE-thorsten-high.onnx + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8095/health')"] + interval: 30s + timeout: 10s + start_period: 60s + retries: 3 + restart: unless-stopped + networks: + - breakpilot-network + + # ========================================================= + # DATA SOVEREIGNTY + # ========================================================= + dsms-node: + build: + context: ./dsms-node + dockerfile: Dockerfile + container_name: bp-compliance-dsms-node + expose: + - "4001" + - "5001" + - "8080" + volumes: + - dsms_data:/data/ipfs + environment: + IPFS_PROFILE: server + healthcheck: + test: ["CMD-SHELL", "ipfs id"] + interval: 30s + timeout: 10s + start_period: 30s + retries: 3 + restart: unless-stopped + networks: + - breakpilot-network + + dsms-gateway: + build: + context: ./dsms-gateway + dockerfile: Dockerfile + container_name: bp-compliance-dsms-gateway + expose: + - "8082" + environment: + IPFS_API_URL: http://dsms-node:5001 + IPFS_GATEWAY_URL: http://dsms-node:8080 + JWT_SECRET: ${JWT_SECRET} + depends_on: + dsms-node: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:8082/health"] + interval: 30s + timeout: 10s + start_period: 15s + retries: 3 + restart: unless-stopped + networks: + - breakpilot-network + + # ========================================================= + # DOCUMENT CRAWLER & AUTO-ONBOARDING + # ========================================================= + document-crawler: + build: + context: ./document-crawler + dockerfile: Dockerfile + container_name: bp-compliance-document-crawler + expose: + - "8098" + environment: + PORT: 8098 + DATABASE_URL: ${COMPLIANCE_DATABASE_URL} + LLM_GATEWAY_URL: http://ai-compliance-sdk:8090 + DSMS_GATEWAY_URL: http://dsms-gateway:8082 + CRAWL_BASE_PATH: /data/crawl + MAX_FILE_SIZE_MB: 50 + volumes: + - /tmp/breakpilot-crawl-data:/data/crawl:ro + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:8098/health"] + interval: 30s + timeout: 10s + start_period: 15s + retries: 3 + restart: unless-stopped + networks: + - breakpilot-network