Ctrl K
ComunidadGuías…

docker-compose.yml

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

Archivo original ↗Preguntar a KiBot
# ─── NovelWorld microservice stack ───────────────────────────────────────────
# Service topology:
#   nginx (80) → gateway (8080) → [novel-service, agent-service,
#                                       narrative-service, user-service]
#   The default stack uses PostgreSQL 18; the Redis projection uses the explicit redis profile.

x-logging: &bounded-logs
  driver: local

networks:
  novel-net:
    driver: bridge

volumes:
  postgres_data:
  redis_data:
  embedding_model_cache:

services:

  # ─── Infrastructure ────────────────────────────────────────────────────────

  postgres:
    image: ${POSTGRES_IMAGE:-pgvector/pgvector:pg18@sha256:2ba9ca5f2e7daa0f0e7723cba1ee9167bab54efd3640516a44ac1a928dd67e7a}
    container_name: ${CONTAINER_PREFIX:-novel}-postgres
    restart: unless-stopped
    logging: *bounded-logs
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-novel}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
      POSTGRES_DB: ${POSTGRES_DB:-novel_world}
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./infra/postgres/init.sql:/docker-entrypoint-initdb.d/01_init.sql:ro
    # ports:
    #   - "5432:5432"  # uncomment for local dev
    networks:
      - novel-net
    healthcheck:
      # pg_isready also succeeds against the temporary server used by the
      # official entrypoint while init scripts are still running. PID 1 becomes
      # postgres only after initialization completes.
      test: ["CMD-SHELL", "test \"$$(cat /proc/1/comm)\" = postgres && pg_isready -U ${POSTGRES_USER:-novel} -d ${POSTGRES_DB:-novel_world}"]
      interval: 10s
      timeout: 5s
      retries: 5

  postgres-migrate:
    image: ${POSTGRES_IMAGE:-pgvector/pgvector:pg18@sha256:2ba9ca5f2e7daa0f0e7723cba1ee9167bab54efd3640516a44ac1a928dd67e7a}
    restart: "no"
    logging: *bounded-logs
    environment:
      PGHOST: postgres
      PGUSER: ${POSTGRES_USER:-novel}
      PGPASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
      PGDATABASE: ${POSTGRES_DB:-novel_world}
    volumes:
      - ./infra/postgres/migrations:/migrations:ro
    depends_on:
      postgres:
        condition: service_healthy
    command:
      - /bin/sh
      - -ec
      - |
        for migration in /migrations/*.sql; do
          echo "Applying $$migration"
          psql -v ON_ERROR_STOP=1 -f "$$migration"
        done
    networks:
      - novel-net

  redis:
    profiles: ["redis"]
    image: ${REDIS_IMAGE:-redis:8.10.2-alpine@sha256:3811787313eba226a2ef38658c6ccb91cd5e110edc89c37767de373120a0e5a0}
    container_name: ${CONTAINER_PREFIX:-novel}-redis
    restart: unless-stopped
    logging: *bounded-logs
    environment:
      CACHE_MODE: ${CACHE_MODE:-postgres}
      REDIS_PASSWORD: ${REDIS_PASSWORD:-}
    entrypoint: ["/bin/sh", "-ec"]
    command:
      - |
        mode=$${CACHE_MODE:-}
        [ "$$mode" = redis ] \
          || { echo 'Redis profile requires CACHE_MODE=redis' >&2; exit 64; }
        password=$${REDIS_PASSWORD:-}
        lower=$$(printf '%s' "$$password" | tr '[:upper:]' '[:lower:]')
        distinct=$$(printf '%s' "$$password" | fold -w1 | sort -u | wc -l | tr -d ' ')
        [ "$${#password}" -ge 16 ] \
          && [ "$$distinct" -ge 8 ] \
          && case "$$password" in *[!A-Za-z0-9._~-]*) false;; *) true;; esac \
          && case "$$lower" in *placeholder*|*change_me*|your_redis_password_here|runtime-redis-only) false;; *) true;; esac \
          || { echo 'REDIS_PASSWORD must be URL-safe, non-placeholder, at least 16 characters, and contain at least 8 distinct characters' >&2; exit 64; }
        exec docker-entrypoint.sh redis-server --requirepass "$$password" --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    # ports:
    #   - "6379:6379"  # uncomment for local dev
    networks:
      - novel-net
    healthcheck:
      test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD:-}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  embedding:
    profiles: ["local-embedding"]
    image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.9.4@sha256:2538ea1c9640d3763b15af668039d24172d063b42337b0c27796fc2be180c78d
    container_name: ${CONTAINER_PREFIX:-novel}-embedding
    restart: unless-stopped
    logging: *bounded-logs
    environment:
      RAYON_NUM_THREADS: "4"
      MKL_NUM_THREADS: "4"
    command:
      - --model-id
      - Qwen/Qwen3-Embedding-0.6B
      - --revision
      - 97b0c614be4d77ee51c0cef4e5f07c00f9eb65b3
      - --served-model-name
      - Qwen/Qwen3-Embedding-0.6B
      - --dtype
      - float32
      - --max-concurrent-requests
      - "4"
      - --max-batch-tokens
      - "4096"
      - --max-client-batch-size
      - "1"
      - --payload-limit
      - "8192"
      - --json-output
    volumes:
      - embedding_model_cache:/data
    networks:
      - novel-net
    read_only: true
    tmpfs:
      - /tmp:size=64m,mode=1777
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    mem_limit: 12g
    cpus: 4.0

  # ─── Services ──────────────────────────────────────────────────────────────

  user-service:
    image: ${USER_SERVICE_IMAGE:-novel-world-user-service:local}
    build:
      context: .
      dockerfile: infra/docker/Dockerfile.rust-service
      args:
        SERVICE: user-service
    container_name: ${CONTAINER_PREFIX:-novel}-user-service
    restart: unless-stopped
    logging: *bounded-logs
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER:-novel}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB:-novel_world}
      JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
      RUNTIME_CONFIG_KEY: ${RUNTIME_CONFIG_KEY:?RUNTIME_CONFIG_KEY is required}
      INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}
      AGENT_SERVICE_URL: http://agent-service:8003
      USER_SERVICE_URL: http://127.0.0.1:8001
      LLM_DIAGNOSTIC_BUDGET_ID: ${LLM_DIAGNOSTIC_BUDGET_ID:-}
      LLM_DIAGNOSTIC_BUDGET_LIMITS: ${LLM_DIAGNOSTIC_BUDGET_LIMITS:-}
      LLM_DIAGNOSTIC_PROFILE: ${LLM_DIAGNOSTIC_PROFILE:-vision-journey-diagnostic-v1}
      LLM_API_URL: ${LLM_API_URL:-https://api.openai.com}
      LLM_API_KEY: ${LLM_API_KEY:-}
      LLM_MODEL: ${LLM_MODEL:-gpt-4o-mini}
      LLM_PRICING_USD_PER_MILLION: '${LLM_PRICING_USD_PER_MILLION:-{}}'
      LLM_PRICING_CNY_PER_MILLION: '${LLM_PRICING_CNY_PER_MILLION:-{}}'
      USD_CNY_RATE: ${USD_CNY_RATE:-}
      PROMETHEUS_URL: ${PROMETHEUS_URL:-http://prometheus:9090}
      AUTH_ACCESS_TOKEN_EXPIRY: ${AUTH_ACCESS_TOKEN_EXPIRY:-3600}
      AUTH_REFRESH_TOKEN_EXPIRY: ${AUTH_REFRESH_TOKEN_EXPIRY:-604800}
      PORT: 8001
      RUST_LOG: ${RUST_LOG:-info}
    depends_on:
      postgres-migrate:
        condition: service_completed_successfully
    networks:
      - novel-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8001/ready"]
      interval: 15s
      timeout: 5s
      retries: 3

  novel-service:
    image: ${NOVEL_SERVICE_IMAGE:-novel-world-novel-service:local}
    build:
      context: .
      dockerfile: infra/docker/Dockerfile.rust-service
      args:
        SERVICE: novel-service
    container_name: ${CONTAINER_PREFIX:-novel}-novel-service
    restart: unless-stopped
    logging: *bounded-logs
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER:-novel}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB:-novel_world}
      LLM_API_URL: ${LLM_API_URL:-https://api.openai.com}
      LLM_API_KEY: ${LLM_API_KEY:-}
      LLM_MODEL: ${LLM_MODEL:-gpt-4o-mini}
      LLM_CHARACTER_OUTPUT_MODE: ${LLM_CHARACTER_OUTPUT_MODE:-legacy-json}
      LAYA_API_URL: ${LAYA_API_URL:-}
      LAYA_API_KEY: ${LAYA_API_KEY:-}
      USER_SERVICE_URL: http://user-service:8001
      LLM_DIAGNOSTIC_BUDGET_ID: ${LLM_DIAGNOSTIC_BUDGET_ID:-}
      LLM_DIAGNOSTIC_PROFILE: ${LLM_DIAGNOSTIC_PROFILE:-vision-journey-diagnostic-v1}
      INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}
      AGENT_SERVICE_URL: http://agent-service:8003
      IMAGE_GEN_API_URL: ${IMAGE_GEN_API_URL:-https://api.openai.com}
      IMAGE_GEN_API_KEY: ${IMAGE_GEN_API_KEY:-}
      S3_ENABLED: ${S3_ENABLED:-false}
      S3_ENDPOINT: ${S3_ENDPOINT:-}
      S3_BUCKET: ${S3_BUCKET:-novel-world-uploads}
      S3_REGION: ${S3_REGION:-us-east-1}
      S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-false}
      S3_ACCESS_KEY: ${S3_ACCESS_KEY:-}
      S3_SECRET_KEY: ${S3_SECRET_KEY:-}
      S3_SESSION_TOKEN: ${S3_SESSION_TOKEN:-}
      PORT: 8002
      RUST_LOG: ${RUST_LOG:-info}
    depends_on:
      postgres-migrate:
        condition: service_completed_successfully
    networks:
      - novel-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8002/ready"]
      interval: 15s
      timeout: 5s
      retries: 3

  agent-service:
    image: ${AGENT_SERVICE_IMAGE:-novel-world-agent-service:local}
    build:
      context: .
      dockerfile: infra/docker/Dockerfile.rust-service
      args:
        SERVICE: agent-service
    container_name: ${CONTAINER_PREFIX:-novel}-agent-service
    restart: unless-stopped
    logging: *bounded-logs
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER:-novel}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB:-novel_world}
      CACHE_MODE: ${CACHE_MODE:-postgres}
      REDIS_URL: ${REDIS_URL:-memory://}
      LLM_API_URL: ${LLM_API_URL:-https://api.openai.com}
      LLM_API_KEY: ${LLM_API_KEY:-}
      LLM_MODEL: ${LLM_MODEL:-gpt-4o}
      USER_SERVICE_URL: http://user-service:8001
      LLM_DIAGNOSTIC_BUDGET_ID: ${LLM_DIAGNOSTIC_BUDGET_ID:-}
      LLM_DIAGNOSTIC_PROFILE: ${LLM_DIAGNOSTIC_PROFILE:-vision-journey-diagnostic-v1}
      INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}
      EMBEDDING_PROVIDER: ${EMBEDDING_PROVIDER:-}
      EMBEDDING_API_URL: ${EMBEDDING_API_URL:-}
      EMBEDDING_API_KEY: ${EMBEDDING_API_KEY:-}
      EMBEDDING_MODEL: ${EMBEDDING_MODEL:-}
      PORT: 8003
      NOVEL_SERVICE_URL: http://novel-service:8002
      NARRATIVE_SERVICE_URL: http://narrative-service:8004
      RUST_LOG: ${RUST_LOG:-info}
    depends_on:
      postgres-migrate:
        condition: service_completed_successfully
      novel-service:
        condition: service_healthy
      narrative-service:
        condition: service_healthy
    networks:
      - novel-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8003/ready"]
      interval: 15s
      timeout: 5s
      retries: 3

  narrative-service:
    image: ${NARRATIVE_SERVICE_IMAGE:-novel-world-narrative-service:local}
    build:
      context: .
      dockerfile: infra/docker/Dockerfile.rust-service
      args:
        SERVICE: narrative-service
    container_name: ${CONTAINER_PREFIX:-novel}-narrative-service
    restart: unless-stopped
    logging: *bounded-logs
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER:-novel}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB:-novel_world}
      LLM_API_URL: ${LLM_API_URL:-https://api.openai.com}
      LLM_API_KEY: ${LLM_API_KEY:-}
      LLM_MODEL: ${LLM_MODEL:-gpt-4o}
      LAYA_API_URL: ${LAYA_API_URL:-}
      LAYA_API_KEY: ${LAYA_API_KEY:-}
      USER_SERVICE_URL: http://user-service:8001
      LLM_DIAGNOSTIC_BUDGET_ID: ${LLM_DIAGNOSTIC_BUDGET_ID:-}
      LLM_DIAGNOSTIC_PROFILE: ${LLM_DIAGNOSTIC_PROFILE:-vision-journey-diagnostic-v1}
      INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}
      PORT: 8004
      NOVEL_SERVICE_URL: http://novel-service:8002
      AGENT_SERVICE_URL: http://agent-service:8003
      RUST_LOG: ${RUST_LOG:-info}
    depends_on:
      postgres-migrate:
        condition: service_completed_successfully
      novel-service:
        condition: service_healthy
    networks:
      - novel-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8004/ready"]
      interval: 15s
      timeout: 5s
      retries: 3

  # ─── API Gateway ───────────────────────────────────────────────────────────

  gateway:
    image: ${GATEWAY_IMAGE:-novel-world-gateway:local}
    build:
      context: .
      dockerfile: infra/docker/Dockerfile.rust-service
      args:
        SERVICE: gateway
    container_name: ${CONTAINER_PREFIX:-novel}-gateway
    restart: unless-stopped
    logging: *bounded-logs
    environment:
      USER_SERVICE_URL: http://user-service:8001
      NOVEL_SERVICE_URL: http://novel-service:8002
      AGENT_SERVICE_URL: http://agent-service:8003
      NARRATIVE_SERVICE_URL: http://narrative-service:8004
      JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
      INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}
      PORT: 8080
      RATE_LIMIT_RPS: ${RATE_LIMIT_RPS:-500}
      CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:5173,http://127.0.0.1:5173,http://localhost,http://127.0.0.1}
      RUST_LOG: ${RUST_LOG:-info}
    # ports:
    #   - "8080:8080"  # uncomment for local dev
    depends_on:
      user-service:
        condition: service_healthy
      novel-service:
        condition: service_healthy
      agent-service:
        condition: service_healthy
      narrative-service:
        condition: service_healthy
    networks:
      - novel-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/ready"]
      interval: 15s
      timeout: 5s
      retries: 3

  # ─── Frontend ──────────────────────────────────────────────────────────────

  frontend:
    image: ${FRONTEND_IMAGE:-novel-world-frontend:local}
    build:
      context: ./frontend
      dockerfile: Dockerfile
      args:
        VITE_API_URL: ${VITE_API_URL:-/api}
    container_name: ${CONTAINER_PREFIX:-novel}-frontend
    restart: unless-stopped
    logging: *bounded-logs
    networks:
      - novel-net

  # ─── Nginx reverse proxy ───────────────────────────────────────────────────

  nginx:
    image: ${NGINX_IMAGE:-nginx:alpine@sha256:060849a174666c53439dc7cecbf8f67e225c7cbfb1a44ca97915d9a172a81e6d}
    container_name: ${CONTAINER_PREFIX:-novel}-nginx
    restart: unless-stopped
    logging: *bounded-logs
    ports:
      - "${NGINX_HTTP_BIND:-127.0.0.1}:${NGINX_HTTP_PORT:-80}:80"
    volumes:
      - ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - gateway
      - frontend
    networks:
      - novel-net