|
Some checks are pending
Backend Blocking IO / backend-blocking-io (push) Waiting to run
Unit Tests / backend-unit-tests (push) Waiting to run
Frontend Unit Tests / frontend-unit-tests (push) Waiting to run
Lint Check / lint-backend (push) Waiting to run
Lint Check / lint-frontend (push) Waiting to run
Replay E2E (front-back contract) / Layer 1 — backend golden (no API key) (push) Waiting to run
Replay E2E (front-back contract) / Layer 2 — full-stack render (no API key) (push) Waiting to run
* feat(provisioner): add ClusterIP mode for sandbox services * feat(provisioner): support optional skills pvc subpath |
||
|---|---|---|
| .. | ||
| app.py | ||
| Dockerfile | ||
| README.md | ||
DeerFlow Sandbox Provisioner
The Sandbox Provisioner is a FastAPI service that dynamically manages sandbox Pods in Kubernetes. It provides a REST API for the DeerFlow backend to create, monitor, and destroy isolated sandbox environments for code execution.
Architecture
┌────────────┐ HTTP ┌─────────────┐ K8s API ┌──────────────┐
│ Backend │ ─────▸ │ Provisioner │ ────────▸ │ Host K8s │
│ (gateway/ │ │ :8002 │ │ API Server │
│ langgraph) │ └─────────────┘ └──────┬───────┘
└────────────┘ │ creates
│
┌─────────────┐ ┌────▼─────┐
│ Backend │ ──────▸ │ Sandbox │
│ (NodePort │ or DNS │ Pod(s) │
│ /ClusterIP)│ └──────────┘
└─────────────┘
How It Works
-
Backend Request: When the backend needs to execute code, it sends a
POST /api/sandboxesrequest with asandbox_id,thread_id, and optionaluser_id. -
Pod Creation: The provisioner creates a dedicated Pod in the
deer-flownamespace with:- The sandbox container image (all-in-one-sandbox)
- HostPath volumes mounted for:
/mnt/skills→ Read-only access to public skills/mnt/user-data→ Read-write access to thread-specific data
- Resource limits (CPU, memory, ephemeral storage)
- Readiness/liveness probes
-
Service Creation: A Service is created to expose the Pod. By default this is a NodePort Service for Docker Compose compatibility. Set
SANDBOX_SERVICE_TYPE=ClusterIPwhen the backend runs inside the Kubernetes cluster. -
Access URL: In NodePort mode, the provisioner returns
http://{NODE_HOST}:{NodePort}. In ClusterIP mode, it returns a Kubernetes service DNS URL likehttp://sandbox-{sandbox_id}-svc.{namespace}.svc.cluster.local:8080. -
Cleanup: When the session ends,
DELETE /api/sandboxes/{sandbox_id}removes both the Pod and Service.
The sandbox business endpoints are implemented as synchronous FastAPI handlers
because the Kubernetes Python client used here is synchronous. Starlette runs
sync handlers in its worker pool, keeping create/read/list/delete K8s API calls
and service access polling off the ASGI event-loop thread. Keep /health
lightweight; do not move the sandbox CRUD handlers back to async def unless
the K8s client path is also made async or explicitly offloaded.
Requirements
Host machine with a running Kubernetes cluster (Docker Desktop K8s, OrbStack, minikube, kind, etc.)
Enable Kubernetes in Docker Desktop
- Open Docker Desktop settings
- Go to "Kubernetes" tab
- Check "Enable Kubernetes"
- Click "Apply & Restart"
Enable Kubernetes in OrbStack
- Open OrbStack settings
- Go to "Kubernetes" tab
- Check "Enable Kubernetes"
API Endpoints
GET /health
Health check endpoint.
Response:
{
"status": "ok"
}
POST /api/sandboxes
Create a new sandbox Pod + Service.
Request:
{
"sandbox_id": "abc-123",
"thread_id": "thread-456",
"user_id": "user-789"
}
user_id is optional for backwards compatibility and defaults to default. When USERDATA_PVC_NAME is set, the provisioner uses it to isolate PVC-backed user-data directories.
Response:
{
"sandbox_id": "abc-123",
"sandbox_url": "http://host.docker.internal:32123",
"status": "Pending"
}
Idempotent: Calling with the same sandbox_id returns the existing sandbox info.
GET /api/sandboxes/{sandbox_id}
Get status and URL of a specific sandbox.
Response:
{
"sandbox_id": "abc-123",
"sandbox_url": "http://host.docker.internal:32123",
"status": "Running"
}
Status Values: Pending, Running, Succeeded, Failed, Unknown, NotFound
DELETE /api/sandboxes/{sandbox_id}
Destroy a sandbox Pod + Service.
Response:
{
"ok": true,
"sandbox_id": "abc-123"
}
GET /api/sandboxes
List all sandboxes currently managed.
Response:
{
"sandboxes": [
{
"sandbox_id": "abc-123",
"sandbox_url": "http://host.docker.internal:32123",
"status": "Running"
}
],
"count": 1
}
Configuration
The provisioner is configured via environment variables (set in docker-compose-dev.yaml):
| Variable | Default | Description |
|---|---|---|
K8S_NAMESPACE |
deer-flow |
Kubernetes namespace for sandbox resources |
SANDBOX_IMAGE |
enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest |
AIO-compatible container image for sandbox Pods |
SKILLS_HOST_PATH |
- | Host machine path to skills directory (must be absolute) |
THREADS_HOST_PATH |
- | Host machine path to threads data directory (must be absolute) |
SKILLS_PVC_NAME |
empty (use hostPath) | PVC name for skills volume; when set, sandbox Pods use PVC instead of hostPath |
SKILLS_PVC_SUBPATH_TEMPLATE |
empty | Optional subPath template for SKILLS_PVC_NAME. Supports {user_id} and {thread_id}. When empty, the skills PVC root is mounted unchanged |
USERDATA_PVC_NAME |
empty (use hostPath) | PVC name for user-data volume; when set, uses PVC with subPath: deer-flow/users/{user_id}/threads/{thread_id}/user-data |
KUBECONFIG_PATH |
/root/.kube/config |
Path to kubeconfig inside the provisioner container |
SANDBOX_SERVICE_TYPE |
NodePort |
Service type for sandbox access. Use ClusterIP when backend and provisioner run inside the same Kubernetes cluster |
NODE_HOST |
host.docker.internal |
Hostname that backend containers use to reach host NodePorts; ignored when SANDBOX_SERVICE_TYPE=ClusterIP |
K8S_API_SERVER |
(from kubeconfig) | Override K8s API server URL (e.g., https://host.docker.internal:26443) |
Custom sandbox image
Provisioner-created sandbox Pods use the provisioner's SANDBOX_IMAGE environment variable. This is separate from sandbox.image in config.yaml, which applies to local Docker or Apple Container mode.
For persistent dependencies, build an image that extends the default all-in-one-sandbox image and set SANDBOX_IMAGE to your published tag. A from-scratch image must remain compatible with the AIO sandbox HTTP API consumed by agent-sandbox, keep /mnt/user-data writable, and listen on the configured sandbox port.
See Building a Custom AIO Sandbox Image for the runtime contract and a minimal Dockerfile example.
PVC User-Data Upgrade Note
Older provisioner versions mounted PVC user-data from threads/{thread_id}/user-data. The user-scoped layout mounts from deer-flow/users/{user_id}/threads/{thread_id}/user-data.
If an existing deployment already has PVC-backed user-data under the legacy layout, migrate the DeerFlow data directory before relying on the new PVC subPath. Mount the same PVC path that the gateway uses as its DeerFlow base directory, then run the existing user-isolation migration script:
cd backend
PYTHONPATH=. python scripts/migrate_user_isolation.py --dry-run
PYTHONPATH=. python scripts/migrate_user_isolation.py --user-id <target-user-id>
This moves legacy threads/{thread_id}/user-data data under users/<target-user-id>/threads/{thread_id}/user-data, which matches the new provisioner PVC subPath when the gateway base directory is mounted at deer-flow/ on the PVC. Use default as the target user only when the legacy data should remain in the default no-auth user namespace. Run the migration while no gateway or sandbox Pods are writing to those paths.
When skills are materialized per thread on the same PVC, set SKILLS_PVC_NAME to that PVC and configure SKILLS_PVC_SUBPATH_TEMPLATE=deer-flow/users/{user_id}/threads/{thread_id}/skills. Leaving the template empty preserves the legacy behavior of mounting the skills PVC root at /mnt/skills.
Important: K8S_API_SERVER Override
If your kubeconfig uses localhost, 127.0.0.1, or 0.0.0.0 as the API server address (common with OrbStack, minikube, kind), the provisioner cannot reach it from inside the Docker container.
Solution: Set K8S_API_SERVER to use host.docker.internal:
# docker-compose-dev.yaml
provisioner:
environment:
- K8S_API_SERVER=https://host.docker.internal:26443 # Replace 26443 with your API port
Check your kubeconfig API server:
kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'
Prerequisites
Host Machine Requirements
-
Kubernetes Cluster:
- Docker Desktop with Kubernetes enabled, or
- OrbStack (built-in K8s), or
- minikube, kind, k3s, etc.
-
kubectl Configured:
~/.kube/configmust exist and be valid- Current context should point to your local cluster
-
Kubernetes Access:
- The provisioner needs permissions to:
- Create/read/delete Pods in the
deer-flownamespace - Create/read/delete Services in the
deer-flownamespace - Read Namespaces (to create
deer-flowif missing)
- Create/read/delete Pods in the
- The provisioner needs permissions to:
-
Host Paths:
- The
SKILLS_HOST_PATHandTHREADS_HOST_PATHmust be absolute paths on the host machine - These paths are mounted into sandbox Pods via K8s HostPath volumes
- The paths must exist and be readable by the K8s node
- The
Docker Compose Setup
The provisioner runs as part of the docker-compose-dev stack:
# Start Docker services (provisioner starts only when config.yaml enables provisioner mode)
make docker-start
# Or start just the provisioner
docker compose -p deer-flow-dev -f docker/docker-compose-dev.yaml up -d provisioner
The compose file:
- Mounts your host's
~/.kube/configinto the container - Adds
extra_hostsentry forhost.docker.internal(required on Linux) - Configures environment variables for K8s access
Testing
Manual API Testing
# Health check
curl http://localhost:8002/health
# Create a sandbox (via provisioner container for internal DNS)
docker exec deer-flow-provisioner curl -X POST http://localhost:8002/api/sandboxes \
-H "Content-Type: application/json" \
-d '{"sandbox_id":"test-001","thread_id":"thread-001","user_id":"user-001"}'
# Check sandbox status
docker exec deer-flow-provisioner curl http://localhost:8002/api/sandboxes/test-001
# List all sandboxes
docker exec deer-flow-provisioner curl http://localhost:8002/api/sandboxes
# Verify Pod and Service in K8s
kubectl get pod,svc -n deer-flow -l sandbox-id=test-001
# Delete sandbox
docker exec deer-flow-provisioner curl -X DELETE http://localhost:8002/api/sandboxes/test-001
Verify from Backend Containers
Once a sandbox is created, the backend containers (gateway, langgraph) can access it:
# Get sandbox URL from provisioner
SANDBOX_URL=$(docker exec deer-flow-provisioner curl -s http://localhost:8002/api/sandboxes/test-001 | jq -r .sandbox_url)
# Test from gateway container
docker exec deer-flow-gateway curl -s $SANDBOX_URL/v1/sandbox
Troubleshooting
Issue: "Kubeconfig not found"
Cause: The kubeconfig file doesn't exist at the mounted path.
Solution:
- Ensure
~/.kube/configexists on your host machine - Run
kubectl config viewto verify - Check the volume mount in docker-compose-dev.yaml
Issue: "Kubeconfig path is a directory"
Cause: The mounted KUBECONFIG_PATH points to a directory instead of a file.
Solution:
- Ensure the compose mount source is a file (e.g.,
~/.kube/config) not a directory - Verify inside container:
docker exec deer-flow-provisioner ls -ld /root/.kube/config - Expected output should indicate a regular file (
-), not a directory (d)
Issue: "Connection refused" to K8s API
Cause: The provisioner can't reach the K8s API server.
Solution:
- Check your kubeconfig server address:
kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' - If it's
localhostor127.0.0.1, setK8S_API_SERVER:environment: - K8S_API_SERVER=https://host.docker.internal:PORT
Issue: "Unprocessable Entity" when creating Pod
Cause: HostPath volumes contain invalid paths (e.g., relative paths with ..).
Solution:
- Use absolute paths for
SKILLS_HOST_PATHandTHREADS_HOST_PATH - Verify the paths exist on your host machine:
ls -la /path/to/skills ls -la /path/to/backend/.deer-flow/threads
Issue: Pod stuck in "ContainerCreating"
Cause: Usually pulling the sandbox image from the registry.
Solution:
- Pre-pull the image:
make docker-init - Check Pod events:
kubectl describe pod sandbox-XXX -n deer-flow - Check node:
kubectl get nodes
Issue: Cannot access sandbox URL from backend
Cause: The backend cannot resolve or reach the sandbox ClusterIP Service DNS. This usually means the backend is not running inside the same Kubernetes cluster/network or cluster DNS/network policy is blocking access.
Solution:
- Verify the Service exists:
kubectl get svc -n deer-flow - In NodePort mode, test from the backend container:
curl http://$NODE_HOST:NODE_PORT/v1/sandbox - In ClusterIP mode, test from the backend Pod:
curl http://sandbox-XXX-svc.deer-flow.svc.cluster.local:8080/v1/sandbox - Check
NODE_HOSTfor NodePort deployments, or cluster DNS / NetworkPolicy / service mesh rules for ClusterIP deployments
Security Considerations
-
HostPath Volumes: The provisioner mounts host directories into sandbox Pods by default. Ensure these paths contain only trusted data. For production, prefer PVC-based volumes (set
SKILLS_PVC_NAMEandUSERDATA_PVC_NAME) to avoid node-specific data loss risks. -
Resource Limits: Each sandbox Pod has CPU, memory, and storage limits to prevent resource exhaustion.
-
Network Isolation: Sandbox Pods run in the configured namespace and are exposed through NodePort or ClusterIP Services. Prefer ClusterIP with NetworkPolicies for in-cluster deployments.
-
kubeconfig Access: The provisioner has full access to your Kubernetes cluster via the mounted kubeconfig. Run it only in trusted environments.
-
Image Trust: The sandbox image should come from a trusted registry. Review and audit the image contents.
Future Enhancements
- Support for custom resource requests/limits per sandbox
- PersistentVolume support for larger data requirements
- Automatic cleanup of stale sandboxes (timeout-based)
- Metrics and monitoring (Prometheus integration)
- Multi-cluster support (route to different K8s clusters)
- Pod affinity/anti-affinity rules for better placement
- NetworkPolicy templates for sandbox isolation