Ctrl K
ComunidadGuías…

docker-compose.yml

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

Archivo original ↗Preguntar a KiBot
services:
  # ============================================
  # NGINX Load Balancer
  # ============================================
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      - rda
    networks:
      - ojuri-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 30s
      timeout: 10s
      retries: 3

  # ============================================
  # Sentinel (operator dashboard)
  # ============================================
  # Static SPA behind its own nginx. Gated behind a profile so a plain
  # `docker compose up` is unchanged: the repo's default `nginx.conf`
  # routes `/` to RDA, and adopters fronting Sentinel with their own
  # ingress do not want a second copy of it running here. Opt in with
  # `--profile sentinel`, and pair it with a config that routes `/` to
  # this service — see `deploy/aws/nginx/nginx.conf` for a worked one.
  sentinel:
    profiles: ["sentinel"]
    build:
      context: ./frontend
      dockerfile: Dockerfile
    networks:
      - ojuri-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost/"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

  # ============================================
  # RDA Service Replicas (Real-Time Detection Agent)
  # ============================================
  # Scale with RDA_REPLICAS (default 3). NGINX resolves the `rda` DNS
  # name once at startup and balances across every replica IP Docker
  # returns — after changing the count, restart nginx so it re-resolves.
  rda:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - NODE_ENV=${NODE_ENV:-development}
      - PORT=3000
      - APP_NAME=ojuri-rda
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_PASSWORD=${REDIS_PASSWORD:-}
      # Use the INTERNAL listener (advertised as kafka:29092) for
      # container-to-container traffic. Port 9092 is the EXTERNAL
      # listener that advertises localhost:9092 for host clients —
      # which from inside this container resolves to its own loopback
      # and produces the "Consumer crashed → restart" flap.
      - KAFKA_BROKERS=kafka:29092
      - KAFKA_TOPIC=transactions.completed
      - KAFKA_BLOCKED_TOPIC=transactions.blocked
      - AUDIT_PIPELINE=${AUDIT_PIPELINE:-queue}
      - MODEL_PATH=/app/models/fraud_model.onnx
      - FRAUD_THRESHOLD=0.65
      - ONNX_SESSION_POOL_SIZE=4
      - LOG_LEVEL=info
      - DB_CLIENT=pg
      - DB_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-fraud_db}
      # Service-health fan-out. Defaults in HealthService point at
      # 127.0.0.1 which can't reach sibling containers from inside RDA;
      # set the Docker-network URLs explicitly. MLA runs natively on
      # the host (not in compose by design), so its URL uses
      # host.docker.internal — override with `MLA_HEALTH_URL=` when
      # MLA is deliberately disabled.
      - PAA_HEALTH_URL=${PAA_HEALTH_URL:-http://paa-1:9090}
      - FIA_HEALTH_URL=${FIA_HEALTH_URL:-http://fia:9094}
      - MLA_HEALTH_URL=${MLA_HEALTH_URL:-http://host.docker.internal:9095}
      - AUTH_JWT_SECRET=${AUTH_JWT_SECRET}
      - AUTH_JWT_TTL_SECONDS=${AUTH_JWT_TTL_SECONDS:-28800}
      - RDA_REQUIRE_API_KEY=${RDA_REQUIRE_API_KEY:-false}
      # Without this the boot-time production guard sees no allowed origin and
      # refuses to start, so NODE_ENV=production was unrunnable from this file.
      - SENTINEL_CORS_ORIGINS=${SENTINEL_CORS_ORIGINS:-}
      - DEMO_CREDENTIALS_URL=${DEMO_CREDENTIALS_URL:-}
      - MLA_SERVICE_TOKEN=${MLA_SERVICE_TOKEN:-}
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_DATABASE=${POSTGRES_DB:-fraud_db}
      - DB_USERNAME=${POSTGRES_USER:-postgres}
      - DB_PASSWORD=${POSTGRES_PASSWORD:-postgres}
    extra_hosts:
      # Required on Linux hosts so `host.docker.internal` resolves
      # for the MLA health probe. macOS Docker Desktop already
      # provides it implicitly.
      - "host.docker.internal:host-gateway"
    volumes:
      - ./models:/app/models:ro
      - ./data/training-imports:/app/data/training-imports:ro
      - ./data/training-uploads:/app/data/training-uploads
    depends_on:
      redis:
        condition: service_healthy
      kafka:
        condition: service_healthy
      postgres:
        condition: service_healthy
      db-migrate:
        condition: service_completed_successfully
    networks:
      - ojuri-network
    restart: unless-stopped
    deploy:
      replicas: ${RDA_REPLICAS:-3}
      resources:
        limits:
          memory: 4G

  # ============================================
  # PAA Service (Pattern Analysis Agent)
  # Singleton Kafka consumer.
  #
  # DO NOT scale this service. The graph + velocity state live in
  # process memory; running a second replica under the same Kafka
  # consumer group splits the partition assignment, so each replica
  # sees only a subset of edges and PageRank / Louvain run on a
  # partial graph. A ring whose members hash to different partitions
  # becomes invisible. The worker emits an ERROR and a
  # `paa_group_members` metric > 1 if a second member joins the
  # `pattern-analysis` group.
  # ============================================
  paa-1:
    build:
      context: ./paa-service
      dockerfile: Dockerfile
    environment:
      - NODE_ENV=${NODE_ENV:-development}
      - APP_NAME=ojuri-paa-1
      - METRICS_PORT=9090
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_PASSWORD=${REDIS_PASSWORD:-}
      # Use the INTERNAL listener (advertised as kafka:29092) for
      # container-to-container traffic. Port 9092 is the EXTERNAL
      # listener that advertises localhost:9092 for host clients —
      # which from inside this container resolves to its own loopback
      # and produces the "Consumer crashed → restart" flap.
      - KAFKA_BROKERS=kafka:29092
      - KAFKA_TOPIC=transactions.completed
      - KAFKA_CONSUMER_GROUP=pattern-analysis
      - KAFKA_CLIENT_ID=paa-1
      - LOG_LEVEL=info
      - DB_CLIENT=pg
      - DB_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-fraud_db}
      - GRAPH_UPDATE_INTERVAL=300000
      - PAGERANK_DAMPING=0.85
      - BATCH_SIZE=100
      - MAX_GRAPH_NODES=1000000
    ports:
      - "9091:9090"
    depends_on:
      redis:
        condition: service_healthy
      kafka:
        condition: service_healthy
      postgres:
        condition: service_healthy
      db-migrate:
        condition: service_completed_successfully
    networks:
      - ojuri-network
    restart: unless-stopped
    deploy:
      replicas: 1
      resources:
        limits:
          cpus: "2"
          memory: 8G

  # ============================================
  # FIA Service (Fraud Investigation Agent)
  # Async LLM-based investigation report generator
  # ============================================
  fia:
    # FIA carries the Phi-3 LLM weights (~7.6 GB on first run) and the
    # torch + transformers + accelerate Python stack. Gating it behind
    # a Compose profile means `docker compose up` is fast for first-time
    # adopters; opt in with `docker compose --profile fia up` once you
    # have the disk + bandwidth to download the model.
    profiles: ["fia"]
    build:
      context: ./fia-service
      dockerfile: Dockerfile
    environment:
      # Use the INTERNAL listener (advertised as kafka:29092) for
      # container-to-container traffic. Port 9092 is the EXTERNAL
      # listener that advertises localhost:9092 for host clients —
      # which from inside this container resolves to its own loopback
      # and produces the "Consumer crashed → restart" flap.
      - KAFKA_BROKERS=kafka:29092
      - KAFKA_BLOCKED_TOPIC=transactions.blocked
      - KAFKA_CONSUMER_GROUP=ojuri-investigation
      - KAFKA_AUTO_OFFSET_RESET=earliest
      - POSTGRES_HOST=postgres
      - POSTGRES_PORT=5432
      - POSTGRES_DB=${POSTGRES_DB:-fraud_db}
      - POSTGRES_USER=${POSTGRES_USER:-postgres}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
      - LLM_MODEL_NAME=${FIA_LLM_MODEL_NAME:-microsoft/Phi-3-mini-4k-instruct}
      - LLM_MODEL_PATH=${FIA_LLM_MODEL_PATH:-}
      - LLM_DEVICE=${FIA_LLM_DEVICE:-auto}
      - LLM_DTYPE=${FIA_LLM_DTYPE:-auto}
      - LLM_MAX_NEW_TOKENS=${FIA_LLM_MAX_NEW_TOKENS:-384}
      - LLM_TEMPERATURE=${FIA_LLM_TEMPERATURE:-0.2}
      - FIA_FALLBACK_ON_LLM_FAILURE=${FIA_FALLBACK_ON_LLM_FAILURE:-true}
      # Set to true to skip the Phi-3 load entirely (no 7.6 GB download,
      # no 15 GB resident model). Reports are served from the
      # deterministic rule-based path. Useful for demos / CI / low-RAM
      # hosts. See fia-service/src/llm/phi3_generator.py.
      - FIA_DISABLE_LLM=${FIA_DISABLE_LLM:-false}
      - PROMPT_TEMPLATE_VERSION=${FIA_PROMPT_TEMPLATE_VERSION:-v1}
      - METRICS_PORT=9094
      - LOG_LEVEL=${FIA_LOG_LEVEL:-INFO}
      # Shared with RDA so the same operator JWT works for both APIs.
      # The HTTP server in handler.py verifies HS256 against this secret.
      - AUTH_JWT_SECRET=${AUTH_JWT_SECRET}
      - AUTH_JWT_ISSUER=${AUTH_JWT_ISSUER:-}
      - AUTH_JWT_AUDIENCE=${AUTH_JWT_AUDIENCE:-}
      # In Compose the container's loopback is isolated from the host, so
      # binding 0.0.0.0 is required to expose port 9094 through the
      # published port mapping. Bare-metal deployments should bind to
      # 127.0.0.1 (the in-process default) and proxy in front.
      - FIA_HTTP_BIND=${FIA_HTTP_BIND:-0.0.0.0}
      # Pin HuggingFace cache into the named volume so the ~8 GB Phi-3 download
      # survives container recreates.
      - HF_HOME=/app/.hf-cache
      # Bound torch's CPU thread pool so it doesn't fight RDA/PAA on the host.
      - OMP_NUM_THREADS=${FIA_OMP_NUM_THREADS:-2}
    ports:
      - "9094:9094"
    volumes:
      - fia-hf-cache:/app/.hf-cache
      - ./fia-service/models:/app/models
    depends_on:
      kafka:
        condition: service_healthy
      postgres:
        condition: service_healthy
    networks:
      - ojuri-network
    restart: unless-stopped
    # Standalone `docker compose up` ignores deploy.resources (those
    # are Swarm-only). Use top-level mem_limit so the container
    # actually gets the headroom. Phi-3-mini in fp32 on CPU needs
    # ~15 GB resident; cap at 16 GiB to leave the host breathing room
    # but not OOM-kill the load step the way the old 8 GiB cap did.
    mem_limit: 16g
    mem_reservation: 6g
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 16G
        reservations:
          memory: 6G

  # ============================================
  # MLA Service (Model Learning Agent)
  # ============================================
  # Async drift monitor + retraining pipeline. Not on the prediction
  # hot path. Gated behind a `mla` profile because adopters often
  # prefer to run the trainer on a host with GPU access or specific
  # Python deps. Opt in with `docker compose --profile mla up`.
  #
  # When MLA runs IN compose under this profile, set
  # `MLA_HEALTH_URL=http://mla:9095` in `.env` so the RDA replicas
  # probe the in-compose service instead of host.docker.internal.
  mla:
    profiles: ["mla"]
    build:
      context: ./mla-service
      dockerfile: Dockerfile
    environment:
      - POSTGRES_HOST=postgres
      - POSTGRES_PORT=5432
      - POSTGRES_DB=${POSTGRES_DB:-fraud_db}
      - POSTGRES_USER=${POSTGRES_USER:-postgres}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
      - KAFKA_BROKERS=kafka:29092
      - KAFKA_TOPIC=transactions.completed
      - KAFKA_CONSUMER_GROUP=mla-drift-monitor
      - METRICS_PORT=9095
      - MODEL_OUTPUT_DIR=/app/models
      - LOG_LEVEL=${MLA_LOG_LEVEL:-INFO}
      - MLA_SERVICE_TOKEN=${MLA_SERVICE_TOKEN:-}
      - AUTH_JWT_SECRET=${AUTH_JWT_SECRET}
      - DRIFT_PSI_THRESHOLD=${DRIFT_PSI_THRESHOLD:-0.25}
      - DRIFT_F1_THRESHOLD=${DRIFT_F1_THRESHOLD:-0.4}
    ports:
      - "9095:9095"
    volumes:
      - ./models:/app/models
      - mla-data:/app/data
    depends_on:
      kafka:
        condition: service_healthy
      postgres:
        condition: service_healthy
      db-migrate:
        condition: service_completed_successfully
    networks:
      - ojuri-network
    restart: unless-stopped

  # ============================================
  # Redis
  # ============================================
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 2gb --maxmemory-policy volatile-lru
    ports:
      - "6380:6379"
    volumes:
      - redis-data:/data
    networks:
      - ojuri-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ============================================
  # Kafka + Zookeeper
  # ============================================
  zookeeper:
    image: confluentinc/cp-zookeeper:7.5.0
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    volumes:
      - zookeeper-data:/var/lib/zookeeper/data
      - zookeeper-logs:/var/lib/zookeeper/log
    networks:
      - ojuri-network
    restart: unless-stopped

  kafka:
    image: confluentinc/cp-kafka:7.5.0
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
      - "29092:29092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_LISTENERS: INTERNAL://0.0.0.0:29092,EXTERNAL://0.0.0.0:9092
      KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:29092,EXTERNAL://localhost:9092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_NUM_PARTITIONS: 12
      KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
      KAFKA_LOG_RETENTION_HOURS: 168
      KAFKA_LOG_RETENTION_BYTES: 10737418240
    volumes:
      - kafka-data:/var/lib/kafka/data
    networks:
      - ojuri-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "kafka-topics", "--bootstrap-server", "localhost:9092", "--list"]
      interval: 30s
      timeout: 10s
      retries: 5

  # ============================================
  # PostgreSQL
  # ============================================
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
      POSTGRES_DB: ${POSTGRES_DB:-fraud_db}
    ports:
      - "5433:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - ojuri-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ============================================
  # DB migrate (one-shot)
  # ============================================
  # Runs Knex migrations + seeds against Postgres, then exits 0.
  # RDA replicas depend on it via service_completed_successfully so a
  # fresh stack has a fully-migrated schema by the time /v1/predict
  # accepts requests. On subsequent `docker compose up`, this re-runs
  # and is a no-op ("Already up to date"). The image is the same RDA
  # build (it ships the compiled migrations + knexfile); when running
  # from prebuilt images, knex reads /app/dist/database/{migrations,seeds}
  # via KNEX_MIGRATIONS_DIR / KNEX_SEEDS_DIR.
  db-migrate:
    build:
      context: .
      dockerfile: Dockerfile
    # The normalize step is guarded on the file existing: this compose file
    # is shipped inside @ojuri/cli and is routinely run against an older
    # published image via OJURI_VERSION, which has no such file. Requiring
    # it outright killed db-migrate, and with it the whole stack.
    #
    # Invokes the knex CLI directly with node, bypassing the package.json
    # `db:migrate` script that wraps it in ts-node + dotenv. The production
    # image runs `npm prune --production` which removes ts-node, and Compose
    # already injects env vars so dotenv isn't needed. Compiled migrations
    # live at /app/dist/database/{migrations,seeds} per KNEX_*_DIR below.
    # The seeds include the SUPER_ADMIN bootstrap that prints a one-time
    # admin password — surfaced via `docker compose logs db-migrate`.
    command:
      - sh
      - -c
      - "if [ -f ./dist/database/normalize-migration-names.cli.js ]; then node ./dist/database/normalize-migration-names.cli.js; fi && node ./node_modules/knex/bin/cli.js --knexfile knexfile.js migrate:latest && node ./node_modules/knex/bin/cli.js --knexfile knexfile.js seed:run"
    environment:
      - NODE_ENV=${NODE_ENV:-production}
      - DB_CLIENT=pg
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_DATABASE=${POSTGRES_DB:-fraud_db}
      - DB_USERNAME=${POSTGRES_USER:-postgres}
      - DB_PASSWORD=${POSTGRES_PASSWORD:-postgres}
      - KNEX_MIGRATIONS_DIR=/app/dist/database/migrations
      - KNEX_SEEDS_DIR=/app/dist/database/seeds
      # Demo rules seed INACTIVE by default (their amount thresholds are
      # demo props that dominate real traffic). Set true for the demo
      # dataset walkthrough. See docs/RULES.md.
      - SEED_DEMO_RULES_ACTIVE=${SEED_DEMO_RULES_ACTIVE:-false}
      # Unset means the seed migration generates a random password and prints it
      # once. Supply one when the deployment needs the credential known ahead of
      # time rather than scraped from migration output.
      - ADMIN_SEED_PASSWORD=${ADMIN_SEED_PASSWORD:-}
      # Seeds a shared, read-mostly `demo` account whose password is meant to be
      # published. Only ever set this on a throwaway public sandbox.
      - SEED_DEMO_USER=${SEED_DEMO_USER:-false}
      - DEMO_USER_PASSWORD=${DEMO_USER_PASSWORD:-}
    # The runtime image ships only dist/ + node_modules — models/ is
    # mounted, exactly as on the RDA replicas. Without it the
    # 00_initial_model_version seed silently skips, leaving the model
    # registry empty and the segment-threshold seed unable to run.
    # (Restores the mount dropped when #101 merged over #100.)
    volumes:
      - ./models:/app/models:ro
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - ojuri-network
    restart: "no"

  # ============================================
  # Demo traffic seed (one-shot, opt-in)
  # ============================================
  # Sends ~500 realistic transactions (recurring senders plus embedded
  # fraud patterns: money ring, mule fan-out, VPN, structuring) straight
  # at the rda service so a fresh stack has a populated dashboard. Opt
  # in with `docker compose --profile demo run --rm demo-seed`.
  demo-seed:
    profiles: ["demo"]
    image: node:20-alpine
    command: ["node", "/scripts/demo-traffic.mjs"]
    environment:
      - RDA_URL=http://rda:3000
      - DEMO_API_KEY=${DEMO_API_KEY:-}
      - DEMO_TX_COUNT=${DEMO_TX_COUNT:-500}
      # rda has no compose healthcheck, so the script polls /livez
      # itself instead of relying on a service_healthy condition.
      - DEMO_WAIT_FOR_RDA_SECONDS=90
    volumes:
      - ./scripts:/scripts:ro
    depends_on:
      rda:
        condition: service_started
    networks:
      - ojuri-network
    restart: "no"

  # ============================================
  # Monitoring Stack (Optional)
  # ============================================
  prometheus:
    image: prom/prometheus:v2.47.0
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.path=/prometheus"
      - "--web.enable-lifecycle"
    networks:
      - ojuri-network
    restart: unless-stopped

  grafana:
    image: grafana/grafana:10.1.0
    ports:
      - "3001:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=${GRAFANA_USER:-admin}
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
    volumes:
      - grafana-data:/var/lib/grafana
    depends_on:
      - prometheus
    networks:
      - ojuri-network
    restart: unless-stopped

# ============================================
# Networks
# ============================================
networks:
  ojuri-network:
    driver: bridge

# ============================================
# Volumes
# ============================================
volumes:
  redis-data:
  postgres-data:
  kafka-data:
  zookeeper-data:
  zookeeper-logs:
  prometheus-data:
  grafana-data:
  fia-hf-cache:
  mla-data: