Problem
The current persistent volume implementation backs every volume with a plain host directory and serves it to sandboxes over NFS v3 (packages/orchestrator/pkg/nfsproxy/). This works for sequential large-file access, but has hard limits for real workloads:
| Workload |
Current behaviour |
| Large-file sequential read/write |
Acceptable, but adds NFS round-trip latency |
| Many small files (node_modules, Python packages, build trees) |
Poor — every stat/open/readdir is a separate NFS RPC |
| High-concurrency writes from multiple sandboxes |
No kernel-side cache; lock contention on the NFS server goroutine |
| Sandboxes on different orchestrator nodes sharing a volume |
Not possible — volume lives on a single node's local filesystem |
The last point is the most critical: as a fleet scales to multiple nodes, sandboxes scheduled on different nodes cannot share persistent volumes at all.
Proposed solution
Introduce a pluggable storage backend interface for persistent volumes, so the NFS chroot layer can be backed by any filesystem that satisfies a simple contract, while the default (local directory) is kept as-is.
Interface sketch
// packages/orchestrator/pkg/volumes/backend/backend.go
type Backend interface {
// CreateVolume provisions storage for the given volume ID.
CreateVolume(ctx context.Context, teamID, volumeID uuid.UUID) error
// DeleteVolume removes all data for the volume.
DeleteVolume(ctx context.Context, teamID, volumeID uuid.UUID) error
// RootPath returns the host-side absolute path that the NFS chroot
// (or future virtiofs passthrough) should expose to sandboxes.
// The path must be accessible on every orchestrator node.
RootPath(teamID, volumeID uuid.UUID) string
}
The existing volumes/volume_create.go + chrooted/builder.go path already calls os.MkdirAll(fullPath) — swapping fullPath to come from a Backend.RootPath() call is the only change needed in the hot path.
Target backends
JuiceFS (preferred first target)
- POSIX-compliant FUSE filesystem backed by object storage (S3, GCS, R2) + Redis/TiKV for metadata
- Single binary, no cluster to operate — easy to self-host
- Metadata latency for small files is ~1 ms (Redis) vs ~10 ms+ (NFS RPC over loopback)
- Mount once on the host at
/mnt/juicefs/<team>/<vol>; sandbox chroot sees it as a normal directory
- Handles millions of small files well (Python envs, npm packages, build caches)
Ceph (CephFS)
- Kernel or FUSE client; fits teams already running Ceph
- Better for large-file, high-throughput workloads (ML checkpoints, video assets)
- Requires a Ceph cluster, so higher operational overhead
Generic mountpoint backend
- Simplest: operator pre-mounts any filesystem at a configured root (NFS, SMB, Lustre, Weka, etc.) and points the backend at it
- Zero code beyond reading the configured path
Configuration
# orchestrator environment / config
volumes:
backend: juicefs # "local" | "juicefs" | "cephfs" | "mountpoint"
juicefs:
meta_url: "redis://redis:6379/1"
storage: s3
bucket: s3://my-bucket/e2b-volumes
access_key: ...
secret_key: ...
mountpoint:
root: /mnt/shared-volumes # pre-mounted by operator
Why this matters
- Cross-node volume sharing: sandboxes on any node can read/write the same volume — essential for stateful multi-step workflows (agent pipelines, CI builds, data processing)
- Small-file performance: JuiceFS with a Redis metadata store is 5–20× faster than NFS for workloads dominated by
stat/open on many small files
- Durability: object-storage backends survive orchestrator node failure; local-directory volumes do not
- No API change: the
POST /volumes + VolumeMounts API surface stays identical; the backend is an operator concern
Scope
Out of scope for this issue: virtiofs transport improvement, cross-team volume sharing.
References
- Current NFS proxy:
packages/orchestrator/pkg/nfsproxy/
- Chroot builder:
packages/orchestrator/pkg/chrooted/builder.go
- Volume create:
packages/orchestrator/pkg/volumes/volume_create.go
- Config model:
packages/orchestrator/pkg/cfg/model.go (PersistentVolumeMounts)
- JuiceFS architecture
- CephFS
Problem
The current persistent volume implementation backs every volume with a plain host directory and serves it to sandboxes over NFS v3 (
packages/orchestrator/pkg/nfsproxy/). This works for sequential large-file access, but has hard limits for real workloads:stat/open/readdiris a separate NFS RPCThe last point is the most critical: as a fleet scales to multiple nodes, sandboxes scheduled on different nodes cannot share persistent volumes at all.
Proposed solution
Introduce a pluggable storage backend interface for persistent volumes, so the NFS chroot layer can be backed by any filesystem that satisfies a simple contract, while the default (local directory) is kept as-is.
Interface sketch
The existing
volumes/volume_create.go+chrooted/builder.gopath already callsos.MkdirAll(fullPath)— swappingfullPathto come from aBackend.RootPath()call is the only change needed in the hot path.Target backends
JuiceFS (preferred first target)
/mnt/juicefs/<team>/<vol>; sandbox chroot sees it as a normal directoryCeph (CephFS)
Generic mountpoint backend
Configuration
Why this matters
stat/openon many small filesPOST /volumes+VolumeMountsAPI surface stays identical; the backend is an operator concernScope
Backendinterface inpackages/orchestrator/pkg/volumes/backend/volumes/volume_create.goandchrooted/builder.goto consumeBackend.RootPath()instead of constructing paths directlyLocalBackend(wraps currentos.MkdirAllbehaviour, default)MountpointBackend(operator-managed pre-mount, zero deps)MountpointBackendbacked by a tmpfsOut of scope for this issue: virtiofs transport improvement, cross-team volume sharing.
References
packages/orchestrator/pkg/nfsproxy/packages/orchestrator/pkg/chrooted/builder.gopackages/orchestrator/pkg/volumes/volume_create.gopackages/orchestrator/pkg/cfg/model.go(PersistentVolumeMounts)