Ctrl K
ComunidadGuías…

docker-compose.yml

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

Archivo original ↗Preguntar a KiBot
volumes:
  appsmith_data:
  caddy-config:
  caddy-data:
  comfyui_data:
  docling_cache:
  flowise:
  grafana:
  keelflow_data:
  langfuse_clickhouse_data:
  langfuse_clickhouse_logs:
  langfuse_minio_data:
  postgres_data:
    name: localai_langfuse_postgres_data
  letta_data:
  libretranslate_api_keys:
  libretranslate_models:
  lightrag_data:
  lightrag_inputs:
  n8n_sandbox_api_data:
  n8n_sandbox_tls:
  n8n_storage:
  nocodb_data:
  ollama_storage:
  open-webui:
  open_terminal_home:
  openclaw_data:
  openclaw_ssh:
  paddle_cache:
  paddleocr_cache:
  paddlex_data:
  portainer_data:
  databasus_data:
  postiz-config:
  postiz-uploads:
  prometheus_data:
  qdrant_storage:
  ragflow_elasticsearch_data:
  ragflow_minio_data:
  ragflow_mysql_data:
  ragflow_redis_data:
  temporal_elasticsearch_data:
  valkey-data:
  uptime_kuma_data:
  weaviate_data:

# Private link between Caddy and the OpenClaw gateway. Only caddy, openclaw and
# openclaw-init join it: OpenClaw trusts the identity header only from this
# subnet (openclaw/init.sh), so containers on the default network cannot
# impersonate the user. The host holds the subnet's gateway address and is
# trusted too, so any host process could - accepted for single-admin servers,
# see openclaw/init.sh.
networks:
  openclaw-proxy:

# Shared logging configuration for services
x-logging: &default-logging
  driver: "json-file"
  options:
    max-size: "1m"
    max-file: "1"

# Shared proxy configuration for services that need outbound proxy support
x-proxy-env: &proxy-env
  HTTP_PROXY: "${GOST_PROXY_URL:-}"
  HTTPS_PROXY: "${GOST_PROXY_URL:-}"
  http_proxy: "${GOST_PROXY_URL:-}"
  https_proxy: "${GOST_PROXY_URL:-}"
  NO_PROXY: "${GOST_NO_PROXY:-}"
  no_proxy: "${GOST_NO_PROXY:-}"

x-n8n: &service-n8n
  build:
    context: ./n8n
    dockerfile: Dockerfile.n8n
    pull: true
  environment: &service-n8n-env
    <<: *proxy-env
    DB_POSTGRESDB_DATABASE: postgres
    DB_POSTGRESDB_HOST: postgres
    DB_POSTGRESDB_PASSWORD: "${POSTGRES_PASSWORD}"
    DB_POSTGRESDB_USER: postgres
    DB_TYPE: postgresdb
    EXECUTIONS_MODE: "${EXECUTIONS_MODE:-queue}"
    GENERIC_TIMEZONE: "${GENERIC_TIMEZONE:-America/New_York}"
    LANGCHAIN_API_KEY: "${LANGCHAIN_API_KEY}"
    LANGCHAIN_ENDPOINT: "${LANGCHAIN_ENDPOINT}"
    LANGCHAIN_TRACING_V2: "${LANGCHAIN_TRACING_V2}"
    N8N_BINARY_DATA_MODE: database
    N8N_BLOCK_ENV_ACCESS_IN_NODE: false
    N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES: "${N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES:-true}"
    N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE: true
    N8N_DIAGNOSTICS_ENABLED: false
    N8N_EMAIL_MODE: "${N8N_EMAIL_MODE:-smtp}"
    # Empty = n8n defaults (instance-ai is on by default). Set "instance-ai,agents"
    # for the Agents preview. An unknown module name stops n8n from booting.
    N8N_ENABLED_MODULES: "${N8N_ENABLED_MODULES:-}"
    N8N_ENCRYPTION_KEY: "${N8N_ENCRYPTION_KEY}"
    N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: true
    # n8n Assistant sandbox (n8n-sandbox profile). ENABLED is written by
    # 05_configure_services.sh; the URL and key point at the sandbox-api service.
    N8N_INSTANCE_AI_SANDBOX_ENABLED: "${N8N_INSTANCE_AI_SANDBOX_ENABLED:-false}"
    N8N_INSTANCE_AI_SEARXNG_URL: "${N8N_INSTANCE_AI_SEARXNG_URL:-}"
    N8N_LOG_LEVEL: "${N8N_LOG_LEVEL:-info}"
    N8N_LOG_OUTPUT: "${N8N_LOG_OUTPUT:-console}"
    N8N_METRICS: true
    N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS: true
    N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL: true
    N8N_METRICS_INCLUDE_WORKFLOW_NAME_LABEL: true
    N8N_METRICS_INCLUDE_WORKFLOW_INFO: true
    N8N_PAYLOAD_SIZE_MAX: "${N8N_PAYLOAD_SIZE_MAX:-256}"
    N8N_PERSONALIZATION_ENABLED: false
    N8N_PROXY_HOPS: "${N8N_PROXY_HOPS:-1}"
    N8N_RESTRICT_FILE_ACCESS_TO: /data/shared
    N8N_RUNNERS_AUTH_TOKEN: "${N8N_RUNNERS_AUTH_TOKEN}"
    N8N_RUNNERS_BROKER_LISTEN_ADDRESS: 0.0.0.0
    N8N_RUNNERS_ENABLED: true
    N8N_RUNNERS_MODE: external
    N8N_RUNNERS_TASK_REQUEST_TIMEOUT: "${N8N_RUNNERS_TASK_REQUEST_TIMEOUT:-300}"
    N8N_SANDBOX_SERVICE_API_KEY: "${N8N_SANDBOX_API_KEY:-}"
    N8N_SANDBOX_SERVICE_URL: "${N8N_SANDBOX_SERVICE_URL:-http://sandbox-api:8080}"
    OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: "${OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS:-true}"
    N8N_SMTP_HOST: "${N8N_SMTP_HOST:-}"
    N8N_SMTP_OAUTH_PRIVATE_KEY: "${N8N_SMTP_OAUTH_PRIVATE_KEY:-}"
    N8N_SMTP_OAUTH_SERVICE_CLIENT: "${N8N_SMTP_OAUTH_SERVICE_CLIENT:-}"
    N8N_SMTP_PASS: "${N8N_SMTP_PASS:-}"
    N8N_SMTP_PORT: "${N8N_SMTP_PORT:-}"
    N8N_SMTP_SENDER: "${N8N_SMTP_SENDER:-}"
    N8N_SMTP_SSL: "${N8N_SMTP_SSL:-true}"
    N8N_SMTP_STARTTLS: "${N8N_SMTP_STARTTLS:-true}"
    N8N_SMTP_USER: "${N8N_SMTP_USER:-}"
    N8N_USER_MANAGEMENT_JWT_SECRET: "${N8N_USER_MANAGEMENT_JWT_SECRET}"
    NODE_ENV: production
    NODES_EXCLUDE: "${NODES_EXCLUDE:-[]}"
    QUEUE_BULL_PREFIX: "${QUEUE_BULL_PREFIX:-n8n}"
    QUEUE_BULL_REDIS_HOST: "${REDIS_HOST:-redis}"
    QUEUE_BULL_REDIS_PORT: "${REDIS_PORT:-6379}"
    QUEUE_HEALTH_CHECK_ACTIVE: true
    WEBHOOK_URL: "${N8N_HOSTNAME:+https://}${N8N_HOSTNAME:-http://localhost:5678}/"

x-ollama: &service-ollama
  image: ollama/ollama:latest
  container_name: ollama
  restart: unless-stopped
  environment:
    OLLAMA_CONTEXT_LENGTH: "${OLLAMA_CONTEXT_LENGTH:-8192}"
    OLLAMA_FLASH_ATTENTION: 1
    OLLAMA_GPU_OVERHEAD: "${OLLAMA_GPU_OVERHEAD:-0}"
    OLLAMA_KEEP_ALIVE: "${OLLAMA_KEEP_ALIVE:-}"
    OLLAMA_KV_CACHE_TYPE: "${OLLAMA_KV_CACHE_TYPE:-q8_0}"
    OLLAMA_MAX_LOADED_MODELS: "${OLLAMA_MAX_LOADED_MODELS:-2}"
    OLLAMA_NUM_PARALLEL: "${OLLAMA_NUM_PARALLEL:-}"
    OLLAMA_SCHED_SPREAD: "${OLLAMA_SCHED_SPREAD:-}"
  # Optional extra environment for every instance (llama.cpp LLAMA_ARG_*,
  # ROCm HSA_OVERRIDE_GFX_VERSION, ...). Kept out of 'environment:' on purpose:
  # an interpolated "${VAR:-}" would hand llama-server an EMPTY value, and an
  # empty LLAMA_ARG_FIT_TARGET makes it exit at startup. A missing file adds
  # nothing. Keys set in 'environment:' above always win over this file.
  # Per-instance files (ollama2.env, ...) are added by generate_ollama_instances.sh.
  env_file:
    - path: ./ollama.env
      required: false
  volumes:
    - ollama_storage:/root/.ollama

x-init-ollama: &init-ollama
  image: ollama/ollama:latest
  container_name: ollama-pull-llama
  volumes:
    - ollama_storage:/root/.ollama
  entrypoint: /bin/sh
  command:
    - "-c"
    - "sleep 3; OLLAMA_HOST=ollama:11434 ollama pull qwen2.5:7b-instruct-q4_K_M; OLLAMA_HOST=ollama:11434 ollama pull nomic-embed-text"

