deer-flow Docker & Self-Hosted Deployment

GitHub

An open-source long-horizon SuperAgent harness that researches, codes, and creates. With the help of sandboxes, memories, tools, skill, subagents and...

RAW Compose
1. Pull Image (DOCKERHUB) docker pull library/redis
2. Docker Run (1-Click Launch) docker run -d --name deer-flow -p 2026:2026 --restart always redis:7-alpine
3. Podman Run podman run -d --name deer-flow -p 2026:2026 --restart always redis:7-alpine
4. Skopeo Inspect (Zero-Pull Manifest) skopeo inspect docker://docker.io/library/redis:7-alpine
5. ORAS Pull (OCI Artifact) oras pull redis:7-alpine
6. Compose Deployment docker compose up -d
docker/docker-compose.yaml
Download .yml
# 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=. 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}
      # 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