docker/provisioner/README.md
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.
┌────────────┐ HTTP ┌─────────────┐ K8s API ┌──────────────┐
│ Backend │ ─────▸ │ Provisioner │ ────────▸ │ Host K8s │
│ (gateway/ │ │ :8002 │ │ API Server │
│ langgraph) │ └─────────────┘ └──────┬───────┘
└────────────┘ │ creates
│
┌─────────────┐ ┌────▼─────┐
│ Backend │ ──────▸ │ Sandbox │
│ (via Docker │ NodePort│ Pod(s) │
│ network) │ └──────────┘
└─────────────┘
Backend Request: When the backend needs to execute code, it sends a POST /api/sandboxes request with a sandbox_id and thread_id.
Pod Creation: The provisioner creates a dedicated Pod in the deer-flow namespace with:
/mnt/skills → Read-only access to public skills/mnt/user-data → Read-write access to thread-specific dataService Creation: A NodePort Service is created to expose the Pod, with Kubernetes auto-allocating a port from the NodePort range (typically 30000-32767).
Access URL: The provisioner returns http://host.docker.internal:{NodePort} to the backend, which the backend containers can reach directly.
Cleanup: When the session ends, DELETE /api/sandboxes/{sandbox_id} removes both the Pod and Service.
Host machine with a running Kubernetes cluster (Docker Desktop K8s, OrbStack, minikube, kind, etc.)
GET /healthHealth check endpoint.
Response:
{
"status": "ok"
}
POST /api/sandboxesCreate a new sandbox Pod + Service.
Request:
{
"sandbox_id": "abc-123",
"thread_id": "thread-456"
}
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/sandboxesList all sandboxes currently managed.
Response:
{
"sandboxes": [
{
"sandbox_id": "abc-123",
"sandbox_url": "http://host.docker.internal:32123",
"status": "Running"
}
],
"count": 1
}
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 | 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) |
KUBECONFIG_PATH | /root/.kube/config | Path to kubeconfig inside the provisioner container |
NODE_HOST | host.docker.internal | Hostname that backend containers use to reach host NodePorts |
K8S_API_SERVER | (from kubeconfig) | Override K8s API server URL (e.g., https://host.docker.internal:26443) |
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}'
Kubernetes Cluster:
kubectl Configured:
~/.kube/config must exist and be validKubernetes Access:
deer-flow namespacedeer-flow namespacedeer-flow if missing)Host Paths:
SKILLS_HOST_PATH and THREADS_HOST_PATH must be absolute paths on the host machineThe 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:
~/.kube/config into the containerextra_hosts entry for host.docker.internal (required on Linux)# 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"}'
# 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
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
Cause: The kubeconfig file doesn't exist at the mounted path.
Solution:
~/.kube/config exists on your host machinekubectl config view to verifyCause: The mounted KUBECONFIG_PATH points to a directory instead of a file.
Solution:
~/.kube/config) not a directorydocker exec deer-flow-provisioner ls -ld /root/.kube/config
-), not a directory (d)Cause: The provisioner can't reach the K8s API server.
Solution:
kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'
localhost or 127.0.0.1, set K8S_API_SERVER:
environment:
- K8S_API_SERVER=https://host.docker.internal:PORT
Cause: HostPath volumes contain invalid paths (e.g., relative paths with ..).
Solution:
SKILLS_HOST_PATH and THREADS_HOST_PATHls -la /path/to/skills
ls -la /path/to/backend/.deer-flow/threads
Cause: Usually pulling the sandbox image from the registry.
Solution:
make docker-initkubectl describe pod sandbox-XXX -n deer-flowkubectl get nodesCause: NodePort not reachable or NODE_HOST misconfigured.
Solution:
kubectl get svc -n deer-flowcurl http://localhost:NODE_PORT/v1/sandboxextra_hosts is set in docker-compose (Linux)NODE_HOST env var matches how backend reaches hostHostPath Volumes: The provisioner mounts host directories into sandbox Pods. Ensure these paths contain only trusted data.
Resource Limits: Each sandbox Pod has CPU, memory, and storage limits to prevent resource exhaustion.
Network Isolation: Sandbox Pods run in the deer-flow namespace but share the host's network namespace via NodePort. Consider NetworkPolicies for stricter isolation.
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.