Ctrl K
ComunidadGuías…

docker-compose.prod.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
##############################################################################
# GCTRL Production Docker Compose
#
# Usage:
#   cp .env.production.example .env.production
#   # Edit .env.production with real secrets
#   docker compose -f docker-compose.prod.yml --env-file .env.production up -d
#
# Features:
#   - Health checks on all services
#   - Resource limits (memory, CPU)
#   - Restart policies
#   - Named volumes for persistence
#   - Caddy reverse proxy with automatic SSL
#   - No exposed internal ports (only Caddy 80/443)
#   - Secrets via environment variables
##############################################################################

services:

  # ─── Reverse Proxy (SSL termination) ────────────────────────────────
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./infrastructure/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      api:
        condition: service_healthy
      web:
        condition: service_started
    networks:
      - GCTRL
    deploy:
      resources:
        limits:
          memory: 128M

  # ─── API Server ─────────────────────────────────────────────────────
  api:
    build: ./services/api
    container_name: gctrl-api
    restart: unless-stopped
    environment:
      - NODE_ENV=production
      - PHOENIX_OTLP_URL=${PHOENIX_OTLP_URL:-}
      - PHOENIX_SERVICE_NAME=gctrl-api
      - PORT=4000
      - DATABASE_URL=postgresql://${PG_USER}:${PG_PASSWORD}@postgres:5432/${PG_DB}
      - REDIS_URL=redis://redis:6379
      - NEO4J_URI=bolt://neo4j:7687
      - NEO4J_USER=${NEO4J_USER}
      - NEO4J_PASSWORD=${NEO4J_PASSWORD}
      - JWT_SECRET=${JWT_SECRET}
      - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
      - MAIL_HOST=${MAIL_HOST:-smtp.mailgun.org}
      - MAIL_PORT=${MAIL_PORT:-587}
      - MAIL_USER=${MAIL_USER:-}
      - MAIL_PASS=${MAIL_PASS:-}
      - FRONTEND_URL=${FRONTEND_URL:-https://app.GCTRL.ai}
      - GCTRL_BASE_URL=${GCTRL_BASE_URL:-https://api.GCTRL.ai}
      - KEX_WORKER_URL=http://kex:4010
      - QDRANT_URL=http://qdrant:6333
      - QDRANT_API_KEY=${QDRANT_API_KEY:-}
      - OLLAMA_BASE=http://ollama:11434
      - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
      - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
      - MICROSOFT_CLIENT_ID=${MICROSOFT_CLIENT_ID:-}
      - MICROSOFT_CLIENT_SECRET=${MICROSOFT_CLIENT_SECRET:-}
      - SLACK_CLIENT_ID=${SLACK_CLIENT_ID:-}
      - SLACK_CLIENT_SECRET=${SLACK_CLIENT_SECRET:-}
      - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-}
      - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-}
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://localhost:4000/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      neo4j:
        condition: service_healthy
    networks:
      - GCTRL
      - ollama-net
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: "2.0"

  # ─── Frontend ───────────────────────────────────────────────────────
  web:
    build:
      context: ./services/web
      args:
        VITE_API_URL: ${VITE_API_URL:-/api}
    container_name: gctrl-web
    restart: unless-stopped
    depends_on:
      api:
        condition: service_healthy
    networks:
      - GCTRL
    deploy:
      resources:
        limits:
          memory: 128M

  # ─── KEX Worker ─────────────────────────────────────────────────────
  kex:
    build: ./services/kex
    container_name: gctrl-kex
    restart: unless-stopped
    environment:
      - OLLAMA_BASE=http://ollama:11434
      - NEO4J_URI=bolt://neo4j:7687
      - NEO4J_USER=${NEO4J_USER}
      - NEO4J_PASSWORD=${NEO4J_PASSWORD}
      - REDIS_URL=redis://redis:6379
      - PG_URL=postgresql://${PG_USER}:${PG_PASSWORD}@postgres:5432/${PG_DB}
      - QDRANT_URL=http://qdrant:6333
      - QDRANT_API_KEY=${QDRANT_API_KEY:-}
      - KEX_WORKER_THREADS=${KEX_WORKER_THREADS:-1}
      - WHISPER_MODEL=${WHISPER_MODEL:-base}
      - PHOENIX_OTLP_URL=${PHOENIX_OTLP_URL:-}
      - PHOENIX_SERVICE_NAME=gctrl-kex
      - GLINER_MODEL=${GLINER_MODEL:-}
      - GLINER_FALLBACK_MODEL=${GLINER_FALLBACK_MODEL:-}
      - NER_THRESHOLD_BI=${NER_THRESHOLD_BI:-}
    healthcheck:
      test: ["CMD", "python3", "-c", "import requests; r=requests.get('http://localhost:4010/health'); exit(0 if r.ok else 1)"]
      interval: 60s
      timeout: 15s
      retries: 3
      start_period: 120s
    volumes:
      - kex-models:/app/models
    depends_on:
      redis:
        condition: service_healthy
      neo4j:
        condition: service_healthy
    networks:
      - GCTRL
      - ollama-net
    deploy:
      resources:
        limits:
          memory: 8G
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

  # ─── FUSE Worker ────────────────────────────────────────────────────
  fuse:
    build: ./services/fuse
    container_name: gctrl-fuse
    restart: unless-stopped
    environment:
      - REDIS_URL=redis://redis:6379
      - NEO4J_URI=bolt://neo4j:7687
      - NEO4J_USER=${NEO4J_USER}
      - NEO4J_PASSWORD=${NEO4J_PASSWORD}
      - PG_URL=postgresql://${PG_USER}:${PG_PASSWORD}@postgres:5432/${PG_DB}
      - QDRANT_URL=http://qdrant:6333
      - QDRANT_API_KEY=${QDRANT_API_KEY:-}
      - PHOENIX_OTLP_URL=${PHOENIX_OTLP_URL:-}
      - PHOENIX_SERVICE_NAME=gctrl-fuse
    depends_on:
      redis:
        condition: service_healthy
      neo4j:
        condition: service_healthy
    networks:
      - GCTRL
    deploy:
      resources:
        limits:
          memory: 2G

  # ─── Semantic Resolver ──────────────────────────────────────────────
  resolver:
    image: ghcr.io/gctrl-tech/fusion-engine:latest
    container_name: gctrl-resolver
    restart: unless-stopped
    environment:
      - JDK_JAVA_OPTIONS=-Xmx2G
    command: ["-s"]
    networks:
      - GCTRL
    deploy:
      resources:
        limits:
          memory: 3G

  # ─── PostgreSQL ─────────────────────────────────────────────────────
  postgres:
    image: postgres:16-alpine
    container_name: gctrl-postgres
    restart: unless-stopped
    environment:
      - POSTGRES_USER=${PG_USER}
      - POSTGRES_PASSWORD=${PG_PASSWORD}
      - POSTGRES_DB=${PG_DB}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${PG_USER:-GCTRL}"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - GCTRL-pgdata:/var/lib/postgresql/data
      - ./shared/db/migrations:/docker-entrypoint-initdb.d
    networks:
      - GCTRL
    deploy:
      resources:
        limits:
          memory: 2G

  # ─── Redis ──────────────────────────────────────────────────────────
  redis:
    image: redis:7-alpine
    container_name: gctrl-redis
    restart: unless-stopped
    command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - GCTRL-redis:/data
    networks:
      - GCTRL
    deploy:
      resources:
        limits:
          memory: 768M

  # ─── Neo4j ──────────────────────────────────────────────────────────
  neo4j:
    image: neo4j:2025.02.3-community
    container_name: gctrl-neo4j
    restart: unless-stopped
    environment:
      - NEO4J_AUTH=${NEO4J_USER}/${NEO4J_PASSWORD}
      - NEO4J_PLUGINS=["apoc"]
      - NEO4J_server_memory_heap_initial__size=512m
      - NEO4J_server_memory_heap_max__size=2G
      - NEO4J_server_memory_pagecache_size=1G
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:7474 || exit 1"]
      interval: 15s
      timeout: 10s
      retries: 5
      start_period: 30s
    volumes:
      - GCTRL-neo4j:/data
    networks:
      - GCTRL
    deploy:
      resources:
        limits:
          memory: 4G

  # ─── Qdrant Vector Store ────────────────────────────────────────────
  qdrant:
    image: qdrant/qdrant:latest
    container_name: gctrl-qdrant
    restart: unless-stopped
    # Optional API key. Qdrant treats an EMPTY QDRANT__SERVICE__API_KEY as the key ""
    # and then answers 401 to every client without a header, so the variable is only
    # exported when QDRANT_API_KEY is set; unset/empty = no auth, exactly as before.
    # (Keep in lockstep with QDRANT_API_KEY on kex, fuse and api.)
    environment:
      - QDRANT_API_KEY=${QDRANT_API_KEY:-}
    command: ["bash", "-c", "if [ -n \"$${QDRANT_API_KEY}\" ]; then export QDRANT__SERVICE__API_KEY=\"$${QDRANT_API_KEY}\"; fi; unset QDRANT_API_KEY; exec ./entrypoint.sh"]
    volumes:
      - GCTRL-qdrant:/qdrant/storage
    networks:
      - GCTRL
    deploy:
      resources:
        limits:
          memory: 2G

  # ─── Prometheus (Monitoring) ────────────────────────────────────────
  prometheus:
    image: prom/prometheus:latest
    container_name: gctrl-prometheus
    restart: unless-stopped
    volumes:
      - ./infrastructure/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - GCTRL-prometheus:/prometheus
    networks:
      - GCTRL
    deploy:
      resources:
        limits:
          memory: 512M

  # ─── Grafana (Dashboards) ──────────────────────────────────────────
  grafana:
    image: grafana/grafana:latest
    container_name: gctrl-grafana
    restart: unless-stopped
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
      - GF_SERVER_ROOT_URL=${GRAFANA_URL:-http://localhost:3100}
    volumes:
      - GCTRL-grafana:/var/lib/grafana
    networks:
      - GCTRL
    deploy:
      resources:
        limits:
          memory: 256M

  # ─── Umami (cookieless web analytics) ───────────────────────────────
  # Self-hosted, GDPR-by-design: no cookies, no consent banner, data stays
  # on our infra. Served at https://analytics.gctrl.tech (Caddy → :3000).
  # Its own Postgres keeps analytics physically isolated from the KG platform.
  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    container_name: gctrl-umami
    restart: unless-stopped
    environment:
      - DATABASE_TYPE=postgresql
      - DATABASE_URL=postgresql://umami:${UMAMI_DB_PASSWORD}@umami-db:5432/umami
      - APP_SECRET=${UMAMI_APP_SECRET}
    depends_on:
      umami-db:
        condition: service_healthy
    networks:
      - GCTRL
    deploy:
      resources:
        limits:
          memory: 512M

  umami-db:
    image: postgres:16-alpine
    container_name: gctrl-umami-db
    restart: unless-stopped
    environment:
      - POSTGRES_USER=umami
      - POSTGRES_PASSWORD=${UMAMI_DB_PASSWORD}
      - POSTGRES_DB=umami
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U umami"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - umami-db-data:/var/lib/postgresql/data
    networks:
      - GCTRL
    deploy:
      resources:
        limits:
          memory: 512M

# ─── Volumes ────────────────────────────────────────────────────────
volumes:
  caddy-data:
  caddy-config:
  GCTRL-pgdata:
  GCTRL-redis:
  GCTRL-neo4j:
  GCTRL-qdrant:
  GCTRL-prometheus:
  GCTRL-grafana:
  kex-models:
  umami-db-data:

# ─── Networks ───────────────────────────────────────────────────────
networks:
  GCTRL:
    driver: bridge
  ollama-net:
    external: true
    name: self-hosted-ai-starter-kit_demo