From b6da3a219e30c64682058e3440e4322644e3ff77 Mon Sep 17 00:00:00 2001 From: JeffJiang Date: Mon, 9 Feb 2026 21:59:13 +0800 Subject: [PATCH] Add Kubernetes-based sandbox provider for multi-instance support (#19) * feat: adds docker-based dev environment * docs: updates Docker command help * fix local dev * feat(sandbox): add Kubernetes-based sandbox provider for multi-instance support * fix: skills path in k8s * feat: add example config for k8s sandbox * fix: docker config * fix: load skills on docker dev * feat: support sandbox execution to Kubernetes Deployment model * chore: rename web service name --- .dockerignore | 17 + CONTRIBUTING.md | 14 +- Makefile | 14 +- README.md | 32 +- backend/Dockerfile | 6 + backend/langgraph.json | 6 +- backend/pyproject.toml | 6 +- backend/src/agents/lead_agent/agent.py | 4 + backend/src/community/aio_sandbox/__init__.py | 5 +- backend/uv.lock | 64 ++- config.example.yaml | 20 +- docker/docker-compose-dev.yaml | 64 ++- docker/k8s/README.md | 427 ++++++++++++++++++ docker/k8s/namespace.yaml | 7 + docker/k8s/sandbox-deployment.yaml | 65 +++ docker/k8s/sandbox-service.yaml | 21 + docker/k8s/setup.sh | 245 ++++++++++ docker/nginx/nginx.conf | 4 +- frontend/Dockerfile | 6 + scripts/docker.sh | 48 +- 20 files changed, 981 insertions(+), 94 deletions(-) create mode 100644 docker/k8s/README.md create mode 100644 docker/k8s/namespace.yaml create mode 100644 docker/k8s/sandbox-deployment.yaml create mode 100644 docker/k8s/sandbox-service.yaml create mode 100755 docker/k8s/setup.sh diff --git a/.dockerignore b/.dockerignore index 3faf7a1..3f151c2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -51,3 +51,20 @@ examples/ assets/ tests/ *.log + +# Exclude directories not needed in Docker context +# Frontend build only needs frontend/ +# Backend build only needs backend/ +scripts/ +logs/ +docker/ +skills/ +frontend/.next +frontend/node_modules +backend/.venv +backend/htmlcov +backend/.coverage +*.md +!README.md +!frontend/README.md +!backend/README.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1d048e..324afd9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,7 +43,7 @@ Docker provides a consistent, isolated environment with all dependencies pre-con 3. **Start development services**: ```bash - make docker-dev + make docker-start ``` All services will start with hot-reload enabled: - Frontend changes are automatically reloaded @@ -59,20 +59,16 @@ Docker provides a consistent, isolated environment with all dependencies pre-con ```bash # View all logs -make docker-dev-logs - -# View specific service logs -./scripts/docker.sh logs --web # Frontend only -./scripts/docker.sh logs --api # Backend only +make docker-logs # Restart services -./scripts/docker.sh restart +make docker-restart # Stop services -make docker-dev-stop +make docker-stop # Get help -./scripts/docker.sh help +make docker-help ``` #### Docker Architecture diff --git a/Makefile b/Makefile index 6a7a664..8d96acc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # DeerFlow - Unified Development Environment -.PHONY: help check install dev stop clean docker-init docker-start docker-stop docker-logs docker-logs-web docker-logs-api +.PHONY: help check install dev stop clean docker-init docker-start docker-stop docker-logs docker-logs-frontend docker-logs-gateway help: @echo "DeerFlow Development Commands:" @@ -16,8 +16,8 @@ help: @echo " make docker-start - Start all services in Docker (localhost:2026)" @echo " make docker-stop - Stop Docker development services" @echo " make docker-logs - View Docker development logs" - @echo " make docker-logs-web - View Docker frontend logs" - @echo " make docker-logs-api - View Docker backend logs" + @echo " make docker-logs-frontend - View Docker frontend logs" + @echo " make docker-logs-gateway - View Docker gateway logs" # Check required tools check: @@ -251,7 +251,7 @@ docker-logs: @./scripts/docker.sh logs # View Docker development logs -docker-logs-web: - @./scripts/docker.sh logs --web -docker-logs-api: - @./scripts/docker.sh logs --api \ No newline at end of file +docker-logs-frontend: + @./scripts/docker.sh logs --frontend +docker-logs-gateway: + @./scripts/docker.sh logs --gateway \ No newline at end of file diff --git a/README.md b/README.md index 4ee83db..b1f03f4 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,7 @@ The fastest way to get started with a consistent environment: 2. **Initialize and start**: ```bash - make docker-init # First time only - make docker-dev # Start all services + make docker-start # Start all services ``` 3. **Access**: http://localhost:2026 @@ -56,6 +55,35 @@ If you prefer running services locally: See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed local development guide. +### Sandbox Configuration + +DeerFlow supports multiple sandbox execution modes. Configure your preferred mode in `config.yaml`: + +**Local Execution** (runs sandbox code directly on the host machine): +```yaml +sandbox: + use: src.sandbox.local:LocalSandboxProvider # Local execution +``` + +**Docker Execution** (runs sandbox code in isolated Docker containers): +```yaml +sandbox: + use: src.community.aio_sandbox:AioSandboxProvider # Docker-based sandbox +``` + +**Docker Execution with Kubernetes** (runs sandbox code in Kubernetes pods): + +Setup Kubernetes sandbox as per [Kubernetes Sandbox Setup](docker/k8s/README.md). +```bash +./docker/k8s/setup.sh +``` +Then configure `config.yaml` with the Kubernetes service URL: +```yaml +sandbox: + use: src.community.k8s_sandbox:AioSandboxProvider # Kubernetes-based sandbox + base_url: http://deer-flow-sandbox.deer-flow.svc.cluster.local:8080 # Kubernetes service URL +``` + ## Features - 🤖 **LangGraph-based Agents** - Multi-agent orchestration with sophisticated workflows diff --git a/backend/Dockerfile b/backend/Dockerfile index e470aea..8058cc0 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -14,6 +14,12 @@ ENV PATH="/root/.local/bin:$PATH" # Set working directory WORKDIR /app +# Copy frontend source code +COPY backend ./backend + +# Install dependencies +RUN sh -c "cd backend && uv sync" + # Expose ports (gateway: 8001, langgraph: 2024) EXPOSE 8001 2024 diff --git a/backend/langgraph.json b/backend/langgraph.json index 7c2c842..c89eeef 100644 --- a/backend/langgraph.json +++ b/backend/langgraph.json @@ -1,8 +1,10 @@ { "$schema": "https://langgra.ph/schema.json", - "dependencies": ["."], + "dependencies": [ + "." + ], "env": ".env", "graphs": { "lead_agent": "src.agents:make_lead_agent" } -} +} \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 680d595..49f089c 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "dotenv>=0.9.9", "fastapi>=0.115.0", "httpx>=0.28.0", + "kubernetes>=30.0.0", "langchain>=1.2.3", "langchain-deepseek>=1.0.1", "langchain-mcp-adapters>=0.1.0", @@ -30,7 +31,4 @@ dependencies = [ ] [dependency-groups] -dev = [ - "pytest>=8.0.0", - "ruff>=0.14.11", -] +dev = ["pytest>=8.0.0", "ruff>=0.14.11"] diff --git a/backend/src/agents/lead_agent/agent.py b/backend/src/agents/lead_agent/agent.py index 9aecbf6..0c44116 100644 --- a/backend/src/agents/lead_agent/agent.py +++ b/backend/src/agents/lead_agent/agent.py @@ -236,8 +236,12 @@ def _build_middlewares(config: RunnableConfig): def make_lead_agent(config: RunnableConfig): # Lazy import to avoid circular dependency + import logging + from src.tools import get_available_tools + logging.basicConfig(level=logging.INFO) + thinking_enabled = config.get("configurable", {}).get("thinking_enabled", True) model_name = config.get("configurable", {}).get("model_name") or config.get("configurable", {}).get("model") is_plan_mode = config.get("configurable", {}).get("is_plan_mode", False) diff --git a/backend/src/community/aio_sandbox/__init__.py b/backend/src/community/aio_sandbox/__init__.py index 41d3a1c..de320e2 100644 --- a/backend/src/community/aio_sandbox/__init__.py +++ b/backend/src/community/aio_sandbox/__init__.py @@ -1,7 +1,4 @@ from .aio_sandbox import AioSandbox from .aio_sandbox_provider import AioSandboxProvider -__all__ = [ - "AioSandbox", - "AioSandboxProvider", -] +__all__ = ["AioSandbox", "AioSandboxProvider"] diff --git a/backend/uv.lock b/backend/uv.lock index ac2eec9..b8ef839 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -606,6 +606,7 @@ dependencies = [ { name = "fastapi" }, { name = "firecrawl-py" }, { name = "httpx" }, + { name = "kubernetes" }, { name = "langchain" }, { name = "langchain-deepseek" }, { name = "langchain-mcp-adapters" }, @@ -638,6 +639,7 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.115.0" }, { name = "firecrawl-py", specifier = ">=1.15.0" }, { name = "httpx", specifier = ">=0.28.0" }, + { name = "kubernetes", specifier = ">=30.0.0" }, { name = "langchain", specifier = ">=1.2.3" }, { name = "langchain-deepseek", specifier = ">=1.0.1" }, { name = "langchain-mcp-adapters", specifier = ">=0.1.0" }, @@ -691,6 +693,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, ] +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, +] + [[package]] name = "et-xmlfile" version = "2.0.0" @@ -1274,6 +1285,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "kubernetes" +version = "35.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "durationpy" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/8f/85bf51ad4150f64e8c665daf0d9dfe9787ae92005efb9a4d1cba592bd79d/kubernetes-35.0.0.tar.gz", hash = "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee", size = 1094642, upload-time = "2026-01-16T01:05:27.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/70/05b685ea2dffcb2adbf3cdcea5d8865b7bc66f67249084cf845012a0ff13/kubernetes-35.0.0-py2.py3-none-any.whl", hash = "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d", size = 2017602, upload-time = "2026-01-16T01:05:25.991Z" }, +] + [[package]] name = "langchain" version = "1.2.3" @@ -1897,6 +1928,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" }, ] +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + [[package]] name = "olefile" version = "0.47" @@ -2841,6 +2881,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + [[package]] name = "requests-toolbelt" version = "1.0.0" @@ -3391,6 +3444,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, ] +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + [[package]] name = "websockets" version = "16.0" diff --git a/config.example.yaml b/config.example.yaml index 862dfe5..9006a8e 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -18,10 +18,10 @@ models: display_name: GPT-4 use: langchain_openai:ChatOpenAI model: gpt-4 - api_key: $OPENAI_API_KEY # Use environment variable + api_key: $OPENAI_API_KEY # Use environment variable max_tokens: 4096 temperature: 0.7 - supports_vision: true # Enable vision support for view_image tool + supports_vision: true # Enable vision support for view_image tool # Example: Anthropic Claude model # - name: claude-3-5-sonnet @@ -210,7 +210,7 @@ title: enabled: true max_words: 6 max_chars: 60 - model_name: null # Use default model (first model in models list) + model_name: null # Use default model (first model in models list) # ============================================================================ # Summarization Configuration @@ -289,10 +289,10 @@ summarization: # Stores user context and conversation history for personalized responses memory: enabled: true - storage_path: .deer-flow/memory.json # Path relative to backend directory - debounce_seconds: 30 # Wait time before processing queued updates - model_name: null # Use default model - max_facts: 100 # Maximum number of facts to store - fact_confidence_threshold: 0.7 # Minimum confidence for storing facts - injection_enabled: true # Whether to inject memory into system prompt - max_injection_tokens: 2000 # Maximum tokens for memory injection + storage_path: .deer-flow/memory.json # Path relative to backend directory + debounce_seconds: 30 # Wait time before processing queued updates + model_name: null # Use default model + max_facts: 100 # Maximum number of facts to store + fact_confidence_threshold: 0.7 # Minimum confidence for storing facts + injection_enabled: true # Whether to inject memory into system prompt + max_injection_tokens: 2000 # Maximum tokens for memory injection diff --git a/docker/docker-compose-dev.yaml b/docker/docker-compose-dev.yaml index 9a0783b..2ee1d00 100644 --- a/docker/docker-compose-dev.yaml +++ b/docker/docker-compose-dev.yaml @@ -3,8 +3,8 @@ # # Services: # - nginx: Reverse proxy (port 2026) -# - web: Frontend Next.js dev server (port 3000) -# - api: Backend Gateway API (port 8001) +# - frontend: Frontend Next.js dev server (port 3000) +# - gateway: Backend Gateway API (port 8001) # - langgraph: LangGraph server (port 2024) # # Access: http://localhost:2026 @@ -19,29 +19,34 @@ services: volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - - web - - api + - frontend + - gateway - langgraph networks: - deer-flow-dev restart: unless-stopped # Frontend - Next.js Development Server - web: + frontend: build: - context: ../frontend - dockerfile: Dockerfile + context: ../ + dockerfile: frontend/Dockerfile args: PNPM_STORE_PATH: ${PNPM_STORE_PATH:-/root/.local/share/pnpm/store} - container_name: deer-flow-web - command: pnpm run dev + container_name: deer-flow-frontend + command: sh -c "cd frontend && pnpm run dev > /app/logs/frontend.log 2>&1" volumes: - - ../frontend:/app + - ../frontend/src:/app/frontend/src + - ../frontend/public:/app/frontend/public + - ../frontend/next.config.js:/app/frontend/next.config.js:ro + - ../logs:/app/logs # Mount pnpm store for caching - ${PNPM_STORE_PATH:-~/.local/share/pnpm/store}:/root/.local/share/pnpm/store + working_dir: /app environment: - NODE_ENV=development - WATCHPACK_POLLING=true + - CI=true env_file: - ../frontend/.env networks: @@ -49,17 +54,26 @@ services: restart: unless-stopped # Backend - Gateway API - api: + gateway: build: - context: ../backend - dockerfile: Dockerfile - container_name: deer-flow-api - command: uv run uvicorn src.gateway.app:app --host 0.0.0.0 --port 8001 --reload + context: ../ + dockerfile: backend/Dockerfile + container_name: deer-flow-gateway + command: sh -c "cd backend && uv run uvicorn src.gateway.app:app --host 0.0.0.0 --port 8001 --reload --reload-include='*.yaml .env' > /app/logs/gateway.log 2>&1" volumes: - - ../backend:/app - - ../config.yaml:/app/config.yaml:ro + - ../backend/src:/app/backend/src + - ../backend/.env:/app/backend/.env + - ../config.yaml:/app/config.yaml + - ../skills:/app/skills + - ../logs:/app/logs + working_dir: /app + environment: + - CI=true env_file: - ../backend/.env + extra_hosts: + # For Linux: map host.docker.internal to host gateway + - "host.docker.internal:host-gateway" networks: - deer-flow-dev restart: unless-stopped @@ -67,13 +81,19 @@ services: # Backend - LangGraph Server langgraph: build: - context: ../backend - dockerfile: Dockerfile + context: ../ + dockerfile: backend/Dockerfile container_name: deer-flow-langgraph - command: uv run langgraph dev --no-browser --allow-blocking --no-reload --host 0.0.0.0 --port 2024 + command: sh -c "cd backend && uv run langgraph dev --no-browser --allow-blocking --host 0.0.0.0 --port 2024 > /app/logs/langgraph.log 2>&1" volumes: - - ../backend:/app - - ../config.yaml:/app/config.yaml:ro + - ../backend/src:/app/backend/src + - ../backend/.env:/app/backend/.env + - ../config.yaml:/app/config.yaml + - ../skills:/app/skills + - ../logs:/app/logs + working_dir: /app + environment: + - CI=true env_file: - ../backend/.env networks: diff --git a/docker/k8s/README.md b/docker/k8s/README.md new file mode 100644 index 0000000..53ce9ad --- /dev/null +++ b/docker/k8s/README.md @@ -0,0 +1,427 @@ +# Kubernetes Sandbox Setup + +This guide explains how to deploy and configure the DeerFlow sandbox execution environment on Kubernetes. + +## Overview + +The Kubernetes sandbox deployment allows you to run DeerFlow's code execution sandbox in a Kubernetes cluster, providing: + +- **Isolated Execution**: Sandbox runs in dedicated Kubernetes pods +- **Scalability**: Easy horizontal scaling with replica configuration +- **Cluster Integration**: Seamless integration with existing Kubernetes infrastructure +- **Persistent Skills**: Skills directory mounted from host or PersistentVolume + +## Prerequisites + +Before you begin, ensure you have: + +1. **Kubernetes Cluster**: One of the following: + - Docker Desktop with Kubernetes enabled + - OrbStack with Kubernetes enabled + - Minikube + - Any production Kubernetes cluster + +2. **kubectl**: Kubernetes command-line tool + ```bash + # macOS + brew install kubectl + + # Linux + # See: https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/ + ``` + +3. **Docker**: For pulling the sandbox image (optional, but recommended) + ```bash + # Verify installation + docker version + ``` + +## Quick Start + +### 1. Enable Kubernetes + +**Docker Desktop:** +``` +Settings → Kubernetes → Enable Kubernetes → Apply & Restart +``` + +**OrbStack:** +``` +Settings → Enable Kubernetes +``` + +**Minikube:** +```bash +minikube start +``` + +### 2. Run Setup Script + +The easiest way to get started: + +```bash +cd docker/k8s +./setup.sh +``` + +This will: +- ✅ Check kubectl installation and cluster connectivity +- ✅ Pull the sandbox Docker image (optional, can be skipped) +- ✅ Create the `deer-flow` namespace +- ✅ Deploy the sandbox service and deployment +- ✅ Verify the deployment is running + +### 3. Configure Backend + +Add the following to `backend/config.yaml`: + +```yaml +sandbox: + use: src.community.aio_sandbox:AioSandboxProvider + base_url: http://deer-flow-sandbox.deer-flow.svc.cluster.local:8080 +``` + +### 4. Verify Deployment + +Check that the sandbox pod is running: + +```bash +kubectl get pods -n deer-flow +``` + +You should see: +``` +NAME READY STATUS RESTARTS AGE +deer-flow-sandbox-xxxxxxxxxx-xxxxx 1/1 Running 0 1m +``` + +## Advanced Configuration + +### Custom Skills Path + +By default, the setup script uses `PROJECT_ROOT/skills`. You can specify a custom path: + +**Using command-line argument:** +```bash +./setup.sh --skills-path /custom/path/to/skills +``` + +**Using environment variable:** +```bash +SKILLS_PATH=/custom/path/to/skills ./setup.sh +``` + +### Custom Sandbox Image + +To use a different sandbox image: + +**Using command-line argument:** +```bash +./setup.sh --image your-registry/sandbox:tag +``` + +**Using environment variable:** +```bash +SANDBOX_IMAGE=your-registry/sandbox:tag ./setup.sh +``` + +### Skip Image Pull + +If you already have the image locally or want to pull it manually later: + +```bash +./setup.sh --skip-pull +``` + +### Combined Options + +```bash +./setup.sh --skip-pull --skills-path /custom/skills --image custom/sandbox:latest +``` + +## Manual Deployment + +If you prefer manual deployment or need more control: + +### 1. Create Namespace + +```bash +kubectl apply -f namespace.yaml +``` + +### 2. Create Service + +```bash +kubectl apply -f sandbox-service.yaml +``` + +### 3. Deploy Sandbox + +First, update the skills path in `sandbox-deployment.yaml`: + +```bash +# Replace __SKILLS_PATH__ with your actual path +sed 's|__SKILLS_PATH__|/Users/feng/Projects/deer-flow/skills|g' \ + sandbox-deployment.yaml | kubectl apply -f - +``` + +Or manually edit `sandbox-deployment.yaml` and replace `__SKILLS_PATH__` with your skills directory path. + +### 4. Verify Deployment + +```bash +# Check all resources +kubectl get all -n deer-flow + +# Check pod status +kubectl get pods -n deer-flow + +# Check pod logs +kubectl logs -n deer-flow -l app=deer-flow-sandbox + +# Describe pod for detailed info +kubectl describe pod -n deer-flow -l app=deer-flow-sandbox +``` + +## Configuration Options + +### Resource Limits + +Edit `sandbox-deployment.yaml` to adjust resource limits: + +```yaml +resources: + requests: + cpu: 100m # Minimum CPU + memory: 256Mi # Minimum memory + limits: + cpu: 1000m # Maximum CPU (1 core) + memory: 1Gi # Maximum memory +``` + +### Scaling + +Adjust the number of replicas: + +```yaml +spec: + replicas: 3 # Run 3 sandbox pods +``` + +Or scale dynamically: + +```bash +kubectl scale deployment deer-flow-sandbox -n deer-flow --replicas=3 +``` + +### Health Checks + +The deployment includes readiness and liveness probes: + +- **Readiness Probe**: Checks if the pod is ready to serve traffic +- **Liveness Probe**: Restarts the pod if it becomes unhealthy + +Configure in `sandbox-deployment.yaml`: + +```yaml +readinessProbe: + httpGet: + path: /v1/sandbox + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 +``` + +## Troubleshooting + +### Pod Not Starting + +Check pod status and events: + +```bash +kubectl describe pod -n deer-flow -l app=deer-flow-sandbox +``` + +Common issues: +- **ImagePullBackOff**: Docker image cannot be pulled + - Solution: Pre-pull image with `docker pull ` +- **Skills path not found**: HostPath doesn't exist + - Solution: Verify the skills path exists on the host +- **Resource constraints**: Not enough CPU/memory + - Solution: Adjust resource requests/limits + +### Service Not Accessible + +Verify the service is running: + +```bash +kubectl get service -n deer-flow +kubectl describe service deer-flow-sandbox -n deer-flow +``` + +Test connectivity from another pod: + +```bash +kubectl run test-pod -n deer-flow --rm -it --image=curlimages/curl -- \ + curl http://deer-flow-sandbox.deer-flow.svc.cluster.local:8080/v1/sandbox +``` + +### Check Logs + +View sandbox logs: + +```bash +# Follow logs in real-time +kubectl logs -n deer-flow -l app=deer-flow-sandbox -f + +# View logs from previous container (if crashed) +kubectl logs -n deer-flow -l app=deer-flow-sandbox --previous +``` + +### Health Check Failures + +If pods show as not ready: + +```bash +# Check readiness probe +kubectl get events -n deer-flow --sort-by='.lastTimestamp' + +# Exec into pod to debug +kubectl exec -it -n deer-flow -- /bin/sh +``` + +## Cleanup + +### Remove All Resources + +Using the setup script: + +```bash +./setup.sh --cleanup +``` + +Or manually: + +```bash +kubectl delete -f sandbox-deployment.yaml +kubectl delete -f sandbox-service.yaml +kubectl delete namespace deer-flow +``` + +### Remove Specific Resources + +```bash +# Delete only the deployment (keeps namespace and service) +kubectl delete deployment deer-flow-sandbox -n deer-flow + +# Delete pods (they will be recreated by deployment) +kubectl delete pods -n deer-flow -l app=deer-flow-sandbox +``` + +## Architecture + +``` +┌─────────────────────────────────────────────┐ +│ DeerFlow Backend │ +│ (config.yaml: base_url configured) │ +└────────────────┬────────────────────────────┘ + │ HTTP requests + ↓ +┌─────────────────────────────────────────────┐ +│ Kubernetes Service (ClusterIP) │ +│ deer-flow-sandbox.deer-flow.svc:8080 │ +└────────────────┬────────────────────────────┘ + │ Load balancing + ↓ +┌─────────────────────────────────────────────┐ +│ Sandbox Pods (replicas) │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Pod 1 │ │ Pod 2 │ │ Pod 3 │ │ +│ │ Port 8080│ │ Port 8080│ │ Port 8080│ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +└────────────────┬────────────────────────────┘ + │ Volume mount + ↓ +┌─────────────────────────────────────────────┐ +│ Host Skills Directory │ +│ /path/to/deer-flow/skills │ +└─────────────────────────────────────────────┘ +``` + +## Setup Script Reference + +### Command-Line Options + +```bash +./setup.sh [options] + +Options: + -h, --help Show help message + -c, --cleanup Remove all Kubernetes resources + -p, --skip-pull Skip pulling sandbox image + --image Use custom sandbox image + --skills-path Custom skills directory path + +Environment Variables: + SANDBOX_IMAGE Custom sandbox image + SKILLS_PATH Custom skills path + +Examples: + ./setup.sh # Use default settings + ./setup.sh --skills-path /custom/path # Use custom skills path + ./setup.sh --skip-pull --image custom:tag # Custom image, skip pull + SKILLS_PATH=/custom/path ./setup.sh # Use env variable +``` + +## Production Considerations + +### Security + +1. **Network Policies**: Restrict pod-to-pod communication +2. **RBAC**: Configure appropriate service account permissions +3. **Pod Security**: Enable pod security standards +4. **Image Security**: Scan images for vulnerabilities + +### High Availability + +1. **Multiple Replicas**: Run at least 3 replicas +2. **Pod Disruption Budget**: Prevent all pods from being evicted +3. **Node Affinity**: Distribute pods across nodes +4. **Resource Quotas**: Set namespace resource limits + +### Monitoring + +1. **Prometheus**: Scrape metrics from pods +2. **Logging**: Centralized log aggregation +3. **Alerting**: Set up alerts for pod failures +4. **Tracing**: Distributed tracing for requests + +### Storage + +For production, consider using PersistentVolume instead of hostPath: + +1. **Create PersistentVolume**: Define storage backend +2. **Create PersistentVolumeClaim**: Request storage +3. **Update Deployment**: Use PVC instead of hostPath + +See `skills-pv-pvc.yaml.bak` for reference implementation. + +## Next Steps + +After successful deployment: + +1. **Start Backend**: `make dev` or `make docker-start` +2. **Test Sandbox**: Create a conversation and execute code +3. **Monitor**: Watch pod logs and resource usage +4. **Scale**: Adjust replicas based on workload + +## Support + +For issues and questions: + +- Check troubleshooting section above +- Review pod logs: `kubectl logs -n deer-flow -l app=deer-flow-sandbox` +- See main project documentation: [../../README.md](../../README.md) +- Report issues on GitHub diff --git a/docker/k8s/namespace.yaml b/docker/k8s/namespace.yaml new file mode 100644 index 0000000..91b2a64 --- /dev/null +++ b/docker/k8s/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: deer-flow + labels: + app.kubernetes.io/name: deer-flow + app.kubernetes.io/component: sandbox diff --git a/docker/k8s/sandbox-deployment.yaml b/docker/k8s/sandbox-deployment.yaml new file mode 100644 index 0000000..0e1ca92 --- /dev/null +++ b/docker/k8s/sandbox-deployment.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deer-flow-sandbox + namespace: deer-flow + labels: + app.kubernetes.io/name: deer-flow + app.kubernetes.io/component: sandbox +spec: + replicas: 1 + selector: + matchLabels: + app: deer-flow-sandbox + template: + metadata: + labels: + app: deer-flow-sandbox + app.kubernetes.io/name: deer-flow + app.kubernetes.io/component: sandbox + spec: + containers: + - name: sandbox + image: enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest + ports: + - name: http + containerPort: 8080 + protocol: TCP + readinessProbe: + httpGet: + path: /v1/sandbox + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /v1/sandbox + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 1000m + memory: 1Gi + volumeMounts: + - name: skills + mountPath: /mnt/skills + readOnly: true + securityContext: + privileged: false + allowPrivilegeEscalation: true + volumes: + - name: skills + hostPath: + # Path to skills directory on the host machine + # This will be replaced by setup.sh with the actual path + path: __SKILLS_PATH__ + type: Directory + restartPolicy: Always diff --git a/docker/k8s/sandbox-service.yaml b/docker/k8s/sandbox-service.yaml new file mode 100644 index 0000000..05075f4 --- /dev/null +++ b/docker/k8s/sandbox-service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: deer-flow-sandbox + namespace: deer-flow + labels: + app.kubernetes.io/name: deer-flow + app.kubernetes.io/component: sandbox +spec: + type: ClusterIP + clusterIP: None # Headless service for direct Pod DNS access + ports: + - name: http + port: 8080 + targetPort: 8080 + protocol: TCP + selector: + app: deer-flow-sandbox + # Enable DNS-based service discovery + # Pods will be accessible at: {pod-name}.deer-flow-sandbox.deer-flow.svc.cluster.local:8080 + publishNotReadyAddresses: false diff --git a/docker/k8s/setup.sh b/docker/k8s/setup.sh new file mode 100755 index 0000000..06cc83b --- /dev/null +++ b/docker/k8s/setup.sh @@ -0,0 +1,245 @@ +#!/bin/bash + +# Kubernetes Sandbox Initialization Script for Deer-Flow +# This script sets up the Kubernetes environment for the sandbox provider + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +# Default sandbox image +DEFAULT_SANDBOX_IMAGE="enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}╔════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ Deer-Flow Kubernetes Sandbox Setup ║${NC}" +echo -e "${BLUE}╚════════════════════════════════════════════╝${NC}" +echo + +# Function to print status messages +info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if kubectl is installed +check_kubectl() { + info "Checking kubectl installation..." + if ! command -v kubectl &> /dev/null; then + error "kubectl is not installed. Please install kubectl first." + echo " - macOS: brew install kubectl" + echo " - Linux: https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/" + exit 1 + fi + success "kubectl is installed" +} + +# Check if Kubernetes cluster is accessible +check_cluster() { + info "Checking Kubernetes cluster connection..." + if ! kubectl cluster-info &> /dev/null; then + error "Cannot connect to Kubernetes cluster." + echo "Please ensure:" + echo " - Docker Desktop: Settings → Kubernetes → Enable Kubernetes" + echo " - Or OrbStack: Enable Kubernetes in settings" + echo " - Or Minikube: minikube start" + exit 1 + fi + success "Connected to Kubernetes cluster" +} + +# Apply Kubernetes resources +apply_resources() { + info "Applying Kubernetes resources..." + + # Determine skills path + SKILLS_PATH="${SKILLS_PATH:-${PROJECT_ROOT}/skills}" + info "Using skills path: ${SKILLS_PATH}" + + # Validate skills path exists + if [[ ! -d "${SKILLS_PATH}" ]]; then + warn "Skills path does not exist: ${SKILLS_PATH}" + warn "Creating directory..." + mkdir -p "${SKILLS_PATH}" + fi + + echo " → Creating namespace..." + kubectl apply -f "${SCRIPT_DIR}/namespace.yaml" + + echo " → Creating sandbox service..." + kubectl apply -f "${SCRIPT_DIR}/sandbox-service.yaml" + + echo " → Creating sandbox deployment with skills path: ${SKILLS_PATH}" + # Replace __SKILLS_PATH__ placeholder with actual path + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + sed "s|__SKILLS_PATH__|${SKILLS_PATH}|g" "${SCRIPT_DIR}/sandbox-deployment.yaml" | kubectl apply -f - + else + # Linux + sed "s|__SKILLS_PATH__|${SKILLS_PATH}|g" "${SCRIPT_DIR}/sandbox-deployment.yaml" | kubectl apply -f - + fi + + success "All Kubernetes resources applied" +} + +# Verify deployment +verify_deployment() { + info "Verifying deployment..." + + echo " → Checking namespace..." + kubectl get namespace deer-flow + + echo " → Checking service..." + kubectl get service -n deer-flow + + echo " → Checking deployment..." + kubectl get deployment -n deer-flow + + echo " → Checking pods..." + kubectl get pods -n deer-flow + + success "Deployment verified" +} + +# Pull sandbox image +pull_image() { + info "Checking sandbox image..." + + IMAGE="${SANDBOX_IMAGE:-$DEFAULT_SANDBOX_IMAGE}" + + # Check if image already exists locally + if docker image inspect "$IMAGE" &> /dev/null; then + success "Image already exists locally: $IMAGE" + return 0 + fi + + info "Pulling sandbox image (this may take a few minutes on first run)..." + echo " → Image: $IMAGE" + echo + + if docker pull "$IMAGE"; then + success "Image pulled successfully" + else + warn "Failed to pull image. Pod startup may be slow on first run." + echo " You can manually pull the image later with:" + echo " docker pull $IMAGE" + fi +} + +# Print next steps +print_next_steps() { + echo + echo -e "${BLUE}╔════════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║ Setup Complete! ║${NC}" + echo -e "${BLUE}╚════════════════════════════════════════════╝${NC}" + echo + echo -e "${YELLOW}To enable Kubernetes sandbox, add the following to backend/config.yaml:${NC}" + echo + echo -e "${GREEN}sandbox:${NC}" + echo -e "${GREEN} use: src.community.aio_sandbox:AioSandboxProvider${NC}" + echo -e "${GREEN} base_url: http://deer-flow-sandbox.deer-flow.svc.cluster.local:8080${NC}" + echo + echo + echo -e "${GREEN}Next steps:${NC}" + echo " make dev # Start backend and frontend in development mode" + echo " make docker-start # Start backend and frontend in Docker containers" + echo +} + +# Cleanup function +cleanup() { + if [[ "$1" == "--cleanup" ]] || [[ "$1" == "-c" ]]; then + info "Cleaning up Kubernetes resources..." + kubectl delete -f "${SCRIPT_DIR}/sandbox-deployment.yaml" --ignore-not-found=true + kubectl delete -f "${SCRIPT_DIR}/sandbox-service.yaml" --ignore-not-found=true + kubectl delete -f "${SCRIPT_DIR}/namespace.yaml" --ignore-not-found=true + success "Cleanup complete" + exit 0 + fi +} + +# Show help +show_help() { + echo "Usage: $0 [options]" + echo + echo "Options:" + echo " -h, --help Show this help message" + echo " -c, --cleanup Remove all Kubernetes resources" + echo " -p, --skip-pull Skip pulling sandbox image" + echo " --image Use custom sandbox image" + echo " --skills-path Custom skills directory path" + echo + echo "Environment variables:" + echo " SANDBOX_IMAGE Custom sandbox image (default: $DEFAULT_SANDBOX_IMAGE)" + echo " SKILLS_PATH Custom skills path (default: PROJECT_ROOT/skills)" + echo + echo "Examples:" + echo " $0 # Use default settings" + echo " $0 --skills-path /custom/path # Use custom skills path" + echo " SKILLS_PATH=/custom/path $0 # Use env variable" + echo + exit 0 +} + +# Parse arguments +SKIP_PULL=false +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + ;; + -c|--cleanup) + cleanup "$1" + ;; + -p|--skip-pull) + SKIP_PULL=true + shift + ;; + --image) + SANDBOX_IMAGE="$2" + shift 2 + ;; + --skills-path) + SKILLS_PATH="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + +# Main execution +main() { + check_kubectl + check_cluster + + # Pull image first to avoid Pod startup timeout + if [[ "$SKIP_PULL" == false ]]; then + pull_image + fi + + apply_resources + verify_deployment + print_next_steps +} + +main diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf index d2cced5..c37c418 100644 --- a/docker/nginx/nginx.conf +++ b/docker/nginx/nginx.conf @@ -16,7 +16,7 @@ http { # Upstream servers (using Docker service names) upstream gateway { - server api:8001; + server gateway:8001; } upstream langgraph { @@ -24,7 +24,7 @@ http { } upstream frontend { - server web:3000; + server frontend:3000; } server { diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 04c826b..941945f 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -12,5 +12,11 @@ RUN pnpm config set store-dir ${PNPM_STORE_PATH} # Set working directory WORKDIR /app +# Copy frontend source code +COPY frontend ./frontend + +# Install dependencies +RUN sh -c "cd /app/frontend && pnpm install --frozen-lockfile" + # Expose Next.js dev server port EXPOSE 3000 diff --git a/scripts/docker.sh b/scripts/docker.sh index 21a305b..c0ad9bb 100755 --- a/scripts/docker.sh +++ b/scripts/docker.sh @@ -60,32 +60,14 @@ init() { echo "" - # Build containers + # Build containers (dependencies are installed during build) echo -e "${BLUE}Building containers...${NC}" + echo -e "${BLUE} - Frontend dependencies will be installed via Dockerfile${NC}" + echo -e "${BLUE} - Backend dependencies will be installed via Dockerfile${NC}" cd "$DOCKER_DIR" && PNPM_STORE_PATH="$PNPM_STORE" $COMPOSE_CMD build echo "" - # Install frontend dependencies - echo -e "${BLUE}Installing frontend dependencies...${NC}" - if ! (cd "$DOCKER_DIR" && PNPM_STORE_PATH="$PNPM_STORE" $COMPOSE_CMD run --rm -it --entrypoint "" web pnpm install --frozen-lockfile); then - echo -e "${YELLOW}Frontend dependencies installation failed or was interrupted${NC}" - exit 1 - fi - echo -e "${GREEN}✓ Frontend dependencies installed${NC}" - - echo "" - - # Install backend dependencies - echo -e "${BLUE}Installing backend dependencies...${NC}" - if ! (cd "$DOCKER_DIR" && $COMPOSE_CMD run --rm -it --entrypoint "" api uv sync); then - echo -e "${YELLOW}Backend dependencies installation failed or was interrupted${NC}" - exit 1 - fi - echo -e "${GREEN}✓ Backend dependencies installed${NC}" - - echo "" - echo "==========================================" echo -e "${GREEN} ✓ Docker initialization complete!${NC}" echo "==========================================" @@ -111,8 +93,8 @@ start() { echo " 📡 API Gateway: http://localhost:2026/api/*" echo " 🤖 LangGraph: http://localhost:2026/api/langgraph/*" echo "" - echo " 📋 View logs: make docker-dev-logs" - echo " 🛑 Stop: make docker-dev-stop" + echo " 📋 View logs: make docker-logs" + echo " 🛑 Stop: make docker-stop" echo "" } @@ -121,20 +103,24 @@ logs() { local service="" case "$1" in - --web) - service="web" + --frontend) + service="frontend" echo -e "${BLUE}Viewing frontend logs...${NC}" ;; - --api) - service="api" - echo -e "${BLUE}Viewing backend logs...${NC}" + --gateway) + service="gateway" + echo -e "${BLUE}Viewing gateway logs...${NC}" + ;; + --nginx) + service="nginx" + echo -e "${BLUE}Viewing nginx logs...${NC}" ;; "") echo -e "${BLUE}Viewing all logs...${NC}" ;; *) echo -e "${YELLOW}Unknown option: $1${NC}" - echo "Usage: $0 logs [--web|--api]" + echo "Usage: $0 logs [--frontend|--gateway]" exit 1 ;; esac @@ -176,8 +162,8 @@ help() { echo " start - Start all services in Docker (localhost:2026)" echo " restart - Restart all running Docker services" echo " logs [option] - View Docker development logs" - echo " --web View frontend logs only" - echo " --api View backend logs only" + echo " --frontend View frontend logs only" + echo " --gateway View gateway logs only" echo " stop - Stop Docker development services" echo " help - Show this help message" echo ""