diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index ca0ca5b..922723a 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -138,3 +138,116 @@ jobs: pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true pip install --quiet --no-cache-dir fastapi uvicorn pydantic pytest pytest-asyncio python -m pytest tests/bqas/ -v --tb=short || true + + # ======================================== + # Build & Deploy auf Hetzner (nur main, kein PR) + # ======================================== + + deploy-hetzner: + runs-on: docker + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: + - test-go-consent + container: docker:27-cli + steps: + - name: Deploy + run: | + set -euo pipefail + DEPLOY_DIR="/opt/breakpilot-core" + 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" + + # Services die deployed werden + SERVICES="postgres valkey qdrant minio ollama mailpit embedding-service rag-service backend-core consent-service health-aggregator" + + echo "=== BreakPilot Core Deploy ===" + echo "Commit: ${SHORT_SHA}" + echo "Deploy Dir: ${DEPLOY_DIR}" + echo "Services: ${SERVICES}" + echo "" + + # 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 + docker run --rm -v "${DEPLOY_DIR}:${DEPLOY_DIR}" alpine \ + sh -c " + if [ ! -f '${DEPLOY_DIR}/.env' ]; then + echo 'WARNUNG: ${DEPLOY_DIR}/.env fehlt!' + echo 'Erstelle .env aus .env.example mit Defaults...' + if [ -f '${DEPLOY_DIR}/.env.example' ]; then + cp '${DEPLOY_DIR}/.env.example' '${DEPLOY_DIR}/.env' + echo '.env aus .env.example erstellt' + else + echo 'Kein .env.example gefunden — Services starten mit Defaults' + fi + else + echo '.env vorhanden' + fi + " + + # 3. Build + Deploy via Helper-Container + 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' + SERVICES='postgres valkey qdrant minio ollama mailpit embedding-service rag-service backend-core consent-service health-aggregator' + + echo '=== Building Docker Images ===' + docker compose \${COMPOSE_FILES} build --parallel \ + backend-core consent-service rag-service embedding-service health-aggregator + + echo '' + echo '=== Starting infrastructure ===' + docker compose \${COMPOSE_FILES} up -d postgres valkey qdrant minio mailpit + + echo 'Warte auf DB + Cache...' + sleep 10 + + echo '' + echo '=== Starting Ollama + pulling bge-m3 ===' + docker compose \${COMPOSE_FILES} up -d ollama + sleep 5 + + # bge-m3 Modell pullen (nur beim ersten Mal ~670MB) + echo 'Pulling bge-m3 model (falls noch nicht vorhanden)...' + docker exec bp-core-ollama ollama pull bge-m3 2>&1 || echo 'WARNUNG: bge-m3 pull fehlgeschlagen (wird spaeter nachgeholt)' + + echo '' + echo '=== Starting application services ===' + docker compose \${COMPOSE_FILES} up -d \ + embedding-service rag-service backend-core consent-service health-aggregator + + echo '' + echo '=== Health Checks ===' + sleep 15 + for svc in bp-core-postgres bp-core-valkey bp-core-qdrant bp-core-ollama bp-core-embedding-service bp-core-rag-service bp-core-backend bp-core-consent-service bp-core-health; do + STATUS=\$(docker inspect --format='{{.State.Status}}' \"\${svc}\" 2>/dev/null || echo 'not found') + echo \"\${svc}: \${STATUS}\" + done + " + + echo "" + echo "=== Deploy abgeschlossen: ${SHORT_SHA} ===" diff --git a/docker-compose.hetzner.yml b/docker-compose.hetzner.yml new file mode 100644 index 0000000..c020a15 --- /dev/null +++ b/docker-compose.hetzner.yml @@ -0,0 +1,165 @@ +# ========================================================= +# BreakPilot Core — Hetzner Override (x86_64) +# ========================================================= +# Verwendung: +# docker compose -f docker-compose.yml -f docker-compose.hetzner.yml up -d \ +# postgres valkey qdrant ollama embedding-service rag-service \ +# backend-core consent-service health-aggregator +# +# Aenderungen gegenueber Basis (docker-compose.yml): +# - platform: linux/amd64 (statt arm64) +# - Ollama Container fuer CPU-Embeddings (bge-m3) +# - Mailpit ersetzt durch Dummy (kein Mail-Dev-Server noetig) +# - Vault, Nginx, Gitea etc. deaktiviert via Profile +# - Netzwerk: auto-create (nicht external) +# ========================================================= + +networks: + breakpilot-network: + external: false + name: breakpilot-network + +services: + + # ========================================================= + # NEUE SERVICES + # ========================================================= + + # Ollama fuer Embeddings (CPU-only, bge-m3) + ollama: + image: ollama/ollama:latest + container_name: bp-core-ollama + platform: linux/amd64 + volumes: + - ollama_models:/root/.ollama + ports: + - "11434:11434" + healthcheck: + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:11434/api/tags || exit 1"] + interval: 15s + timeout: 10s + retries: 5 + start_period: 30s + restart: unless-stopped + networks: + - breakpilot-network + + # ========================================================= + # PLATFORM OVERRIDES (arm64 → amd64) + # ========================================================= + + backend-core: + platform: linux/amd64 + ports: + - "8000:8000" + environment: + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-breakpilot}:${POSTGRES_PASSWORD:-breakpilot123}@postgres:5432/${POSTGRES_DB:-breakpilot_db}?options=-csearch_path%3Dcore,public + JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production} + ENVIRONMENT: ${ENVIRONMENT:-production} + VALKEY_URL: redis://valkey:6379/0 + SESSION_TTL_HOURS: ${SESSION_TTL_HOURS:-24} + CONSENT_SERVICE_URL: http://consent-service:8081 + USE_VAULT_SECRETS: "false" + SMTP_HOST: ${SMTP_HOST:-smtp.example.com} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USERNAME: ${SMTP_USERNAME:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_FROM_NAME: ${SMTP_FROM_NAME:-BreakPilot} + SMTP_FROM_ADDR: ${SMTP_FROM_ADDR:-noreply@breakpilot.app} + + consent-service: + platform: linux/amd64 + environment: + DATABASE_URL: postgres://${POSTGRES_USER:-breakpilot}:${POSTGRES_PASSWORD:-breakpilot123}@postgres:5432/${POSTGRES_DB:-breakpilot_db} + JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production} + JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:-your-refresh-secret} + PORT: 8081 + ENVIRONMENT: ${ENVIRONMENT:-production} + ALLOWED_ORIGINS: "*" + VALKEY_URL: redis://valkey:6379/0 + SESSION_TTL_HOURS: ${SESSION_TTL_HOURS:-24} + SMTP_HOST: ${SMTP_HOST:-smtp.example.com} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USERNAME: ${SMTP_USERNAME:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_FROM_NAME: ${SMTP_FROM_NAME:-BreakPilot} + SMTP_FROM_ADDR: ${SMTP_FROM_ADDR:-noreply@breakpilot.app} + FRONTEND_URL: ${FRONTEND_URL:-https://admin-dev.breakpilot.ai} + + billing-service: + platform: linux/amd64 + + rag-service: + platform: linux/amd64 + ports: + - "8097:8097" + environment: + PORT: 8097 + QDRANT_URL: http://qdrant:6333 + MINIO_ENDPOINT: nbg1.your-objectstorage.com + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-T18RGFVXXG2ZHQ5404TP} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-KOUU4WO6wh07cQjNgh0IZHkeKQrVfBz6hnIGpNss} + MINIO_BUCKET: ${MINIO_BUCKET:-breakpilot-rag} + MINIO_SECURE: "true" + EMBEDDING_SERVICE_URL: http://embedding-service:8087 + OLLAMA_URL: http://ollama:11434 + OLLAMA_EMBED_MODEL: ${OLLAMA_EMBED_MODEL:-bge-m3} + JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production} + ENVIRONMENT: ${ENVIRONMENT:-production} + + embedding-service: + platform: linux/amd64 + ports: + - "8087:8087" + + health-aggregator: + platform: linux/amd64 + environment: + PORT: 8099 + CHECK_SERVICES: "postgres:5432,valkey:6379,qdrant:6333,backend-core:8000,rag-service:8097,embedding-service:8087" + + # ========================================================= + # DUMMY-ERSATZ FUER ABHAENGIGKEITEN + # ========================================================= + # backend-core + consent-service haengen von mailpit ab + # (depends_on merged bei compose override, kann nicht entfernt werden) + # → Mailpit durch leichtgewichtigen Dummy ersetzen + + mailpit: + image: alpine:3.19 + entrypoint: ["sh", "-c", "echo 'Mailpit dummy on Hetzner' && tail -f /dev/null"] + volumes: [] + ports: [] + environment: {} + + # minio: rag-service haengt davon ab (depends_on) + # Lokal laufen lassen, aber rag-service nutzt externe Hetzner Object Storage + # minio bleibt unveraendert (klein, ~50MB RAM) + + # ========================================================= + # DEAKTIVIERTE SERVICES (via profiles) + # ========================================================= + + nginx: + profiles: ["disabled"] + vault: + profiles: ["disabled"] + vault-init: + profiles: ["disabled"] + vault-agent: + profiles: ["disabled"] + gitea: + profiles: ["disabled"] + gitea-runner: + profiles: ["disabled"] + night-scheduler: + profiles: ["disabled"] + admin-core: + profiles: ["disabled"] + pitch-deck: + profiles: ["disabled"] + levis-holzbau: + profiles: ["disabled"] + +volumes: + ollama_models: