WeKnora Docker & Self-Hosted Deployment

GitHub

Open-source LLM knowledge platform: turn raw documents into a queryable RAG, an autonomous reasoning agent, and a self-maintaining Wiki.

RAW Compose
1. Pull Image (DOCKERHUB) docker pull library/weknora-odl-hybrid
2. Docker Run (1-Click Launch) docker run -d --name weknora -p 5432:5432 --restart always weknora-odl-hybrid:local
3. Podman Run podman run -d --name weknora -p 5432:5432 --restart always weknora-odl-hybrid:local
4. Skopeo Inspect (Zero-Pull Manifest) skopeo inspect docker://docker.io/library/weknora-odl-hybrid:local
5. ORAS Pull (OCI Artifact) oras pull weknora-odl-hybrid:local
6. Compose Deployment docker compose up -d
docker-compose.dev.yml
Download .yml
# 开发环境配置 - 只启动基础设施服务,app 和 frontend 在本地运行
services:
  # 只启动依赖的基础设施服务
  postgres:
    image: paradedb/paradedb:v0.25.11-pg17
    container_name: WeKnora-postgres-dev
    ports:
      - "${DB_PORT:-5432}:5432"
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=${DB_NAME}
    volumes:
      - postgres-data-dev:/var/lib/postgresql/data
    networks:
      - WeKnora-network-dev
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 10s
      retries: 3
      start_period: 30s
    restart: unless-stopped
    stop_grace_period: 1m

  redis:
    image: redis:7.0-alpine
    container_name: WeKnora-redis-dev
    ports:
      - "${REDIS_PORT:-6379}:6379"
    volumes:
      - redis_data_dev:/data
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    restart: always
    networks:
      - WeKnora-network-dev

  # See docker-compose.yml searxng-init for rationale (avoid in-place rewrite of
  # the repo-tracked settings.yml by the searxng entrypoint).
  searxng-init:
    image: busybox:1.36
    container_name: WeKnora-searxng-init-dev
    command: ["sh", "-c", "cp /template/settings.yml /etc/searxng/settings.yml && chmod 0644 /etc/searxng/settings.yml"]
    volumes:
      - ./docker/searxng/settings.yml:/template/settings.yml:ro
      - searxng_config_dev:/etc/searxng
    restart: "no"
    networks:
      - WeKnora-network-dev
    profiles:
      - searxng
      - full

  searxng:
    image: searxng/searxng:latest
    container_name: WeKnora-searxng-dev
    # Local dev: bind to loopback by default; the host-side WeKnora process
    # reaches it at http://127.0.0.1:${SEARXNG_PORT}.
    ports:
      - "${SEARXNG_BIND:-127.0.0.1}:${SEARXNG_PORT:-8888}:8080"
    volumes:
      - searxng_config_dev:/etc/searxng
    environment:
      - SEARXNG_BASE_URL=http://localhost:${SEARXNG_PORT:-8888}/
      - INSTANCE_NAME=weknora-searxng-dev
      # See docker-compose.yml for rationale on the default secret.
      - SEARXNG_SECRET=${SEARXNG_SECRET:-weknora-default-searxng-secret-rotate-before-exposing-publicly}
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
    restart: unless-stopped
    depends_on:
      searxng-init:
        condition: service_completed_successfully
    networks:
      - WeKnora-network-dev
    profiles:
      - searxng
      - full

  minio:
    image: pgsty/minio:latest
    container_name: WeKnora-minio-dev
    ports:
      - "${MINIO_PORT:-9000}:9000"
      - "${MINIO_CONSOLE_PORT:-9001}:9001"
    environment:
      - MINIO_ROOT_USER=${MINIO_ACCESS_KEY_ID:-minioadmin}
      - MINIO_ROOT_PASSWORD=${MINIO_SECRET_ACCESS_KEY:-minioadmin}
    command: server --console-address ":9001" /data
    volumes:
      - minio_data_dev:/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3
    networks:
      - WeKnora-network-dev
    profiles:
      - minio
      - full

  qdrant:
    image: qdrant/qdrant:v1.16.2
    container_name: WeKnora-qdrant-dev
    ports:
      - "${QDRANT_REST_PORT:-6333}:6333"
      - "${QDRANT_PORT:-6334}:6334"
    volumes:
      - qdrant_data_dev:/qdrant/storage
    networks:
      - WeKnora-network-dev
    restart: unless-stopped
    profiles:
      - qdrant
      - full

  # OpenSearch k-NN (Phase 3 driver). Single-node dev profile with the
  # security plugin disabled → plain HTTP on :9200, no auth/TLS. The image
  # bundles the opensearch-knn plugin. For production use a secured,
  # multi-node cluster. See website-docs/03-features/05-retrieval-engines.md.
  opensearch:
    image: opensearchproject/opensearch:3.3.2
    container_name: WeKnora-opensearch-dev
    environment:
      - discovery.type=single-node
      # dev only: plain HTTP on :9200, no TLS/auth. The entrypoint script
      # honours DISABLE_SECURITY_PLUGIN (env var) to skip both the demo
      # install and the OPENSEARCH_INITIAL_ADMIN_PASSWORD requirement.
      - DISABLE_SECURITY_PLUGIN=true
      - DISABLE_INSTALL_DEMO_CONFIG=true
      - OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m
      - bootstrap.memory_lock=true
    ulimits:
      memlock:
        soft: -1
        hard: -1
    ports:
      - "${OPENSEARCH_PORT:-9200}:9200"
    volumes:
      - opensearch_data_dev:/usr/share/opensearch/data
    networks:
      - WeKnora-network-dev
    restart: unless-stopped
    profiles:
      - opensearch
      - full
      # Also a member of opensearch-ui so the Dashboards depends_on resolves
      # when only that profile is active (`--profile opensearch-ui up`).
      - opensearch-ui

  # Optional UI for visual index/mapping/query inspection. Decoupled from the
  # "opensearch" / "full" profiles so the heavy Dashboards container is never
  # forced up alongside the cluster — the driver e2e is fully curl-verifiable
  # against :9200. Start it on demand with `--profile opensearch-ui up -d`
  # (depends_on pulls the cluster in automatically).
  opensearch-dashboards:
    image: opensearchproject/opensearch-dashboards:3.3.0
    container_name: WeKnora-opensearch-dashboards-dev
    environment:
      - OPENSEARCH_HOSTS=["http://opensearch:9200"]
      - DISABLE_SECURITY_DASHBOARDS_PLUGIN=true
    ports:
      - "${OPENSEARCH_DASHBOARDS_PORT:-5601}:5601"
    networks:
      - WeKnora-network-dev
    depends_on:
      - opensearch
    restart: unless-stopped
    profiles:
      - opensearch-ui

  milvus:
    image: milvusdb/milvus:v2.6.11
    container_name: WeKnora-milvus-dev
    security_opt:
      - seccomp:unconfined
    command: ["milvus", "run", "standalone"]
    environment:
      - ETCD_USE_EMBED=true
      - ETCD_DATA_DIR=/var/lib/milvus/etcd
      - COMMON_STORAGETYPE=local
      - DEPLOY_MODE=STANDALONE
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
      interval: 30s
      start_period: 90s
      timeout: 20s
      retries: 3
    ports:
      - "${MILVUS_PORT:-19530}:19530"
      - "${MILVUS_HEALTH_PORT:-9091}:9091"
    volumes:
      - milvus_data_dev:/var/lib/milvus
    networks:
      - WeKnora-network-dev
    restart: unless-stopped
    profiles:
      - milvus
      - full

  neo4j:
    image: neo4j:latest
    container_name: WeKnora-neo4j-dev
    volumes:
      - neo4j-data-dev:/data
    environment:
      - NEO4J_AUTH=${NEO4J_USERNAME:-neo4j}/${NEO4J_PASSWORD:-password}
      - NEO4J_apoc_export_file_enabled=true
      - NEO4J_apoc_import_file_enabled=true
      - NEO4J_apoc_import_file_use__neo4j__config=true
      - NEO4JLABS_PLUGINS=["apoc"]
    ports:
      - "7474:7474"
      - "7687:7687"
    restart: always
    networks:
      - WeKnora-network-dev
    profiles:
      - neo4j
      - full

  # Sandbox 镜像:仅用于 build/pull,非常驻服务;本地 app 执行 Skills 时按需 docker run 该镜像,用毕即释
  sandbox:
    image: wechatopenai/weknora-sandbox:${WEKNORA_VERSION:-latest}
    container_name: WeKnora-sandbox-dev
    build:
      context: .
      dockerfile: docker/Dockerfile.sandbox
    profiles:
      - full
    command: ["true"]
    restart: "no"

  docreader:
    build:
      context: .
      dockerfile: docker/Dockerfile.docreader
    image: wechatopenai/weknora-docreader:${WEKNORA_VERSION:-latest}
    container_name: WeKnora-docreader-dev
    ports:
      - "${DOCREADER_PORT:-50051}:50051"
    volumes:
      - docreader-tmp-dev:/tmp/docreader
    environment:
      # ========== 基础 ==========
      - TZ=${TZ:-Asia/Shanghai}
      - LOG_LEVEL=${LOG_LEVEL:-}
      - DOCREADER_IMAGE_OUTPUT_DIR=/tmp/docreader
      - MAX_FILE_SIZE_MB=${MAX_FILE_SIZE_MB:-}
      # docreader 对外 HTTP 抓取的 SSRF 白名单(逗号分隔;与 app 共用同名变量)
      - SSRF_WHITELIST=${SSRF_WHITELIST:-}
      - SSRF_WHITELIST_EXTRA=${SSRF_WHITELIST_EXTRA:-}
      - SSRF_DNS_WHITELIST_ONLY=${SSRF_DNS_WHITELIST_ONLY:-}
      # ========== gRPC 服务端 TLS / 认证 ==========
      - GRPC_TLS_ENABLED=${GRPC_TLS_ENABLED:-false}
      - GRPC_TLS_CERT=${GRPC_TLS_CERT:-}
      - GRPC_TLS_KEY=${GRPC_TLS_KEY:-}
      - GRPC_TLS_CA=${GRPC_TLS_CA:-}
      - GRPC_TLS_SERVER_NAME=${GRPC_TLS_SERVER_NAME:-}
      # true 时强制要求客户端证书(mTLS)
      - GRPC_MTLS_REQUIRE_CLIENT_CERT=${GRPC_MTLS_REQUIRE_CLIENT_CERT:-false}
      - GRPC_AUTH_TOKEN=${GRPC_AUTH_TOKEN:-}
      # ========== gRPC 服务参数 ==========
      - DOCREADER_GRPC_MAX_WORKERS=${DOCREADER_GRPC_MAX_WORKERS:-4}
      - DOCREADER_GRPC_MAX_FILE_SIZE_MB=${DOCREADER_GRPC_MAX_FILE_SIZE_MB:-${MAX_FILE_SIZE_MB:-50}}
      - DOCREADER_GRPC_PORT=${DOCREADER_GRPC_PORT:-50051}
      # Optional RapidOCR subprocesses, independent of ordinary parser RPCs.
      - DOCREADER_PREPROCESS_MAX_WORKERS=${DOCREADER_PREPROCESS_MAX_WORKERS:-1}
      - DOCREADER_PREPROCESS_TIMEOUT_SECONDS=${DOCREADER_PREPROCESS_TIMEOUT_SECONDS:-30}
      - DOCREADER_PREPROCESS_THREADS=${DOCREADER_PREPROCESS_THREADS:-2}
      - DOCREADER_PREPROCESS_MEMORY_MB=${DOCREADER_PREPROCESS_MEMORY_MB:-2048}
      - DOCREADER_PREPROCESS_MAX_PIXELS=${DOCREADER_PREPROCESS_MAX_PIXELS:-16000000}
      - DOCREADER_PREPROCESS_MAX_EDGE=${DOCREADER_PREPROCESS_MAX_EDGE:-2000}
      # ========== 解析通用 ==========
      - DOCREADER_DOCX_MAX_PAGES=${DOCREADER_DOCX_MAX_PAGES:-0}
      - DOCREADER_MARKITDOWN_MAX_WORKERS=${DOCREADER_MARKITDOWN_MAX_WORKERS:-1}
      # 出站 HTTP/HTTPS 代理(抓取网页 / 远程图片时)
      - DOCREADER_EXTERNAL_HTTP_PROXY=${DOCREADER_EXTERNAL_HTTP_PROXY:-}
      - DOCREADER_EXTERNAL_HTTPS_PROXY=${DOCREADER_EXTERNAL_HTTPS_PROXY:-}
      # ========== OpenDataLoader hybrid ==========
      - DOCREADER_ODL_MAX_WORKERS=${DOCREADER_ODL_MAX_WORKERS:-1}
      - DOCREADER_ODL_HYBRID=${DOCREADER_ODL_HYBRID:-off}
      - DOCREADER_ODL_HYBRID_URL=${DOCREADER_ODL_HYBRID_URL:-http://odl-hybrid:5002}
      - DOCREADER_ODL_HYBRID_MODE=${DOCREADER_ODL_HYBRID_MODE:-auto}
      - DOCREADER_ODL_HYBRID_FALLBACK=${DOCREADER_ODL_HYBRID_FALLBACK:-false}
      - DOCREADER_ODL_MARKDOWN_WITH_HTML=${DOCREADER_ODL_MARKDOWN_WITH_HTML:-false}
      # ========== PDF 渲染 ==========
      - DOCREADER_PDF_RENDER_MAX_WORKERS=${DOCREADER_PDF_RENDER_MAX_WORKERS:-1}
      # 单个 PDF 内部扫描页渲染并行进程数(pdfium 非线程安全)。留空走代码默认 min(4,cpu),
      # 避免在低核容器上过度并行(写死数值会覆盖 CPU 感知逻辑)。
      - DOCREADER_PDF_RENDER_PARALLELISM=${DOCREADER_PDF_RENDER_PARALLELISM:-}
      - DOCREADER_PDF_RENDER_DPI=${DOCREADER_PDF_RENDER_DPI:-200}
      # JPEG 质量(代码默认 85;compose 此前误写 90,已对齐)
      - DOCREADER_PDF_JPEG_QUALITY=${DOCREADER_PDF_JPEG_QUALITY:-85}
      # 渲染页图最大长边像素(防超大页撑爆 gRPC 消息上限;密集中文建议 >=1600)
      - DOCREADER_PDF_RENDER_MAX_EDGE=${DOCREADER_PDF_RENDER_MAX_EDGE:-2000}
      # 强制 PDF 全页按扫描件解析(文本层差但可 OCR 时;速度/成本上升)
      - DOCREADER_PDF_FORCE_SCANNED=${DOCREADER_PDF_FORCE_SCANNED:-false}
      # ========== PDF 版式 / 文本调优 ==========
      # glyph 间距超过 median 字宽的多少倍时插空格(默认 0.4)
      - DOCREADER_PDF_WORD_GAP_WIDTH_RATIO=${DOCREADER_PDF_WORD_GAP_WIDTH_RATIO:-0.4}
      # 丢弃窄边栏(arXiv 侧栏)的页宽比例阈值(默认 0.12)
      - DOCREADER_PDF_MARGIN_COL_WIDTH_RATIO=${DOCREADER_PDF_MARGIN_COL_WIDTH_RATIO:-0.12}
      - DOCREADER_PDF_MIN_HEADING_LINE_CHARS=${DOCREADER_PDF_MIN_HEADING_LINE_CHARS:-8}
      # 清理 U+FFFE/软连字符、剥离矢量图轴文字、图表区按 JPEG 渲染
      - DOCREADER_PDF_SANITIZE_TEXT=${DOCREADER_PDF_SANITIZE_TEXT:-true}
      - DOCREADER_PDF_STRIP_CHART_DEBRIS=${DOCREADER_PDF_STRIP_CHART_DEBRIS:-true}
      - DOCREADER_PDF_RENDER_VECTOR_FIGURES=${DOCREADER_PDF_RENDER_VECTOR_FIGURES:-true}
      - DOCREADER_PDF_DETECT_HEADINGS=${DOCREADER_PDF_DETECT_HEADINGS:-}
      - DOCREADER_PDF_LAYOUT_ORDERING=${DOCREADER_PDF_LAYOUT_ORDERING:-}
      - DOCREADER_PDF_FILTER_HIDDEN_TEXT=${DOCREADER_PDF_FILTER_HIDDEN_TEXT:-}
      # 扫描件判定:图片面积占比 >= 此值视为扫描页(默认留空走代码默认)
      - DOCREADER_PDF_SCAN_IMAGE_RATIO=${DOCREADER_PDF_SCAN_IMAGE_RATIO:-}
      - DOCREADER_PDF_SCAN_MIN_CHARS=${DOCREADER_PDF_SCAN_MIN_CHARS:-}
      # 嵌入图片抽取
      - DOCREADER_PDF_EXTRACT_EMBEDDED_IMAGES=${DOCREADER_PDF_EXTRACT_EMBEDDED_IMAGES:-}
      - DOCREADER_PDF_EMBED_MIN_PIXELS=${DOCREADER_PDF_EMBED_MIN_PIXELS:-}
      - DOCREADER_PDF_EMBED_MIN_AREA_RATIO=${DOCREADER_PDF_EMBED_MIN_AREA_RATIO:-}
      - DOCREADER_PDF_EMBED_REPEAT_PAGE_FRAC=${DOCREADER_PDF_EMBED_REPEAT_PAGE_FRAC:-}
      - DOCREADER_PDF_EMBED_MAX_IMAGES=${DOCREADER_PDF_EMBED_MAX_IMAGES:-}
      # 图表区识别
      - DOCREADER_PDF_MIN_CHART_REGION_AREA=${DOCREADER_PDF_MIN_CHART_REGION_AREA:-}
      - DOCREADER_PDF_MIN_CHART_REGION_CHARS=${DOCREADER_PDF_MIN_CHART_REGION_CHARS:-}
      - DOCREADER_PDF_MAX_CHART_REGION_AREA=${DOCREADER_PDF_MAX_CHART_REGION_AREA:-}
      - DOCREADER_PDF_MAX_FIGURE_HEIGHT_RATIO=${DOCREADER_PDF_MAX_FIGURE_HEIGHT_RATIO:-}
    healthcheck:
      test: ["CMD", "grpc_health_probe", "-addr=localhost:50051"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    networks:
      - WeKnora-network-dev
    restart: unless-stopped
    extra_hosts:
      - "host.docker.internal:host-gateway"

  # OpenDataLoader hybrid backend (optional). Enable profile "odl-hybrid" and set
  # DOCREADER_ODL_HYBRID=docling-fast on docreader. Default --no-ocr (no EasyOCR).
  # Local build only — not published to Docker Hub.
  odl-hybrid:
    build:
      context: .
      dockerfile: docker/Dockerfile.odl-hybrid
    image: weknora-odl-hybrid:local
    container_name: WeKnora-odl-hybrid
    profiles:
      - odl-hybrid
    ports:
      - "${ODL_HYBRID_PORT:-5002}:5002"
    environment:
      # Default --no-ocr (digital PDFs). Scanned PDFs: use builtin OCR / MinerU, or
      # ODL_HYBRID_EXTRA_ARGS="--force-ocr" (needs EasyOCR + libGL in image).
      - ODL_HYBRID_EXTRA_ARGS=${ODL_HYBRID_EXTRA_ARGS:---no-ocr}
    networks:
      - WeKnora-network-dev
    restart: unless-stopped

  dex:
    image: dexidp/dex:latest
    container_name: WeKnora-dex-dev
    ports:
      - "5556:5556"
    volumes:
      - ./misc/dex-config.yaml:/etc/dex/config.yaml
    command: ["dex", "serve", "/etc/dex/config.yaml"]
    profiles:
      - dex
      - full

  # ---------------------------------------------------------------------------
  # Langfuse 自建栈(dev 对称版)
  #
  # 用法:
  #   docker compose -f docker-compose.dev.yml --profile langfuse up -d
  #
  # 本地 app (go run) 需要的环境变量:
  #   export LANGFUSE_HOST=http://localhost:3000
  #   export LANGFUSE_PUBLIC_KEY=pk-lf-xxx
  #   export LANGFUSE_SECRET_KEY=sk-lf-xxx
  #
  # 复用 dev 已有的 postgres(独立 langfuse 数据库)和 redis(DB 1),
  # 新增:clickhouse、minio、web、worker + 一次性 db-init,和生产版结构一致。
  # ---------------------------------------------------------------------------
  # 复用 dev 已有的 ParadeDB 镜像,不额外拉 postgres 镜像
  langfuse-db-init:
    image: paradedb/paradedb:v0.25.11-pg17
    container_name: WeKnora-langfuse-db-init-dev
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      PGPASSWORD: ${DB_PASSWORD}
    # ${LANGFUSE_DB_NAME:-langfuse} / ${DB_USER} 由 compose 解析成字面量后再传给 shell。
    entrypoint: ["sh", "-c"]
    command:
      - |
        set -e
        echo "[langfuse-db-init] ensuring database '${LANGFUSE_DB_NAME:-langfuse}' exists..."
        # 先刷新现有库的 collation(镜像 ICU 2.36 与宿主 2.41 不匹配时必须做),否则 CREATE DATABASE 会失败
        psql -h postgres -U ${DB_USER} -d postgres -v ON_ERROR_STOP=0 -c "ALTER DATABASE template1 REFRESH COLLATION VERSION;" >/dev/null 2>&1 || true
        psql -h postgres -U ${DB_USER} -d postgres -v ON_ERROR_STOP=0 -c "ALTER DATABASE postgres REFRESH COLLATION VERSION;" >/dev/null 2>&1 || true
        # 幂等创建:已存在则跳过;不存在则从 template0 克隆(template0 永远不会有 collation 漂移)
        if psql -h postgres -U ${DB_USER} -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='${LANGFUSE_DB_NAME:-langfuse}'" | grep -q 1; then
          echo "[langfuse-db-init] database '${LANGFUSE_DB_NAME:-langfuse}' already exists, skipping."
        else
          psql -h postgres -U ${DB_USER} -d postgres -v ON_ERROR_STOP=1 -c "CREATE DATABASE \"${LANGFUSE_DB_NAME:-langfuse}\" TEMPLATE template0;"
          echo "[langfuse-db-init] database '${LANGFUSE_DB_NAME:-langfuse}' created."
        fi
        echo "[langfuse-db-init] done."
    networks:
      - WeKnora-network-dev
    restart: "no"
    profiles:
      - langfuse
      - full

  langfuse-clickhouse:
    image: clickhouse/clickhouse-server:24.8
    container_name: WeKnora-langfuse-clickhouse-dev
    restart: unless-stopped
    user: "101:101"
    environment:
      CLICKHOUSE_DB: default
      CLICKHOUSE_USER: ${LANGFUSE_CLICKHOUSE_USER:-clickhouse}
      CLICKHOUSE_PASSWORD: ${LANGFUSE_CLICKHOUSE_PASSWORD:-clickhouse}
    volumes:
      - langfuse_clickhouse_data_dev:/var/lib/clickhouse
      - langfuse_clickhouse_logs_dev:/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: 10s
    networks:
      - WeKnora-network-dev
    profiles:
      - langfuse
      - full

  langfuse-minio:
    image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z
    container_name: WeKnora-langfuse-minio-dev
    restart: unless-stopped
    entrypoint: sh
    command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
    environment:
      MINIO_ROOT_USER: ${LANGFUSE_MINIO_USER:-langfuseminio}
      MINIO_ROOT_PASSWORD: ${LANGFUSE_MINIO_PASSWORD:-langfuseminiosecret}
    ports:
      - "${LANGFUSE_MINIO_S3_PORT:-9100}:9000"
      - "${LANGFUSE_MINIO_CONSOLE_PORT:-9101}:9001"
    volumes:
      - langfuse_minio_data_dev:/data
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 5s
      timeout: 10s
      retries: 5
    networks:
      - WeKnora-network-dev
    profiles:
      - langfuse
      - full

  langfuse-worker:
    image: langfuse/langfuse-worker:3
    container_name: WeKnora-langfuse-worker-dev
    restart: unless-stopped
    depends_on: &langfuse-dev-depends-on
      langfuse-db-init:
        condition: service_completed_successfully
      redis:
        condition: service_started
      langfuse-clickhouse:
        condition: service_healthy
      langfuse-minio:
        condition: service_healthy
    # wrapper entrypoint 把 DB_PASSWORD / REDIS_PASSWORD URL 编码后再拼 URL,
    # 避免密码含 '@' / '#' 等字符导致 Prisma P1013 解析失败。
    # 注意:compose 覆盖 entrypoint 会清空镜像默认 CMD,所以末尾写死原始命令。
    entrypoint:
      - /bin/sh
      - -ec
      - |
        _enc() { node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$$1"; }
        DU=$$(_enc "$$_LF_DB_USER")
        DP=$$(_enc "$$_LF_DB_PASSWORD")
        RP=$$(_enc "$$_LF_REDIS_PASSWORD")
        export DATABASE_URL="postgresql://$$DU:$$DP@postgres:5432/$$_LF_DB_NAME"
        export REDIS_CONNECTION_STRING="redis://:$$RP@redis:6379/$$_LF_REDIS_DB"
        unset _LF_DB_USER _LF_DB_PASSWORD _LF_REDIS_PASSWORD
        exec dumb-init -- ./worker/entrypoint.sh node worker/dist/index.js
    environment: &langfuse-dev-env
      # 原始凭证(未 URL 编码),由 entrypoint wrapper 读取并组装
      _LF_DB_USER: ${DB_USER}
      _LF_DB_PASSWORD: ${DB_PASSWORD}
      _LF_DB_NAME: ${LANGFUSE_DB_NAME:-langfuse}
      _LF_REDIS_PASSWORD: ${REDIS_PASSWORD}
      _LF_REDIS_DB: ${LANGFUSE_REDIS_DB:-1}
      SALT: ${LANGFUSE_SALT:-weknora-langfuse-dev-salt-change-me}
      ENCRYPTION_KEY: ${LANGFUSE_ENCRYPTION_KEY:-0000000000000000000000000000000000000000000000000000000000000000}
      NEXTAUTH_URL: ${LANGFUSE_NEXTAUTH_URL:-http://localhost:3000}
      NEXTAUTH_SECRET: ${LANGFUSE_NEXTAUTH_SECRET:-weknora-langfuse-dev-nextauth-secret-change-me}
      TELEMETRY_ENABLED: ${LANGFUSE_TELEMETRY_ENABLED:-false}
      LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: "false"
      CLICKHOUSE_URL: http://langfuse-clickhouse:8123
      CLICKHOUSE_MIGRATION_URL: clickhouse://langfuse-clickhouse:9000
      CLICKHOUSE_USER: ${LANGFUSE_CLICKHOUSE_USER:-clickhouse}
      CLICKHOUSE_PASSWORD: ${LANGFUSE_CLICKHOUSE_PASSWORD:-clickhouse}
      CLICKHOUSE_CLUSTER_ENABLED: "false"
      LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
      LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
      LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_MINIO_USER:-langfuseminio}
      LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_MINIO_PASSWORD:-langfuseminiosecret}
      LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://langfuse-minio:9000
      LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
      LANGFUSE_S3_EVENT_UPLOAD_PREFIX: events/
      LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse
      LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto
      LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_MINIO_USER:-langfuseminio}
      LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_MINIO_PASSWORD:-langfuseminiosecret}
      LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://localhost:9100}
      LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true"
      LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: media/
    networks:
      - WeKnora-network-dev
    profiles:
      - langfuse
      - full

  langfuse-web:
    image: langfuse/langfuse:3
    container_name: WeKnora-langfuse-web-dev
    restart: unless-stopped
    depends_on: *langfuse-dev-depends-on
    ports:
      - "${LANGFUSE_WEB_PORT:-3000}:3000"
    entrypoint:
      - /bin/sh
      - -ec
      - |
        _enc() { node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$$1"; }
        DU=$$(_enc "$$_LF_DB_USER")
        DP=$$(_enc "$$_LF_DB_PASSWORD")
        RP=$$(_enc "$$_LF_REDIS_PASSWORD")
        export DATABASE_URL="postgresql://$$DU:$$DP@postgres:5432/$$_LF_DB_NAME"
        export REDIS_CONNECTION_STRING="redis://:$$RP@redis:6379/$$_LF_REDIS_DB"
        unset _LF_DB_USER _LF_DB_PASSWORD _LF_REDIS_PASSWORD
        if [ -n "$$NEXT_PUBLIC_LANGFUSE_CLOUD_REGION" ]; then
          exec dumb-init -- ./web/entrypoint.sh node --import dd-trace/initialize.mjs ./web/server.js --keepAliveTimeout 110000
        else
          exec dumb-init -- ./web/entrypoint.sh node ./web/server.js --keepAliveTimeout 110000
        fi
    environment:
      <<: *langfuse-dev-env
      LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
      LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
      LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
      LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
      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:-}
    networks:
      - WeKnora-network-dev
    profiles:
      - langfuse
      - full


networks:
  WeKnora-network-dev:
    driver: bridge

volumes:
  postgres-data-dev:
  redis_data_dev:
  minio_data_dev:
  neo4j-data-dev:
  qdrant_data_dev:
  opensearch_data_dev:
  milvus_data_dev:
  docreader-tmp-dev:
  langfuse_clickhouse_data_dev:
  langfuse_clickhouse_logs_dev:
  langfuse_minio_data_dev:
  searxng_config_dev: