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

## 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:
2026-03-13 10:45:35 +00:00
parent 499ddc04d5
commit f1710fdb9e
9 changed files with 377 additions and 121 deletions

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