Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox
Recipes

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-permissions

docker 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 with Custom 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":BUCKET

GCS works here because Google Cloud Storage exposes the S3 API with HMAC keysprovider=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 -d

Things 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-back delay elapses (rclone default 5s; the example above uses 1s), with a final flush on unmount. The upload is asynchronous even at 0s, 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 low vfs-write-back costs 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/context only 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.com

On 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 config

Then 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.conf makes the plugin hang on startup, so docker plugin enable fails with dial 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.

On this page