version: "3.9"

# Docker Swarm Stack Configuration
# Deploy with: docker stack deploy -c infra/stacks/vello/stack.yml vello

secrets:
  # Database
  database_url:
    external: true
  supabase_url:
    external: true
  supabase_key:
    external: true

  # Security
  jwt_secret:
    external: true
  encryption_key:
    external: true
  service_token:
    external: true

  # Redis
  redis_password:
    external: true

  # OpenAI
  openai_api_key:
    external: true

configs:
  redis_config:
    file: ../../../../platform/services/redis/config/redis.conf
  nginx_config7:
    file: ../../../../platform/services/models-gateway/nginx/nginx.conf

services:
  # Redis Service - Stateful, single instance
  redis:
    image: redis:7-alpine
    networks:
      - vello-overlay
    secrets:
      - redis_password
    configs:
      - source: redis_config
        target: /usr/local/etc/redis/redis.conf
    volumes:
      - redis_data:/data
    environment:
      - REDIS_PASSWORD_FILE=/run/secrets/redis_password
    command: sh -c 'redis-server /usr/local/etc/redis/redis.conf --requirepass "$$(cat $$REDIS_PASSWORD_FILE)"'
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
          - node.labels.redis == true
      resources:
        limits:
          cpus: "1"
          memory: 1G
        reservations:
          cpus: "0.5"
          memory: 512M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    healthcheck:
      test:
        [
          "CMD",
          "sh",
          "-c",
          "redis-cli -a $$(cat /run/secrets/redis_password) ping",
        ]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s

  # API Service - Stateless, scalable
  api:
    image: vello-api:latest
    networks:
      - vello-overlay
    ports:
      - target: ${API_INTERNAL_PORT}
        published: ${API_PUBLIC_PORT}
        protocol: tcp
        mode: ingress
    secrets:
      - database_url
      - supabase_url
      - supabase_key
      - jwt_secret
      - encryption_key
      - service_token
      - redis_password
    environment:
      # Use secret files
      - DATABASE_URL_FILE=/run/secrets/database_url
      - SUPABASE_URL_FILE=/run/secrets/supabase_url
      - SUPABASE_KEY_FILE=/run/secrets/supabase_key
      - SECRET_KEY_FILE=/run/secrets/jwt_secret
      - ENCRYPTION_KEY_FILE=/run/secrets/encryption_key
      - SERVICE_TOKEN_FILE=/run/secrets/service_token

      # Non-sensitive configs
      - ALLOWED_ORIGINS=${ALLOWED_ORIGINS}
      - API_HOST=${API_HOST}
      - API_PORT=${API_INTERNAL_PORT}
      - ACCESS_TOKEN_EXPIRE_MINUTES=15
      - REFRESH_TOKEN_EXPIRE_DAYS=7
      - WS_HEARTBEAT_INTERVAL=30
      - ENVIRONMENT=production
      # Public base URL used in email links
      - WEB_USER_PUBLIC_BASE_URL=${WEB_USER_PUBLIC_BASE_URL}

      # Redis with secret
      - REDIS_HOST=${REDIS_INTERNAL_HOST}
      - REDIS_PORT=${REDIS_INTERNAL_PORT}
      - REDIS_PASSWORD_FILE=/run/secrets/redis_password
      - JOBS_ADD_COMPAT_ENABLED=false
    volumes:
      - api_logs:/app/logs
      - api_temp:/app/temp
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 30s
        failure_action: rollback
        monitor: 60s
        max_failure_ratio: 0.3
        order: start-first
      rollback_config:
        parallelism: 1
        delay: 30s
        failure_action: continue
        monitor: 60s
        max_failure_ratio: 0.3
      placement:
        constraints:
          - node.labels.api == true
        preferences:
          - spread: node.id
      resources:
        limits:
          cpus: "2"
          memory: 2G
        reservations:
          cpus: "0.5"
          memory: 512M
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3
        window: 120s
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.api.rule=Host(`api.agenticca.ai/vello`)"
        - "traefik.http.services.api.loadbalancer.server.port=8003"
        - "traefik.http.services.api.loadbalancer.sticky=true"
        - "traefik.http.services.api.loadbalancer.sticky.cookie.name=api_session"
    healthcheck:
      test: ["CMD", "curl", "-f", "${API_HEALTHCHECK_URL}"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  # Models Gateway Service - Stateless, scalable
  models-gateway:
    image: vello-modelsgateway:latest
    networks:
      - vello-overlay
    ports:
      - target: 80
        published: ${MODELSGATEWAY_PUBLIC_PORT}
        protocol: tcp
        mode: ingress
    secrets:
      - jwt_secret
      - openai_api_key
      - redis_password
    configs:
      - source: nginx_config7
        target: /usr/local/openresty/nginx/conf/nginx.conf
    environment:
      - BACKEND_URL=${API_INTERNAL_BASE_URL}
      - REDIS_HOST=${REDIS_INTERNAL_HOST}
      - REDIS_PORT=${REDIS_INTERNAL_PORT}
      - REDIS_PASSWORD_FILE=/run/secrets/redis_password
      - JWT_SECRET_FILE=/run/secrets/jwt_secret
      - OPENAI_API_KEY_FILE=/run/secrets/openai_api_key
      - MODEL_SHUB_OUTBOX_DIR=/tmp/modelsgateway/outbox
      - LOG_LEVEL=info
      - ENVIRONMENT=production
    volumes:
      - modelsgateway_data:/data
      - modelsgateway_logs:/usr/local/openresty/nginx/logs
      - nginx_cache:/var/cache/nginx
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 30s
        failure_action: rollback
        monitor: 60s
        max_failure_ratio: 0.3
        order: start-first
      placement:
        constraints:
          - node.labels.modelsgateway == true
        preferences:
          - spread: node.id
      resources:
        limits:
          cpus: "2"
          memory: 2G
        reservations:
          cpus: "1"
          memory: 1G
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3
        window: 120s
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.modelsgateway.rule=Host(`models.agenticca.ai/vello`)"
        - "traefik.http.services.modelsgateway.loadbalancer.server.port=80"
    healthcheck:
      test: ["CMD", "curl", "-f", "${MODELSGATEWAY_HEALTHCHECK_URL}"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  # Job Collector Service - Stateless, scalable
  job-collector:
    image: vello-job-collector:latest
    networks:
      - vello-overlay
    ports:
      - target: ${JOBCOLLECTOR_INTERNAL_PORT}
        published: ${JOBCOLLECTOR_PUBLIC_PORT}
        protocol: tcp
        mode: ingress
    secrets:
      - service_token
      - redis_password
    environment:
      - BACKEND_URL=${API_INTERNAL_BASE_URL}
      - REDIS_HOST=${REDIS_INTERNAL_HOST}
      - REDIS_PORT=${REDIS_INTERNAL_PORT}
      - REDIS_PASSWORD_FILE=/run/secrets/redis_password
      - SERVICE_TOKEN_FILE=/run/secrets/service_token
      - SERVICE_NAME=job-collector
      - BATCH_SIZE=100
      - BATCH_TIMEOUT=5
      - LOG_LEVEL=INFO
      - PYTHONUNBUFFERED=1
      - ENVIRONMENT=production
      - CANONICAL_EVENTS_ENABLED=true
      - CANONICAL_EVENTS_STREAM=agenticca:events:modelsgateway:v1
      - BACKEND_DISPATCH_ENABLED=false
    volumes:
      - job_collector_logs:/app/logs
      - modelsgateway_data:/data:ro
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 30s
        failure_action: rollback
        monitor: 60s
        max_failure_ratio: 0.3
        order: start-first
      placement:
        constraints:
          - node.labels.job-collector == true
        preferences:
          - spread: node.id
      resources:
        limits:
          cpus: "1"
          memory: 1G
        reservations:
          cpus: "0.5"
          memory: 512M
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3
        window: 120s
    command: ["python", "-m", "service.job_collector_daemon"]
    healthcheck:
      test: ["CMD", "curl", "-f", "${JOBCOLLECTOR_HEALTHCHECK_URL}"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  # Vello Ingester Service - Product-side persistence from canonical events
  vello-ingester:
    image: vello-ingester:latest
    networks:
      - vello-overlay
    secrets:
      - database_url
      - redis_password
    environment:
      - DATABASE_URL_FILE=/run/secrets/database_url
      - REDIS_PASSWORD_FILE=/run/secrets/redis_password
      - REDIS_INTERNAL_BASE_URL=${REDIS_INTERNAL_BASE_URL}
      - CANONICAL_EVENTS_STREAM=agenticca:events:modelsgateway:v1
      - INGESTER_WRITE_ENABLED=true
      - INGESTER_DRY_RUN=false
      - INGESTER_CONSUMER_GROUP=vello-ingester
      - LOG_LEVEL=INFO
      - ENVIRONMENT=production
    volumes:
      - vello_ingester_logs:/app/logs
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 30s
        failure_action: rollback
        monitor: 60s
        max_failure_ratio: 0.3
        order: start-first
      placement:
        constraints:
          - node.labels.job-collector == true
        preferences:
          - spread: node.id
      resources:
        limits:
          cpus: "1"
          memory: 1G
        reservations:
          cpus: "0.5"
          memory: 512M
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3
        window: 120s

  # Web Panel Service - Administration interface
  web-panel:
    image: vello-web-panel:latest
    networks:
      - vello-overlay
    ports:
      - target: 5000
        published: 5001
        protocol: tcp
        mode: ingress
    secrets:
      - database_url
      - jwt_secret
    environment:
      # Use secret files
      - DATABASE_URL_FILE=/run/secrets/database_url
      - SECRET_KEY_FILE=/run/secrets/jwt_secret

      # Non-sensitive configs
      - NODE_ENV=production
      - PORT=${WEB_ADMIN_INTERNAL_PORT}
      - API_URL=${API_INTERNAL_BASE_URL}
    volumes:
      - web_panel_data:/app/data
    deploy:
      replicas: 1
      update_config:
        parallelism: 1
        delay: 30s
        failure_action: rollback
        monitor: 60s
        max_failure_ratio: 0.3
        order: start-first
      placement:
        constraints:
          - node.role == manager
      resources:
        limits:
          cpus: "1"
          memory: 1G
        reservations:
          cpus: "0.5"
          memory: 512M
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3
        window: 120s
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.web-panel.rule=Host(`panel.agenticca.ai/vello`)"
        - "traefik.http.services.web-panel.loadbalancer.server.port=5000"
    healthcheck:
      test: ["CMD", "curl", "-f", "${WEB_ADMIN_INTERNAL_BASE_URL}/"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  # Traefik Load Balancer (optional)
  traefik:
    image: traefik:v3.0
    networks:
      - vello-overlay
    ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
      - target: 8080
        published: 8080
        protocol: tcp
        mode: ingress
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik_certs:/letsencrypt
    command:
      - "--api.dashboard=true"
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.swarmMode=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.letsencrypt.acme.email=vello@agenticca.ai"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--log.level=INFO"
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
      resources:
        limits:
          cpus: "0.5"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s

networks:
  vello-overlay:
    driver: overlay
    attachable: true
    driver_opts:
      encrypted: "true"
    ipam:
      config:
        - subnet: 10.0.0.0/24

volumes:
  # Persistent volumes
  redis_data:
    driver: local
    driver_opts:
      type: nfs
      o: addr=nfs-server.local,rw,nfsvers=4
      device: ":/exports/vello/redis"

  # Application volumes
  api_logs:
    driver: local
  api_temp:
    driver: local
  modelsgateway_data:
    driver: local
  modelsgateway_logs:
    driver: local
  job_collector_logs:
    driver: local
  vello_ingester_logs:
    driver: local
  nginx_cache:
    driver: local
  traefik_certs:
    driver: local
  web_panel_data:
    driver: local
