mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* fix(deploy): exec the gateway server so SIGTERM reaches uvicorn The production compose command, both CMDs in backend/Dockerfile and the Helm chart start the Gateway as `sh -c "cd backend && ... uv run ..."`. dash forks uv and stays PID 1 without a SIGTERM handler, so the kernel drops the stop signal: uvicorn never shuts down, the lifespan teardown (memory flush, run drain, service stops) never runs, and every stop waits out the grace period and ends in SIGKILL (exit 137). exec the server in every production launch path, as docker/dev-entrypoint.sh already does, and give the compose gateway `init: true` so orphaned grandchildren are still reaped once sh is no longer PID 1. A regression test pins exec in each launch path. Fixes #6835 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): link the gateway SIGTERM entry to #6836 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(gateway): decouple dependency sync from exec launch form * docs: compact startup guidance to pass inherited size limits * docs: trim inherited startup guidance after main merge --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
264 lines
13 KiB
YAML
264 lines
13 KiB
YAML
# DeerFlow Production Environment
|
|
# Usage: make up
|
|
#
|
|
# Requires Docker Compose >= 2.24. ../.env and ../frontend/.env are gitignored,
|
|
# so a fresh checkout has neither; the env_file entries below use the long-form
|
|
# path/required syntax so Compose loads them when present instead of aborting
|
|
# with "env file ... not found". Older clients reject this file with:
|
|
# services.<name>.env_file.0 must be a string
|
|
#
|
|
# 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
|
|
# DEER_FLOW_CREDENTIALS_KEY — at-rest Fernet key for stored credentials (deploy.sh persists it as $DEER_FLOW_HOME/.credentials_key)
|
|
#
|
|
# 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:
|
|
- path: ../frontend/.env
|
|
required: false
|
|
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: seven hooks
|
|
# bounded at 5s each (trash sweep, notification worker, channel service,
|
|
# scheduled task service, subagent batch 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 71s -- plus slack. backend/tests/_gateway_shutdown_budget.py
|
|
# pins the arithmetic.
|
|
command: sh -c "cd backend && PYTHONPATH=. exec 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
|
|
# `exec` in the command above lets the stop signal reach uvicorn: a `sh -c`
|
|
# that forks the server stays PID 1 with no SIGTERM handler, the kernel
|
|
# drops the signal, and the lifespan shutdown never runs; every stop then
|
|
# waits out stop_grace_period and ends in SIGKILL (#6835). `init` runs
|
|
# Docker's tini as PID 1: it forwards the signal to uv, which forwards it
|
|
# to uvicorn, and it reaps orphaned grandchildren, which `sh` used to do
|
|
# as a side effect of waiting.
|
|
init: true
|
|
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}
|
|
# At-rest key for stored credentials (scripts/deploy.sh resolves or generates it).
|
|
# Empty falls back to .credentials_key in the runtime home mounted above.
|
|
- DEER_FLOW_CREDENTIALS_KEY=${DEER_FLOW_CREDENTIALS_KEY:-}
|
|
# DooD path/network translation
|
|
- DEER_FLOW_HOST_BASE_DIR=${DEER_FLOW_HOME}
|
|
- DEER_FLOW_SANDBOX_HOST=host.docker.internal
|
|
# The login lockout counts failures per client IP, but every browser
|
|
# request reaches the Gateway from nginx. Trust nginx's X-Real-IP (it
|
|
# overwrites the header on every Gateway route) so one client's wrong
|
|
# passwords cannot lock everyone out. Hostname, re-resolved every 10s.
|
|
# `:-` on purpose: an empty value also means nginx, because every browser
|
|
# request arrives through it, so direct mode would only bring back the
|
|
# shared lockout. Set another list in .env to change whom to trust.
|
|
- AUTH_TRUSTED_PROXIES=${AUTH_TRUSTED_PROXIES:-nginx}
|
|
# 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:
|
|
- path: ../.env
|
|
required: false
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
depends_on:
|
|
redis:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
# /health/ready runs its probes (database, checkpointer, Redis stream
|
|
# bridge, provisioner) concurrently within a 3s endpoint deadline; the
|
|
# client timeout must stay above that bound. The gateway is unhealthy
|
|
# while Redis is unreachable; the provisioner verdict is report-only.
|
|
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:
|
|
- path: ../.env
|
|
required: false
|
|
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
|