mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-14 16:08:41 +00:00
* fix(docker): allow aio DooD socket preflight on Windows Git Bash
* test(deploy): add regression tests for deploy.sh DooD socket preflight
* fix(docker): restrict Windows DooD socket bypass to default path
Limit the Windows socket bypass in docker.sh and deploy.sh to the default /var/run/docker.sock path, and add regression tests ensuring custom missing socket paths are rejected.
* test(docker): skip unreachable socket controls on hosts with live docker socket
Add @pytest.mark.skipif on Path('/var/run/docker.sock').is_socket() to prevent false test failures on daemon-live hosts.
541 lines
18 KiB
Bash
Executable File
541 lines
18 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"
|
|
# 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"
|
|
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; 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%\"}"
|
|
value="${value#\"}"
|
|
value="${value%\'}"
|
|
value="${value#\'}"
|
|
value="${value%$'\r'}"
|
|
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 development logs
|
|
logs() {
|
|
local service=""
|
|
|
|
compose_preflight
|
|
|
|
case "$1" in
|
|
--frontend)
|
|
service="frontend"
|
|
echo -e "${BLUE}Viewing frontend logs...${NC}"
|
|
;;
|
|
--gateway)
|
|
service="gateway"
|
|
echo -e "${BLUE}Viewing gateway logs...${NC}"
|
|
;;
|
|
--nginx)
|
|
service="nginx"
|
|
echo -e "${BLUE}Viewing nginx logs...${NC}"
|
|
;;
|
|
--redis)
|
|
service="redis"
|
|
echo -e "${BLUE}Viewing redis logs...${NC}"
|
|
;;
|
|
--provisioner)
|
|
service="provisioner"
|
|
echo -e "${BLUE}Viewing provisioner logs...${NC}"
|
|
;;
|
|
"")
|
|
echo -e "${BLUE}Viewing all logs...${NC}"
|
|
;;
|
|
*)
|
|
echo -e "${YELLOW}Unknown option: $1${NC}"
|
|
echo "Usage: $0 logs [--frontend|--gateway|--nginx|--redis|--provisioner]"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
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 development logs"
|
|
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)
|
|
logs "$2"
|
|
;;
|
|
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
|