# InvokeAI shared config (hardware variants below, mutually exclusive profiles like Ollama)
x-invokeai: &service-invokeai
  container_name: invokeai
  restart: unless-stopped
  logging: *default-logging
  environment: &invokeai-env
    <<: *proxy-env
  volumes:
    - ./invokeai:/invokeai
  healthcheck:
    test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:9090/api/v1/app/version', timeout=5)\" || exit 1"]
    interval: 30s
    timeout: 10s
    retries: 5
    start_period: 90s

# ComfyUI shared config (hardware variants below, mutually exclusive profiles like InvokeAI).
# The volume MUST be /root: the image runs as root, its entrypoint copies the
# bundled ComfyUI to /root/ComfyUI on first start and models, custom nodes and
# user data live under it. (/home/runner, the old mount point, is the non-root
# layout of the archived cu121 image; every tag this stack has used runs as
# root, so nothing was ever written there.) Consequence: the ComfyUI code itself lives
# in the volume and is updated through ComfyUI-Manager, not by pulling the image.
x-comfyui: &service-comfyui
  container_name: comfyui
  restart: unless-stopped
  logging: *default-logging
  volumes:
    - comfyui_data:/root
  healthcheck:
    test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8188', timeout=5)"]
    interval: 10s
    timeout: 5s
    retries: 5
    start_period: 60s

# Worker-runner anchor for sidecar pattern (runner connects to worker via localhost)
x-n8n-worker-runner: &service-n8n-worker-runner
  build:
    context: ./n8n
    dockerfile: Dockerfile.runner
    pull: true
  environment:
    <<: *proxy-env
    GENERIC_TIMEZONE: "${GENERIC_TIMEZONE:-America/New_York}"
    N8N_RUNNERS_AUTH_TOKEN: "${N8N_RUNNERS_AUTH_TOKEN}"
    N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT: "${N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT:-0}"
    N8N_RUNNERS_MAX_CONCURRENCY: "${N8N_RUNNERS_MAX_CONCURRENCY:-10}"
    N8N_RUNNERS_TASK_BROKER_URI: http://127.0.0.1:5679

