Migrate deployment from Hetzner to Coolify #1

Merged
sharang merged 14 commits from coolify into main 2026-03-13 10:45:35 +00: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
#
# 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 }}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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