fix(docker): don't abort Docker startup when .env is missing (#4956)

* fix(docker): create compose env files and keep Windows compose paths relative

Windows Docker reports a generic file-not-found when env_file targets are missing, or when compose paths are doubled. Make docker-start copy .env examples and invoke compose with filenames relative to docker/.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(docker): make dev compose env files optional and repair test fixture

Address review feedback on #4956.

[P1] prepare_compose_env aborted before the mocked COMPOSE_CMD in
test_compose_commands_set_deer_flow_root_before_compose, because the
temp root had no compose file or .env examples. Seed them in the
fixture so the preflight reaches the mock.

[P2] .env is gitignored, so a fresh clone has none and a direct
`docker compose -f docker/docker-compose-dev.yaml up --build` aborts on
Windows before scripts/docker.sh can help. Mark the dev env_file entries
`required: false` so a missing .env is not fatal, and document that
direct Compose must be run from the repository root.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(docker): declare Compose 2.24 floor and keep non-start commands read-only

Address the second review round on #4956.

- Document Compose >= 2.24 (CONTRIBUTING, README, compose header) and fail
  early from make docker-start with an actionable message; probe both
  `docker compose` and the hyphenated `docker-compose` binary.
- Document DEER_FLOW_ROOT for direct Compose callers (bash + PowerShell);
  leave the variable without a $PWD fallback because PowerShell/cmd do not
  export it.
- Split prepare_compose_env: compose_preflight is shared and read-only;
  ensure_env_files runs only from start.
- Expand tests for version boundaries, hyphenated fallback, env-file
  creation, and read-only stop/logs/restart behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(docker): reuse the probed Compose binary for wrapper operations

The version probe could accept a standalone docker-compose install while
COMPOSE_CMD stayed hardcoded to `docker compose`, so preflight passed and
start/logs/stop/restart then failed. Keep the selected executable in
COMPOSE_BIN (array), refresh COMPOSE_CMD from it in the current shell, and
extend the fallback test through an actual stop invocation.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
YZJF,YCDG,DJLY,ZZZB 2026-08-24 21:48:37 +08:00 committed by GitHub
parent 641a4147e7
commit 851e76661b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 434 additions and 37 deletions

View File

@ -13,6 +13,12 @@ Docker provides a consistent, isolated environment with all dependencies pre-con
#### Prerequisites
- Docker Desktop or Docker Engine
- Docker Compose **v2.24 or newer** (check with `docker compose version`). The dev
Compose file marks its `env_file` entries optional using the long-form
`path`/`required` syntax; older clients reject it with
`services.gateway.env_file.0 must be a string`. `make docker-start` verifies the
version and tells you to upgrade — direct `docker compose` callers get that raw
message instead.
- pnpm (for caching optimization)
#### Setup Steps
@ -43,6 +49,30 @@ Docker provides a consistent, isolated environment with all dependencies pre-con
```
`make docker-start` reads `config.yaml` and starts `provisioner` only for provisioner/Kubernetes sandbox mode.
Prefer this wrapper over invoking Compose yourself: it checks your Compose
version, creates the missing `.env` files, and exports `DEER_FLOW_ROOT`.
If you do run Compose directly, run it **from the repository root** and set
`DEER_FLOW_ROOT` to the absolute path of your checkout. Compose interpolates
that variable into host-side paths (`DEER_FLOW_HOST_BASE_DIR`,
`THREADS_HOST_PATH`) that the AIO and provisioner sandbox modes bind-mount;
leaving it unset renders them as `/backend/.deer-flow`, so those mounts
silently miss your checkout instead of failing:
```bash
# macOS / Linux
DEER_FLOW_ROOT="$PWD" docker compose -f docker/docker-compose-dev.yaml up --build
```
```powershell
# Windows PowerShell
$env:DEER_FLOW_ROOT = (Get-Location).Path
docker compose -f docker/docker-compose-dev.yaml up --build
```
Do not reuse that `-f` path from inside `docker/` — it resolves to
`docker/docker/docker-compose-dev.yaml` and fails with a file-not-found error.
All services will start with hot-reload enabled:
- Frontend changes are automatically reloaded
- Backend changes trigger automatic restart

View File

@ -260,6 +260,10 @@ Use the table below as a practical starting point when choosing how to run DeerF
#### Option 1: Docker (Recommended)
Requires Docker Desktop / Docker Engine and **Docker Compose v2.24+**
(`docker compose version`). Older Compose clients cannot parse the optional
`env_file` syntax in `docker/docker-compose-dev.yaml`.
**Development** (hot-reload, source mounts):
```bash

