fix: migrate deployment from Hetzner to Coolify (#1)
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
## 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 <parnerkarsharang@gmail.com> Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
61
.env.coolify.example
Normal file
61
.env.coolify.example
Normal 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
|
||||||
@@ -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} ==="
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
272
docker-compose.coolify.yml
Normal 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
|
||||||
Reference in New Issue
Block a user