MinIO / S3 Cloud Storage Volumes
Persist sandbox files to MinIO or any S3-compatible store via an rclone-backed Docker volume — config inline, no rclone.conf
Persist sandbox files to MinIO — or any S3-compatible store (AWS S3, Cloudflare R2, Backblaze B2, GCS via HMAC) — through the rclone Docker volume plugin. The library never learns about rclone or credentials; it just attaches an ordinary Docker volume.
There are only two steps: install the plugin once, then hand createDockerSandbox a volume whose driverOptions carry an rclone connection string. No rclone.conf, no daemon to run, nothing to maintain.
1. Install the rclone volume plugin (once per machine)
# arm64 on Apple silicon, amd64 on Intel / most Linux
docker plugin install rclone/docker-volume-rclone:arm64 \
args="-v" --alias rclone --grant-all-permissionsdocker plugin ls should now show rclone:latest enabled. That is the entire host-level setup — rclone is now a volume driver Docker knows about, like the built-in local driver but backed by your bucket.
2. Attach a bucket as a volume
Put the whole backend config inline as a connection string. With lifecycle: 'managed' the sandbox creates the volume on start and removes it on dispose:
import { createDockerSandbox } from '@deepagents/context';
const e = process.env; // your bucket's endpoint + credentials
await using sandbox = await createDockerSandbox({
image: 'alpine:latest',
volumes: [
{
type: 'volume',
name: 'agent-storage',
containerPath: '/workspace/storage',
readOnly: false,
lifecycle: 'managed',
driver: 'rclone',
driverOptions: {
remote: `:s3,provider=Minio,access_key_id=${e.MINIO_KEY},secret_access_key=${e.MINIO_SECRET},endpoint="${e.MINIO_ENDPOINT}":${e.MINIO_BUCKET}`,
'vfs-cache-mode': 'writes',
'vfs-write-back': '1s',
},
},
],
});
await sandbox.executeCommand('echo "result" > /workspace/storage/output.txt');A file written to /workspace/storage/output.txt becomes the object output.txt in your bucket.
Quote the endpoint. The
://must be quoted (endpoint="https://…"), or rclone splits on the colon and fails withCustom endpoint 'http' was not a valid URI.
Same volume, any backend — just change the connection string
The provider preset and endpoint are the only differences:
# MinIO: :s3,provider=Minio,access_key_id=…,secret_access_key=…,endpoint="https://minio-api.you.com":BUCKET
# AWS S3: :s3,provider=AWS,access_key_id=…,secret_access_key=…,region=us-east-1:BUCKET
# GCS: :s3,provider=GCS,access_key_id=GOOG…,secret_access_key=…,endpoint="https://storage.googleapis.com":BUCKET
# R2: :s3,provider=Cloudflare,access_key_id=…,secret_access_key=…,endpoint="https://<acct>.r2.cloudflarestorage.com":BUCKETGCS works here because Google Cloud Storage exposes the S3 API with HMAC keys — provider=GCS. (The native GCS rclone backend can't go in a connection string; for that, use a named remote — see below.)
Try it locally (optional)
Spin up a local MinIO, then use endpoint="http://host.docker.internal:9000" in the connection string — the plugin runs inside Docker Desktop's VM and reaches the published port that way:
services:
minio:
image: minio/minio
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports: ['9000:9000', '9001:9001']
volumes: ['minio-data:/data']
createbucket:
image: minio/mc
depends_on: [minio]
entrypoint: >
/bin/sh -c "until mc alias set local http://minio:9000 minioadmin minioadmin; do sleep 1; done; mc mb -p local/agent-storage; exit 0;"
volumes:
minio-data:docker compose up -dThings to know
- Writes are eventually consistent. With
vfs-cache-mode=writes, a write lands in a local cache and uploads after the file closes and the--vfs-write-backdelay elapses (rclone default5s; the example above uses1s), with a final flush on unmount. The upload is asynchronous even at0s, so a reader on the S3 side (your app, another service) must poll for the object (HEAD until it appears) or read back through the mounted volume instead. A lowvfs-write-backcosts extra PUT requests — rapid re-writes of the same file upload each time instead of coalescing — a fine trade for the create-and-close pattern below. - It's FUSE over object storage. Whole-file create-and-close is the reliable write pattern; in-place random writes can misbehave.
- Credentials in the connection string show up in
docker volume inspect. Fine for local dev. To keep them out of volume metadata, use a named remote (below). - No coupling in the library.
@deepagents/contextonly attaches an ordinary Docker volume; switching providers is a one-line connection-string change.
Keeping credentials out of volume metadata
If you don't want the access key visible in docker volume inspect, define a named remote in the plugin's rclone.conf and reference it by name. The config lives at /var/lib/docker-plugins/rclone/config/rclone.conf on the daemon host.
[minio]
type = s3
provider = Minio
access_key_id = YOUR_KEY
secret_access_key = YOUR_SECRET
endpoint = https://minio-api.you.comOn a Linux daemon, write it with sudo tee. On Docker Desktop the path is inside the VM, so write it through a one-shot helper:
docker run --rm --privileged --pid=host justincormack/nsenter1 /bin/sh -c '
cat > /var/lib/docker-plugins/rclone/config/rclone.conf <<EOF
[minio]
type = s3
provider = Minio
access_key_id = YOUR_KEY
secret_access_key = YOUR_SECRET
endpoint = https://minio-api.you.com
EOF'
docker plugin disable rclone && docker plugin enable rclone # reload configThen reference the named remote instead of a connection string:
driverOptions: { remote: 'minio:agent-storage', 'vfs-cache-mode': 'writes', 'vfs-write-back': '1s' }Gotcha: a bad or stale remote in
rclone.confmakes the plugin hang on startup, sodocker plugin enablefails withdial unix …/rclone.sock: connect: no such file or directory. Remove the broken stanza and re-enable. The connection-string method sidesteps this entirely — there is no config file to break.