View File

@ -246,6 +246,10 @@ DeerFlow 新近集成了 BytePlus 自研的智能搜索与抓取工具集——[
#### 方式一Docker推荐
需要 Docker Desktop / Docker Engine以及 **Docker Compose v2.24+**
`docker compose version`)。更旧的 Compose 客户端无法解析
`docker/docker-compose-dev.yaml` 里的可选 `env_file` 语法。
**开发模式**(支持热更新,挂载源码):
```bash

View File

@ -118,3 +118,16 @@ def _bind_address(mapping: str) -> str | None:
# ADDR:HOST:CONTAINER -> bound; HOST:CONTAINER or CONTAINER -> unbound.
return segments[0] if len(segments) >= 3 else None
def test_dev_compose_env_files_are_optional():
"""Missing .env files must not fail `docker compose -f docker/docker-compose-dev.yaml`."""
compose = yaml.safe_load(COMPOSE_PATHS["dev"].read_text(encoding="utf-8"))
expected = {
"provisioner": "../.env",
"frontend": "../frontend/.env",
"gateway": "../.env",
}
for service_name, path in expected.items():
entries = compose["services"][service_name]["env_file"]
assert entries == [{"path": path, "required": False}], f"{service_name} env_file must be optional; got: {entries!r}"

View File

@ -106,17 +106,213 @@ sandbox:
assert _detect_mode_with_config(config) == "local"
@pytest.mark.parametrize("docker_command", ["logs --gateway", "restart"])
def test_compose_commands_set_deer_flow_root_before_compose(docker_command):
"""Log and restart commands should resolve mounts from the repository root."""
with tempfile.TemporaryDirectory() as tmpdir:
command = f"""
def _seed_compose_file(tmp_root: Path) -> None:
"""Give require_compose_file the file it validates."""
(tmp_root / "docker-compose-dev.yaml").write_text("services: {}\n", encoding="utf-8")
def _seed_env_examples(tmp_root: Path) -> None:
"""Provide the templates ensure_env_files copies from."""
(tmp_root / ".env.example").write_text("# test\n", encoding="utf-8")
frontend = tmp_root / "frontend"
frontend.mkdir(exist_ok=True)
(frontend / ".env.example").write_text("# test\n", encoding="utf-8")
def _run_docker_sh(tmp_root: Path, body: str) -> None:
"""Run docker.sh against a temp checkout, stubbing the real Compose version probe.
Keep SCRIPT_DIR at the real scripts/ directory so stop's cleanup-containers.sh
path still resolves; only PROJECT_ROOT / DOCKER_DIR are redirected.
"""
command = f"""
source '{SCRIPT_PATH}'
PROJECT_ROOT='{tmpdir}'
DOCKER_DIR='{tmpdir}'
PROJECT_ROOT='{tmp_root}'
DOCKER_DIR='{tmp_root}'
require_compose_version() {{ :; }}
{body}
"""
subprocess.check_call([BASH_EXECUTABLE, "-lc", command])
@pytest.mark.parametrize("docker_command", ["logs --gateway", "stop", "restart"])
def test_compose_commands_set_deer_flow_root_before_compose(docker_command):
"""Read-only compose commands should resolve mounts from the repository root."""
with tempfile.TemporaryDirectory() as tmpdir:
tmp_root = Path(tmpdir)
_seed_compose_file(tmp_root)
_run_docker_sh(
tmp_root,
f"""
COMPOSE_CMD=capture_compose
capture_compose() {{ test "${{DEER_FLOW_ROOT:-}}" = "$PROJECT_ROOT"; }}
unset DEER_FLOW_ROOT
{docker_command}
""",
)
@pytest.mark.parametrize("docker_command", ["logs --gateway", "stop", "restart"])
def test_read_only_commands_do_not_create_env_files(docker_command):
"""Only start may write configuration; logs/stop/restart must leave a checkout alone."""
with tempfile.TemporaryDirectory() as tmpdir:
tmp_root = Path(tmpdir)
_seed_compose_file(tmp_root)
_seed_env_examples(tmp_root)
_run_docker_sh(tmp_root, f"COMPOSE_CMD=true\n{docker_command}")
assert not (tmp_root / ".env").exists(), f"{docker_command} created .env"
assert not (tmp_root / "frontend" / ".env").exists(), f"{docker_command} created frontend/.env"
@pytest.mark.parametrize("docker_command", ["logs --gateway", "stop", "restart"])
def test_read_only_commands_run_without_env_examples(docker_command):
"""A missing .env.example must never block stopping or inspecting containers."""
with tempfile.TemporaryDirectory() as tmpdir:
tmp_root = Path(tmpdir)
_seed_compose_file(tmp_root)
_run_docker_sh(tmp_root, f"COMPOSE_CMD=true\n{docker_command}")
def test_ensure_env_files_copies_from_examples():
"""start's env-file step should create .env files from their examples when missing."""
with tempfile.TemporaryDirectory() as tmpdir:
tmp_root = Path(tmpdir)
_seed_env_examples(tmp_root)
command = f"""
source '{SCRIPT_PATH}'
PROJECT_ROOT='{tmp_root}'
ensure_env_files
"""
subprocess.check_call([BASH_EXECUTABLE, "-lc", command])
assert (tmp_root / ".env").is_file()
assert (tmp_root / "frontend" / ".env").is_file()
assert (tmp_root / ".env").read_text(encoding="utf-8") == "# test\n"
assert (tmp_root / "frontend" / ".env").read_text(encoding="utf-8") == "# test\n"
def test_ensure_env_files_leaves_existing_env_untouched():
"""ensure_env_files must not overwrite an already-present .env."""
with tempfile.TemporaryDirectory() as tmpdir:
tmp_root = Path(tmpdir)
_seed_env_examples(tmp_root)
(tmp_root / ".env").write_text("KEEP=me\n", encoding="utf-8")
frontend = tmp_root / "frontend"
(frontend / ".env").write_text("KEEP=frontend\n", encoding="utf-8")
command = f"""
source '{SCRIPT_PATH}'
PROJECT_ROOT='{tmp_root}'
ensure_env_files
"""
subprocess.check_call([BASH_EXECUTABLE, "-lc", command])
assert (tmp_root / ".env").read_text(encoding="utf-8") == "KEEP=me\n"
assert (frontend / ".env").read_text(encoding="utf-8") == "KEEP=frontend\n"
@pytest.mark.parametrize(
("reported_version", "expected_returncode"),
[
("2.23.3", 1),
("2.5.0", 1),
("2.24.0", 0),
("v2.40.2-desktop.1", 0),
("3.0.1", 0),
("", 0), # undetectable: warn, but do not block
],
)
def test_require_compose_version_enforces_minimum(reported_version, expected_returncode):
"""Old clients get our actionable message instead of a raw Compose parser error."""
# Stub both binaries so an empty plugin probe cannot fall through to the
# real hyphenated docker-compose installed on the developer machine.
command = f"""
source '{SCRIPT_PATH}'
docker() {{ echo '{reported_version}'; }}
docker-compose() {{ echo '{reported_version}'; }}
require_compose_version
"""
result = subprocess.run(
[BASH_EXECUTABLE, "-lc", command],
capture_output=True,
text=True,
encoding="utf-8",
)
assert result.returncode == expected_returncode, result.stdout + result.stderr
if expected_returncode != 0:
assert "too old" in result.stdout
assert "docs.docker.com/compose/install" in result.stdout
def test_require_compose_version_falls_back_to_hyphenated_binary():
"""Plugin missing + docker-compose 2.24: version check passes and stop uses that binary.
Regression for the half-fallback where require_compose_version accepted
docker-compose but COMPOSE_CMD stayed hardcoded to `docker compose`.
"""
with tempfile.TemporaryDirectory() as tmpdir:
tmp_root = Path(tmpdir)
_seed_compose_file(tmp_root)
marker = tmp_root / "hyphenated_invoke.txt"
command = f"""
source '{SCRIPT_PATH}'
PROJECT_ROOT='{tmp_root}'
DOCKER_DIR='{tmp_root}'
docker() {{
if [ "$1" = compose ]; then
echo "docker: unknown command" >&2
return 1
fi
command docker "$@"
}}
docker-compose() {{
if [ "$1" = version ]; then
echo '2.24.0'
return 0
fi
# Real wrapper ops (down/logs/...) must hit this binary, not `docker compose`.
printf '%s\n' "$*" > '{marker}'
}}
unset DEER_FLOW_ROOT
stop
"""
result = subprocess.run(
[BASH_EXECUTABLE, "-lc", command],
capture_output=True,
text=True,
encoding="utf-8",
)
assert result.returncode == 0, result.stdout + result.stderr
assert marker.is_file(), "stop never invoked docker-compose for the compose operation"
assert "down" in marker.read_text(encoding="utf-8")
def test_require_compose_version_rejects_old_hyphenated_binary():
"""An old docker-compose binary must still fail the floor check."""
command = f"""
source '{SCRIPT_PATH}'
docker() {{
if [ "$1" = compose ]; then
return 1
fi
command docker "$@"
}}
docker-compose() {{ echo '2.23.3'; }}
require_compose_version
"""
result = subprocess.run(
[BASH_EXECUTABLE, "-lc", command],
capture_output=True,
text=True,
encoding="utf-8",
)
assert result.returncode == 1, result.stdout + result.stderr
assert "too old" in result.stdout

View File

@ -1,5 +1,27 @@
# DeerFlow Development Environment
# Usage: docker-compose -f docker-compose-dev.yaml up --build
#
# Requires Docker Compose >= 2.24. The env_file entries below use the long-form
# path/required syntax so a missing ../.env or ../frontend/.env does not abort
# Compose on Windows ("file not found" / "Le fichier spécifique est
# introuvable"). Clients older than 2.24 do not understand that field and
# reject this file with:
# validating docker-compose-dev.yaml: services.gateway.env_file.0 must be a string
# make docker-start checks the version and says so; direct `docker compose`
# callers get the message above instead.
#
# Supported entry: from the repository root, run `make docker-start`. That
# wrapper checks the Compose version, creates missing .env files, exports
# DEER_FLOW_ROOT, and invokes Compose from this directory with a relative
# filename.
#
# Running Compose yourself works too, but export DEER_FLOW_ROOT as the absolute
# checkout path first (see CONTRIBUTING.md for the Windows form). Unset, the
# DEER_FLOW_HOST_BASE_DIR and THREADS_HOST_PATH values below render as
# /backend/.deer-flow, so the provisioner bind-mounts the wrong host paths:
# DEER_FLOW_ROOT="$PWD" docker compose -f docker/docker-compose-dev.yaml up --build
# The -f path resolves against your shell's directory, not against this file:
# from the repository root use docker/docker-compose-dev.yaml; from inside
# docker/ use the bare docker-compose-dev.yaml, as make docker-start does.
#
# Services:
# - nginx: Reverse proxy (port 2026)
@ -57,6 +79,12 @@ services:
# On Docker Desktop/OrbStack, use your actual host paths like /Users/username/...
# Set these in your shell before running docker-compose:
# export DEER_FLOW_ROOT=/absolute/path/to/deer-flow
# Deliberately left without a ${DEER_FLOW_ROOT:-...} fallback. The obvious
# candidate, $PWD, is exported by POSIX shells but not by PowerShell or
# cmd, so the default would resolve to an empty string on Windows — the
# platform this variable exists to get right. A wrong-but-plausible host
# path mounts an empty directory instead of failing, so we require the
# caller to be explicit; make docker-start sets it for you.
- THREADS_HOST_PATH=${DEER_FLOW_ROOT}/backend/.deer-flow/threads
# Per-user data base directory for user-scoped skill mounts
- DEER_FLOW_HOST_BASE_DIR=${DEER_FLOW_ROOT}/backend/.deer-flow
@ -74,7 +102,8 @@ services:
# The same value must be set on the gateway side via config.yaml sandbox.provisioner_api_key.
- PROVISIONER_API_KEY=${PROVISIONER_API_KEY:-}
env_file:
- ../.env
- path: ../.env
required: false
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
@ -140,7 +169,8 @@ services:
- DEER_FLOW_INTERNAL_GATEWAY_BASE_URL=http://gateway:8001
- DEER_FLOW_DEV_ALLOWED_ORIGINS=${DEER_FLOW_DEV_ALLOWED_ORIGINS:-127.0.0.1,::1}
env_file:
- ../frontend/.env
- path: ../frontend/.env
required: false
networks:
- deer-flow-dev
restart: unless-stopped
@ -213,7 +243,8 @@ services:
- 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
- path: ../.env
required: false
extra_hosts:
# For Linux: map host.docker.internal to host gateway
- "host.docker.internal:host-gateway"

View File

@ -12,8 +12,137 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
DOCKER_DIR="$PROJECT_ROOT/docker"
# Docker Compose command with project name
COMPOSE_CMD="docker compose -p deer-flow-dev -f docker-compose-dev.yaml"
# Docker Compose command with project name.
# Use a filename relative to DOCKER_DIR (we always `cd` there) so Windows
# Docker Desktop does not receive a Git Bash `/c/...` path it cannot open.
COMPOSE_FILE="docker-compose-dev.yaml"
# Selected by require_compose_version: prefer the V2 plugin, else hyphenated binary.
# Kept as an array so "docker compose" stays two words under set -u / quoting.
COMPOSE_BIN=(docker compose)
_refresh_compose_cmd() {
COMPOSE_CMD="${COMPOSE_BIN[*]} -p deer-flow-dev -f ${COMPOSE_FILE}"
}
_refresh_compose_cmd
# docker-compose-dev.yaml marks its env_file entries optional with the long-form
# `- path: ... / required: false` syntax, understood by Compose v2.24.0 and up.
# Older clients abort while parsing the file, before any preflight below can run.
COMPOSE_MIN_VERSION="2.24.0"
ensure_from_example() {
local dest="$1"
local src="$2"
local label="$3"
if [ -f "$dest" ]; then
return 0
fi
if [ -f "$src" ]; then
cp "$src" "$dest"
echo -e "${BLUE}Created ${label} from $(basename "$src")${NC}"
return 0
fi
echo -e "${YELLOW}${label} not found and no $(basename "$src") to copy from.${NC}"
echo "Create ${dest} before starting Docker."
exit 1
}
require_compose_file() {
if [ -f "$DOCKER_DIR/$COMPOSE_FILE" ]; then
return 0
fi
echo -e "${YELLOW}${COMPOSE_FILE} not found at ${DOCKER_DIR}/${COMPOSE_FILE}${NC}"
echo "Run this from the DeerFlow repository root, e.g. 'make docker-start'."
echo "Do not run 'docker compose -f docker/${COMPOSE_FILE}' from inside docker/ — that resolves to docker/docker/${COMPOSE_FILE}."
exit 1
}
# Prefer the Compose V2 plugin (`docker compose`); fall back to the legacy
# hyphenated binary (`docker-compose`) when the plugin is missing. Whatever
# binary answers is retained in COMPOSE_BIN / COMPOSE_CMD so start/logs/stop/
# restart use the same executable. Must run in the current shell (not $(...))
# so the COMPOSE_BIN assignment survives. Direct callers get no such check:
# see CONTRIBUTING.md.
_probe_compose() {
local out
out="$(docker compose version --short 2>/dev/null || true)"
if [ -n "$out" ]; then
COMPOSE_BIN=(docker compose)
_refresh_compose_cmd
COMPOSE_VERSION_RAW="$out"
return 0
fi
out="$(docker-compose version --short 2>/dev/null || true)"
if [ -n "$out" ]; then
COMPOSE_BIN=(docker-compose)
_refresh_compose_cmd
COMPOSE_VERSION_RAW="$out"
return 0
fi
COMPOSE_VERSION_RAW=""
return 1
}
# Fail with an actionable message instead of the parser error an older client
# emits for the optional env_file syntax.
require_compose_version() {
local raw major minor min_major min_minor
min_major="${COMPOSE_MIN_VERSION%%.*}"
min_minor="${COMPOSE_MIN_VERSION#*.}"
min_minor="${min_minor%%.*}"
COMPOSE_VERSION_RAW=""
_probe_compose || true
raw="${COMPOSE_VERSION_RAW#v}"
major="${raw%%.*}"
minor="${raw#*.}"
minor="${minor%%.*}"
major="${major//[!0-9]/}"
minor="${minor//[!0-9]/}"
if [ -z "$major" ] || [ -z "$minor" ]; then
echo -e "${YELLOW}⚠ Could not determine the Docker Compose version; ${COMPOSE_MIN_VERSION} or newer is required.${NC}"
return 0
fi
if [ "$major" -gt "$min_major" ] || { [ "$major" -eq "$min_major" ] && [ "$minor" -ge "$min_minor" ]; }; then
return 0
fi
echo -e "${YELLOW}✗ Docker Compose ${raw} is too old — ${COMPOSE_MIN_VERSION} or newer is required.${NC}"
echo "${COMPOSE_FILE} marks its env_file entries optional using the long-form"
echo "'- path: ... / required: false' syntax, which your client cannot parse."
echo "Update Docker Desktop, or install a current Compose v2 plugin:"
echo " https://docs.docker.com/compose/install/"
exit 1
}
# Compose interpolates ${DEER_FLOW_ROOT} into host-side paths
# (DEER_FLOW_HOST_BASE_DIR, THREADS_HOST_PATH) that AIO/provisioner sandbox
# modes bind-mount. Unset, those render as /backend/.deer-flow — a plausible
# looking absolute path on the wrong root, so mounts silently miss the checkout.
ensure_deer_flow_root() {
if [ -z "$DEER_FLOW_ROOT" ]; then
export DEER_FLOW_ROOT="$PROJECT_ROOT"
fi
}
# Read-only with respect to configuration; safe for logs/stop/restart.
compose_preflight() {
require_compose_file
require_compose_version
ensure_deer_flow_root
}
# Only `start` may create files. Compose env_file entries fail closed on Windows
# when .env is missing ("The specified file cannot be found" /
# "Le fichier spécifique est introuvable").
ensure_env_files() {
ensure_from_example "$PROJECT_ROOT/.env" "$PROJECT_ROOT/.env.example" ".env"
ensure_from_example "$PROJECT_ROOT/frontend/.env" "$PROJECT_ROOT/frontend/.env.example" "frontend/.env"
}
load_proxy_env_from_dotenv() {
local env_file="$PROJECT_ROOT/.env"
@ -189,6 +318,9 @@ start() {
echo "=========================================="
echo ""
# Validate the toolchain before creating any config files below.
compose_preflight
sandbox_mode="$(detect_sandbox_mode)"
services="redis frontend gateway nginx"
@ -207,7 +339,7 @@ start() {
exit 1
fi
echo -e "${YELLOW}Mounting host Docker socket into gateway (DooD = host root-equivalent). See SECURITY.md.${NC}"
COMPOSE_CMD="$COMPOSE_CMD -f $DOCKER_DIR/docker-compose.dood.yaml"
COMPOSE_CMD="$COMPOSE_CMD -f docker-compose.dood.yaml"
fi
echo -e "${BLUE}Runtime: Gateway embedded agent runtime${NC}"
@ -219,13 +351,11 @@ start() {
fi
echo ""
# Set DEER_FLOW_ROOT for provisioner if not already set
if [ -z "$DEER_FLOW_ROOT" ]; then
export DEER_FLOW_ROOT="$PROJECT_ROOT"
echo -e "${BLUE}Setting DEER_FLOW_ROOT=$DEER_FLOW_ROOT${NC}"
echo ""
fi
# Set by compose_preflight above; shown because the provisioner turns it into
# host-side bind-mount paths.
echo -e "${BLUE}Using DEER_FLOW_ROOT=$DEER_FLOW_ROOT${NC}"
echo ""
# Ensure config.yaml exists before starting.
if [ ! -f "$PROJECT_ROOT/config.yaml" ]; then
if [ -f "$PROJECT_ROOT/config.example.yaml" ]; then
@ -260,6 +390,7 @@ start() {
fi
fi
ensure_env_files
load_proxy_env_from_dotenv
echo "Building and starting containers..."
@ -283,12 +414,8 @@ start() {
logs() {
local service=""
# DEER_FLOW_ROOT is referenced in docker-compose-dev.yaml; set it before
# reading logs so Compose does not resolve mounted paths from an empty root.
if [ -z "$DEER_FLOW_ROOT" ]; then
export DEER_FLOW_ROOT="$PROJECT_ROOT"
fi
compose_preflight
case "$1" in
--frontend)
service="frontend"
@ -325,11 +452,7 @@ logs() {
# Stop Docker development environment
stop() {
# DEER_FLOW_ROOT is referenced in docker-compose-dev.yaml; set it before
# running compose down to suppress "variable is not set" warnings.
if [ -z "$DEER_FLOW_ROOT" ]; then
export DEER_FLOW_ROOT="$PROJECT_ROOT"
fi
compose_preflight
echo "Stopping Docker development services..."
cd "$DOCKER_DIR" && $COMPOSE_CMD down
echo "Cleaning up sandbox containers..."
@ -339,11 +462,7 @@ stop() {
# Restart Docker development environment
restart() {
# DEER_FLOW_ROOT is referenced in docker-compose-dev.yaml; set it before
# restarting services so Compose resolves mounted paths from this checkout.
if [ -z "$DEER_FLOW_ROOT" ]; then
export DEER_FLOW_ROOT="$PROJECT_ROOT"
fi
compose_preflight
echo "========================================"
echo " Restarting DeerFlow Docker Services"
echo "========================================"