Skip to content

Commit 4de69fd

Browse files
committed
feat(provisioner): add optional PVC support for sandbox volumes (#1978)
Add SKILLS_PVC_NAME and USERDATA_PVC_NAME env vars to allow sandbox Pods to use PersistentVolumeClaims instead of hostPath volumes. This prevents data loss in production when pods are rescheduled across nodes. When USERDATA_PVC_NAME is set, a subPath of threads/{thread_id}/user-data is used so a single PVC can serve multiple threads. Falls back to hostPath when the new env vars are not set, preserving backward compatibility.
1 parent ad6d934 commit 4de69fd

3 files changed

Lines changed: 71 additions & 30 deletions

File tree

docker/docker-compose-dev.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ services:
3636
# export DEER_FLOW_ROOT=/absolute/path/to/deer-flow
3737
- SKILLS_HOST_PATH=${DEER_FLOW_ROOT}/skills
3838
- THREADS_HOST_PATH=${DEER_FLOW_ROOT}/backend/.deer-flow/threads
39+
# Production: use PVC instead of hostPath to avoid data loss on node failure.
40+
# When set, hostPath vars above are ignored for the corresponding volume.
41+
# USERDATA_PVC_NAME uses subPath (threads/{thread_id}/user-data) automatically.
42+
# - SKILLS_PVC_NAME=deer-flow-skills-pvc
43+
# - USERDATA_PVC_NAME=deer-flow-userdata-pvc
3944
- KUBECONFIG_PATH=/root/.kube/config
4045
- NODE_HOST=host.docker.internal
4146
# Override K8S API server URL since kubeconfig uses 127.0.0.1

docker/provisioner/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ The provisioner is configured via environment variables (set in [docker-compose-
137137
| `SANDBOX_IMAGE` | `enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest` | Container image for sandbox Pods |
138138
| `SKILLS_HOST_PATH` | - | **Host machine** path to skills directory (must be absolute) |
139139
| `THREADS_HOST_PATH` | - | **Host machine** path to threads data directory (must be absolute) |
140+
| `SKILLS_PVC_NAME` | empty (use hostPath) | PVC name for skills volume; when set, sandbox Pods use PVC instead of hostPath |
141+
| `USERDATA_PVC_NAME` | empty (use hostPath) | PVC name for user-data volume; when set, uses PVC with `subPath: threads/{thread_id}/user-data` |
140142
| `KUBECONFIG_PATH` | `/root/.kube/config` | Path to kubeconfig **inside** the provisioner container |
141143
| `NODE_HOST` | `host.docker.internal` | Hostname that backend containers use to reach host NodePorts |
142144
| `K8S_API_SERVER` | (from kubeconfig) | Override K8s API server URL (e.g., `https://host.docker.internal:26443`) |
@@ -309,7 +311,7 @@ docker exec deer-flow-gateway curl -s $SANDBOX_URL/v1/sandbox
309311

310312
## Security Considerations
311313

312-
1. **HostPath Volumes**: The provisioner mounts host directories into sandbox Pods. Ensure these paths contain only trusted data.
314+
1. **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_NAME` and `USERDATA_PVC_NAME`) to avoid node-specific data loss risks.
313315

314316
2. **Resource Limits**: Each sandbox Pod has CPU, memory, and storage limits to prevent resource exhaustion.
315317

@@ -322,7 +324,7 @@ docker exec deer-flow-gateway curl -s $SANDBOX_URL/v1/sandbox
322324
## Future Enhancements
323325

324326
- [ ] Support for custom resource requests/limits per sandbox
325-
- [ ] PersistentVolume support for larger data requirements
327+
- [x] PersistentVolume support for larger data requirements
326328
- [ ] Automatic cleanup of stale sandboxes (timeout-based)
327329
- [ ] Metrics and monitoring (Prometheus integration)
328330
- [ ] Multi-cluster support (route to different K8s clusters)

docker/provisioner/app.py

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
)
6161
SKILLS_HOST_PATH = os.environ.get("SKILLS_HOST_PATH", "/skills")
6262
THREADS_HOST_PATH = os.environ.get("THREADS_HOST_PATH", "/.deer-flow/threads")
63+
SKILLS_PVC_NAME = os.environ.get("SKILLS_PVC_NAME", "")
64+
USERDATA_PVC_NAME = os.environ.get("USERDATA_PVC_NAME", "")
6365
SAFE_THREAD_ID_PATTERN = r"^[A-Za-z0-9_\-]+$"
6466

6567
# Path to the kubeconfig *inside* the provisioner container.
@@ -243,6 +245,64 @@ def _sandbox_url(node_port: int) -> str:
243245
return f"http://{NODE_HOST}:{node_port}"
244246

245247

248+
def _build_volumes(thread_id: str) -> list[k8s_client.V1Volume]:
249+
"""Build volume list: PVC when configured, otherwise hostPath."""
250+
if SKILLS_PVC_NAME:
251+
skills_vol = k8s_client.V1Volume(
252+
name="skills",
253+
persistent_volume_claim=k8s_client.V1PersistentVolumeClaimVolumeSource(
254+
claim_name=SKILLS_PVC_NAME,
255+
read_only=True,
256+
),
257+
)
258+
else:
259+
skills_vol = k8s_client.V1Volume(
260+
name="skills",
261+
host_path=k8s_client.V1HostPathVolumeSource(
262+
path=SKILLS_HOST_PATH,
263+
type="Directory",
264+
),
265+
)
266+
267+
if USERDATA_PVC_NAME:
268+
userdata_vol = k8s_client.V1Volume(
269+
name="user-data",
270+
persistent_volume_claim=k8s_client.V1PersistentVolumeClaimVolumeSource(
271+
claim_name=USERDATA_PVC_NAME,
272+
),
273+
)
274+
else:
275+
userdata_vol = k8s_client.V1Volume(
276+
name="user-data",
277+
host_path=k8s_client.V1HostPathVolumeSource(
278+
path=join_host_path(THREADS_HOST_PATH, thread_id, "user-data"),
279+
type="DirectoryOrCreate",
280+
),
281+
)
282+
283+
return [skills_vol, userdata_vol]
284+
285+
286+
def _build_volume_mounts(thread_id: str) -> list[k8s_client.V1VolumeMount]:
287+
"""Build volume mount list, using subPath for PVC user-data."""
288+
userdata_mount = k8s_client.V1VolumeMount(
289+
name="user-data",
290+
mount_path="/mnt/user-data",
291+
read_only=False,
292+
)
293+
if USERDATA_PVC_NAME:
294+
userdata_mount.sub_path = f"threads/{thread_id}/user-data"
295+
296+
return [
297+
k8s_client.V1VolumeMount(
298+
name="skills",
299+
mount_path="/mnt/skills",
300+
read_only=True,
301+
),
302+
userdata_mount,
303+
]
304+
305+
246306
def _build_pod(sandbox_id: str, thread_id: str) -> k8s_client.V1Pod:
247307
"""Construct a Pod manifest for a single sandbox."""
248308
thread_id = _validate_thread_id(thread_id)
@@ -302,40 +362,14 @@ def _build_pod(sandbox_id: str, thread_id: str) -> k8s_client.V1Pod:
302362
"ephemeral-storage": "500Mi",
303363
},
304364
),
305-
volume_mounts=[
306-
k8s_client.V1VolumeMount(
307-
name="skills",
308-
mount_path="/mnt/skills",
309-
read_only=True,
310-
),
311-
k8s_client.V1VolumeMount(
312-
name="user-data",
313-
mount_path="/mnt/user-data",
314-
read_only=False,
315-
),
316-
],
365+
volume_mounts=_build_volume_mounts(thread_id),
317366
security_context=k8s_client.V1SecurityContext(
318367
privileged=False,
319368
allow_privilege_escalation=True,
320369
),
321370
)
322371
],
323-
volumes=[
324-
k8s_client.V1Volume(
325-
name="skills",
326-
host_path=k8s_client.V1HostPathVolumeSource(
327-
path=SKILLS_HOST_PATH,
328-
type="Directory",
329-
),
330-
),
331-
k8s_client.V1Volume(
332-
name="user-data",
333-
host_path=k8s_client.V1HostPathVolumeSource(
334-
path=join_host_path(THREADS_HOST_PATH, thread_id, "user-data"),
335-
type="DirectoryOrCreate",
336-
),
337-
),
338-
],
372+
volumes=_build_volumes(thread_id),
339373
restart_policy="Always",
340374
),
341375
)

0 commit comments

Comments
 (0)