mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
* fix(security): add DEER_FLOW_CREDENTIALS_KEY for at-rest credential encryption
There was no at-rest encryption key. ChannelCredentialCipher had no
production caller: the channel service, the scheduled-task notification
outbox and the channel-connections router all built
ChannelConnectionRepository without a cipher, so store_credentials refused
(RuntimeError) and get_credentials always returned None.
- New harness module deerflow.config.credentials_key: env-only
DEER_FLOW_CREDENTIALS_KEY (comma-separated Fernet keys; first encrypts,
all decrypt via MultiFernet), CredentialsCipher with encrypt_text /
decrypt_text / rotate_text, a fernet:v2: prefix with legacy fernet:v1:
values still readable, and get_credentials_cipher() as the process-wide
entry point. Malformed keys are refused without echoing key material.
Without the variable a single instance generates
{base_dir}/.credentials_key. Never derived from AUTH_JWT_SECRET.
- New deerflow.config.secret_file.read_or_create_secret_file: O_EXCL
create (0600) plus read-back with a bounded settle window, shared by
.credentials_key, .jwt_secret (fixes two replicas cold-starting on a
shared volume keeping different JWT secrets) and the managed-model key.
- ChannelCredentialCipher now builds on CredentialsCipher (same
constructors); the Gateway loads the cipher off the event loop at
startup when channel_connections is enabled and passes it to every
ChannelConnectionRepository. Undecryptable values stay "missing + warn".
- Startup gate: a malformed key stops startup in any topology; a declared
multi-instance deployment with channel_connections enabled and no key
is refused. Plain multi-worker shares the generated file and is allowed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(deploy): generate and inject DEER_FLOW_CREDENTIALS_KEY in Helm, compose and deploy.sh
- Helm: the app Secret generates a Fernet key (randBytes 32 translated to
the urlsafe alphabet) and preserves it across upgrades via lookup like
AUTH_JWT_SECRET; the gateway reads it through an optional secretKeyRef
so a user-managed existingAppSecret without it still boots (the Gateway
gate refuses only multi-instance deployments that store credentials).
README, NOTES and values document backup, rotation and existingAppSecret.
- scripts/deploy.sh resolves the key shell -> .env -> persisted file ->
generated, persisting it as $DEER_FLOW_HOME/.credentials_key (0600,
noclobber/O_EXCL with read-back): the same file the Gateway generates in
its runtime home, so either side's key is the one both keep.
- Both compose files pass DEER_FLOW_CREDENTIALS_KEY=${...:-} to the gateway.
- .env.example entry with a generation hint; README and CHANGELOG notes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(security): make abandoned secret-file replacement single-winner
A rename is last-writer-wins, so replicas that restart together and all
find the same abandoned empty secret file could each replace it and keep
a different value. The process that exclusively creates
<name>.replacing now owns the replacement, re-checks the file is still
empty, and publishes by renaming the claim over it; peers wait for that
value, bounded so a crashed replacer cannot block startup forever.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(changelog): link DEER_FLOW_CREDENTIALS_KEY entries to #6611
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(security): name the setting that turns off a credentials-key consumer
The multi-instance refusal ended with "or disable
channel_connections.enabled=true"; it now says "or set
channel_connections.enabled=false".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(security): never expose a secret file before its value is written
O_EXCL published the name before its content, so a creator that stalled
mid-write left an empty file a peer replaced after the settle window; the
resumed creator then returned a value nobody persisted, or deleted the
peer's valid file when its own write failed. Creators now write and sync
a private temporary file and hard-link it to the name, which fails if
the name exists; a failed creator only removes its own temporary file.
Filesystems without hard links publish through the same single-winner
<name>.replacing claim that replaces abandoned empty files, so creation
and replacement are serialized there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(models): recover an empty managed-model key when no catalog exists
An interrupted first save could leave an empty key file; every later save
then passed empty bytes to Fernet and the endpoint returned 503 until the
file was removed by hand. An empty key with no catalog encrypted nothing,
so it now goes through the secret-file recovery path; next to an existing
catalog it is still refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(docker): pass the dotenv DEER_FLOW_CREDENTIALS_KEY through make docker-start
scripts/docker.sh runs Compose from docker/ without --env-file, so the dev
gateway's DEER_FLOW_CREDENTIALS_KEY=${DEER_FLOW_CREDENTIALS_KEY:-} entry
interpolated to empty and overrode the key the checkout .env supplied via
env_file. Export it from the dotenv like AUTH_TRUSTED_PROXIES.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
583 lines
21 KiB
Bash
Executable File
583 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -e
|
|
|
|
# Colors for output
|
|
GREEN='\033[0;32m'
|
|
BLUE='\033[0;34m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Get script directory
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
DOCKER_DIR="$PROJECT_ROOT/docker"
|
|
|
|
# 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"
|
|
# Dev stack project name. `logs --prod` swaps both values for the production
|
|
# stack started by `make up` (scripts/deploy.sh: project `deer-flow`).
|
|
COMPOSE_PROJECT="deer-flow-dev"
|
|
# 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 ${COMPOSE_PROJECT} -f ${COMPOSE_FILE}"
|
|
}
|
|
_refresh_compose_cmd
|
|
|
|
# Both compose files mark their 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"
|
|
}
|
|
|
|
# start runs Compose from docker/ without --env-file, so ${VAR} interpolation in
|
|
# docker-compose-dev.yaml sees the shell only, never the checkout .env. Export
|
|
# the .env values that interpolation consumes; a value already set in the shell
|
|
# (even empty) wins, as it does for Compose. AUTH_TRUSTED_PROXIES and
|
|
# DEER_FLOW_CREDENTIALS_KEY belong here because the Gateway's `environment:`
|
|
# entries (defaulting to nginx and to empty) outrank the same keys loaded through
|
|
# env_file and would replace the operator's values.
|
|
load_proxy_env_from_dotenv() {
|
|
local env_file="$PROJECT_ROOT/.env"
|
|
local var
|
|
local line
|
|
local value
|
|
|
|
if [ ! -f "$env_file" ]; then
|
|
return
|
|
fi
|
|
|
|
for var in HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY http_proxy https_proxy all_proxy no_proxy AUTH_TRUSTED_PROXIES DEER_FLOW_CREDENTIALS_KEY; do
|
|
if [ -z "${!var+x}" ]; then
|
|
line="$(grep -E "^[[:space:]]*${var}=" "$env_file" | tail -n 1 || true)"
|
|
if [ -n "$line" ]; then
|
|
value="${line#*=}"
|
|
value="${value%$'\r'}"
|
|
value="${value%\"}"
|
|
value="${value#\"}"
|
|
value="${value%\'}"
|
|
value="${value#\'}"
|
|
export "${var}=${value}"
|
|
fi
|
|
fi
|
|
done
|
|
}
|
|
|
|
detect_sandbox_mode() {
|
|
local config_file="$PROJECT_ROOT/config.yaml"
|
|
local sandbox_use=""
|
|
local provisioner_url=""
|
|
|
|
if [ ! -f "$config_file" ]; then
|
|
echo "local"
|
|
return
|
|
fi
|
|
|
|
sandbox_use=$(awk '
|
|
/^[[:space:]]*sandbox:[[:space:]]*$/ { in_sandbox=1; next }
|
|
in_sandbox && /^[^[:space:]#]/ { in_sandbox=0 }
|
|
in_sandbox && /^[[:space:]]*use:[[:space:]]*/ {
|
|
line=$0
|
|
sub(/^[[:space:]]*use:[[:space:]]*/, "", line)
|
|
print line
|
|
exit
|
|
}
|
|
' "$config_file")
|
|
|
|
provisioner_url=$(awk '
|
|
/^[[:space:]]*sandbox:[[:space:]]*$/ { in_sandbox=1; next }
|
|
in_sandbox && /^[^[:space:]#]/ { in_sandbox=0 }
|
|
in_sandbox && /^[[:space:]]*provisioner_url:[[:space:]]*/ {
|
|
line=$0
|
|
sub(/^[[:space:]]*provisioner_url:[[:space:]]*/, "", line)
|
|
print line
|
|
exit
|
|
}
|
|
' "$config_file")
|
|
|
|
if [[ "$sandbox_use" == *"deerflow.sandbox.local:LocalSandboxProvider"* ]]; then
|
|
echo "local"
|
|
elif [[ "$sandbox_use" == *"deerflow.community.aio_sandbox:AioSandboxProvider"* ]]; then
|
|
if [ -n "$provisioner_url" ]; then
|
|
echo "provisioner"
|
|
else
|
|
echo "aio"
|
|
fi
|
|
else
|
|
echo "local"
|
|
fi
|
|
}
|
|
|
|
# Cleanup function for Ctrl+C
|
|
cleanup() {
|
|
echo ""
|
|
echo -e "${YELLOW}Operation interrupted by user${NC}"
|
|
exit 130
|
|
}
|
|
|
|
# Set up trap for Ctrl+C
|
|
trap cleanup INT TERM
|
|
|
|
docker_available() {
|
|
# Check that the docker CLI exists
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
|
|
# Check that the Docker daemon is reachable
|
|
if ! docker info >/dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Initialize: pre-pull the sandbox image so first Pod startup is fast
|
|
init() {
|
|
echo "=========================================="
|
|
echo " DeerFlow Init — Pull Sandbox Image"
|
|
echo "=========================================="
|
|
echo ""
|
|
|
|
SANDBOX_IMAGE="enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest"
|
|
|
|
# Detect sandbox mode from config.yaml
|
|
local sandbox_mode
|
|
sandbox_mode="$(detect_sandbox_mode)"
|
|
|
|
# Skip image pull for local sandbox mode (no container image needed)
|
|
if [ "$sandbox_mode" = "local" ]; then
|
|
echo -e "${GREEN}Detected local sandbox mode — no Docker image required.${NC}"
|
|
echo ""
|
|
|
|
if docker_available; then
|
|
echo -e "${GREEN}✓ Docker environment is ready.${NC}"
|
|
echo ""
|
|
echo -e "${YELLOW}Next step: make docker-start${NC}"
|
|
else
|
|
echo -e "${YELLOW}Docker does not appear to be installed, or the Docker daemon is not reachable.${NC}"
|
|
echo "Local sandbox mode itself does not require Docker, but Docker-based workflows (e.g., docker-start) will fail until Docker is available."
|
|
echo ""
|
|
echo -e "${YELLOW}Install and start Docker, then run: make docker-init && make docker-start${NC}"
|
|
fi
|
|
|
|
return 0
|
|
fi
|
|
|
|
if ! docker images --format '{{.Repository}}:{{.Tag}}' | grep -q "^${SANDBOX_IMAGE}$"; then
|
|
echo -e "${BLUE}Pulling sandbox image: $SANDBOX_IMAGE ...${NC}"
|
|
echo ""
|
|
|
|
if ! docker pull "$SANDBOX_IMAGE" 2>&1; then
|
|
echo ""
|
|
echo -e "${YELLOW}⚠ Failed to pull sandbox image.${NC}"
|
|
echo ""
|
|
echo "This is expected if:"
|
|
echo " 1. You are using local sandbox mode (default — no image needed)"
|
|
echo " 2. You are behind a corporate proxy or firewall"
|
|
echo " 3. The registry requires authentication"
|
|
echo ""
|
|
echo -e "${GREEN}The Docker development environment can still be started.${NC}"
|
|
echo "If you need AIO sandbox (container-based execution):"
|
|
echo " - Ensure you have network access to the registry"
|
|
echo " - Or configure a custom sandbox image in config.yaml"
|
|
echo ""
|
|
echo -e "${YELLOW}Next step: make docker-start${NC}"
|
|
return 0
|
|
fi
|
|
else
|
|
echo -e "${GREEN}Sandbox image already exists locally: $SANDBOX_IMAGE${NC}"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${GREEN}✓ Sandbox image is ready.${NC}"
|
|
echo ""
|
|
echo -e "${YELLOW}Next step: make docker-start${NC}"
|
|
}
|
|
|
|
# Start Docker development environment
|
|
start() {
|
|
local sandbox_mode
|
|
local services
|
|
|
|
if [ "$#" -gt 0 ]; then
|
|
echo -e "${YELLOW}Unknown option for start: $1${NC}"
|
|
echo "Usage: $0 start"
|
|
exit 1
|
|
fi
|
|
|
|
echo "=========================================="
|
|
echo " Starting DeerFlow Docker Development"
|
|
echo "=========================================="
|
|
echo ""
|
|
|
|
# Validate the toolchain before creating any config files below.
|
|
compose_preflight
|
|
|
|
sandbox_mode="$(detect_sandbox_mode)"
|
|
|
|
services="redis frontend gateway nginx"
|
|
if [ "$sandbox_mode" = "provisioner" ]; then
|
|
services="redis frontend gateway provisioner nginx"
|
|
fi
|
|
|
|
# Only aio mode (AioSandboxProvider without provisioner_url) needs the host
|
|
# Docker socket. Mount it via the opt-in docker-compose.dood.yaml overlay so
|
|
# the default (local) and provisioner modes never expose the host daemon.
|
|
# Mounting the socket = root-equivalent host control; see SECURITY.md.
|
|
if [ "$sandbox_mode" = "aio" ]; then
|
|
local docker_socket="${DEER_FLOW_DOCKER_SOCKET:-/var/run/docker.sock}"
|
|
if [ ! -S "$docker_socket" ]; then
|
|
# On Windows (Git Bash / MSYS), Docker Desktop mounts the default
|
|
# /var/run/docker.sock into containers even though no host socket file exists.
|
|
if [ "$docker_socket" = "/var/run/docker.sock" ] && [[ "$(uname -s)" =~ ^(MINGW|MSYS|CYGWIN) ]] && docker info >/dev/null 2>&1; then
|
|
:
|
|
else
|
|
echo -e "${YELLOW}⚠ Docker socket not found at $docker_socket — AioSandboxProvider (DooD) will not work.${NC}"
|
|
exit 1
|
|
fi
|
|
fi
|
|
echo -e "${YELLOW}Mounting host Docker socket into gateway (DooD = host root-equivalent). See SECURITY.md.${NC}"
|
|
COMPOSE_CMD="$COMPOSE_CMD -f docker-compose.dood.yaml"
|
|
fi
|
|
|
|
echo -e "${BLUE}Runtime: Gateway embedded agent runtime${NC}"
|
|
echo -e "${BLUE}Detected sandbox mode: $sandbox_mode${NC}"
|
|
if [ "$sandbox_mode" = "provisioner" ]; then
|
|
echo -e "${BLUE}Provisioner enabled (Kubernetes mode).${NC}"
|
|
else
|
|
echo -e "${BLUE}Provisioner disabled (not required for this sandbox mode).${NC}"
|
|
fi
|
|
echo ""
|
|
|
|
# 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
|
|
cp "$PROJECT_ROOT/config.example.yaml" "$PROJECT_ROOT/config.yaml"
|
|
echo ""
|
|
echo -e "${YELLOW}============================================================${NC}"
|
|
echo -e "${YELLOW} config.yaml has been created from config.example.yaml.${NC}"
|
|
echo -e "${YELLOW} Please edit config.yaml to set your API keys and model ${NC}"
|
|
echo -e "${YELLOW} configuration before starting DeerFlow. ${NC}"
|
|
echo -e "${YELLOW}============================================================${NC}"
|
|
echo ""
|
|
echo -e "${YELLOW} Recommended: run 'make setup' before starting Docker. ${NC}"
|
|
echo -e "${YELLOW} Edit the file: $PROJECT_ROOT/config.yaml${NC}"
|
|
echo -e "${YELLOW} Then run: make docker-start${NC}"
|
|
echo ""
|
|
exit 0
|
|
else
|
|
echo -e "${YELLOW}✗ config.yaml not found and no config.example.yaml to copy from.${NC}"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Ensure extensions_config.json exists as a file before mounting.
|
|
# Docker creates a directory when bind-mounting a non-existent host path.
|
|
if [ ! -f "$PROJECT_ROOT/extensions_config.json" ]; then
|
|
if [ -f "$PROJECT_ROOT/extensions_config.example.json" ]; then
|
|
cp "$PROJECT_ROOT/extensions_config.example.json" "$PROJECT_ROOT/extensions_config.json"
|
|
echo -e "${BLUE}Created extensions_config.json from example${NC}"
|
|
else
|
|
echo "{}" > "$PROJECT_ROOT/extensions_config.json"
|
|
echo -e "${BLUE}Created empty extensions_config.json${NC}"
|
|
fi
|
|
fi
|
|
|
|
ensure_env_files
|
|
load_proxy_env_from_dotenv
|
|
|
|
echo "Building and starting containers..."
|
|
cd "$DOCKER_DIR" && $COMPOSE_CMD up --build -d --remove-orphans $services
|
|
echo ""
|
|
echo "=========================================="
|
|
echo " DeerFlow Docker is starting!"
|
|
echo "=========================================="
|
|
echo ""
|
|
echo " 🌐 Application: http://localhost:2026"
|
|
echo " 📡 API Gateway: http://localhost:2026/api/*"
|
|
echo " 🤖 Runtime: Gateway embedded"
|
|
echo " API: /api/langgraph/* → Gateway"
|
|
echo ""
|
|
echo " 📋 View logs: make docker-logs"
|
|
echo " 🛑 Stop: make docker-stop"
|
|
echo ""
|
|
}
|
|
|
|
# View Docker logs. The dev stack (make docker-start) is the default;
|
|
# `--prod` tails the production stack started by `make up` (scripts/deploy.sh).
|
|
logs() {
|
|
local service=""
|
|
local is_prod=0
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--prod)
|
|
is_prod=1
|
|
;;
|
|
--frontend|--gateway|--nginx|--redis|--provisioner)
|
|
if [ -n "$service" ]; then
|
|
echo -e "${YELLOW}Only one service option is allowed (got --$service and $1).${NC}"
|
|
exit 1
|
|
fi
|
|
service="${1#--}"
|
|
;;
|
|
*)
|
|
echo -e "${YELLOW}Unknown option: $1${NC}"
|
|
echo "Usage: $0 logs [--prod] [--frontend|--gateway|--nginx|--redis|--provisioner]"
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ "$is_prod" = 1 ]; then
|
|
# Target the same project deploy.sh started. Relative paths: this
|
|
# runs with cwd=$DOCKER_DIR.
|
|
COMPOSE_FILE="docker-compose.yaml"
|
|
COMPOSE_PROJECT="deer-flow"
|
|
# deploy.sh exports these before every compose invocation so the
|
|
# volume specs in docker-compose.yaml interpolate; without them even
|
|
# `logs` fails to parse the file on checkouts without a .env.
|
|
export DEER_FLOW_HOME="${DEER_FLOW_HOME:-$PROJECT_ROOT/backend/.deer-flow}"
|
|
export DEER_FLOW_CONFIG_PATH="${DEER_FLOW_CONFIG_PATH:-$DEER_FLOW_HOME/config.yaml}"
|
|
export DEER_FLOW_EXTENSIONS_CONFIG_PATH="${DEER_FLOW_EXTENSIONS_CONFIG_PATH:-$DEER_FLOW_HOME/extensions_config.json}"
|
|
export DEER_FLOW_REPO_ROOT="${DEER_FLOW_REPO_ROOT:-$PROJECT_ROOT}"
|
|
export BETTER_AUTH_SECRET="${BETTER_AUTH_SECRET:-placeholder}"
|
|
export DEER_FLOW_INTERNAL_AUTH_TOKEN="${DEER_FLOW_INTERNAL_AUTH_TOKEN:-placeholder}"
|
|
elif [ -z "$service" ]; then
|
|
# The dev and production stacks use different compose projects, so
|
|
# `make docker-logs` after `make up` would exit with empty output;
|
|
# point at the production entry point instead of staying silent.
|
|
if [ -z "$(cd "$DOCKER_DIR" && $COMPOSE_CMD ps -q 2>/dev/null)" ]; then
|
|
echo -e "${YELLOW}No dev containers are running.${NC}"
|
|
echo "Started the production stack with 'make up'? View its logs with 'make prod-logs'."
|
|
fi
|
|
fi
|
|
|
|
compose_preflight
|
|
|
|
# Append --env-file only after compose_preflight(): its Compose detection
|
|
# may rebuild COMPOSE_CMD, which would drop anything appended before it.
|
|
if [ "$is_prod" = 1 ] && [ -f "$PROJECT_ROOT/.env" ]; then
|
|
COMPOSE_CMD="$COMPOSE_CMD --env-file ../.env"
|
|
fi
|
|
|
|
if [ -n "$service" ]; then
|
|
echo -e "${BLUE}Viewing $service logs...${NC}"
|
|
elif [ "$is_prod" = 1 ]; then
|
|
echo -e "${BLUE}Viewing production stack logs...${NC}"
|
|
else
|
|
echo -e "${BLUE}Viewing all logs...${NC}"
|
|
fi
|
|
|
|
cd "$DOCKER_DIR" && $COMPOSE_CMD logs -f $service
|
|
}
|
|
|
|
# Stop Docker development environment
|
|
stop() {
|
|
compose_preflight
|
|
echo "Stopping Docker development services..."
|
|
cd "$DOCKER_DIR" && $COMPOSE_CMD down
|
|
echo "Cleaning up sandbox containers..."
|
|
bash "$SCRIPT_DIR/cleanup-containers.sh" deer-flow-sandbox 2>/dev/null || true
|
|
echo -e "${GREEN}✓ Docker services stopped${NC}"
|
|
}
|
|
|
|
# Restart Docker development environment
|
|
restart() {
|
|
compose_preflight
|
|
echo "========================================"
|
|
echo " Restarting DeerFlow Docker Services"
|
|
echo "========================================"
|
|
echo ""
|
|
echo -e "${BLUE}Restarting containers...${NC}"
|
|
cd "$DOCKER_DIR" && $COMPOSE_CMD restart
|
|
echo ""
|
|
echo -e "${GREEN}✓ Docker services restarted${NC}"
|
|
echo ""
|
|
echo " 🌐 Application: http://localhost:2026"
|
|
echo " 📋 View logs: make docker-logs"
|
|
echo ""
|
|
}
|
|
|
|
# Show help
|
|
help() {
|
|
echo "DeerFlow Docker Management Script"
|
|
echo ""
|
|
echo "Usage: $0 <command> [options]"
|
|
echo ""
|
|
echo "Commands:"
|
|
echo " init - Pull the sandbox image (speeds up first Pod startup)"
|
|
echo " start - Start Docker services (auto-detects sandbox mode from config.yaml)"
|
|
echo " restart - Restart all running Docker services"
|
|
echo " logs [option] - View Docker logs (dev stack by default, production with --prod)"
|
|
echo " --prod View production stack logs (containers from 'make up')"
|
|
echo " --frontend View frontend logs only"
|
|
echo " --gateway View gateway logs only"
|
|
echo " --nginx View nginx logs only"
|
|
echo " --redis View redis logs only"
|
|
echo " --provisioner View provisioner logs only"
|
|
echo " stop - Stop Docker development services"
|
|
echo " help - Show this help message"
|
|
echo ""
|
|
}
|
|
|
|
main() {
|
|
# Main command dispatcher
|
|
case "$1" in
|
|
init)
|
|
init
|
|
;;
|
|
start)
|
|
shift
|
|
start "$@"
|
|
;;
|
|
restart)
|
|
restart
|
|
;;
|
|
logs)
|
|
shift
|
|
logs "$@"
|
|
;;
|
|
stop)
|
|
stop
|
|
;;
|
|
help|--help|-h|"")
|
|
help
|
|
;;
|
|
*)
|
|
echo -e "${YELLOW}Unknown command: $1${NC}"
|
|
echo ""
|
|
help
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
main "$@"
|
|
fi
|