deer-flow/docker/docker-compose.yaml
Hyeonsang Cho 1dfd307c92
fix(docker): strip the IPv6 listen in the production nginx launcher too (#5900)
* fix(docker): strip the IPv6 listen in the production nginx launcher too

docker/nginx/nginx.conf listens on 2026 and [::]:2026. On a host booted
with ipv6.disable=1 (or a kernel without IPv6) nginx refuses to start —
socket() [::]:2026 failed (97: Address family not supported by protocol) —
so the container restart-loops under `restart: unless-stopped` and
`make up` waits on a readiness probe that never turns healthy. #2027 added
a launcher guard that deletes the IPv6 listen when /proc/net/if_inet6 is
absent, but only to docker-compose-dev.yaml; the Helm chart mirrors it;
docker-compose.yaml — the file behind `make up` and scripts/deploy.sh —
still ran the bare copy-and-start command.

Give the production nginx service the same launcher as the dev file:
`set -e`, copy the template, guard, and `exec nginx` so nginx is PID 1 and
receives docker stop's signal directly.

The new test pins the guard in all three launchers (prod, dev, Helm),
requires the three sed patterns to be identical so they cannot drift again,
checks the guard edits the file nginx actually loads, and applies the
pattern to the shipped compose and Helm configs to confirm it removes only
the IPv6 listen. Verified on nginx:alpine: BusyBox sed strips exactly that
line and `nginx -t` accepts the result; the rendered production launcher
runs unchanged when IPv6 is present.

* docs(changelog): reference #5900 in the nginx IPv6 guard entries
2026-09-27 07:36:35 +08:00

221 lines
10 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 with in-process RunManager + StreamBridge
# singletons -- run state lives in this worker's memory. Default to a single
# worker: the Redis stream bridge solves cross-worker SSE delivery and
# reconnect, but run cancel, request dedup, and per-worker IM channel
# services remain worker-local. Override GATEWAY_WORKERS only when the
# Redis stream bridge is enabled and those limitations are acceptable.
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}"
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