Ctrl K
ComunidadGuías…

docker-compose.yml

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

Archivo original ↗Preguntar a KiBot
name: panther-minor

# -- Shared network -----------------------------------------------------------
networks:
  ai:
    driver: bridge

# -- Persistent volumes -------------------------------------------------------
volumes:
  llama-cpp-comgr:
  sd-cpp-comgr:
  open-webui-data:
  prometheus-data:
  grafana-data:
  proxy-logs:

services:
  # -- llama.cpp --------------------------------------------------------------
  # OpenAI-compatible LLM inference, native RDNA 4 and ROCm 10 support
  llama-cpp:
    container_name: llama-cpp
    build:
      context: ./llama-cpp
      args:
        HOST_UID: '${HOST_UID}'
        HOST_GID: '${HOST_GID}'
        LLAMA_CPP_REPO: '${LLAMA_CPP_REPO}'
        LLAMA_CPP_REF: '${LLAMA_CPP_REF}'
        LLAMA_CPP_USER: 'llama-cpp'
        ROCM_ARCH: '${ROCM_ARCH}'
    restart: unless-stopped
    devices:
      - '/dev/kfd:/dev/kfd'
      - '/dev/dri:/dev/dri'
    group_add:
      - '${VIDEO_GID}'
      - '${RENDER_GID}'
    # GPU access is the two device nodes plus the video/render groups; `privileged` is not needed. seccomp is
    # unconfined per the ROCm Docker docs ("enables memory mapping"): the default profile blocks
    # mbind/set_mempolicy/get_mempolicy without SYS_NICE, which ROCm can use for host memory (n-cpu-moe experts).
    security_opt:
      - 'seccomp=unconfined'
    environment:
      HF_TOKEN: '${HF_TOKEN}'
      HIP_VISIBLE_DEVICES: '${ROCM_VISIBLE_DEVICES}'
      LLAMA_CPP_BATCH_SIZE: '${LLAMA_CPP_BATCH_SIZE}'
      LLAMA_CPP_CACHE_RAM: '${LLAMA_CPP_CACHE_RAM}'
      LLAMA_CPP_MODELS_MAX: '${LLAMA_CPP_MODELS_MAX}'
      LLAMA_CPP_PARALLEL: '${LLAMA_CPP_PARALLEL}'
      LLAMA_CPP_SLEEP_IDLE_SECONDS: '${LLAMA_CPP_SLEEP_IDLE_SECONDS}'
      LLAMA_CPP_UBATCH_SIZE: '${LLAMA_CPP_UBATCH_SIZE}'
    volumes:
      - 'llama-cpp-comgr:/home/llama-cpp/.cache/comgr'
      - './models/.huggingface:/home/llama-cpp/.cache/huggingface/hub'
      - './llama-cpp/preset.ini:/home/llama-cpp/.cache/huggingface/preset.ini:ro'
    ulimits:
      memlock: -1
    # Top-level systemd slice, whose MemoryLow (set by `setup docker`) keeps the host-resident weights cached under
    # pressure from other workloads. A container under the default system.slice can't be protected: memory.low only
    # takes effect up to its ancestors' protection, and system.slice has none. No dash in the name: systemd reads a
    # dash as nesting (a-b.slice sits inside a.slice). See docs/llm.md#host-memory-protection.
    cgroup_parent: llm.slice
    expose:
      - 8000
    networks:
      - ai
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:8000/health']
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s

  # -- llama.cpp manager (proxy + idle unload) ----------------------------------
  # Activity-aware reverse proxy: records inference activity and unloads models
  # through llama-server when the cluster stays idle.
  llama-manager:
    container_name: llama-manager
    image: node:26-trixie-slim
    restart: unless-stopped
    command: ['node', '/app/main.js']
    environment:
      IDLE_UNLOAD_RETRY_SECONDS: 15
      LLAMA_CPP_SLEEP_IDLE_SECONDS: '${LLAMA_CPP_SLEEP_IDLE_SECONDS}'
      LLAMA_SERVER_URL: 'http://llama-cpp:8000'
      LOG_LEVEL: info
      PORT: 8000
      # Inactivity ceiling for proxied inference streams (cold loads stream no
      # bytes for minutes); matches proxy_read_timeout in proxy/nginx.conf.
      PROXY_TIMEOUT_SECONDS: 900
      # Control-plane bound only: /models and /models/unload fetches.
      UPSTREAM_TIMEOUT_SECONDS: 4
    volumes:
      - './llama-cpp/manager.js:/app/main.js:ro'
      - './llama-cpp/models.js:/app/models.js:ro'
    expose:
      - 8000
    networks:
      - ai
    healthcheck:
      test:
        [
          'CMD',
          'node',
          '-e',
          "fetch('http://localhost:8000/health').then((r)=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
        ]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s

  # -- llama.cpp metrics exporter ---------------------------------------------
  llama-metrics-exporter:
    container_name: llama-metrics-exporter
    image: node:26-trixie-slim
    restart: unless-stopped
    command: ['node', '/app/main.js']
    environment:
      CACHE_TTL_SECONDS: 5
      LLAMA_SERVER_URL: 'http://llama-cpp:8000'
      MANAGER_URL: 'http://llama-manager:8000'
      LOG_LEVEL: info
      PORT: 9090
      UPSTREAM_TIMEOUT_SECONDS: 4
    volumes:
      - './llama-cpp/metrics-exporter.js:/app/main.js:ro'
      - './llama-cpp/models.js:/app/models.js:ro'
    expose:
      - 9090
    networks:
      - ai
    healthcheck:
      test:
        [
          'CMD',
          'node',
          '-e',
          "fetch('http://localhost:9090/health').then((r)=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
        ]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    depends_on:
      - llama-manager

  # -- stable-diffusion.cpp ---------------------------------------------------
  # OpenAI-compatible image generation (sd-server), native RDNA 4 and ROCm 10 support.
  # Pinned to a single GPU via HIP_VISIBLE_DEVICES so it does not contend with the LLMs.
  stable-diffusion-cpp:
    container_name: stable-diffusion-cpp
    build:
      context: ./stable-diffusion-cpp
      args:
        HOST_UID: '${HOST_UID}'
        HOST_GID: '${HOST_GID}'
        SD_CPP_REPO: '${SD_CPP_REPO}'
        SD_CPP_REF: '${SD_CPP_REF}'
        SD_CPP_USER: 'stable-diffusion-cpp'
        ROCM_ARCH: '${ROCM_ARCH}'
    restart: unless-stopped
    devices:
      - '/dev/kfd:/dev/kfd'
      - '/dev/dri:/dev/dri'
    group_add:
      - '${VIDEO_GID}'
      - '${RENDER_GID}'
    # Same GPU access as llama-cpp; see the comment there. --offload-to-cpu relies on ROCm host memory.
    security_opt:
      - 'seccomp=unconfined'
    environment:
      HIP_VISIBLE_DEVICES: '${SD_VISIBLE_DEVICES}'
      # Active text-to-image model, managed by `./bin/panther-minor models t2i load`.
      # sd-server loads exactly one model, so only one is ever resident. These
      # come straight from .env with no fallback, so .env is the single source
      # of truth: a missing value surfaces as a compose warning instead of a
      # silently wrong default (e.g. a stale unconditional diffusion model).
      SD_CPP_MODEL: '${SD_CPP_MODEL}'
      SD_CPP_DIFFUSION_MODEL: '${SD_CPP_DIFFUSION_MODEL}'
      SD_CPP_UNCOND_DIFFUSION_MODEL: '${SD_CPP_UNCOND_DIFFUSION_MODEL}'
      SD_CPP_LLM: '${SD_CPP_LLM}'
      SD_CPP_VAE: '${SD_CPP_VAE}'
      SD_CPP_MODEL_ARGS: '${SD_CPP_MODEL_ARGS}'
    volumes:
      - 'sd-cpp-comgr:/home/stable-diffusion-cpp/.cache/comgr'
      - './models/.huggingface:/home/stable-diffusion-cpp/.cache/huggingface/hub'
    ulimits:
      memlock: -1
    expose:
      - 8000
    networks:
      - ai
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:8000/v1/models']
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 120s

  # -- stable-diffusion.cpp manager (proxy + activity tracking) ---------------
  # Activity-aware reverse proxy in front of sd-server: records image activity and
  # exposes /status for the exporter. Uses a long proxy timeout because image
  # generation streams no bytes for tens of seconds.
  sd-manager:
    container_name: sd-manager
    image: node:26-trixie-slim
    restart: unless-stopped
    command: ['node', '/app/main.js']
    environment:
      LOG_LEVEL: info
      PORT: 8000
      PROXY_TIMEOUT_SECONDS: 900
      SD_CPP_SLEEP_IDLE_SECONDS: '${SD_CPP_SLEEP_IDLE_SECONDS}'
      SD_SERVER_URL: 'http://stable-diffusion-cpp:8000'
    volumes:
      - './stable-diffusion-cpp/manager.js:/app/main.js:ro'
      - './stable-diffusion-cpp/models.js:/app/models.js:ro'
    expose:
      - 8000
    networks:
      - ai
    healthcheck:
      test:
        [
          'CMD',
          'node',
          '-e',
          "fetch('http://localhost:8000/health').then((r)=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
        ]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    depends_on:
      - stable-diffusion-cpp

  # -- stable-diffusion.cpp metrics exporter ----------------------------------
  sd-metrics-exporter:
    container_name: sd-metrics-exporter
    image: node:26-trixie-slim
    restart: unless-stopped
    command: ['node', '/app/main.js']
    environment:
      CACHE_TTL_SECONDS: 5
      LOG_LEVEL: info
      MANAGER_URL: 'http://sd-manager:8000'
      PORT: 9090
      SD_SERVER_URL: 'http://stable-diffusion-cpp:8000'
      UPSTREAM_TIMEOUT_SECONDS: 4
    volumes:
      - './stable-diffusion-cpp/metrics-exporter.js:/app/main.js:ro'
      - './stable-diffusion-cpp/models.js:/app/models.js:ro'
    expose:
      - 9090
    networks:
      - ai
    healthcheck:
      test:
        [
          'CMD',
          'node',
          '-e',
          "fetch('http://localhost:9090/health').then((r)=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
        ]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    depends_on:
      - sd-manager

  # -- Open WebUI (chat interface) --------------------------------------------
  open-webui:
    container_name: open-webui
    image: ghcr.io/open-webui/open-webui:v0.10.2
    restart: unless-stopped
    environment:
      DEFAULT_MODELS: 'Qwen3.8-27B'
      ENABLE_EVALUATION_ARENA_MODELS: false
      ENABLE_IMAGE_GENERATION: true
      ENABLE_MODELS_CACHE: true
      ENABLE_OLLAMA_API: false
      ENABLE_PERSISTENT_CONFIG: true
      ENABLE_WEB_SEARCH: true
      IMAGE_GENERATION_ENGINE: 'openai'
      IMAGE_GENERATION_MODEL: 'default'
      IMAGE_SIZE: '1024x1024'
      IMAGES_OPENAI_API_BASE_URL: 'http://sd-manager:8000/v1'
      IMAGES_OPENAI_API_KEY: 'stable-diffusion-cpp'
      MODELS_CACHE_TTL: 3600
      OPENAI_API_BASE_URL: 'http://llama-manager:8000/v1'
      OPENAI_API_KEY: 'llama-cpp'
      RAG_EMBEDDING_ENGINE: 'openai'
      RAG_EMBEDDING_MODEL: 'Qwen3-Embedding-0.6B'
      RAG_OPENAI_API_BASE_URL: 'http://llama-manager:8000/v1'
      RAG_OPENAI_API_KEY: 'llama-cpp'
      TASK_MODEL: 'Qwen3.5-2B'
      TASK_MODEL_EXTERNAL: 'Qwen3.5-2B'
      WEBUI_AUTH: false
    volumes:
      - 'open-webui-data:/app/backend/data'
    expose:
      - 8080
    networks:
      - ai
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:8080/api/health']
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    depends_on:
      - llama-manager
      - sd-manager

  # -- AMD GPU metrics exporter -----------------------------------------------
  amd-gpu-exporter:
    container_name: amd-gpu-exporter
    image: rocm/device-metrics-exporter:v1.5.1-beta.0
    restart: unless-stopped
    devices:
      - '/dev/kfd:/dev/kfd'
      - '/dev/dri:/dev/dri'
    group_add:
      - '${VIDEO_GID}'
      - '${RENDER_GID}'
    expose:
      - 5000
    networks:
      - ai

  # -- Host metrics (CPU, RAM, disk, network) ---------------------------------
  node-exporter:
    container_name: node-exporter
    image: prom/node-exporter:v1.12.1
    restart: unless-stopped
    # Root (uid 0) is required to read RAPL energy_uj (CPU package power, mode 0400 root) under
    # /sys/class/powercap. Owner read needs no capabilities, so all are dropped: root-owned host files stay
    # readable through /rootfs, but other users' private files and signalling non-root processes do not.
    user: root
    cap_drop:
      - ALL
    security_opt:
      - 'no-new-privileges:true'
    pid: host
    volumes:
      - '/proc:/host/proc:ro'
      - '/sys:/host/sys:ro'
      - '/:/rootfs:ro'
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.hwmon'
      - '--collector.thermal_zone'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    expose:
      - 9100
    networks:
      - ai

  # -- Prometheus -------------------------------------------------------------
  prometheus:
    container_name: prometheus
    image: prom/prometheus:v3.16.0
    restart: unless-stopped
    volumes:
      - 'prometheus-data:/prometheus'
      - './monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro'
      - './monitoring/prometheus.rules.yml:/etc/prometheus/prometheus.rules.yml:ro'
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=30d'
      - '--web.enable-lifecycle'
    expose:
      - 9090
    networks:
      - ai
    healthcheck:
      test: ['CMD-SHELL', 'wget -q -O- http://localhost:9090/-/healthy >/dev/null || exit 1']
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    depends_on:
      - node-exporter
      - amd-gpu-exporter
      - llama-metrics-exporter
      - sd-metrics-exporter

  # -- Grafana ----------------------------------------------------------------
  grafana:
    container_name: grafana
    image: grafana/grafana-oss:13.0.2
    restart: unless-stopped
    environment:
      GF_AUTH_ANONYMOUS_ENABLED: true
      GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
      GF_AUTH_DISABLE_LOGIN_FORM: true
      GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /var/lib/grafana/dashboards/cockpit.json
    volumes:
      - 'grafana-data:/var/lib/grafana'
      - './monitoring/grafana/provisioning:/etc/grafana/provisioning:ro'
      - './monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro'
    expose:
      - 3000
    networks:
      - ai
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:3000/api/health']
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    depends_on:
      - prometheus

  # -- Nginx proxy ------------------------------------------------------------
  proxy:
    container_name: proxy
    image: nginx:1.31.8-trixie
    restart: unless-stopped
    # The only service that publishes ports, and the whole internet-facing
    # surface of the stack. Each port is published twice: on loopback (SSH
    # tunnels, host-local curl) and on this node's Tailscale address.
    #
    # Docker's published-port DNAT rules match the bound address, so an
    # address-scoped publish is what keeps these services off the public
    # internet. A firewall rule cannot do it: DNAT'd packets are forwarded to
    # the container and never traverse the INPUT chain UFW manages
    # (moby/moby#17496).
    #
    # BIND_ADDR is mandatory on purpose. Unset, the stack refuses to start
    # (loud, fixable) instead of falling back to 0.0.0.0 (silent exposure).
    ports:
      - '127.0.0.1:3000:3000'
      - '${BIND_ADDR:?run "sudo ./bin/panther-minor setup env" after "sudo tailscale up" to set BIND_ADDR}:3000:3000'
      - '127.0.0.1:4200:4200'
      - '${BIND_ADDR}:4200:4200'
      - '127.0.0.1:8000:8000'
      - '${BIND_ADDR}:8000:8000'
      - '127.0.0.1:8001:8001'
      - '${BIND_ADDR}:8001:8001'
      - '127.0.0.1:8080:8080'
      - '${BIND_ADDR}:8080:8080'
      - '127.0.0.1:9090:9090'
      - '${BIND_ADDR}:9090:9090'
    volumes:
      - 'proxy-logs:/var/log/nginx'
      - './proxy/nginx.conf:/etc/nginx/nginx.conf:ro'
      - './proxy/ssl:/etc/nginx/ssl:ro'
    networks:
      - ai
    depends_on:
      - llama-manager
      - sd-manager
      - open-webui
      - grafana
      - prometheus