Files
xinmi/docker-compose.yml
T

1042 lines
48 KiB
YAML
Raw Normal View History

services:
frontend:
image: wechatopenai/weknora-ui:${WEKNORA_VERSION:-latest}
# Requires frontend/dist — run ./scripts/build_frontend_dist.sh before --build
build:
context: ./frontend
container_name: WeKnora-frontend
ports:
- "${FRONTEND_PORT:-80}:80"
environment:
- MAX_FILE_SIZE_MB=${MAX_FILE_SIZE_MB:-50}
- MAX_SKILL_BUNDLE_SIZE_MB=${MAX_SKILL_BUNDLE_SIZE_MB:-256}
- DEFAULT_LOCALE=${DEFAULT_LOCALE:-}
- APP_HOST=${APP_HOST:-app}
# APP_BACKEND_PORT: the port NGINX proxies to (default 8080).
# For local deployment this is the App container's listening port, independent of host-mapped APP_PORT.
# For remote deployment, set this to the remote App's service port.
- APP_PORT=${APP_BACKEND_PORT:-8080}
- APP_SCHEME=${APP_SCHEME:-http}
# NOTE: If using a remote App backend, comment out or remove the depends_on
# block below and set APP_HOST/APP_BACKEND_PORT/APP_SCHEME in your .env file.
depends_on:
app:
condition: service_healthy
networks:
- WeKnora-network
restart: unless-stopped
app:
image: wechatopenai/weknora-app:${WEKNORA_VERSION:-latest}
build:
context: .
dockerfile: docker/Dockerfile.app
args:
- APK_MIRROR_ARG=${APK_MIRROR_ARG:-}
- WITH_ANYDOC=${WITH_ANYDOC:-1}
container_name: WeKnora-app
ports:
- "${APP_PORT:-8080}:8080"
volumes:
- data-files:/data/files
- docreader-tmp:/tmp/docreader:ro
- ./config/config.yaml:/app/config/config.yaml
# Optional: mount custom skills directory (allows adding skills without rebuilding image)
- ./skills/preloaded:/app/skills/preloaded
# Docker 沙箱默认关闭(见 WEKNORA_SANDBOX_DOCKER_ENABLED)。需要本机 daemon 时
# 同时设 true 并挂载实际 socket——这等同宿主机 root,仅私有化单机。
# 入口脚本会按 socket GID 把 appuser 加入对应组(compose group_add 在 gosu 后无效)。
# - /var/run/docker.sock:/var/run/docker.sock
# Optional: declarative built-in models. Copy config/builtin_models.yaml.example
# to config/builtin_models.yaml, edit it for your deployment, then
# uncomment the line below. See docs/BUILTIN_MODELS.md.
# - ./config/builtin_models.yaml:/app/config/builtin_models.yaml:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
# Source .env so deployment-specific variables (e.g. those referenced by
# config/builtin_models.yaml via ${ENV}, or any future feature) reach the
# app container without having to be enumerated under `environment:`.
#
# NOTE: the array form below is used instead of the map form
# (env_file: - path: .env, required: false) because the map syntax
# requires Docker Compose v2.24+ (Jan 2024) and would break parsing on
# older deploys. The trade-off is that Compose errors out if .env is
# absent — see scripts/start_all.sh for the pre-flight touch that keeps
# fresh clones working.
env_file:
- .env
environment:
# ========== 日志 ==========
# 日志级别,默认debug
- LOG_LEVEL=${LOG_LEVEL:-debug}
# 日志文件路径(留空则只输出 stdout);LLM_DEBUG_LOG 开启时同目录写 llm_debug.log
- LOG_PATH=${LOG_PATH:-}
# 自定义日志格式模板(留空用内置默认格式)
- LOG_FORMAT=${LOG_FORMAT:-}
# LLM 调试日志:true 写 LOG_PATH 同目录 llm_debug.log,或指定路径;留空关闭
- LLM_DEBUG_LOG=${LLM_DEBUG_LOG:-false}
# ========== 运行时基础 ==========
# gin mod
# 可选值: debug(开发模式,有详细日志), release(生产模式,禁用Swagger文档)
- GIN_MODE=${GIN_MODE:-release}
# 时区,默认为Asia/Shanghai
- TZ=${TZ:-Asia/Shanghai}
# 文档处理语言(问题/摘要生成)。优先级:此环境变量(部署级覆盖,非空即生效)
# > Accept-Language 请求头 > 内置默认值 (zh-CN)。留空则跟随请求方 Accept-Language。
- WEKNORA_LANGUAGE=${WEKNORA_LANGUAGE:-}
# Docker 沙箱默认关闭:本机 docker.sock 等同宿主机 root。
# 系统管理员也可在「设置 → 系统设置」打开(立即生效)。未落库时读本变量。
- WEKNORA_SANDBOX_DOCKER_ENABLED=${WEKNORA_SANDBOX_DOCKER_ENABLED:-false}
# gin 信任的代理 CIDR(逗号分隔);显式设为空字符串则禁用代理信任。
# 默认信任 loopback + 私网段(容器网络内 nginx 已覆盖)
# - WEKNORA_TRUSTED_PROXIES=${WEKNORA_TRUSTED_PROXIES:-}
# 启动时自动执行数据库迁移;设为 false 禁用,默认启用
- AUTO_MIGRATE=${AUTO_MIGRATE:-true}
# IM 渠道图片/文件外链的外部可达 URL。IM 显示知识库图片需二选一:
# 存储后端公网可达(如 MINIO_ENDPOINT 设为公网),或设本变量走 <APP_EXTERNAL_URL>/r/<token>(需 nginx 代理 /r/)。
# 默认 MinIO 内网部署 / local 后端需设本变量。
# - APP_EXTERNAL_URL=${APP_EXTERNAL_URL:-}
# 前端外部 origin,用于邀请链接等绝对 URL(留空走 host-relative)
- FRONTEND_BASE_URL=${FRONTEND_BASE_URL:-}
# ========== 对象存储 - 腾讯云 COS ==========
- COS_SECRET_ID=${COS_SECRET_ID:-}
- COS_SECRET_KEY=${COS_SECRET_KEY:-}
- COS_REGION=${COS_REGION:-}
- COS_BUCKET_NAME=${COS_BUCKET_NAME:-}
- COS_APP_ID=${COS_APP_ID:-}
- COS_PATH_PREFIX=${COS_PATH_PREFIX:-}
- COS_TEMP_BUCKET_NAME=${COS_TEMP_BUCKET_NAME:-}
- COS_TEMP_REGION=${COS_TEMP_REGION:-}
# 认证 / 空间 / RBAC / 审计
# 禁止新用户注册(生产建议 true)
- DISABLE_REGISTRATION=${DISABLE_REGISTRATION:-false}
# 开启后密码必须包含大小写字母、数字和特殊字符,特殊字符包含:!@#$%^&*()_+-=[]{}|;:,.<>?
- WEKNORA_AUTH_COMPLEX_PASSWORD_ENABLED=${WEKNORA_AUTH_COMPLEX_PASSWORD_ENABLED:-false}
# 公开注册后的默认空间策略:create_personal(默认,自动创建个人空间)/ tenantless
- WEKNORA_AUTH_DEFAULT_TENANT_MODE=${WEKNORA_AUTH_DEFAULT_TENANT_MODE:-create_personal}
# 是否允许普通用户登录后主动创建空间(默认 true;false 仅能邀请加入,超管不受影响)
- WEKNORA_TENANT_SELF_SERVICE_CREATION_ENABLED=${WEKNORA_TENANT_SELF_SERVICE_CREATION_ENABLED:-true}
# 启用跨空间访问(需配合用户的 CanAccessAllTenants 权限;默认 false
- WEKNORA_TENANT_ENABLE_CROSS_TENANT_ACCESS=${WEKNORA_TENANT_ENABLE_CROSS_TENANT_ACCESS:-false}
# 新建空间默认存储配额(GB,默认 10)
- WEKNORA_TENANT_DEFAULT_STORAGE_QUOTA_GB=${WEKNORA_TENANT_DEFAULT_STORAGE_QUOTA_GB:-10}
# 空间邀请链接 TTL(Go duration,默认 168h=7 天)
- WEKNORA_INVITATION_TTL=${WEKNORA_INVITATION_TTL:-168h}
# 审计日志保留天数(0 禁用清理,默认 90)
- WEKNORA_AUDIT_RETENTION_DAYS=${WEKNORA_AUDIT_RETENTION_DAYS:-90}
# 数据库设置
- DB_DRIVER=${DB_DRIVER:-postgres}
- DB_HOST=${DB_HOST:-postgres}
- DB_PORT=${DB_PORT:-5432}
- DB_USER=${DB_USER:-}
- DB_PASSWORD=${DB_PASSWORD:-}
- DB_NAME=${DB_NAME:-}
# SQLite 驱动时使用(DB_DRIVER=sqlite);postgres 驱动忽略
# - DB_PATH=${DB_PATH:-}
# ========== Langfuse (optional observability) ==========
# Enabled automatically when LANGFUSE_PUBLIC_KEY + LANGFUSE_SECRET_KEY are set.
# See docs/Langfuse集成.md for the full list of tuning knobs.
- LANGFUSE_ENABLED=${LANGFUSE_ENABLED:-}
- LANGFUSE_HOST=${LANGFUSE_HOST:-https://cloud.langfuse.com}
- LANGFUSE_PUBLIC_KEY=${LANGFUSE_PUBLIC_KEY:-}
- LANGFUSE_SECRET_KEY=${LANGFUSE_SECRET_KEY:-}
- LANGFUSE_RELEASE=${LANGFUSE_RELEASE:-}
- LANGFUSE_ENVIRONMENT=${LANGFUSE_ENVIRONMENT:-}
- LANGFUSE_FLUSH_AT=${LANGFUSE_FLUSH_AT:-}
- LANGFUSE_FLUSH_INTERVAL=${LANGFUSE_FLUSH_INTERVAL:-}
- LANGFUSE_QUEUE_SIZE=${LANGFUSE_QUEUE_SIZE:-}
- LANGFUSE_REQUEST_TIMEOUT=${LANGFUSE_REQUEST_TIMEOUT:-}
- LANGFUSE_SAMPLE_RATE=${LANGFUSE_SAMPLE_RATE:-}
- LANGFUSE_DEBUG=${LANGFUSE_DEBUG:-}
# 向量库 / 检索引擎
- RETRIEVE_DRIVER=${RETRIEVE_DRIVER:-postgres}
- ELASTICSEARCH_ADDR=${ELASTICSEARCH_ADDR:-}
- ELASTICSEARCH_USERNAME=${ELASTICSEARCH_USERNAME:-}
- ELASTICSEARCH_PASSWORD=${ELASTICSEARCH_PASSWORD:-}
- ELASTICSEARCH_INDEX=${ELASTICSEARCH_INDEX:-}
- QDRANT_HOST=${QDRANT_HOST:-qdrant}
- QDRANT_PORT=${QDRANT_PORT:-6334}
- QDRANT_COLLECTION=${QDRANT_COLLECTION:-weknora_embeddings}
- QDRANT_API_KEY=${QDRANT_API_KEY:-}
- QDRANT_USE_TLS=${QDRANT_USE_TLS:-false}
- MILVUS_ADDRESS=${MILVUS_ADDRESS:-milvus:19530}
- MILVUS_COLLECTION=${MILVUS_COLLECTION:-weknora_embeddings}
- MILVUS_METRIC_TYPE=${MILVUS_METRIC_TYPE:-IP}
- MILVUS_USERNAME=${MILVUS_USERNAME:-}
- MILVUS_PASSWORD=${MILVUS_PASSWORD:-}
- MILVUS_DB_NAME=${MILVUS_DB_NAME:-}
# 多向量库并行检索超时(秒,RETRIEVE_DRIVER 含多个驱动时生效)
- MULTI_STORE_RETRIEVE_TIMEOUT_SEC=${MULTI_STORE_RETRIEVE_TIMEOUT_SEC:-}
# ========== OpenSearch(RETRIEVE_DRIVER=opensearch)==========
- OPENSEARCH_ADDR=${OPENSEARCH_ADDR:-}
- OPENSEARCH_USERNAME=${OPENSEARCH_USERNAME:-}
- OPENSEARCH_PASSWORD=${OPENSEARCH_PASSWORD:-}
- OPENSEARCH_INSECURE_SKIP_VERIFY=${OPENSEARCH_INSECURE_SKIP_VERIFY:-false}
- OPENSEARCH_INDEX=${OPENSEARCH_INDEX:-}
# ========== 腾讯云 VectorDB(RETRIEVE_DRIVER=tencent_vectordb)==========
- TENCENT_VECTORDB_ADDR=${TENCENT_VECTORDB_ADDR:-}
- TENCENT_VECTORDB_USERNAME=${TENCENT_VECTORDB_USERNAME:-}
- TENCENT_VECTORDB_API_KEY=${TENCENT_VECTORDB_API_KEY:-}
- TENCENT_VECTORDB_DATABASE=${TENCENT_VECTORDB_DATABASE:-}
- TENCENT_VECTORDB_COLLECTION=${TENCENT_VECTORDB_COLLECTION:-}
- TENCENT_VECTORDB_REPLICA_NUMBER=${TENCENT_VECTORDB_REPLICA_NUMBER:-}
# Docreader 解析调优
- DOCREADER_ADDR=${DOCREADER_ADDR:-docreader:50051}
- DOCREADER_TRANSPORT=${DOCREADER_TRANSPORT:-grpc}
# docreader 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:-}
- GRPC_AUTH_TOKEN=${GRPC_AUTH_TOKEN:-}
- WEAVIATE_HOST=${WEAVIATE_HOST:-}
- WEAVIATE_GRPC_ADDRESS=${WEAVIATE_GRPC_ADDRESS:-}
- WEAVIATE_SCHEME=${WEAVIATE_SCHEME:-}
- WEAVIATE_AUTH_ENABLED=${WEAVIATE_AUTH_ENABLED:-}
- WEAVIATE_API_KEY=${WEAVIATE_API_KEY:-}
- DORIS_ADDR=${DORIS_ADDR:-}
- DORIS_HTTP_PORT=${DORIS_HTTP_PORT:-}
- DORIS_DATABASE=${DORIS_DATABASE:-}
- DORIS_USERNAME=${DORIS_USERNAME:-}
- DORIS_PASSWORD=${DORIS_PASSWORD:-}
- DORIS_TABLE_PREFIX=${DORIS_TABLE_PREFIX:-}
- DORIS_COMPAT_MODE=${DORIS_COMPAT_MODE:-}
# ========== 文件存储 ==========
- STORAGE_TYPE=${STORAGE_TYPE:-local}
# 允许用户使用的存储类型白名单(逗号分隔,留空允许全部)
- STORAGE_ALLOW_LIST=${STORAGE_ALLOW_LIST:-}
- LOCAL_STORAGE_BASE_DIR=${LOCAL_STORAGE_BASE_DIR:-/data/files}
# 本地存储路径前缀(legacy 回填用,一般留空)
- LOCAL_STORAGE_PATH_PREFIX=${LOCAL_STORAGE_PATH_PREFIX:-}
- AUTO_RECOVER_DIRTY=${AUTO_RECOVER_DIRTY:-true}
# ----- MinIO(STORAGE_TYPE=minio)-----
- MINIO_ENDPOINT=${MINIO_ENDPOINT:-minio:9000}
- MINIO_ACCESS_KEY_ID=${MINIO_ACCESS_KEY_ID:-minioadmin}
- MINIO_SECRET_ACCESS_KEY=${MINIO_SECRET_ACCESS_KEY:-minioadmin}
- MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME:-}
- MINIO_PATH_PREFIX=${MINIO_PATH_PREFIX:-}
- MINIO_USE_SSL=${MINIO_USE_SSL:-false}
# ----- 火山引擎 TOS(STORAGE_TYPE=tos)-----
- TOS_ENDPOINT=${TOS_ENDPOINT:-}
- TOS_REGION=${TOS_REGION:-}
- TOS_ACCESS_KEY=${TOS_ACCESS_KEY:-}
- TOS_SECRET_KEY=${TOS_SECRET_KEY:-}
- TOS_BUCKET_NAME=${TOS_BUCKET_NAME:-}
- TOS_PATH_PREFIX=${TOS_PATH_PREFIX:-}
- TOS_TEMP_BUCKET_NAME=${TOS_TEMP_BUCKET_NAME:-}
- TOS_TEMP_REGION=${TOS_TEMP_REGION:-}
# ----- AWS S3(STORAGE_TYPE=s3)-----
- S3_ENDPOINT=${S3_ENDPOINT:-}
- S3_REGION=${S3_REGION:-}
- S3_ACCESS_KEY=${S3_ACCESS_KEY:-}
- S3_SECRET_KEY=${S3_SECRET_KEY:-}
- S3_BUCKET_NAME=${S3_BUCKET_NAME:-}
- S3_PATH_PREFIX=${S3_PATH_PREFIX:-weknora/}
- S3_USE_SSL=${S3_USE_SSL:-true}
- S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE:-false}
# ----- 华为云 OBS(STORAGE_TYPE=obs)-----
- OBS_ENDPOINT=${OBS_ENDPOINT:-}
- OBS_REGION=${OBS_REGION:-}
- OBS_ACCESS_KEY=${OBS_ACCESS_KEY:-}
- OBS_SECRET_KEY=${OBS_SECRET_KEY:-}
- OBS_BUCKET_NAME=${OBS_BUCKET_NAME:-}
- OBS_PATH_PREFIX=${OBS_PATH_PREFIX:-weknora/}
- OBS_USE_SSL=${OBS_USE_SSL:-true}
- OBS_PROXY_DOMAIN=${OBS_PROXY_DOMAIN:-}
# ----- 阿里云 OSS(STORAGE_TYPE=oss)-----
- OSS_ENDPOINT=${OSS_ENDPOINT:-}
- OSS_REGION=${OSS_REGION:-}
- OSS_ACCESS_KEY=${OSS_ACCESS_KEY:-}
- OSS_SECRET_KEY=${OSS_SECRET_KEY:-}
- OSS_BUCKET_NAME=${OSS_BUCKET_NAME:-}
- OSS_PATH_PREFIX=${OSS_PATH_PREFIX:-weknora/}
- OSS_TEMP_BUCKET_NAME=${OSS_TEMP_BUCKET_NAME:-}
- OSS_TEMP_REGION=${OSS_TEMP_REGION:-}
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
# Ollama 不可用时仅告警不阻断(true 时生效)
- OLLAMA_OPTIONAL=${OLLAMA_OPTIONAL:-true}
# 批量 embedding 大小
- BATCH_EMBED_SIZE=${BATCH_EMBED_SIZE:-}
# VLM 单次 HTTP 请求超时(秒,默认 180)
- VLM_HTTP_TIMEOUT_SECONDS=${VLM_HTTP_TIMEOUT_SECONDS:-}
# LLM 流原始转储(排查上下文问题;1 启用,默认目录 ~/.weknora/investigate/llm-stream)
- WEKNORA_LLM_STREAM_RAW_DUMP=${WEKNORA_LLM_STREAM_RAW_DUMP:-}
- WEKNORA_LLM_STREAM_RAW_DUMP_DIR=${WEKNORA_LLM_STREAM_RAW_DUMP_DIR:-}
- STREAM_MANAGER_TYPE=${STREAM_MANAGER_TYPE:-}
- REDIS_ADDR=${REDIS_ADDR:-redis:6379}
- REDIS_USERNAME=${REDIS_USERNAME:-}
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
- REDIS_DB=${REDIS_DB:-}
- REDIS_PREFIX=${REDIS_PREFIX:-}
# Redis TLS(托管 Redis 加密传输;REDIS_USE_TLS=true 时生效,详见 .env.example)
- REDIS_TLS_SERVER_NAME=${REDIS_TLS_SERVER_NAME:-}
- REDIS_TLS_INSECURE_SKIP_VERIFY=${REDIS_TLS_INSECURE_SKIP_VERIFY:-}
# Redis 频道命名空间后缀(多部署共享同一 Redis 时隔离用)
- WEKNORA_REDIS_NAMESPACE=${WEKNORA_REDIS_NAMESPACE:-}
# Asynq 客户端 Redis 读写超时(毫秒,默认 500)
- WEKNORA_REDIS_OP_TIMEOUT_MS=${WEKNORA_REDIS_OP_TIMEOUT_MS:-}
# 知识图谱唯一开关是 NEO4J_ENABLE(见下方)。ENABLE_GRAPH_RAG 自 v0.1.6 起已被
# NEO4J_ENABLE 取代,Go 主应用不再读取,此处不再透传。
- NEO4J_ENABLE=${NEO4J_ENABLE:-}
- NEO4J_URI=${NEO4J_URI:-bolt://neo4j:7687}
- NEO4J_USERNAME=${NEO4J_USERNAME:-neo4j}
- NEO4J_PASSWORD=${NEO4J_PASSWORD:-password}
# 数据库中 API Key 等敏感字段落盘加密的 AES-256 主密钥(必须为 32 字节)。
# 加密范围:tenants.api_key、模型 API key、向量库凭证、web search provider key、
# WeKnoraCloud.AppSecret 等。缺失或长度不对时,已加密字段无法解密(UI 显示为空,
# 需重新填写)。生产环境必须显式设置且妥善保管,丢失则所有加密数据不可恢复。
# (TENANT_AES_KEY / CRYPTO_MASTER_KEY / CRYPTO_SALT 已废弃:v0.4.0 起加密统一
# 改用 SYSTEM_AES_KEY,Go 主应用不再读取这三个变量。)
- SYSTEM_AES_KEY=${SYSTEM_AES_KEY:-}
- SSRF_WHITELIST=${SSRF_WHITELIST:-}
# 保留原始 URL 的图片域名白名单(逗号分隔,不替换为 provider://)
- IMAGE_HOST_KEEP_URL=${IMAGE_HOST_KEEP_URL:-}
# Always allow the optional searxng sidecar, bundled vector stores, and
# the default MinIO storage host (compose service names). User-controlled
# endpoints are SSRF-validated and these private docker-network hosts
# would otherwise be rejected; whitelisting them keeps the
# out-of-box experience. Merged on top of SSRF_WHITELIST so user
# overrides don't clobber it. (postgres is intentionally NOT listed: it
# is not a user-registerable vector store, and whitelisting the app's own
# DB host would skip the 5432 port block.)
# doris-be: Stream Load follows FE→BE redirects that must receive Basic
# credentials on whitelisted BE hosts.
- SSRF_WHITELIST_EXTRA=${SSRF_WHITELIST_EXTRA:-searxng,qdrant,milvus,weaviate,doris-fe,doris-be,minio}
- CONCURRENCY_POOL_SIZE=${CONCURRENCY_POOL_SIZE:-5}
- JWT_SECRET=${JWT_SECRET:-}
# File size limit (in MB)
- MAX_FILE_SIZE_MB=${MAX_FILE_SIZE_MB:-50}
# Skill zip / GitHub source download cap (in MB); the GitHub zipball is the whole repo, not the skill subtree
- MAX_SKILL_BUNDLE_SIZE_MB=${MAX_SKILL_BUNDLE_SIZE_MB:-256}
# 文档处理任务总超时(Go duration,默认 2h)
- WEKNORA_DOCUMENT_PROCESS_TIMEOUT=${WEKNORA_DOCUMENT_PROCESS_TIMEOUT:-}
# 单次 DocReader RPC 超时(默认 30m,须小于上一项)
- WEKNORA_DOCREADER_CALL_TIMEOUT=${WEKNORA_DOCREADER_CALL_TIMEOUT:-}
# 知识库巡检回收(回收卡在 processing 的脏数据;false 禁用,不建议)
- WEKNORA_HOUSEKEEPING_ENABLED=${WEKNORA_HOUSEKEEPING_ENABLED:-}
# 自定义 Skills 目录(挂载后指定,免重建镜像)
- WEKNORA_SKILLS_DIR=${WEKNORA_SKILLS_DIR:-}
- WEKNORA_CHAT_ATTACHMENT_TTL_HOURS=${WEKNORA_CHAT_ATTACHMENT_TTL_HOURS:-24}
- WEKNORA_CHAT_ATTACHMENT_WAIT_TIMEOUT_SEC=${WEKNORA_CHAT_ATTACHMENT_WAIT_TIMEOUT_SEC:-60}
- WEKNORA_CHAT_ATTACHMENT_OCR_CONCURRENCY=${WEKNORA_CHAT_ATTACHMENT_OCR_CONCURRENCY:-8}
- WEKNORA_CHAT_ATTACHMENT_OCR_MAX_PAGES=${WEKNORA_CHAT_ATTACHMENT_OCR_MAX_PAGES:-8}
# Agent LLM call timeout
- WEKNORA_AGENT_LLM_TIMEOUT=${WEKNORA_AGENT_LLM_TIMEOUT:-}
- WEKNORA_AGENT_TOOL_APPROVAL_TIMEOUT=${WEKNORA_AGENT_TOOL_APPROVAL_TIMEOUT:-}
# MCP 工具人工审批失败时是否放行(true 放行 / 默认 fail-close 拒绝)
- WEKNORA_AGENT_TOOL_APPROVAL_FAIL_OPEN=${WEKNORA_AGENT_TOOL_APPROVAL_FAIL_OPEN:-}
# Tenant / RBAC(详见 docs/RBAC说明.md 与 .env.example 注释)
# - WEKNORA_TENANT_ENABLE_RBAC: 是否启用空间角色强制鉴权(true / false),默认 true
# - WEKNORA_TENANT_MAX_OWNED_PER_USER: 单个非超管自助创建空间的上限
# >0 强制限额;=0 走 handler 默认;<0 关闭限额(不建议共享部署使用)
# - WEKNORA_TENANT_AUTO_CREATE_API_KEY: 创建空间时自动生成 full_access API Key 并在响应中返回 token
# (兼容旧版默认下发 API Key 行为,破坏性变更回退开关;true / false,默认 false)
- WEKNORA_TENANT_ENABLE_RBAC=${WEKNORA_TENANT_ENABLE_RBAC:-}
- WEKNORA_TENANT_MAX_OWNED_PER_USER=${WEKNORA_TENANT_MAX_OWNED_PER_USER:-}
- WEKNORA_TENANT_AUTO_CREATE_API_KEY=${WEKNORA_TENANT_AUTO_CREATE_API_KEY:-}
- WEKNORA_ASYNQ_CORE_CONCURRENCY=${WEKNORA_ASYNQ_CORE_CONCURRENCY:-8}
- WEKNORA_ASYNQ_POSTPROCESS_CONCURRENCY=${WEKNORA_ASYNQ_POSTPROCESS_CONCURRENCY:-2}
- WEKNORA_ASYNQ_ENRICHMENT_CONCURRENCY=${WEKNORA_ASYNQ_ENRICHMENT_CONCURRENCY:-12}
- WEKNORA_ASYNQ_MAINTENANCE_CONCURRENCY=${WEKNORA_ASYNQ_MAINTENANCE_CONCURRENCY:-4}
- WEKNORA_ASYNQ_SHARED_CONCURRENCY=${WEKNORA_ASYNQ_SHARED_CONCURRENCY:-6}
- WEKNORA_WIKI_ASYNQ_CONCURRENCY=${WEKNORA_WIKI_ASYNQ_CONCURRENCY:-8}
- WEKNORA_MODEL_MAX_CONCURRENCY=${WEKNORA_MODEL_MAX_CONCURRENCY:-32}
- APK_MIRROR_ARG=${APK_MIRROR_ARG:-}
- WEKNORA_BOOTSTRAP_SYSTEM_ADMIN_EMAIL=${WEKNORA_BOOTSTRAP_SYSTEM_ADMIN_EMAIL:-}
# ========== OIDC 认证(可选,OIDC_AUTH_ENABLE=true 启用,详见 .env.example)==========
- OIDC_AUTH_ENABLE=${OIDC_AUTH_ENABLE:-false}
- OIDC_AUTH_ISSUER_URL=${OIDC_AUTH_ISSUER_URL:-}
- OIDC_AUTH_DISCOVERY_URL=${OIDC_AUTH_DISCOVERY_URL:-}
- OIDC_AUTH_PROVIDER_DISPLAY_NAME=${OIDC_AUTH_PROVIDER_DISPLAY_NAME:-}
- OIDC_AUTH_CLIENT_ID=${OIDC_AUTH_CLIENT_ID:-}
- OIDC_AUTH_CLIENT_SECRET=${OIDC_AUTH_CLIENT_SECRET:-}
- OIDC_AUTH_AUTHORIZATION_ENDPOINT=${OIDC_AUTH_AUTHORIZATION_ENDPOINT:-}
- OIDC_AUTH_TOKEN_ENDPOINT=${OIDC_AUTH_TOKEN_ENDPOINT:-}
- OIDC_AUTH_USER_INFO_ENDPOINT=${OIDC_AUTH_USER_INFO_ENDPOINT:-}
- OIDC_AUTH_JWKS_URI=${OIDC_AUTH_JWKS_URI:-}
- OIDC_AUTH_SCOPES=${OIDC_AUTH_SCOPES:-}
- OIDC_USER_INFO_MAPPING_USER_NAME=${OIDC_USER_INFO_MAPPING_USER_NAME:-}
- OIDC_USER_INFO_MAPPING_EMAIL=${OIDC_USER_INFO_MAPPING_EMAIL:-}
# 飞书云文档解析模式 export:导出为docx,blocks:根据飞书云文档块解析为markdown。默认为:export
- FEISHU_DOCX_PARSE_MODE=${FEISHU_DOCX_PARSE_MODE:-export}
depends_on:
redis:
condition: service_started
postgres:
condition: service_healthy
docreader:
condition: service_healthy
networks:
- WeKnora-network
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
# Sandbox 镜像:仅用于 build/pull,非常驻。Docker 沙箱默认关闭,需
# WEKNORA_SANDBOX_DOCKER_ENABLED=true 并挂载 docker.sock 后,app 才会按会话创建容器。
sandbox:
image: wechatopenai/weknora-sandbox:${WEKNORA_VERSION:-latest}
container_name: WeKnora-sandbox
build:
context: .
dockerfile: docker/Dockerfile.sandbox
target: sandbox
profiles:
- full
command: ["true"]
restart: "no"
docreader:
image: wechatopenai/weknora-docreader:${WEKNORA_VERSION:-latest}
build:
context: .
dockerfile: docker/Dockerfile.docreader
args:
- APT_MIRROR=${APT_MIRROR:-}
container_name: WeKnora-docreader
# docreader gRPC ships without auth/TLS and is only consumed by the app
# container over the WeKnora-network (DOCREADER_ADDR defaults to
# docreader:50051), so we don't publish 50051 to the host. `expose` keeps
# the port reachable inside the compose network for documentation only.
# If you need to call docreader from outside the host (e.g. for debugging),
# add a `ports:` entry locally, preferably bound to 127.0.0.1.
expose:
- "50051"
volumes:
- docreader-tmp:/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:-}
# ========== 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}
# ========== 解析通用 ==========
- 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
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
# OpenDataLoader hybrid backend (optional). Default --no-ocr (no EasyOCR/libGL).
# Local build only — not published to Docker Hub; use --profile odl-hybrid --build.
odl-hybrid:
build:
context: .
dockerfile: docker/Dockerfile.odl-hybrid
image: weknora-odl-hybrid:local
container_name: WeKnora-odl-hybrid
profiles:
- odl-hybrid
expose:
- "5002"
environment:
- ODL_HYBRID_EXTRA_ARGS=${ODL_HYBRID_EXTRA_ARGS:---no-ocr}
networks:
- WeKnora-network
restart: unless-stopped
# 修改的PostgreSQL配置
postgres:
image: paradedb/paradedb:v0.22.2-pg17
container_name: WeKnora-postgres
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME}
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- WeKnora-network
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
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
restart: always
networks:
- WeKnora-network
# One-shot init: copy the read-only template settings.yml into a named volume
# before searxng starts. Necessary because the searxng entrypoint sed-replaces
# `ultrasecretkey` in /etc/searxng/settings.yml in place; bind-mounting the
# repo file directly would write the resolved SEARXNG_SECRET back into the
# working tree (and prevent re-templating on subsequent restarts).
searxng-init:
image: busybox:1.36
container_name: WeKnora-searxng-init
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:/etc/searxng
restart: "no"
networks:
- WeKnora-network
profiles:
- searxng
- full
searxng:
image: searxng/searxng:latest
container_name: WeKnora-searxng
# Bind to loopback by default; override SEARXNG_BIND=0.0.0.0 only after
# rotating SEARXNG_SECRET and (optionally) re-enabling limiter in settings.yml.
ports:
- "${SEARXNG_BIND:-127.0.0.1}:${SEARXNG_PORT:-8888}:8080"
volumes:
- searxng_config:/etc/searxng
environment:
- SEARXNG_BASE_URL=http://localhost:${SEARXNG_PORT:-8888}/
- INSTANCE_NAME=weknora-searxng
# Default value lets `docker compose --profile searxng up` work zero-config.
# Override via .env (e.g. `openssl rand -hex 32`) before exposing the
# instance beyond the default 127.0.0.1 bind, since secret_key signs
# image-proxy URLs and a shared default would be guessable by anyone.
- 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
profiles:
- searxng
- full
minio:
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
container_name: WeKnora-minio
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:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
networks:
- WeKnora-network
profiles:
- minio
- full
neo4j:
image: neo4j:2025.10.1
container_name: WeKnora-neo4j
volumes:
- neo4j-data:/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
profiles:
- neo4j
- full
qdrant:
image: qdrant/qdrant:v1.16.2
container_name: WeKnora-qdrant
ports:
- "${QDRANT_REST_PORT:-6333}:6333"
- "${QDRANT_PORT:-6334}:6334"
volumes:
- qdrant_data:/qdrant/storage
networks:
- WeKnora-network
restart: unless-stopped
profiles:
- qdrant
- full
milvus:
image: milvusdb/milvus:v2.6.11
container_name: WeKnora-milvus
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:
- "19530:19530"
- "9091:9091"
volumes:
- milvus_data:/var/lib/milvus
networks:
- WeKnora-network
restart: unless-stopped
profiles:
- milvus
weaviate:
image: semitechnologies/weaviate:1.28.4
container_name: WeKnora-weaviate
environment:
- PERSISTENCE_DATA_PATH=/var/lib/weaviate
- CLUSTER_HOSTNAME=node1
- DEFAULT_VECTORIZER_MODULE=none
- ENABLE_MODULES=none
- AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true
- CLUSTER_GOSSIP_BIND_PORT=7000
- CLUSTER_DATA_BIND_PORT=7001
- RAFT_BOOTSTRAP_EXPECT=1
ports:
- "9035:8080"
- "50052:50051"
volumes:
- weaviate_data:/var/lib/weaviate
networks:
- WeKnora-network
restart: unless-stopped
profiles:
- weaviate
# ---------------------------------------------------------------------------
# Apache Doris 4.1(FE + BE 单实例 standalone 部署,opt-in via `--profile doris`)
#
# 版本要求:必须 >= 3.0(HNSW ANN 索引、cosine_distance_approximate、
# Stream Load partial_columns)。本 compose 使用 4.1 官方稳定 tag。
#
# 端口说明:
# - 9030 (FE MySQL):WeKnora 主链路读写、SQL 查询都走此端口。
# - 8030 (FE HTTP):Stream Load partial update 入口。
# - 8040 (BE) :BE 心跳与数据传输;FE 会把 stream load 重定向到 BE。
#
# 启用方式:
# docker compose --profile doris up -d
#
# 数据持久化在 doris_fe_data / doris_be_data 卷中。
# ---------------------------------------------------------------------------
doris-fe:
image: apache/doris:fe-4.1.0
container_name: WeKnora-doris-fe
hostname: doris-fe
environment:
- FE_SERVERS=fe1:doris-fe:9010
- FE_ID=1
ports:
- "${DORIS_FE_HTTP_PORT:-8030}:8030"
- "${DORIS_FE_MYSQL_PORT:-9030}:9030"
volumes:
- doris_fe_meta:/opt/apache-doris/fe/doris-meta
- doris_fe_log:/opt/apache-doris/fe/log
networks:
- WeKnora-network
restart: unless-stopped
profiles:
- doris
doris-be:
image: apache/doris:be-4.1.0
container_name: WeKnora-doris-be
hostname: doris-be
environment:
- FE_SERVERS=fe1:doris-fe:9010
- BE_ADDR=doris-be:9050
depends_on:
- doris-fe
ports:
- "${DORIS_BE_HTTP_PORT:-8040}:8040"
volumes:
- doris_be_storage:/opt/apache-doris/be/storage
- doris_be_log:/opt/apache-doris/be/log
networks:
- WeKnora-network
restart: unless-stopped
profiles:
- doris
dex:
image: dexidp/dex:latest
container_name: dex
ports:
- "5556:5556"
volumes:
- ./misc/dex-config.yaml:/etc/dex/config.yaml
command: ["dex", "serve", "/etc/dex/config.yaml"]
profiles:
- dex
- full
# ---------------------------------------------------------------------------
# Langfuse 自建可观测栈 (opt-in via `--profile langfuse`)
#
# 为了最小化资源占用,Langfuse 会复用 WeKnora 已有的 postgres 和 redis:
# - postgres:在 WeKnora-postgres 中创建独立的 "langfuse" 数据库
# - redis :复用 WeKnora-redis 的 DB 1(WeKnora 自己用 DB 0)
# 需要新增的只有 Langfuse 应用自身 + ClickHouse(OLAP)+ 专用 MinIO(事件/媒体 S3)。
#
# 启用方式:
# docker compose --profile langfuse up -d
# 启用后:
# 1. Langfuse UI: http://localhost:3000 (首次访问注册管理员并在 Settings → API Keys 生成 key)
# 2. 在 .env 里把 LANGFUSE_HOST 改成 http://langfuse-web:3000(容器间通信)
# 并填入刚生成的 LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY
# 3. 重启 app 容器:docker compose up -d app
# 相关默认密码已写成占位符,生产部署务必用 openssl rand 重新生成!
# ---------------------------------------------------------------------------
# 一次性初始化脚本:在已有的 WeKnora-postgres 里创建 langfuse 数据库(幂等,数据库已存在则跳过)
# 复用 WeKnora-postgres 的 ParadeDB 镜像,避免多拉一个 postgres 镜像
langfuse-db-init:
image: paradedb/paradedb:v0.22.2-pg17
container_name: WeKnora-langfuse-db-init
depends_on:
postgres:
condition: service_healthy
environment:
PGPASSWORD: ${DB_PASSWORD}
# ${LANGFUSE_DB_NAME:-langfuse} / ${DB_USER} 由 compose 解析成字面量后再传给 shell;
# 脚本中需要 shell 自己展开的变量(无)一律用 $$ 转义。
entrypoint: ["sh", "-c"]
command:
- |
set -e
echo "[langfuse-db-init] ensuring database '${LANGFUSE_DB_NAME:-langfuse}' exists in WeKnora-postgres..."
# 先刷新现有库的 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
restart: "no"
profiles:
- langfuse
- full
langfuse-clickhouse:
image: clickhouse/clickhouse-server:24.8
container_name: WeKnora-langfuse-clickhouse
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:/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: 10s
networks:
- WeKnora-network
profiles:
- langfuse
- full
langfuse-minio:
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
container_name: WeKnora-langfuse-minio
restart: unless-stopped
entrypoint: sh
# 启动前创建 `langfuse` 桶,Langfuse 开箱即用
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:
# 9100:S3 API(浏览器上传媒体时会直连此端口,必须对外暴露)
- "${LANGFUSE_MINIO_S3_PORT:-9100}:9000"
# 9101:MinIO 控制台(可选,方便排障)
- "${LANGFUSE_MINIO_CONSOLE_PORT:-9101}:9001"
volumes:
- langfuse_minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
timeout: 10s
retries: 5
networks:
- WeKnora-network
profiles:
- langfuse
- full
langfuse-worker:
image: langfuse/langfuse-worker:3
container_name: WeKnora-langfuse-worker
restart: unless-stopped
depends_on: &langfuse-depends-on
# 复用 WeKnora 已有的 postgres/redis,需要先完成 langfuse 数据库的创建
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 编码,
# 避免 DB_PASSWORD 含 '@' / '#' 等字符导致 Prisma 无法解析 DATABASE_URL (P1013)。
# Langfuse 镜像基于 Node.js,直接用 node 的 encodeURIComponent,不引外部依赖。
# 注意:compose 覆盖 entrypoint 会清空镜像默认 CMD,因此在 wrapper 末尾写死 exec 的原始命令。
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-env
# 原始凭证(未 URL 编码),由 entrypoint wrapper 读取并组装成 DATABASE_URL / REDIS_CONNECTION_STRING
_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 / ENCRYPTION_KEY 生产环境务必重新生成:
# SALT: openssl rand -base64 32
# ENCRYPTION_KEY: openssl rand -hex 32
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}
# 媒体上传用的外部 endpoint(浏览器直连),默认指向宿主机映射的 9100 端口
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
profiles:
- langfuse
- full
langfuse-web:
image: langfuse/langfuse:3
container_name: WeKnora-langfuse-web
restart: unless-stopped
depends_on: *langfuse-depends-on
ports:
- "${LANGFUSE_WEB_PORT:-3000}:3000"
# 同样走 wrapper entrypoint,末尾写死 web 的原始启动命令(见镜像 Dockerfile 的 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
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-env
# 可选:首次启动时自动创建组织、项目和管理员(填了就不用在 UI 上手动注册)
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
profiles:
- langfuse
- full
mcp:
build:
context: ./mcp-server
container_name: WeKnora-mcp
ports:
- "${MCP_PORT:-8082}:8000"
environment:
- WEKNORA_BASE_URL=http://app:8080/api/v1
- WEKNORA_API_KEY=${WEKNORA_API_KEY:-}
# HTTP/SSE 传输必填:mcp-server 启动时若缺少此项会拒绝启动(客户端以 Authorization: Bearer 携带)
- MCP_SERVER_AUTH_TOKEN=${MCP_SERVER_AUTH_TOKEN:-}
# 调用 WeKnora REST 的读超时(秒,默认 300)
- WEKNORA_CHAT_TIMEOUT=${WEKNORA_CHAT_TIMEOUT:-300}
# 是否校验 WeKnora 后端 TLS 证书(默认 true;自签证书可设 false)
- WEKNORA_VERIFY_SSL=${WEKNORA_VERIFY_SSL:-true}
# 允许上传的目录白名单(逗号分隔;留空禁用文件上传工具)
- MCP_ALLOWED_UPLOAD_DIRS=${MCP_ALLOWED_UPLOAD_DIRS:-}
depends_on:
app:
condition: service_healthy
networks:
- WeKnora-network
restart: unless-stopped
profiles:
- full
networks:
WeKnora-network:
driver: bridge
volumes:
postgres-data:
data-files:
docreader-tmp:
minio_data:
neo4j-data:
qdrant_data:
milvus_data:
weaviate_data:
doris_fe_meta:
doris_fe_log:
doris_be_storage:
doris_be_log:
langfuse_clickhouse_data:
langfuse_clickhouse_logs:
langfuse_minio_data:
searxng_config: