Merge branch 'main' of ssh://gitea.meghsakha.com:22222/Benjamin_Boenisch/breakpilot-compliance

This commit is contained in:
Benjamin Admin
2026-03-13 13:23:30 +01:00
9 changed files with 377 additions and 121 deletions

61
.env.coolify.example Normal file
View File

@@ -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@<coolify-postgres-hostname>:5432/breakpilot_db
# --- Security ---
JWT_SECRET=CHANGE_ME_SAME_AS_CORE
# --- External S3 Storage (same as Core) ---
S3_ENDPOINT=<s3-endpoint-host:port>
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-hostname>
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

View File

@@ -7,7 +7,7 @@
# Node.js: admin-compliance, developer-portal # Node.js: admin-compliance, developer-portal
# #
# Workflow: # Workflow:
# Push auf main → Tests → Build → Deploy (Hetzner) # Push auf main → Tests → Deploy (Coolify)
# Pull Request → Lint + Tests (kein Deploy) # Pull Request → Lint + Tests (kein Deploy)
name: CI/CD name: CI/CD
@@ -186,10 +186,11 @@ jobs:
python scripts/validate-controls.py 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 runs-on: docker
if: github.event_name == 'push' && github.ref == 'refs/heads/main' if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: needs:
@@ -198,92 +199,11 @@ jobs:
- test-python-document-crawler - test-python-document-crawler
- test-python-dsms-gateway - test-python-dsms-gateway
- validate-canonical-controls - validate-canonical-controls
container: docker:27-cli container:
image: alpine:latest
steps: steps:
- name: Deploy - name: Trigger Coolify deploy
run: | run: |
set -euo pipefail apk add --no-cache curl
DEPLOY_DIR="/opt/breakpilot-compliance" curl -sf "${{ secrets.COOLIFY_WEBHOOK }}" \
COMPOSE_FILES="-f docker-compose.yml -f docker-compose.hetzner.yml" -H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"
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} ==="

View File

@@ -37,8 +37,8 @@ WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
# Create non-root user # Create non-root user
RUN addgroup --system --gid 1001 nodejs RUN addgroup -S -g 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser -S -u 1001 -G nodejs nextjs
# Copy built assets # Copy built assets
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public

View File

@@ -17,7 +17,7 @@ COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /ai-compliance-sdk ./cmd/server RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /ai-compliance-sdk ./cmd/server
# Runtime stage # Runtime stage
FROM alpine:3.19 FROM alpine:3.21
WORKDIR /app WORKDIR /app

View File

@@ -15,6 +15,7 @@ from typing import Any, Optional
from fastapi import APIRouter, HTTPException, Header from fastapi import APIRouter, HTTPException, Header
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import text
from database import SessionLocal from database import SessionLocal
@@ -75,13 +76,13 @@ async def get_compliance_scope(
db = SessionLocal() db = SessionLocal()
try: try:
row = db.execute( row = db.execute(
"""SELECT tenant_id, text("""SELECT tenant_id,
state->'compliance_scope' AS scope, state->'compliance_scope' AS scope,
created_at, created_at,
updated_at updated_at
FROM sdk_states FROM sdk_states
WHERE tenant_id = :tid WHERE tenant_id = :tid
AND state ? 'compliance_scope'""", AND state ? 'compliance_scope'"""),
{"tid": tid}, {"tid": tid},
).fetchone() ).fetchone()
@@ -106,22 +107,22 @@ async def upsert_compliance_scope(
db = SessionLocal() db = SessionLocal()
try: try:
db.execute( 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)) VALUES (:tid, jsonb_build_object('compliance_scope', :scope::jsonb))
ON CONFLICT (tenant_id) DO UPDATE ON CONFLICT (tenant_id) DO UPDATE
SET state = sdk_states.state || jsonb_build_object('compliance_scope', :scope::jsonb), SET state = sdk_states.state || jsonb_build_object('compliance_scope', :scope::jsonb),
updated_at = NOW()""", updated_at = NOW()"""),
{"tid": tid, "scope": scope_json}, {"tid": tid, "scope": scope_json},
) )
db.commit() db.commit()
row = db.execute( row = db.execute(
"""SELECT tenant_id, text("""SELECT tenant_id,
state->'compliance_scope' AS scope, state->'compliance_scope' AS scope,
created_at, created_at,
updated_at updated_at
FROM sdk_states FROM sdk_states
WHERE tenant_id = :tid""", WHERE tenant_id = :tid"""),
{"tid": tid}, {"tid": tid},
).fetchone() ).fetchone()

View File

@@ -15,6 +15,7 @@ from typing import Optional
import httpx import httpx
from fastapi import APIRouter, File, Form, Header, UploadFile, HTTPException from fastapi import APIRouter, File, Form, Header, UploadFile, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import text
from database import SessionLocal from database import SessionLocal
@@ -291,11 +292,11 @@ async def analyze_document(
db = SessionLocal() db = SessionLocal()
try: try:
db.execute( 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, (id, tenant_id, filename, file_type, file_size, detected_type, detection_confidence,
extracted_text, extracted_entities, recommendations, status, analyzed_at) extracted_text, extracted_entities, recommendations, status, analyzed_at)
VALUES (:id, :tenant_id, :filename, :file_type, :file_size, :detected_type, :confidence, 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, "id": doc_id,
"tenant_id": tenant_id, "tenant_id": tenant_id,
@@ -313,9 +314,9 @@ async def analyze_document(
if total_gaps > 0: if total_gaps > 0:
import json import json
db.execute( 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) (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, "tenant_id": tenant_id,
"document_id": doc_id, "document_id": doc_id,
@@ -358,7 +359,7 @@ async def get_gap_analysis(
db = SessionLocal() db = SessionLocal()
try: try:
result = db.execute( 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}, {"doc_id": document_id, "tid": tid},
).fetchone() ).fetchone()
if not result: if not result:
@@ -374,11 +375,11 @@ async def list_documents(tenant_id: str = "default"):
db = SessionLocal() db = SessionLocal()
try: try:
result = db.execute( 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 extracted_entities, recommendations, status, analyzed_at, created_at
FROM compliance_imported_documents FROM compliance_imported_documents
WHERE tenant_id = :tenant_id WHERE tenant_id = :tenant_id
ORDER BY created_at DESC""", ORDER BY created_at DESC"""),
{"tenant_id": tenant_id}, {"tenant_id": tenant_id},
) )
rows = result.fetchall() rows = result.fetchall()
@@ -424,11 +425,11 @@ async def delete_document(
try: try:
# Delete gap analysis first (FK dependency) # Delete gap analysis first (FK dependency)
db.execute( 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}, {"doc_id": document_id, "tid": tid},
) )
result = db.execute( 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}, {"doc_id": document_id, "tid": tid},
) )
db.commit() db.commit()

View File

@@ -17,6 +17,7 @@ from typing import Optional
import httpx import httpx
from fastapi import APIRouter, File, Form, UploadFile, HTTPException from fastapi import APIRouter, File, Form, UploadFile, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import text
from database import SessionLocal from database import SessionLocal
@@ -366,13 +367,13 @@ async def scan_dependencies(
db = SessionLocal() db = SessionLocal()
try: try:
db.execute( db.execute(
"""INSERT INTO compliance_screenings text("""INSERT INTO compliance_screenings
(id, tenant_id, status, sbom_format, sbom_version, (id, tenant_id, status, sbom_format, sbom_version,
total_components, total_issues, critical_issues, high_issues, medium_issues, low_issues, total_components, total_issues, critical_issues, high_issues, medium_issues, low_issues,
sbom_data, started_at, completed_at) sbom_data, started_at, completed_at)
VALUES (:id, :tenant_id, 'completed', 'CycloneDX', '1.5', VALUES (:id, :tenant_id, 'completed', 'CycloneDX', '1.5',
:total_components, :total_issues, :critical, :high, :medium, :low, :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, "id": screening_id,
"tenant_id": tenant_id, "tenant_id": tenant_id,
@@ -391,11 +392,11 @@ async def scan_dependencies(
# Persist security issues # Persist security issues
for issue in issues: for issue in issues:
db.execute( db.execute(
"""INSERT INTO compliance_security_issues text("""INSERT INTO compliance_security_issues
(id, screening_id, severity, title, description, cve, cvss, (id, screening_id, severity, title, description, cve, cvss,
affected_component, affected_version, fixed_in, remediation, status) affected_component, affected_version, fixed_in, remediation, status)
VALUES (:id, :screening_id, :severity, :title, :description, :cve, :cvss, VALUES (:id, :screening_id, :severity, :title, :description, :cve, :cvss,
:component, :version, :fixed_in, :remediation, :status)""", :component, :version, :fixed_in, :remediation, :status)"""),
{ {
"id": issue["id"], "id": issue["id"],
"screening_id": screening_id, "screening_id": screening_id,
@@ -486,10 +487,10 @@ async def get_screening(screening_id: str):
db = SessionLocal() db = SessionLocal()
try: try:
result = db.execute( 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, total_components, total_issues, critical_issues, high_issues,
medium_issues, low_issues, sbom_data, started_at, completed_at 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}, {"id": screening_id},
) )
row = result.fetchone() row = result.fetchone()
@@ -498,9 +499,9 @@ async def get_screening(screening_id: str):
# Fetch issues # Fetch issues
issues_result = db.execute( 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 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}, {"id": screening_id},
) )
issues_rows = issues_result.fetchall() issues_rows = issues_result.fetchall()
@@ -566,12 +567,12 @@ async def list_screenings(tenant_id: str = "default"):
db = SessionLocal() db = SessionLocal()
try: try:
result = db.execute( 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, critical_issues, high_issues, medium_issues, low_issues,
started_at, completed_at, created_at started_at, completed_at, created_at
FROM compliance_screenings FROM compliance_screenings
WHERE tenant_id = :tenant_id WHERE tenant_id = :tenant_id
ORDER BY created_at DESC""", ORDER BY created_at DESC"""),
{"tenant_id": tenant_id}, {"tenant_id": tenant_id},
) )
rows = result.fetchall() rows = result.fetchall()

View File

@@ -12,7 +12,7 @@ RUN npm install
# Copy source code # Copy source code
COPY . . COPY . .
# Ensure public directory exists # Ensure public directory exists (may not have static assets)
RUN mkdir -p public RUN mkdir -p public
# Build the application # Build the application
@@ -27,8 +27,8 @@ WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
# Create non-root user # Create non-root user
RUN addgroup --system --gid 1001 nodejs RUN addgroup -S -g 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser -S -u 1001 -G nodejs nextjs
# Copy built assets # Copy built assets
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public

272
docker-compose.coolify.yml Normal file
View File

@@ -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