services:
  appsmith:
    image: appsmith/appsmith-ce:release
    container_name: appsmith
    profiles: ["appsmith"]
    restart: unless-stopped
    logging: *default-logging
    environment:
      <<: *proxy-env
      APPSMITH_ENCRYPTION_PASSWORD: "${APPSMITH_ENCRYPTION_PASSWORD}"
      APPSMITH_ENCRYPTION_SALT: "${APPSMITH_ENCRYPTION_SALT}"
      APPSMITH_DISABLE_TELEMETRY: "true"
    volumes:
      - appsmith_data:/appsmith-stacks
    healthcheck:
      test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= curl -fsS http://localhost/api/v1/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 120s

  flowise:
    # Pinned: 3.1.4 crashes at startup with SQLite ("this.db.exec is not a
    # function") and runs as 'node', ignoring the /root/.flowise mount.
    # Keep in sync with scripts/update_preview.sh.
    image: flowiseai/flowise:3.1.3
    restart: unless-stopped
    container_name: flowise
    profiles: ["flowise"]
    environment:
      <<: *proxy-env
      PORT: 3001
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - ~/.flowise:/root/.flowise
    entrypoint: /bin/sh -c "sleep 3; flowise start"

  keelflow:
    image: ghcr.io/perruer/keelflow:latest
    restart: unless-stopped
    container_name: keelflow
    profiles: ["keelflow"]
    logging: *default-logging
    environment:
      <<: *proxy-env
      KEELFLOW_HOME: /home/node/.keelflow
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - keelflow_data:/home/node/.keelflow
    healthcheck:
      test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= curl -fsS http://localhost:3000/api/v1/ping || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s

  # ---------------------------------------------------------------------------
  # Open Terminal (profile "open-terminal") - execution environment for Open WebUI
  # agents: shell, filesystem, runtime package installs. Internal only: no ports,
  # no Caddy block. Open WebUI reaches it as http://open-terminal:8000; the admin
  # adds it under Admin Settings > Integrations > Open Terminal, so the key stays
  # server-side. Only the full image ("latest" or a release tag) supports
  # multi-user and runtime installs; the slim/alpine/openshift variants do
  # neither, and 05_configure_services.sh refuses them in multi-user mode.
  # Multi-user creates a plain Linux
  # account (no sudo) per Open WebUI user id, hence the volume is /home, not
  # /home/user; with multi-user off the chat gets the image's sudo-capable user.
  # Never mount docker.sock here: the entrypoint would hand the agent's shell the
  # host Docker daemon. OPEN_TERMINAL_ALLOWED_DOMAINS is deliberately absent: the
  # entrypoint reads a set-but-empty value as "block all egress" and needs
  # NET_ADMIN for iptables, so an interpolated default would crash the container.
  # Add it together with cap_add NET_ADMIN in docker-compose.override.yml if wanted.
  # ---------------------------------------------------------------------------
  open-terminal:
    image: "ghcr.io/open-webui/open-terminal:${OPEN_TERMINAL_VERSION:-latest}"
    container_name: open-terminal
    profiles: ["open-terminal"]
    restart: unless-stopped
    logging: *default-logging
    environment:
      <<: *proxy-env
      OPEN_TERMINAL_API_KEY: "${OPEN_TERMINAL_API_KEY}"
      OPEN_TERMINAL_MULTI_USER: "${OPEN_TERMINAL_MULTI_USER:-true}"
      # Reinstalled on every container start. In multi-user mode this is the only
      # way to add apt packages (per-user accounts have no sudo).
      OPEN_TERMINAL_PACKAGES: "${OPEN_TERMINAL_PACKAGES:-}"
      OPEN_TERMINAL_PIP_PACKAGES: "${OPEN_TERMINAL_PIP_PACKAGES:-}"
      OPEN_TERMINAL_NPM_PACKAGES: "${OPEN_TERMINAL_NPM_PACKAGES:-}"
    volumes:
      - open_terminal_home:/home
    # Safety valve against a runaway build or a stuck port proxy, not a quota.
    deploy:
      resources:
        limits:
          cpus: "${OPEN_TERMINAL_CPU_LIMIT:-2.0}"
          memory: "${OPEN_TERMINAL_MEMORY_LIMIT:-2G}"
    healthcheck:
      test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= curl -fs http://localhost:8000/docs || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s

  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    restart: unless-stopped
    container_name: open-webui
    profiles: ["open-webui"]
    environment:
      <<: *proxy-env
      OLLAMA_BASE_URL: http://ollama:11434
      # Without it the key lives in the container layer and changes on every recreate
      WEBUI_SECRET_KEY: "${WEBUI_SECRET_KEY:-}"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - open-webui:/app/backend/data
    healthcheck:
      test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= curl -fs http://localhost:8080/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 120s

  # ---------------------------------------------------------------------------
  # OpenClaw (profile "openclaw") - personal AI agent with the Control UI and a
  # Telegram bot. The dashboard sits behind Caddy basic auth only: the gateway runs in
  # trusted-proxy mode, Caddy passes the basic-auth user in X-Forwarded-User over
  # the private openclaw-proxy network, and browsers are auto-approved, so there
  # is no second password and no device pairing screen. openclaw-init (see
  # openclaw/init.sh) copies the SSH key and enforces the gateway keys on every
  # 'compose up'. OPENCLAW_GATEWAY_PASSWORD is only for the local CLI
  # ('make openclaw a="..."') and init.sh's password-login fallback.
  # FULL HOST ACCESS BY DESIGN: docker.sock (root-equivalent) plus an SSH key
  # that 05_configure_services.sh authorizes for the installing user
  # ('ssh host' inside the container). Anyone who passes the OpenClaw basic
  # auth or is approved in Telegram controls the server; keep the Telegram DM
  # policy on pairing or an allowlist.
  # ---------------------------------------------------------------------------
  openclaw-init:
    build: &openclaw-build
      context: ./openclaw
      pull: true
      args:
        OPENCLAW_VERSION: "${OPENCLAW_VERSION:-latest}"
    image: localai-openclaw
    container_name: openclaw-init
    profiles: ["openclaw"]
    restart: "no"
    logging: *default-logging
    user: "0:0"
    entrypoint: ["sh", "/openclaw-init.sh"]
    # Same values as the gateway: 'config set' validates the whole config,
    # including the ${OPENCLAW_HOSTNAME} / ${OPENCLAW_USERNAME} references and,
    # on the password-login fallback, the gateway password.
    # TELEGRAM_BOT_TOKEN decides whether init.sh switches the channel on.
    # OPENROUTER_API_KEY is gateway-only: init.sh writes no reference to it into
    # openclaw.json (a hand-added ${OPENROUTER_API_KEY} reference would need it here too).
    environment:
      OPENCLAW_GATEWAY_PASSWORD: "${OPENCLAW_GATEWAY_PASSWORD}"
      OPENCLAW_HOSTNAME: "${OPENCLAW_HOSTNAME:-openclaw.localhost}"
      OPENCLAW_USERNAME: "${OPENCLAW_USERNAME}"
      TELEGRAM_BOT_TOKEN: "${OPENCLAW_TELEGRAM_BOT_TOKEN:-}"
      # Marks this container as attached to openclaw-proxy only; init.sh refuses
      # to trust its own subnet without it (a stale pre-1.14.1 container).
      OPENCLAW_INIT_PROXY_NETWORK: "true"
    # Only the proxy network: init.sh reads that network's subnet from its own
    # interface and makes it the gateway's only trusted proxy range.
    networks:
      - openclaw-proxy
    volumes:
      - ./openclaw/init.sh:/openclaw-init.sh:ro
      - ./openclaw/ssh:/ssh-src:ro
      - openclaw_data:/home/node/.openclaw
      - openclaw_ssh:/home/node/.ssh

  openclaw:
    build: *openclaw-build
    image: localai-openclaw
    container_name: openclaw
    profiles: ["openclaw"]
    restart: unless-stopped
    logging: *default-logging
    init: true
    command: ["node", "dist/index.js", "gateway", "--bind", "lan", "--port", "18789"]
    environment:
      <<: *proxy-env
      OPENCLAW_GATEWAY_PASSWORD: "${OPENCLAW_GATEWAY_PASSWORD}"
      OPENCLAW_HOSTNAME: "${OPENCLAW_HOSTNAME:-openclaw.localhost}"
      OPENCLAW_USERNAME: "${OPENCLAW_USERNAME}"
      OPENROUTER_API_KEY: "${OPENCLAW_OPENROUTER_API_KEY:-}"
      TELEGRAM_BOT_TOKEN: "${OPENCLAW_TELEGRAM_BOT_TOKEN:-}"
      TZ: "${GENERIC_TIMEZONE:-UTC}"
    # Caddy reaches the gateway as openclaw-gateway, an alias that exists only on
    # openclaw-proxy, so its requests always come from the trusted subnet.
    networks:
      default:
      openclaw-proxy:
        aliases:
          - openclaw-gateway
    # GID of /var/run/docker.sock, written by 05_configure_services.sh
    group_add:
      - "${OPENCLAW_DOCKER_GID}"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - openclaw_data:/home/node/.openclaw
      - openclaw_ssh:/home/node/.ssh:ro
      - /var/run/docker.sock:/var/run/docker.sock
    healthcheck:
      test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= node dist/docker-healthcheck.js || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    depends_on:
      openclaw-init:
        condition: service_completed_successfully

  n8n-import:
    <<: *service-n8n
    container_name: n8n-import
    profiles: ["n8n"]
    environment:
      <<: *service-n8n-env
      RUN_N8N_IMPORT: "${RUN_N8N_IMPORT:-false}"
    entrypoint: /bin/sh
    command: /scripts/import_workflows.sh
    volumes:
      - ./n8n/backup:/backup
      - ./scripts/import_workflows.sh:/scripts/import_workflows.sh:ro
    depends_on:
      postgres:
        condition: service_healthy

  n8n:
    <<: *service-n8n
    container_name: n8n
    profiles: ["n8n"]
    restart: unless-stopped
    volumes:
      - n8n_storage:/home/node/.n8n
      - ./n8n/backup:/backup
      - ./shared:/data/shared
    healthcheck:
      test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -qO- http://localhost:5678/healthz || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    depends_on:
      n8n-import:
        condition: service_completed_successfully

  # Template services for worker-runner pairs (used by docker-compose.n8n-workers.yml via extends)
  # These templates use profile "n8n-template" which is never activated directly
  n8n-worker-template:
    <<: *service-n8n
    profiles: ["n8n-template"]
    command: worker
    volumes:
      - n8n_storage:/home/node/.n8n
      - ./shared:/data/shared
    healthcheck:
      test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -qO- http://localhost:5678/healthz || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s

  n8n-runner-template:
    <<: *service-n8n-worker-runner
    profiles: ["n8n-template"]
    entrypoint: ["/bin/sh", "-c", "/usr/local/bin/task-runner-launcher javascript python"]
    healthcheck:
      test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -qO- http://localhost:5680/healthz || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s

  n8n-mcp:
    image: "${N8N_MCP_IMAGE:-ghcr.io/czlonkowski/n8n-mcp:latest}"
    container_name: n8n-mcp
    profiles: ["n8n-mcp"]
    restart: unless-stopped
    logging: *default-logging
    environment:
      <<: *proxy-env
      # The image defaults MCP_MODE to "stdio"; "http" is required to serve HTTP.
      # Do NOT set USE_FIXED_HTTP - upstream deprecated it and it breaks streaming.
      MCP_MODE: http
      NODE_ENV: production
      PORT: "3000"
      LOG_LEVEL: "${N8N_MCP_LOG_LEVEL:-info}"
      # Mandatory: the entrypoint exits 1 in http mode without a token.
      # Caddy gates on this same token, so one Bearer header satisfies both.
      AUTH_TOKEN: "${N8N_MCP_AUTH_TOKEN}"
      # Real client IPs behind Caddy. Without it the failed-auth rate limit is
      # shared by every client via Caddy's single container IP.
      TRUST_PROXY: "1"
      # What the server advertises as its own address. ':+' prepends the scheme
      # only when a hostname is set, so an unset N8N_MCP_HOSTNAME falls back to
      # the whole localhost URL rather than to a bare "https://".
      BASE_URL: "${N8N_MCP_HOSTNAME:+https://}${N8N_MCP_HOSTNAME:-http://localhost:3000}"
      # Anonymous telemetry is ENABLED by default upstream.
      N8N_MCP_TELEMETRY_DISABLED: "${N8N_MCP_TELEMETRY_DISABLED:-true}"
      # Blank N8N_API_KEY = documentation-only mode (node docs, schemas,
      # templates). Paste an n8n API key to unlock the workflow-management
      # tools. An empty value fails upstream's validation and is treated as
      # unconfigured, so the service still starts cleanly.
      N8N_API_URL: "${N8N_API_URL:-http://n8n:5678}"
      N8N_API_KEY: "${N8N_API_KEY:-}"
      # Optional. The access token of n8n's own MCP server (Settings ->
      # Instance-level MCP, n8n >= 2.34) - a separate secret from the Public
      # API key. It unlocks tools the Public API cannot serve (agents, dynamic
      # node resources, workflow version history, datatable columns). The
      # endpoint is derived from N8N_API_URL. Blank = Public API only, as before.
      N8N_MCP_ACCESS_TOKEN: "${N8N_MCP_ACCESS_TOKEN:-}"
      # n8n resolves to a private Docker IP, and n8n-MCP's own SSRF guard
      # rejects private ranges in both "strict" (upstream default) and
      # "moderate", which would fail every n8n API call. "permissive" still
      # blocks cloud-metadata endpoints.
      WEBHOOK_SECURITY_MODE: "${N8N_MCP_WEBHOOK_SECURITY_MODE:-permissive}"
    # Deliberately no volume: the node database is baked into the image and
    # re-seeded only when absent, so a named volume would mask the image copy
    # and freeze the node catalogue after every image update. The only state
    # here is a regenerable, read-mostly SQLite index.
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= curl -fsS http://127.0.0.1:3000/health > /dev/null || exit 1",
        ]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s

  # ---------------------------------------------------------------------------
  # n8n Assistant sandbox (profile "n8n-sandbox") - upstream n8n-sandbox-service.
  # Runs the code the AI Assistant / Agents generate. Three services, no ports,
  # no Caddy block: n8n reaches sandbox-api by service name. The mTLS certs are
  # issued for the names "sandbox-api" and "sandbox-runner-1" - keep them.
  # The three images are released together; N8N_SANDBOX_VERSION selects one tag for all.
  # ---------------------------------------------------------------------------
  # One-shot: generates the CA and leaf certs into the shared volume, then exits.
  # Skips generation when the certs already exist, so it is safe on every start
  # (set SANDBOX_TLS_REGEN=1 on this service to force new certs). Keys stay 0600:
  # /tls/api is chowned to the api image's user, /tls/runner stays root's and the
  # runner runs as root (privileged, or ID-mapped root under sysbox-runc).
  sandbox-certs:
    image: "ghcr.io/n8n-io/n8n-sandbox-service-api:${N8N_SANDBOX_VERSION:-latest}"
    container_name: sandbox-certs
    profiles: ["n8n-sandbox"]
    restart: "no"
    logging: *default-logging
    user: "0:0"
    entrypoint: ["sh", "-c"]
    command:
      - >
        bootstrap-mtls.sh --out-dir /tls --api-san sandbox-api
        --control-san-prefix sandbox-runner &&
        chown -R sandbox-api:sandbox-api /tls/api
    environment:
      NUM_RUNNERS: "1"
    volumes:
      - n8n_sandbox_tls:/tls

  sandbox-api:
    image: "ghcr.io/n8n-io/n8n-sandbox-service-api:${N8N_SANDBOX_VERSION:-latest}"
    container_name: sandbox-api
    profiles: ["n8n-sandbox"]
    restart: unless-stopped
    logging: *default-logging
    environment:
      # n8n authenticates with N8N_SANDBOX_SERVICE_API_KEY, which must be one of these.
      SANDBOX_API_KEYS: "${N8N_SANDBOX_API_KEY}"
      SANDBOX_API_RUNNER_REGISTRATION_TOKEN: "${N8N_SANDBOX_RUNNER_REGISTRATION_TOKEN}"
      SANDBOX_API_RUNNER_API_KEY: "${N8N_SANDBOX_RUNNER_API_KEY}"
      SANDBOX_API_GRPC_TLS_CERT_FILE: /tls/api/grpc-server.crt
      SANDBOX_API_GRPC_TLS_KEY_FILE: /tls/api/grpc-server.key
      SANDBOX_API_GRPC_TLS_CLIENT_CA_FILE: /tls/api/ca.crt
      SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CA_FILE: /tls/api/ca.crt
      SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/api/control-grpc-api-client.crt
      SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/api/control-grpc-api-client.key
      SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_SERVER_NAME: sandbox-runner-1
    volumes:
      - n8n_sandbox_tls:/tls:ro
      - n8n_sandbox_api_data:/var/lib/n8n-sandbox-api
    healthcheck:
      test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -qO- http://localhost:8080/healthz || exit 1"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
    depends_on:
      sandbox-certs:
        condition: service_completed_successfully

  # Docker-in-Docker host for the sandboxes. Isolated with sysbox-runc when
  # 05_configure_services.sh could install it (N8N_SANDBOX_RUNNER_RUNTIME); with
  # the privileged fallback accepted there it runs privileged, which is
  # root-equivalent on the host. The shipped runc + false pair cannot run
  # Docker-in-Docker at all ('make doctor' flags it). Never publish its ports.
  # Deliberately no volume for the inner Docker: the sandbox image is
  # pulled on first use after each recreate, so it always matches the runner's tag
  # instead of a cached copy going stale behind it.
  sandbox-runner-1:
    image: "ghcr.io/n8n-io/n8n-sandbox-service-runner-dind:${N8N_SANDBOX_VERSION:-latest}"
    container_name: sandbox-runner-1
    profiles: ["n8n-sandbox"]
    restart: unless-stopped
    logging: *default-logging
    runtime: "${N8N_SANDBOX_RUNNER_RUNTIME:-runc}"
    privileged: "${N8N_SANDBOX_RUNNER_PRIVILEGED:-false}"
    environment:
      SANDBOX_RUNNER_REGISTRATION_TOKEN: "${N8N_SANDBOX_RUNNER_REGISTRATION_TOKEN}"
      SANDBOX_RUNNER_API_KEYS: "${N8N_SANDBOX_RUNNER_API_KEY}"
      SANDBOX_RUNNER_API_GRPC_ADDR: sandbox-api:9090
      SANDBOX_RUNNER_HTTP_BASE_URL: https://sandbox-runner-1:8080
      SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR: ":9091"
      SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR: sandbox-runner-1:9091
      SANDBOX_RUNNER_ID: runner-1
      SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE: "ghcr.io/n8n-io/n8n-sandbox-service-sandbox:${N8N_SANDBOX_VERSION:-latest}"
      SANDBOX_RUNNER_REGISTRATION_GRPC_CA_FILE: /tls/runner/ca.crt
      SANDBOX_RUNNER_REGISTRATION_GRPC_CERT_FILE: /tls/runner/grpc-client.crt
      SANDBOX_RUNNER_REGISTRATION_GRPC_KEY_FILE: /tls/runner/grpc-client.key
      SANDBOX_RUNNER_REGISTRATION_GRPC_SERVER_NAME: sandbox-api
      SANDBOX_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/runner/control-grpc-server.crt
      SANDBOX_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/runner/control-grpc-server.key
      SANDBOX_RUNNER_CONTROL_GRPC_TLS_CLIENT_CA_FILE: /tls/runner/ca.crt
    volumes:
      - n8n_sandbox_tls:/tls:ro
    # The runner listener is TLS-only, hence https and --no-check-certificate.
    healthcheck:
      test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -q -O /dev/null --no-check-certificate https://localhost:8080/readyz || exit 1"]
      interval: 5s
      timeout: 5s
      retries: 24
      start_period: 20s
    depends_on:
      sandbox-api:
        condition: service_healthy

  qdrant:
    image: qdrant/qdrant
    container_name: qdrant
    profiles: ["qdrant"]
    restart: unless-stopped
    volumes:
      - qdrant_storage:/qdrant/storage
    environment:
      QDRANT__SERVICE__API_KEY: "${QDRANT_API_KEY}"
    expose:
      - "6333"

  neo4j:
    image: neo4j:latest
    container_name: neo4j
    profiles: ["neo4j"]
    restart: unless-stopped
    volumes:
      - ./neo4j/logs:/logs
      - ./neo4j/config:/config
      - ./neo4j/data:/data
      - ./neo4j/plugins:/plugins
    environment:
      - "NEO4J_AUTH=${NEO4J_AUTH_USERNAME}/${NEO4J_AUTH_PASSWORD}"
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "wget --no-verbose --tries=1 --spider http://localhost:7474 || exit 1",
        ]
      interval: 5s
      timeout: 3s
      retries: 5
    ulimits:
      nofile:
        soft: 40000
        hard: 40000

  nocodb:
    image: nocodb/nocodb:latest
    container_name: nocodb
    profiles: ["nocodb"]
    restart: unless-stopped
    logging: *default-logging
    environment:
      # NocoDB rejects external data sources whose host resolves to a private
      # range (SSRF protection, GHSA-w43h-r5m5-p832), which covers every
      # hostname on this Docker network - including "postgres". The check first
      # appears in helpers/validateDbConnectionHost.ts at tag 2026.05.2; the
      # advisory metadata says 2026.05.1, and the two genuinely disagree.
      # This opt-in is deliberately the narrow one: it re-allows internal
      # databases only. Webhooks stay gated by NC_ALLOW_LOCAL_HOOKS, and on
      # 2026.07.0+ data import by NC_ALLOW_LOCAL_DATA_IMPORT (before that
      # release, import was gated by NC_ALLOW_LOCAL_HOOKS too). Never swap this
      # for NC_DISABLE_SSRF_PROTECTION, which turns all of them off.
      # Compared with === 'true', so it must be the lowercase string.
      NC_ALLOW_LOCAL_EXTERNAL_DBS: "true"
      NC_AUTH_JWT_SECRET: "${NOCODB_JWT_SECRET}"
      NC_DB: "pg://postgres:5432?u=postgres&p=${POSTGRES_PASSWORD}&d=nocodb"
      NC_PUBLIC_URL: "https://${NOCODB_HOSTNAME}"
      NC_REDIS_URL: redis://redis:6379/1
    volumes:
      - nocodb_data:/usr/app/data
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:8080/api/v1/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  caddy:
    container_name: caddy
    image: docker.io/library/caddy:2-alpine
    networks:
      - default
      - openclaw-proxy
    ports:
      - "80:80"
      - "443:443"
    # Bolt (7687) is published by docker-compose.neo4j.yml, only while the
    # neo4j profile is active (issue #134) - do not add it back here.
    restart: unless-stopped
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy-addon:/etc/caddy/addons:ro
      - ./certs:/etc/caddy/certs:ro
      - ./welcome:/srv/welcome:ro
      - caddy-data:/data:rw
      - caddy-config:/config:rw
    environment:
      APPSMITH_HOSTNAME: "${APPSMITH_HOSTNAME}"
      COMFYUI_HOSTNAME: "${COMFYUI_HOSTNAME}"
      COMFYUI_PASSWORD_HASH: "${COMFYUI_PASSWORD_HASH}"
      COMFYUI_USERNAME: "${COMFYUI_USERNAME}"
      DIFY_HOSTNAME: "${DIFY_HOSTNAME}"
      DOCLING_HOSTNAME: "${DOCLING_HOSTNAME}"
      DOCLING_PASSWORD_HASH: "${DOCLING_PASSWORD_HASH}"
      DOCLING_USERNAME: "${DOCLING_USERNAME}"
      FLOWISE_HOSTNAME: "${FLOWISE_HOSTNAME}"
      GRAFANA_HOSTNAME: "${GRAFANA_HOSTNAME}"
      INVOKEAI_HOSTNAME: "${INVOKEAI_HOSTNAME}"
      INVOKEAI_PASSWORD_HASH: "${INVOKEAI_PASSWORD_HASH}"
      INVOKEAI_USERNAME: "${INVOKEAI_USERNAME}"
      KEELFLOW_HOSTNAME: "${KEELFLOW_HOSTNAME}"
      LANGFUSE_HOSTNAME: "${LANGFUSE_HOSTNAME}"
      LETSENCRYPT_EMAIL: "${LETSENCRYPT_EMAIL:-internal}"
      LETTA_HOSTNAME: "${LETTA_HOSTNAME}"
      LIGHTRAG_HOSTNAME: "${LIGHTRAG_HOSTNAME}"
      LT_HOSTNAME: "${LT_HOSTNAME}"
      LT_PASSWORD_HASH: "${LT_PASSWORD_HASH}"
      LT_USERNAME: "${LT_USERNAME}"
      N8N_HOSTNAME: "${N8N_HOSTNAME}"
      N8N_MCP_AUTH_TOKEN: "${N8N_MCP_AUTH_TOKEN}"
      # Defaulted, unlike the other hostnames: 'git pull' + 'make restart' does
      # not add new .env variables, so an .env from before this service existed
      # would expand to an empty Caddy site address, which is a Caddyfile PARSE
      # error - Caddy would crash-loop and take HTTPS down for every service,
      # not just this one. '.localhost' gets an internal cert, so no ACME noise.
      N8N_MCP_HOSTNAME: "${N8N_MCP_HOSTNAME:-n8n-mcp.localhost}"
      NEO4J_HOSTNAME: "${NEO4J_HOSTNAME}"
      NOCODB_HOSTNAME: "${NOCODB_HOSTNAME}"
      OLLAMA_HOSTNAME: "${OLLAMA_HOSTNAME}"
      OLLAMA_CADDY_API_TOKEN: "${OLLAMA_CADDY_API_TOKEN}"
      # Defaulted for the same reason as N8N_MCP_HOSTNAME above.
      OPENCLAW_HOSTNAME: "${OPENCLAW_HOSTNAME:-openclaw.localhost}"
      OPENCLAW_PASSWORD_HASH: "${OPENCLAW_PASSWORD_HASH}"
      OPENCLAW_USERNAME: "${OPENCLAW_USERNAME}"
      PADDLEOCR_HOSTNAME: "${PADDLEOCR_HOSTNAME}"
      PADDLEOCR_PASSWORD_HASH: "${PADDLEOCR_PASSWORD_HASH}"
      PADDLEOCR_USERNAME: "${PADDLEOCR_USERNAME}"
      PORTAINER_HOSTNAME: "${PORTAINER_HOSTNAME}"
      DATABASUS_HOSTNAME: "${DATABASUS_HOSTNAME}"
      POSTIZ_HOSTNAME: "${POSTIZ_HOSTNAME}"
      TEMPORAL_UI_HOSTNAME: "${TEMPORAL_UI_HOSTNAME}"
      TEMPORAL_UI_USERNAME: "${TEMPORAL_UI_USERNAME}"
      TEMPORAL_UI_PASSWORD_HASH: "${TEMPORAL_UI_PASSWORD_HASH}"
      PROMETHEUS_HOSTNAME: "${PROMETHEUS_HOSTNAME}"
      PROMETHEUS_PASSWORD_HASH: "${PROMETHEUS_PASSWORD_HASH}"
      PROMETHEUS_USERNAME: "${PROMETHEUS_USERNAME}"
      QDRANT_HOSTNAME: "${QDRANT_HOSTNAME}"
      RAGAPP_HOSTNAME: "${RAGAPP_HOSTNAME}"
      RAGAPP_PASSWORD_HASH: "${RAGAPP_PASSWORD_HASH}"
      RAGAPP_USERNAME: "${RAGAPP_USERNAME}"
      RAGFLOW_HOSTNAME: "${RAGFLOW_HOSTNAME}"
      SEARXNG_HOSTNAME: "${SEARXNG_HOSTNAME}"
      SEARXNG_PASSWORD_HASH: "${SEARXNG_PASSWORD_HASH}"
      SEARXNG_USERNAME: "${SEARXNG_USERNAME}"
      SUPABASE_HOSTNAME: "${SUPABASE_HOSTNAME}"
      UPTIME_KUMA_HOSTNAME: "${UPTIME_KUMA_HOSTNAME}"
      WAHA_HOSTNAME: "${WAHA_HOSTNAME}"
      WEAVIATE_HOSTNAME: "${WEAVIATE_HOSTNAME}"
      WEBUI_HOSTNAME: "${WEBUI_HOSTNAME}"
      WELCOME_HOSTNAME: "${WELCOME_HOSTNAME}"
      WELCOME_PASSWORD_HASH: "${WELCOME_PASSWORD_HASH}"
      WELCOME_USERNAME: "${WELCOME_USERNAME}"
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    logging: *default-logging

  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    profiles: ["cloudflare-tunnel"]
    restart: unless-stopped
    command: "tunnel --no-autoupdate run --protocol ${CLOUDFLARE_TUNNEL_PROTOCOL:-auto}"
    environment:
      TUNNEL_TOKEN: "${CLOUDFLARE_TUNNEL_TOKEN}"
    logging: *default-logging

  gost:
    image: gogost/gost:latest
    container_name: gost
    profiles: ["gost"]
    restart: unless-stopped
    command:
      - "-L"
      - "http://${GOST_USERNAME}:${GOST_PASSWORD}@:8080"
      - "-F"
      - "${GOST_UPSTREAM_PROXY}"
    healthcheck:
      test: ["CMD-SHELL", "nc -z localhost 8080 || exit 1"]
      interval: 60s
      timeout: 10s
      retries: 3
      start_period: 10s
    logging: *default-logging

  langfuse-worker:
    image: langfuse/langfuse-worker:3
    container_name: langfuse-worker
    restart: always
    profiles: ["langfuse"]
    depends_on: &langfuse-depends-on
      postgres:
        condition: service_healthy
      minio:
        condition: service_healthy
      redis:
        condition: service_healthy
      clickhouse:
        condition: service_healthy
    environment: &langfuse-worker-env
      <<: *proxy-env
      DATABASE_URL: "postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/langfuse"
      SALT: "${LANGFUSE_SALT}"
      ENCRYPTION_KEY: "${ENCRYPTION_KEY}"
      TELEMETRY_ENABLED: "${TELEMETRY_ENABLED:-true}"
      LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: "${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}"
      CLICKHOUSE_MIGRATION_URL: "${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}"
      CLICKHOUSE_URL: "${CLICKHOUSE_URL:-http://clickhouse:8123}"
      CLICKHOUSE_USER: "${CLICKHOUSE_USER:-clickhouse}"
      CLICKHOUSE_PASSWORD: "${CLICKHOUSE_PASSWORD}"
      CLICKHOUSE_CLUSTER_ENABLED: "${CLICKHOUSE_CLUSTER_ENABLED:-false}"
      LANGFUSE_S3_EVENT_UPLOAD_BUCKET: "${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}"
      LANGFUSE_S3_EVENT_UPLOAD_REGION: "${LANGFUSE_S3_EVENT_UPLOAD_REGION:-auto}"
      LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: "${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}"
      LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: "${MINIO_ROOT_PASSWORD}"
      LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: "${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}"
      LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}"
      LANGFUSE_S3_EVENT_UPLOAD_PREFIX: "${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}"
      LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: "${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse}"
      LANGFUSE_S3_MEDIA_UPLOAD_REGION: "${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-auto}"
      LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: "${LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID:-minio}"
      LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: "${MINIO_ROOT_PASSWORD}"
      LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: "${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://localhost:9090}"
      LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true}"
      LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: "${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/}"
      LANGFUSE_S3_BATCH_EXPORT_ENABLED: "${LANGFUSE_S3_BATCH_EXPORT_ENABLED:-false}"
      LANGFUSE_S3_BATCH_EXPORT_BUCKET: "${LANGFUSE_S3_BATCH_EXPORT_BUCKET:-langfuse}"
      LANGFUSE_S3_BATCH_EXPORT_PREFIX: "${LANGFUSE_S3_BATCH_EXPORT_PREFIX:-exports/}"
      LANGFUSE_S3_BATCH_EXPORT_REGION: "${LANGFUSE_S3_BATCH_EXPORT_REGION:-auto}"
      LANGFUSE_S3_BATCH_EXPORT_ENDPOINT: "${LANGFUSE_S3_BATCH_EXPORT_ENDPOINT:-http://minio:9000}"
      LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT: "${LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT:-http://localhost:9090}"
      LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID: "${LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID:-minio}"
      LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY: "${MINIO_ROOT_PASSWORD}"
      LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE: "${LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE:-true}"
      LANGFUSE_INGESTION_QUEUE_DELAY_MS: "${LANGFUSE_INGESTION_QUEUE_DELAY_MS:-}"
      LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS: "${LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS:-}"
      REDIS_HOST: "${REDIS_HOST:-redis}"
      REDIS_PORT: "${REDIS_PORT:-6379}"
      REDIS_AUTH: "${REDIS_AUTH:-}"
      REDIS_TLS_ENABLED: "${REDIS_TLS_ENABLED:-false}"
      REDIS_TLS_CA: "${REDIS_TLS_CA:-/certs/ca.crt}"
      REDIS_TLS_CERT: "${REDIS_TLS_CERT:-/certs/redis.crt}"
      REDIS_TLS_KEY: "${REDIS_TLS_KEY:-/certs/redis.key}"

  langfuse-web:
    image: langfuse/langfuse:3
    container_name: langfuse-web
    restart: always
    profiles: ["langfuse"]
    depends_on: *langfuse-depends-on
    environment:
      <<: *langfuse-worker-env
      NEXTAUTH_URL: "https://${LANGFUSE_HOSTNAME}"
      NEXTAUTH_SECRET: "${NEXTAUTH_SECRET}"
      LANGFUSE_INIT_ORG_ID: "${LANGFUSE_INIT_ORG_ID:-organization_id}"
      LANGFUSE_INIT_ORG_NAME: "${LANGFUSE_INIT_ORG_NAME:-Organization}"
      LANGFUSE_INIT_PROJECT_ID: "${LANGFUSE_INIT_PROJECT_ID:-project_id}"
      LANGFUSE_INIT_PROJECT_NAME: "${LANGFUSE_INIT_PROJECT_NAME:-Project}"
      LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}"
      LANGFUSE_INIT_PROJECT_SECRET_KEY: "${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}"
      LANGFUSE_INIT_USER_EMAIL: "${LANGFUSE_INIT_USER_EMAIL:-}"
      LANGFUSE_INIT_USER_NAME: "${LANGFUSE_INIT_USER_NAME:-}"
      LANGFUSE_INIT_USER_PASSWORD: "${LANGFUSE_INIT_USER_PASSWORD:-}"
      AUTH_DISABLE_SIGNUP: "${AUTH_DISABLE_SIGNUP:-true}"

  clickhouse:
    image: clickhouse/clickhouse-server
    container_name: clickhouse
    restart: always
    profiles: ["langfuse"]
    user: "101:101"
    environment:
      CLICKHOUSE_DB: default
      CLICKHOUSE_USER: clickhouse
      CLICKHOUSE_PASSWORD: "${CLICKHOUSE_PASSWORD}"
    volumes:
      - langfuse_clickhouse_data:/var/lib/clickhouse
      - langfuse_clickhouse_logs:/var/log/clickhouse-server
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 1s

  minio:
    # Silo: maintained MinIO fork with the same data format and MINIO_* vars
    # (quay.io/minio/minio rejects anonymous pulls). The server binary is
    # 'silo', with no 'minio' alias. Not the 'distroless' tag: it has no sh/curl.
    image: docker.io/pgsty/silo:latest
    container_name: minio
    restart: always
    profiles: ["langfuse"]
    entrypoint: sh
    # create the 'langfuse' bucket before starting the service
    command: -c 'mkdir -p /data/langfuse && silo server --address ":9000" --console-address ":9001" /data'
    environment:
      MINIO_ROOT_USER: minio
      MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD}"
    volumes:
      - langfuse_minio_data:/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/cluster"]
      interval: 1s
      timeout: 5s
      retries: 5
      start_period: 1s

  postgres:
    container_name: postgres
    image: "pgvector/pgvector:pg${POSTGRES_VERSION:-17}"
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 3s
      timeout: 3s
      retries: 10
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
      POSTGRES_DB: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    container_name: redis
    image: docker.io/valkey/valkey:8-alpine
    command: valkey-server --save 30 1 --loglevel warning
    restart: unless-stopped
    stop_grace_period: 30s
    volumes:
      - valkey-data:/data
    cap_drop:
      - ALL
    cap_add:
      - SETGID
      - SETUID
      - DAC_OVERRIDE
    logging: *default-logging
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 3s
      timeout: 10s
      retries: 10

  searxng:
    container_name: searxng
    image: docker.io/searxng/searxng:latest
    profiles: ["searxng"]
    restart: unless-stopped
    volumes:
      - ./searxng:/etc/searxng:rw
    environment:
      SEARXNG_BASE_URL: "https://${SEARXNG_HOSTNAME:-localhost}/"
      UWSGI_WORKERS: "${SEARXNG_UWSGI_WORKERS:-4}"
      UWSGI_THREADS: "${SEARXNG_UWSGI_THREADS:-4}"
    # cap_drop: - ALL  # Temporarily commented out for first run
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
    logging: *default-logging

  ollama-cpu:
    profiles: ["cpu"]
    <<: *service-ollama

  ollama-gpu:
    profiles: ["gpu-nvidia"]
    <<: *service-ollama
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: "${OLLAMA_GPU_COUNT:-1}"
              capabilities: [gpu]

  ollama-gpu-amd:
    profiles: ["gpu-amd"]
    <<: *service-ollama
    image: ollama/ollama:rocm
    devices:
      - "/dev/kfd"
      - "/dev/dri"

  # Templates for extra Ollama instances on multi-GPU hosts, used by
  # docker-compose.ollama-instances.yml via 'extends'. The "ollama-template"
  # profile is never activated directly.
  #
  # These deliberately carry NO 'deploy:' block. Compose APPENDS
  # deploy.resources.reservations.devices across 'extends', so a count-based
  # reservation here would leak an extra, arbitrary GPU into every instance
  # that pins specific device IDs. Do not add one.
  ollama-instance-template:
    <<: *service-ollama
    container_name: ollama-instance-template
    profiles: ["ollama-template"]

  ollama-instance-template-amd:
    <<: *service-ollama
    container_name: ollama-instance-template-amd
    profiles: ["ollama-template"]
    image: ollama/ollama:rocm
    devices:
      - "/dev/kfd"
      - "/dev/dri"

  ollama-pull-llama-cpu:
    profiles: ["cpu"]
    <<: *init-ollama
    depends_on:
      - ollama-cpu

  ollama-pull-llama-gpu:
    profiles: ["gpu-nvidia"]
    <<: *init-ollama
    depends_on:
      - ollama-gpu

  ollama-pull-llama-gpu-amd:
    profiles: [gpu-amd]
    <<: *init-ollama
    image: ollama/ollama:rocm
    depends_on:
      - ollama-gpu-amd

  invokeai-nvidia:
    <<: *service-invokeai
    image: ghcr.io/invoke-ai/invokeai:latest
    profiles: ["invokeai-nvidia"]
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: "${INVOKEAI_GPU_COUNT:-1}"
              capabilities: [gpu]

  invokeai-amd:
    <<: *service-invokeai
    image: ghcr.io/invoke-ai/invokeai:main-rocm
    profiles: ["invokeai-amd"]
    devices:
      - "/dev/kfd"
      - "/dev/dri"
    environment:
      <<: *invokeai-env
      AMD_VISIBLE_DEVICES: all
      RENDER_GROUP_ID: "${RENDER_GROUP_ID:-}"

  invokeai-cpu:
    <<: *service-invokeai
    image: ghcr.io/invoke-ai/invokeai:main-cpu
    profiles: ["invokeai-cpu"]

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    profiles: ["monitoring"]
    restart: unless-stopped
    volumes:
      - ./prometheus:/etc/prometheus:ro
      - prometheus_data:/prometheus
    extra_hosts:
      - "host.docker.internal:host-gateway"

  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    profiles: ["monitoring"]
    restart: unless-stopped
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - "--path.procfs=/host/proc"
      - "--path.sysfs=/host/sys"
      - "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)"
    expose:
      - 9100

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.55.1
    container_name: cadvisor
    profiles: ["monitoring"]
    restart: unless-stopped
    mem_limit: 1g
    mem_reservation: 256m
    cpus: "1.0"
    command:
      - --housekeeping_interval=30s
      - --docker_only=true
      # disk (du over every container's layers) is costly with many images. The flag
      # replaces cAdvisor's default list, so it repeats the v0.55.1 one; re-sync on bumps.
      - --disable_metrics=advtcp,cpu_topology,cpuset,disk,diskIO,hugetlb,memory_numa,process,referenced_memory,resctrl,sched,tcp,udp
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:rw
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    expose:
      - 8080

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    profiles: ["monitoring"]
    restart: unless-stopped
    environment:
      GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_ADMIN_PASSWORD:-admin}"
      GF_PROVISIONING_PATH: /etc/grafana/provisioning
    volumes:
      - grafana:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
      - ./grafana/dashboards:/var/lib/grafana/dashboards # Standard path often used, let's use Grafana's managed dashboards dir
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      - prometheus

  crawl4ai:
    image: unclecode/crawl4ai:latest # Use official image
    container_name: crawl4ai
    profiles: ["crawl4ai"]
    restart: unless-stopped
    shm_size: 1g # Recommended for browser operations
    # No 'env_file: .env' here: it would hand every secret of the stack to the
    # crawler. Pass the variables it needs explicitly.
    environment:
      <<: *proxy-env
      # Without a token Crawl4AI 0.9+ binds 127.0.0.1 only and is unreachable
      # from other containers; with it the API listens on the Docker network.
      CRAWL4AI_API_TOKEN: "${CRAWL4AI_API_TOKEN}"
      # Key for the default LLM provider (openai/gpt-4o-mini).
      OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 4G # Increased based on documentation recommendation

  gotenberg:
    image: gotenberg/gotenberg:8
    container_name: gotenberg
    profiles: ["gotenberg"]
    restart: unless-stopped
    environment:
      DISABLE_GOOGLE_CHROME: false
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  letta:
    image: letta/letta:latest
    container_name: letta
    profiles: ["letta"]
    restart: unless-stopped
    volumes:
      - letta_data:/var/lib/postgresql/data
    environment:
      <<: *proxy-env
      OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
      ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
      OLLAMA_BASE_URL: "${OLLAMA_BASE_URL:-}"
      SECURE: "${LETTA_SECURE:-true}"
      LETTA_SERVER_PASSWORD: "${LETTA_SERVER_PASSWORD:-}"
    extra_hosts:
      - "host.docker.internal:host-gateway"

  weaviate:
    image: cr.weaviate.io/semitechnologies/weaviate:latest
    container_name: weaviate
    profiles: ["weaviate"]
    restart: unless-stopped
    volumes:
      - weaviate_data:/var/lib/weaviate
    environment:
      QUERY_DEFAULTS_LIMIT: 25
      AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "false"
      AUTHENTICATION_APIKEY_ENABLED: "true"
      AUTHENTICATION_APIKEY_ALLOWED_KEYS: "${WEAVIATE_API_KEY}"
      AUTHENTICATION_APIKEY_USERS: "${WEAVIATE_USERNAME}"
      AUTHORIZATION_ENABLE_RBAC: "true"
      AUTHORIZATION_RBAC_ROOT_USERS: "${WEAVIATE_USERNAME}"
      PERSISTENCE_DATA_PATH: "/var/lib/weaviate"
      ENABLE_API_BASED_MODULES: "true"
      CLUSTER_HOSTNAME: "node1"
      DEFAULT_VECTORIZER_MODULE: "none"
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "wget -q --spider http://localhost:8080/v1/.well-known/ready || exit 1",
        ]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s

  ragapp:
    image: ragapp/ragapp:latest
    container_name: ragapp
    profiles: ["ragapp"]
    restart: unless-stopped
    environment:
      <<: *proxy-env

  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    profiles: ["portainer"]
    restart: unless-stopped
    volumes:
      - portainer_data:/data
      - "${DOCKER_SOCKET_LOCATION:-/var/run/docker.sock}:/var/run/docker.sock"

  temporal-elasticsearch:
    image: elasticsearch:7.17.27
    container_name: temporal-elasticsearch
    profiles: ["postiz"]
    restart: unless-stopped
    logging: *default-logging
    environment:
      cluster.routing.allocation.disk.threshold_enabled: "true"
      cluster.routing.allocation.disk.watermark.low: 512mb
      cluster.routing.allocation.disk.watermark.high: 256mb
      cluster.routing.allocation.disk.watermark.flood_stage: 128mb
      discovery.type: single-node
      ES_JAVA_OPTS: -Xms512m -Xmx512m
      xpack.security.enabled: "false"
    volumes:
      - temporal_elasticsearch_data:/usr/share/elasticsearch/data
    healthcheck:
      test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health | grep -qE '\"status\":\"(green|yellow)\"'"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s

  temporal:
    image: temporalio/auto-setup:latest
    container_name: temporal
    profiles: ["postiz"]
    restart: unless-stopped
    logging: *default-logging
    environment:
      DB: postgres12
      POSTGRES_USER: postgres
      POSTGRES_PWD: "${POSTGRES_PASSWORD}"
      POSTGRES_SEEDS: postgres
      DB_PORT: 5432
      TEMPORAL_NAMESPACE: default
      ENABLE_ES: "true"
      ES_SEEDS: temporal-elasticsearch
      ES_VERSION: v7
    depends_on:
      postgres:
        condition: service_healthy
      temporal-elasticsearch:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "temporal operator cluster health --address $(hostname -i):7233 | grep -q SERVING || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s

  temporal-ui:
    image: temporalio/ui:latest
    container_name: temporal-ui
    profiles: ["postiz"]
    restart: unless-stopped
    logging: *default-logging
    environment:
      TEMPORAL_ADDRESS: temporal:7233
      TEMPORAL_CORS_ORIGINS: http://localhost:3000
    depends_on:
      temporal:
        condition: service_healthy

  postiz:
    image: ghcr.io/gitroomhq/postiz-app:latest
    container_name: postiz
    profiles: ["postiz"]
    restart: always
    environment:
      <<: *proxy-env
      BACKEND_INTERNAL_URL: http://localhost:3000
      DATABASE_URL: "postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/${POSTIZ_DB_NAME:-postiz}?schema=postiz"
      DISABLE_REGISTRATION: "${POSTIZ_DISABLE_REGISTRATION}"
      FRONTEND_URL: "${POSTIZ_HOSTNAME:+https://}${POSTIZ_HOSTNAME}"
      IS_GENERAL: "true" # Required for self-hosting.
      JWT_SECRET: "${JWT_SECRET}"
      MAIN_URL: "${POSTIZ_HOSTNAME:+https://}${POSTIZ_HOSTNAME}"
      NEXT_PUBLIC_BACKEND_URL: "${POSTIZ_HOSTNAME:+https://}${POSTIZ_HOSTNAME}/api"
      NEXT_PUBLIC_UPLOAD_DIRECTORY: "/uploads"
      REDIS_URL: "redis://redis:6379"
      STORAGE_PROVIDER: "local"
      TEMPORAL_ADDRESS: temporal:7233
      UPLOAD_DIRECTORY: "/uploads"
      # Social Media API Settings
      X_API_KEY: "${X_API_KEY}"
      X_API_SECRET: "${X_API_SECRET}"
      LINKEDIN_CLIENT_ID: "${LINKEDIN_CLIENT_ID}"
      LINKEDIN_CLIENT_SECRET: "${LINKEDIN_CLIENT_SECRET}"
      REDDIT_CLIENT_ID: "${REDDIT_CLIENT_ID}"
      REDDIT_CLIENT_SECRET: "${REDDIT_CLIENT_SECRET}"
      GITHUB_CLIENT_ID: "${GITHUB_CLIENT_ID}"
      GITHUB_CLIENT_SECRET: "${GITHUB_CLIENT_SECRET}"
      BEEHIIVE_API_KEY: "${BEEHIIVE_API_KEY}"
      BEEHIIVE_PUBLICATION_ID: "${BEEHIIVE_PUBLICATION_ID}"
      THREADS_APP_ID: "${THREADS_APP_ID}"
      THREADS_APP_SECRET: "${THREADS_APP_SECRET}"
      FACEBOOK_APP_ID: "${FACEBOOK_APP_ID}"
      FACEBOOK_APP_SECRET: "${FACEBOOK_APP_SECRET}"
      YOUTUBE_CLIENT_ID: "${YOUTUBE_CLIENT_ID}"
      YOUTUBE_CLIENT_SECRET: "${YOUTUBE_CLIENT_SECRET}"
      TIKTOK_CLIENT_ID: "${TIKTOK_CLIENT_ID}"
      TIKTOK_CLIENT_SECRET: "${TIKTOK_CLIENT_SECRET}"
      PINTEREST_CLIENT_ID: "${PINTEREST_CLIENT_ID}"
      PINTEREST_CLIENT_SECRET: "${PINTEREST_CLIENT_SECRET}"
      DRIBBBLE_CLIENT_ID: "${DRIBBBLE_CLIENT_ID}"
      DRIBBBLE_CLIENT_SECRET: "${DRIBBBLE_CLIENT_SECRET}"
      DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID}"
      DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET}"
      DISCORD_BOT_TOKEN_ID: "${DISCORD_BOT_TOKEN_ID}"
      SLACK_ID: "${SLACK_ID}"
      SLACK_SECRET: "${SLACK_SECRET}"
      SLACK_SIGNING_SECRET: "${SLACK_SIGNING_SECRET}"
      MASTODON_URL: "${MASTODON_URL}"
      MASTODON_CLIENT_ID: "${MASTODON_CLIENT_ID}"
      MASTODON_CLIENT_SECRET: "${MASTODON_CLIENT_SECRET}"
    volumes:
      - postiz-config:/config/
      - postiz-uploads:/uploads/
      - ./postiz.env:/app/.env:ro
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      temporal:
        condition: service_healthy

  databasus:
    image: databasus/databasus:latest
    container_name: databasus
    profiles: ["databasus"]
    restart: unless-stopped
    logging: *default-logging
    volumes:
      - databasus_data:/databasus-data
    healthcheck:
      test: ["CMD", "databasus", "healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s

  # ComfyUI hardware variants (mutually exclusive profiles, like InvokeAI).
  # Deliberately NO CLI_ARGS here: each image ships the right default
  # ("--cpu" baked into the cpu image, empty on the GPU images) and the
  # entrypoint already adds --listen --port 8188. An explicit CLI_ARGS: ""
  # would strip --cpu from the CPU image.
  comfyui-nvidia:
    <<: *service-comfyui
    image: yanwk/comfyui-boot:cu126-slim
    profiles: ["comfyui-nvidia"]
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: "${COMFYUI_GPU_COUNT:-1}"
              capabilities: [gpu]

  comfyui-amd:
    <<: *service-comfyui
    image: yanwk/comfyui-boot:rocm
    profiles: ["comfyui-amd"]
    devices:
      - "/dev/kfd"
      - "/dev/dri"

  comfyui-cpu:
    <<: *service-comfyui
    image: yanwk/comfyui-boot:cpu
    profiles: ["comfyui-cpu"]

  libretranslate:
    image: libretranslate/libretranslate:latest
    container_name: libretranslate
    profiles: ["libretranslate"]
    restart: unless-stopped
    environment:
      LT_API_KEYS: "${LT_API_KEYS:-false}"
      LT_BATCH_LIMIT: "${LT_BATCH_LIMIT:-}"
      LT_CHAR_LIMIT: "${LT_CHAR_LIMIT:-10000}"
      LT_DEBUG: "${LT_DEBUG:-false}"
      LT_FRONTEND_LANGUAGE_SOURCE: "${LT_FRONTEND_LANGUAGE_SOURCE:-auto}"
      LT_FRONTEND_LANGUAGE_TARGET: "${LT_FRONTEND_LANGUAGE_TARGET:-en}"
      LT_FRONTEND_TIMEOUT: "${LT_FRONTEND_TIMEOUT:-2000}"
      LT_HOST: "${LT_HOST:-0.0.0.0}"
      LT_LOAD_ONLY: "${LT_LOAD_ONLY:-}"
      LT_METRICS: "${LT_METRICS:-false}"
      LT_PORT: "${LT_PORT:-5000}"
      LT_REQ_LIMIT: "${LT_REQ_LIMIT:-}"
      LT_SSL: "${LT_SSL:-false}"
      LT_SUGGESTIONS: "${LT_SUGGESTIONS:-false}"
      LT_THREADS: "${LT_THREADS:-4}"
      LT_UPDATE_MODELS: "${LT_UPDATE_MODELS:-false}"
    volumes:
      - libretranslate_api_keys:/app/db
      - libretranslate_models:/home/libretranslate/.local:rw
    healthcheck:
      test: ["CMD-SHELL", "./venv/bin/python scripts/healthcheck.py"]

  python-runner:
    image: python:3.11-slim
    container_name: python-runner
    profiles: ["python-runner"]
    restart: unless-stopped
    # init + exec make SIGTERM reach python so the container stops fast
    init: true
    working_dir: /app
    command: /bin/sh -c 'if [ -f /app/requirements.txt ]; then python -m pip install --no-cache-dir -r /app/requirements.txt; fi; exec python /app/main.py'
    volumes:
      - ./python-runner:/app

  paddleocr:
    image: paddlepaddle/paddle:3.1.0
    container_name: paddleocr
    profiles: ["paddleocr"]
    restart: unless-stopped
    # python -m paddlex --get_pipeline_config PP-ChatOCRv4-doc --save_path /app; \
    command: /bin/sh -c "set -e; \
      python -m pip install --upgrade pip; \
      python -m pip install --no-cache-dir 'paddlex[all,ocr,ie]'; \
      python -m paddlex --install serving; \
      exec python -m paddlex --serve --pipeline '/app/ocr_config.yml' --device cpu --port 8080"
    volumes:
      - ./paddlex/ocr_config.yml:/app/ocr_config.yml:ro
      - paddleocr_cache:/root/.paddleocr
      - paddle_cache:/root/.cache/paddle
      - paddlex_data:/root/.paddlex
    healthcheck:
      test:
        ["CMD-SHELL", "wget -qO- http://localhost:8080/health > /dev/null || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5

  waha:
    image: devlikeapro/waha:latest
    container_name: waha
    profiles: ["waha"]
    restart: unless-stopped
    environment:
      REDIS_URL: redis://redis:6379
      WAHA_API_KEY: "${WAHA_API_KEY}"
      WAHA_BASE_URL: http://waha:3000
      WAHA_DASHBOARD_ENABLED: "${WAHA_DASHBOARD_ENABLED:-true}"
      WAHA_DASHBOARD_PASSWORD: "${WAHA_DASHBOARD_PASSWORD}"
      WAHA_DASHBOARD_USERNAME: "${WAHA_DASHBOARD_USERNAME}"
      WAHA_ENGINE: "${WAHA_ENGINE}"
      WHATSAPP_SESSIONS_POSTGRESQL_URL: "postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/${WAHA_DB_NAME:-waha}?sslmode=disable"
      WHATSAPP_SWAGGER_ENABLED: "${WHATSAPP_SWAGGER_ENABLED:-true}"
      WHATSAPP_SWAGGER_PASSWORD: "${WHATSAPP_SWAGGER_PASSWORD}"
      WHATSAPP_SWAGGER_USERNAME: "${WHATSAPP_SWAGGER_USERNAME}"
    depends_on:
      redis:
        condition: service_healthy
      postgres:
        condition: service_healthy

  ragflow:
    # Pinned: 'latest' moved to 1.0 release candidates, which need kvrocks (and NATS)
    # the stack does not provide. Keep the tag in sync with the update preview.
    image: infiniflow/ragflow:v0.27.2
    container_name: ragflow
    profiles: ["ragflow"]
    restart: unless-stopped
    environment:
      <<: *proxy-env
      DOC_ENGINE: elasticsearch
      ELASTIC_PASSWORD: "${RAGFLOW_ELASTICSEARCH_PASSWORD}"
      ES_HOST: ragflow-elasticsearch
      ES_PORT: 9200
      MINIO_BUCKET: ragflow
      MINIO_HOST: ragflow-minio
      MINIO_PASSWORD: "${RAGFLOW_MINIO_ROOT_PASSWORD}"
      MINIO_PORT: 9000
      MINIO_USER: minio
      MYSQL_DATABASE: rag_flow
      MYSQL_HOST: ragflow-mysql
      MYSQL_PASSWORD: "${RAGFLOW_MYSQL_ROOT_PASSWORD}"
      MYSQL_PORT: 3306
      MYSQL_USER: root
      REDIS_HOST: ragflow-redis
      REDIS_PASSWORD: "${RAGFLOW_REDIS_PASSWORD}"
      REDIS_PORT: 6379
      SVR_HTTP_PORT: 9380
    depends_on:
      ragflow-elasticsearch:
        condition: service_healthy
      ragflow-mysql:
        condition: service_healthy
      ragflow-minio:
        condition: service_healthy
      ragflow-redis:
        condition: service_healthy

  ragflow-mysql:
    image: mysql:8
    container_name: ragflow-mysql
    profiles: ["ragflow"]
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: "${RAGFLOW_MYSQL_ROOT_PASSWORD}"
      MYSQL_DATABASE: rag_flow
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h localhost || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s
    volumes:
      - ragflow_mysql_data:/var/lib/mysql

  ragflow-minio:
    # Silo (MinIO fork), see the 'minio' service above
    image: docker.io/pgsty/silo:latest
    container_name: ragflow-minio
    profiles: ["ragflow"]
    restart: unless-stopped
    entrypoint: sh
    command: -c 'mkdir -p /data/ragflow && silo server --address ":9000" --console-address ":9001" /data'
    environment:
      MINIO_ROOT_USER: minio
      MINIO_ROOT_PASSWORD: "${RAGFLOW_MINIO_ROOT_PASSWORD}"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/cluster"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 10s
    volumes:
      - ragflow_minio_data:/data

  ragflow-redis:
    image: docker.io/valkey/valkey:8-alpine
    container_name: ragflow-redis
    profiles: ["ragflow"]
    restart: unless-stopped
    command: "valkey-server --save 30 1 --loglevel warning --requirepass ${RAGFLOW_REDIS_PASSWORD}"
    volumes:
      - ragflow_redis_data:/data
    cap_drop:
      - ALL
    cap_add:
      - SETGID
      - SETUID
      - DAC_OVERRIDE
    logging: *default-logging
    healthcheck:
      test: ["CMD", "valkey-cli", "-a", "${RAGFLOW_REDIS_PASSWORD}", "ping"]
      interval: 3s
      timeout: 10s
      retries: 10

  ragflow-elasticsearch:
    image: elasticsearch:8.11.3
    container_name: ragflow-elasticsearch
    profiles: ["ragflow"]
    restart: unless-stopped
    environment:
      node.name: ragflow-es01
      discovery.type: single-node
      ELASTIC_PASSWORD: "${RAGFLOW_ELASTICSEARCH_PASSWORD}"
      bootstrap.memory_lock: false
      xpack.security.enabled: true
      xpack.security.http.ssl.enabled: false
      xpack.security.transport.ssl.enabled: false
      cluster.routing.allocation.disk.watermark.low: 5gb
      cluster.routing.allocation.disk.watermark.high: 3gb
      cluster.routing.allocation.disk.watermark.flood_stage: 2gb
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - ragflow_elasticsearch_data:/usr/share/elasticsearch/data
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "curl -u elastic:${RAGFLOW_ELASTICSEARCH_PASSWORD} http://localhost:9200/_cluster/health || exit 1",
        ]
      interval: 10s
      timeout: 10s
      retries: 120
      start_period: 30s
    deploy:
      resources:
        limits:
          memory: 8g

  lightrag:
    image: ghcr.io/hkuds/lightrag:latest
    container_name: lightrag
    profiles: ["lightrag"]
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      <<: *proxy-env
      # Server Configuration
      HOST: 0.0.0.0
      PORT: 9621
      WEBUI_TITLE: LightRAG Knowledge Graph
      WEBUI_DESCRIPTION: Graph-based RAG with Knowledge Extraction
      # Authentication (Built-in)
      AUTH_ACCOUNTS: "${LIGHTRAG_USERNAME}:${LIGHTRAG_PASSWORD}"
      LIGHTRAG_API_KEY: "${LIGHTRAG_API_KEY}"
      TOKEN_SECRET: "${LIGHTRAG_TOKEN_SECRET}"
      # LLM Configuration (Ollama)
      LLM_BINDING: ollama
      LLM_MODEL: qwen2.5:32b
      LLM_BINDING_HOST: http://ollama:11434
      OLLAMA_LLM_NUM_CTX: 32768
      MAX_ASYNC: 4
      # Embedding Configuration (Ollama)
      EMBEDDING_BINDING: ollama
      EMBEDDING_MODEL: bge-m3:latest
      EMBEDDING_DIM: 1024
      EMBEDDING_BINDING_HOST: http://ollama:11434
      OLLAMA_EMBEDDING_NUM_CTX: 8192
      EMBEDDING_FUNC_MAX_ASYNC: 16
      EMBEDDING_BATCH_NUM: 32
      # Query Configuration
      ENABLE_LLM_CACHE: true
      TOP_K: 60
      CHUNK_TOP_K: 20
      COSINE_THRESHOLD: 0.2
      MAX_ENTITY_TOKENS: 6000
      MAX_RELATION_TOKENS: 8000
      MAX_TOTAL_TOKENS: 30000
      # Document Processing
      ENABLE_LLM_CACHE_FOR_EXTRACT: true
      SUMMARY_LANGUAGE: English
      CHUNK_SIZE: 1200
      CHUNK_OVERLAP_SIZE: 100
      SUMMARY_MAX_TOKENS: 500
      SUMMARY_CONTEXT_SIZE: 10000
      # Storage Configuration (Flexible - uses PostgreSQL/Neo4j if available)
      LIGHTRAG_KV_STORAGE: JsonKVStorage
      LIGHTRAG_DOC_STATUS_STORAGE: JsonDocStatusStorage
      LIGHTRAG_GRAPH_STORAGE: "${LIGHTRAG_GRAPH_STORAGE:-NetworkXStorage}"
      LIGHTRAG_VECTOR_STORAGE: "${LIGHTRAG_VECTOR_STORAGE:-NanoVectorDBStorage}"
      # PostgreSQL Configuration (if using PostgreSQL storage)
      POSTGRES_HOST: postgres
      POSTGRES_PORT: 5432
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
      POSTGRES_DATABASE: "${LIGHTRAG_DB_NAME:-lightrag}"
      POSTGRES_MAX_CONNECTIONS: 12
      POSTGRES_VECTOR_INDEX_TYPE: HNSW
      POSTGRES_HNSW_M: 16
      POSTGRES_HNSW_EF: 200
      # Neo4j Configuration (if using Neo4j storage)
      NEO4J_URI: bolt://neo4j:7687
      NEO4J_USERNAME: "${NEO4J_AUTH_USERNAME:-neo4j}"
      NEO4J_PASSWORD: "${NEO4J_AUTH_PASSWORD}"
      NEO4J_DATABASE: neo4j
      # Directories
      INPUT_DIR: /app/data/inputs
      WORKING_DIR: /app/data/rag_storage
    volumes:
      - lightrag_data:/app/data/rag_storage
      - lightrag_inputs:/app/data/inputs

    extra_hosts:
      - "host.docker.internal:host-gateway"

    healthcheck:
      test:
        [
          "CMD-SHELL",
          "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= python -c \"import urllib.request; urllib.request.urlopen('http://localhost:9621/health', timeout=5)\" > /dev/null 2>&1 || exit 1",
        ]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s

  docling:
    image: "${DOCLING_IMAGE:-ghcr.io/docling-project/docling-serve-cpu}"
    container_name: docling
    profiles: ["docling"]
    restart: unless-stopped
    environment:
      <<: *proxy-env
      DOCLING_SERVE_ENABLE_UI: 1
      DOCLING_SERVE_ENABLE_REMOTE_SERVICES: "${DOCLING_SERVE_ENABLE_REMOTE_SERVICES:-true}"
      DOCLING_SERVE_LOAD_MODELS_AT_BOOT: "${DOCLING_SERVE_LOAD_MODELS_AT_BOOT:-false}"
      DOCLING_DEVICE: "${DOCLING_DEVICE:-cpu}"
    volumes:
      - docling_cache:/opt/app-root/src/.cache
    shm_size: 1g
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -qO- http://localhost:5001/docs > /dev/null || exit 1",
        ]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s

  uptime-kuma:
    image: louislam/uptime-kuma:2
    container_name: uptime-kuma
    profiles: ["uptime-kuma"]
    restart: unless-stopped
    logging: *default-logging
    environment:
      <<: *proxy-env
      UPTIME_KUMA_WS_ORIGIN_CHECK: bypass
    volumes:
      - uptime_kuma_data:/app/data
    healthcheck:
      test: ["CMD-SHELL", "node -e \"const http=require('http');const r=http.get('http://localhost:3001',res=>{process.exit(res.statusCode<400?0:1)});r.on('error',()=>process.exit(1));r.setTimeout(5000,()=>{r.destroy();process.exit(1)})\""]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s