mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
* fix(deploy): wire the multi-replica prerequisites into the Helm chart and compose The chart shipped gateway.replicas: 1 with a README citing the long-closed issue #3948, while its rendered config lacked run_ownership heartbeats and db run events: raising replicas made every starting Pod write its peers' lease-less runs off as orphans, and the Gateway's startup gate could not notice because its worker count only sees one Pod. Enable heartbeats and db run events in the default config, export DEER_FLOW_MULTI_INSTANCE from the replica count for the startup gate, generate and inject a shared AUTH_JWT_SECRET (per-Pod .jwt_secret files logged users out across replicas), declare a surge-then-drain rollout strategy, render a PodDisruptionBudget when replicas > 1, and bound uvicorn's graceful shutdown inside a 90s termination grace period. docker-compose gets the same uvicorn bound and a 60s stop_grace_period so the 30s memory flush is no longer SIGKILLed. Docs and comments now list what actually remains single-instance: IM channels, WeChat QR login and browser tools. * fix(deploy): honor existingAppSecret, declare multi-instance explicitly, cover the dev stack's shutdown Follow-up to the multi-replica prerequisites wiring, addressing review: - `deer-flow.appSecret` returns `existingAppSecret` when it is set. The gateway and frontend kept referencing the generated `<release>-app` Secret, which the chart stopped rendering, so every Pod sat in CreateContainerConfigError. The value is now declared in values.yaml. - `gateway.multiInstance` (boolean, default null) overrides the replica-derived DEER_FLOW_MULTI_INSTANCE, PodDisruptionBudget and AUTH_JWT_SECRET requirement for deployments scaled with kubectl or an HPA, which change the replica count without a Helm upgrade. - AUTH_JWT_SECRET is required, not optional, whenever the gateway is multi-instance: concurrently booting Pods otherwise race to write different `.jwt_secret` files on the home volume and sign sessions with different keys. - `gateway.uvicornGracefulShutdownSeconds: 0` is honored instead of being swallowed by `default 10`. - docker-compose-dev.yaml and dev-entrypoint.sh get the same uvicorn graceful-shutdown bound and `stop_grace_period: 60s`. - The chart README documents the one-time sign-out when upgrading a release that predates AUTH_JWT_SECRET and how to seed the previous key into the Secret first. - Tests read the shutdown budget from the Gateway through a shared helper instead of hard-coding 5/5/30, the raw-template string matches give way to `helm template` renders, and new renders cover multiInstance, existingAppSecret, a disabled PDB and a zero graceful-shutdown bound. * docs(changelog): link the multi-replica deploy entry to #6347 * fix(deploy): budget every bounded teardown hook and flag the multiInstance PDB window Review follow-ups on #6347: - The shutdown budget modeled only channel stop + run drain + memory flush (40s). The lifespan also runs the startup trash sweep, the notification delivery worker stop, browser-session close and the MCP session-pool close, each bounded at _SHUTDOWN_HOOK_TIMEOUT_SECONDS, plus a 1s retrieval-warm wait: about 61s worst case. _gateway_shutdown_budget.py now reads all of them from the Gateway, a drift guard pins the hook count against app.py, and both compose files move stop_grace_period from 60s to 90s (Helm's 90s already had room: 5 + 10 + 61). Comments and the chart README cite the full arithmetic. - gateway.multiInstance: true renders the PodDisruptionBudget immediately, which blocks drains of the single Pod until the scale-up happens; the README, values comment and both deployment guides say so and name podDisruptionBudget.enabled: false as the interim escape hatch.
232 lines
11 KiB
YAML
232 lines
11 KiB
YAML
# DeerFlow Production Environment
|
|
# Usage: make up
|
|
#
|
|
# Services:
|
|
# - nginx: Reverse proxy (port 2026, configurable via PORT env var)
|
|
# - frontend: Next.js production server
|
|
# - gateway: FastAPI Gateway API + agent runtime
|
|
# - redis: Redis Streams backend for cross-worker SSE stream bridge
|
|
# - provisioner: (optional) Sandbox provisioner for Kubernetes mode
|
|
#
|
|
# Key environment variables (set via environment/.env or scripts/deploy.sh):
|
|
# DEER_FLOW_PROJECT_ROOT — project root for relative runtime paths
|
|
# DEER_FLOW_HOME — runtime data dir, default .deer-flow under $DEER_FLOW_PROJECT_ROOT (or cwd)
|
|
# DEER_FLOW_CONFIG_PATH — path to config.yaml
|
|
# DEER_FLOW_EXTENSIONS_CONFIG_PATH — path to extensions_config.json
|
|
# DEER_FLOW_SKILLS_PATH — skills dir, default $DEER_FLOW_PROJECT_ROOT/skills
|
|
# DEER_FLOW_DOCKER_SOCKET — Docker socket path for aio/DooD mode, default /var/run/docker.sock (used only by the opt-in docker-compose.dood.yaml overlay)
|
|
# DEER_FLOW_REPO_ROOT — repo root (used for skills host path in DooD)
|
|
# BETTER_AUTH_SECRET — required for frontend auth/session security
|
|
# DEER_FLOW_INTERNAL_AUTH_TOKEN — shared internal Gateway auth token for multi-worker IM channels
|
|
#
|
|
# LangSmith tracing is disabled by default (LANGSMITH_TRACING=false).
|
|
# Set LANGSMITH_TRACING=true and LANGSMITH_API_KEY in .env to enable it.
|
|
#
|
|
# Access: http://localhost:${PORT:-2026}
|
|
|
|
services:
|
|
# ── Redis Stream Bridge ────────────────────────────────────────────────────
|
|
redis:
|
|
image: redis:7-alpine
|
|
container_name: deer-flow-redis
|
|
command: ["redis-server", "--appendonly", "yes"]
|
|
volumes:
|
|
- redis-data:/data
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
networks:
|
|
- deer-flow
|
|
restart: unless-stopped
|
|
|
|
# ── Reverse Proxy ──────────────────────────────────────────────────────────
|
|
nginx:
|
|
image: nginx:alpine
|
|
container_name: deer-flow-nginx
|
|
# Loopback-only by default: DeerFlow's agent can execute commands, so the
|
|
# documented default deployment is a local trusted environment. A bare
|
|
# "${PORT}:2026" would bind 0.0.0.0 instead, which does not match that.
|
|
# Set BIND_HOST=0.0.0.0 only behind your own TLS/auth front door, and
|
|
# complete first-run setup before the host becomes reachable.
|
|
ports:
|
|
- "${BIND_HOST:-127.0.0.1}:${PORT:-2026}:2026"
|
|
volumes:
|
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf.template:ro
|
|
# Same launcher as the dev compose file and the Helm chart: nginx.conf
|
|
# listens on [::]:2026 as well, and on a host with IPv6 disabled that
|
|
# listen makes nginx exit at startup (socket() ... 97: Address family not
|
|
# supported by protocol), so the container restart-loops and `make up`
|
|
# never becomes healthy. Strip the IPv6 listen when the kernel has no
|
|
# IPv6, and exec nginx so it is PID 1 for signal handling.
|
|
command:
|
|
- sh
|
|
- -c
|
|
- |
|
|
set -e
|
|
cp /etc/nginx/nginx.conf.template /etc/nginx/nginx.conf
|
|
test -e /proc/net/if_inet6 || sed -i '/^[[:space:]]*listen[[:space:]]\+\[::\]:2026[[:space:]]/d' /etc/nginx/nginx.conf
|
|
exec nginx -g 'daemon off;'
|
|
depends_on:
|
|
frontend:
|
|
condition: service_started
|
|
gateway:
|
|
condition: service_healthy
|
|
networks:
|
|
- deer-flow
|
|
restart: unless-stopped
|
|
|
|
# ── Frontend: Next.js Production ───────────────────────────────────────────
|
|
frontend:
|
|
build:
|
|
context: ../
|
|
dockerfile: frontend/Dockerfile
|
|
target: prod
|
|
args:
|
|
PNPM_STORE_PATH: ${PNPM_STORE_PATH:-/root/.local/share/pnpm/store}
|
|
NPM_REGISTRY: ${NPM_REGISTRY:-}
|
|
container_name: deer-flow-frontend
|
|
environment:
|
|
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
|
|
- DEER_FLOW_INTERNAL_GATEWAY_BASE_URL=http://gateway:8001
|
|
env_file:
|
|
- ../frontend/.env
|
|
networks:
|
|
- deer-flow
|
|
restart: unless-stopped
|
|
|
|
# ── Gateway API ────────────────────────────────────────────────────────────
|
|
gateway:
|
|
build:
|
|
context: ../
|
|
dockerfile: backend/Dockerfile
|
|
args:
|
|
APT_MIRROR: ${APT_MIRROR:-}
|
|
UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:0.11.1}
|
|
UV_INDEX_URL: ${UV_INDEX_URL:-https://pypi.org/simple}
|
|
UV_EXTRAS: ${UV_EXTRAS:-}
|
|
NPM_REGISTRY: ${NPM_REGISTRY:-}
|
|
LARK_CLI_NPM_VERSION: ${LARK_CLI_NPM_VERSION:-1.0.65}
|
|
container_name: deer-flow-gateway
|
|
# Gateway hosts the agent runtime (RunManager + StreamBridge) in process.
|
|
# Default to a single worker: GATEWAY_WORKERS > 1 is supported only with
|
|
# Postgres (database.backend: postgres, run_events.backend: db,
|
|
# run_ownership.heartbeat_enabled: true) and the Redis stream bridge wired
|
|
# below -- the Gateway refuses to start otherwise. IM channels, the WeChat
|
|
# QR login and browser tools stay worker-local, so keep one worker while
|
|
# they are enabled. --timeout-graceful-shutdown bounds how long open SSE
|
|
# connections may delay lifespan shutdown after SIGTERM; stop_grace_period
|
|
# below must cover that bound plus the lifespan's worst case: five hooks
|
|
# bounded at 5s each (trash sweep, notification worker, channel service,
|
|
# browser sessions, MCP session pool), the 1s retrieval-warm wait, the 5s
|
|
# run drain and the 30s memory flush (memory.shutdown_flush_timeout_seconds)
|
|
# -- about 61s -- plus slack. backend/tests/_gateway_shutdown_budget.py
|
|
# pins the arithmetic.
|
|
command: sh -c "cd backend && PYTHONPATH=. uv run --no-sync uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001 --workers ${GATEWAY_WORKERS:-1} --timeout-graceful-shutdown 10"
|
|
# Docker's 10s default SIGKILLed the 30s memory queue flush
|
|
# (memory.shutdown_flush_timeout_seconds) on every restart.
|
|
stop_grace_period: 90s
|
|
volumes:
|
|
- ${DEER_FLOW_CONFIG_PATH}:/app/backend/config.yaml:ro
|
|
# Writable on purpose: the Gateway edits this file at runtime (MCP
|
|
# enable/disable, PUT/PATCH /api/mcp/config, skill updates). config.yaml
|
|
# above stays read-only because no API writes it.
|
|
- ${DEER_FLOW_EXTENSIONS_CONFIG_PATH}:/app/backend/extensions_config.json
|
|
- ../skills:/app/skills:ro
|
|
- ${DEER_FLOW_HOME}:/app/backend/.deer-flow
|
|
# DooD: the host Docker socket is NOT mounted by default. It is added only
|
|
# for aio (pure-DooD) sandbox mode via the opt-in docker-compose.dood.yaml
|
|
# overlay (appended by scripts/deploy.sh). See backend/docs/CONFIGURATION.md
|
|
# Security Note section for details.
|
|
|
|
# CLI auth dirs (Claude Code / Codex) are NOT mounted by default: they
|
|
# expose the entire ~/.claude and ~/.codex (history, projects, global
|
|
# config, credentials) into the container. Mount them only when you use
|
|
# the Claude/Codex CLI login as a model provider or ACP agent, via the
|
|
# opt-in docker-compose.cli-auth.yaml overlay. Prefer an env token
|
|
# (CLAUDE_CODE_OAUTH_TOKEN, see .env.example / backend/docs/CONFIGURATION.md).
|
|
working_dir: /app
|
|
environment:
|
|
- CI=true
|
|
- DEER_FLOW_PROJECT_ROOT=/app
|
|
- DEER_FLOW_HOME=/app/backend/.deer-flow
|
|
- DEER_FLOW_CONFIG_PATH=/app/backend/config.yaml
|
|
- DEER_FLOW_EXTENSIONS_CONFIG_PATH=/app/backend/extensions_config.json
|
|
- DEER_FLOW_STREAM_BRIDGE_REDIS_URL=${DEER_FLOW_STREAM_BRIDGE_REDIS_URL:-redis://redis:6379/0}
|
|
- DEER_FLOW_CHANNELS_LANGGRAPH_URL=${DEER_FLOW_CHANNELS_LANGGRAPH_URL:-http://gateway:8001/api}
|
|
- DEER_FLOW_CHANNELS_GATEWAY_URL=${DEER_FLOW_CHANNELS_GATEWAY_URL:-http://gateway:8001}
|
|
- DEER_FLOW_INTERNAL_AUTH_TOKEN=${DEER_FLOW_INTERNAL_AUTH_TOKEN}
|
|
# DooD path/network translation
|
|
- DEER_FLOW_HOST_BASE_DIR=${DEER_FLOW_HOME}
|
|
- DEER_FLOW_SANDBOX_HOST=host.docker.internal
|
|
# Proxy values (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY) are inherited from ../.env via env_file.
|
|
# Only NO_PROXY is declared here so internal service hostnames are always exempt from the proxy.
|
|
- NO_PROXY=${NO_PROXY:-}${NO_PROXY:+,}localhost,127.0.0.1,::1,gateway,frontend,nginx,provisioner,openviking,host.docker.internal
|
|
- no_proxy=${no_proxy:-}${no_proxy:+,}localhost,127.0.0.1,::1,gateway,frontend,nginx,provisioner,openviking,host.docker.internal
|
|
env_file:
|
|
- ../.env
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
depends_on:
|
|
redis:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
# /health/ready runs both probes concurrently within a 3s endpoint
|
|
# deadline; the client timeout must stay above that bound.
|
|
test: ["CMD", "python", "-c", "import urllib.request; response = urllib.request.urlopen('http://127.0.0.1:8001/health/ready', timeout=5); raise SystemExit(0 if response.status == 200 else 1)"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 30
|
|
start_period: 20s
|
|
networks:
|
|
- deer-flow
|
|
restart: unless-stopped
|
|
|
|
# ── Sandbox Provisioner (optional, Kubernetes mode) ────────────────────────
|
|
provisioner:
|
|
build:
|
|
context: ./provisioner
|
|
dockerfile: Dockerfile
|
|
args:
|
|
APT_MIRROR: ${APT_MIRROR:-}
|
|
PIP_INDEX_URL: ${PIP_INDEX_URL:-}
|
|
container_name: deer-flow-provisioner
|
|
volumes:
|
|
- ~/.kube/config:/root/.kube/config:ro
|
|
environment:
|
|
- K8S_NAMESPACE=deer-flow
|
|
- SANDBOX_IMAGE=enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest
|
|
# Optional lark-cli init image (Pattern A). Empty ⇒ legacy runtime mount.
|
|
# Set to a published tag (e.g. deer-flow/lark-cli-init:v1.0.65) to provision
|
|
# the sandbox lark-cli runtime via an init container + emptyDir.
|
|
- LARK_CLI_INIT_IMAGE=${LARK_CLI_INIT_IMAGE:-}
|
|
# Optional lark-cli broker image (Pattern B, issue #4338). When set, the
|
|
# sandbox gets a shim + broker sidecar that holds the credentials, so the
|
|
# plaintext config/data are never mounted into the sandbox. Supersedes
|
|
# LARK_CLI_INIT_IMAGE when both are set. Empty ⇒ broker off.
|
|
- LARK_CLI_BROKER_IMAGE=${LARK_CLI_BROKER_IMAGE:-}
|
|
- THREADS_HOST_PATH=${DEER_FLOW_HOME}/threads
|
|
- DEER_FLOW_HOST_BASE_DIR=${DEER_FLOW_HOME}
|
|
- KUBECONFIG_PATH=/root/.kube/config
|
|
- NODE_HOST=host.docker.internal
|
|
- K8S_API_SERVER=https://host.docker.internal:26443
|
|
env_file:
|
|
- ../.env
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
networks:
|
|
- deer-flow
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:8002/health"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 6
|
|
volumes:
|
|
redis-data:
|
|
|
|
networks:
|
|
deer-flow:
|
|
driver: bridge
|