Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox
Recipes

GCS Cloud Storage Volumes

Persist sandbox files to Google Cloud Storage with either a remote Docker volume or a same-host gcsfuse mount

This recipe covers two production paths for persisting sandbox files to Google Cloud Storage:

  • A remote Docker daemon on a GCP VM, where an rclone volume plugin backs a named Docker volume. The @deepagents/context side uses the standard type: 'volume' primitive with lifecycle: 'external'.
  • A same-host Linux daemon, where gcsfuse mounts the bucket on the host and gcs() exposes that mountpoint to the sandbox as a typed bind volume.

In both cases, the cloud wiring (plugin, credentials, IAM, and host mounts) stays outside DeepAgents, exactly where the sandbox docs say it belongs.

Remote daemon on a GCP VM

Topology

The recommended deployment runs the agent on one server and the Docker daemon on a GCP VM. Every docker call DeepAgents makes is routed to the VM, so the bucket mount, the FUSE process, and the containers all live on the VM:

┌──────────── App server ────────────┐        ┌─────────────── GCP VM ───────────────┐
│ Node app → createDockerSandbox      │  ssh   │ Docker Engine (daemon)                │
│ Docker CLI                          │ ─────▶ │  ├─ rclone plugin (env_auth = true)   │
│ DOCKER_CONTEXT=gcp-vm               │        │  ├─ volume "agent-storage" → gcs:…    │
│ (no daemon here)                    │        │  └─ sandbox containers mount it       │
└─────────────────────────────────────┘        │ Attached service account ──▶ metadata │
                                                │ server ──▶ Google Cloud Storage       │
                                                └───────────────────────────────────────┘

Named volumes (not bind mounts) are required for a remote daemon. A bind mount's hostPath is validated on the app server and resolved by the daemon on the VM — two different machines. A type: 'volume' with lifecycle: 'external' is inspected on the daemon, so it stays entirely VM-side.

VM setup

1. Let the VM's service account reach the bucket

GCE credentials are read automatically from the metadata server — no key files. Grant the VM's attached service account object access to the bucket:

gcloud storage buckets add-iam-policy-binding gs://YOUR_BUCKET \
  --member="serviceAccount:VM_SERVICE_ACCOUNT_EMAIL" \
  --role="roles/storage.objectAdmin" \
  --project YOUR_PROJECT

The VM also needs an OAuth access scope that includes storage. cloud-platform is simplest:

# Changing scopes requires the VM to be stopped first.
gcloud compute instances set-service-account YOUR_VM \
  --service-account VM_SERVICE_ACCOUNT_EMAIL \
  --scopes cloud-platform \
  --zone YOUR_ZONE --project YOUR_PROJECT

2. Install the rclone volume plugin

Use the tag matching the VM's architecture (amd64 or arm64):

docker plugin install rclone/docker-volume-rclone:amd64 \
  args="-v" --alias rclone --grant-all-permissions

3. Write the rclone config, then enable

On a GCP VM, env_auth = true makes rclone use the attached service account via the metadata server — no credentials in the file. bucket_policy_only = true is required for buckets with uniform bucket-level access.

sudo mkdir -p /var/lib/docker-plugins/rclone/config
sudo tee /var/lib/docker-plugins/rclone/config/rclone.conf >/dev/null <<'EOF'
[gcs]
type = google cloud storage
env_auth = true
bucket_policy_only = true
EOF

docker plugin enable rclone

Enable the plugin after the config exists. A managed plugin only re-reads its config on disable/enable, and once a volume references it you can no longer disable it. Enabling against a missing or invalid config leaves a plugin whose background uploader is dead — reads work, writes silently stay in the local cache and never reach GCS.

4. Create the volume(s)

docker volume create agent-storage -d rclone \
  -o remote=gcs:YOUR_BUCKET \
  -o vfs-cache-mode=writes \
  -o vfs-write-back=1s

The bucket-to-volume mapping is decided here, so both isolation models fall out of how you create volumes — no DeepAgents change:

# Bucket per agent
docker volume create agent-a -d rclone -o remote=gcs:bucket-a -o vfs-cache-mode=writes
docker volume create agent-b -d rclone -o remote=gcs:bucket-b -o vfs-cache-mode=writes

# One bucket, a prefix per agent
docker volume create agent-a -d rclone -o remote=gcs:shared/agent-a -o vfs-cache-mode=writes
docker volume create agent-b -d rclone -o remote=gcs:shared/agent-b -o vfs-cache-mode=writes

App server setup

5. Point the docker CLI at the VM

A docker context is the cleanest way to target the remote daemon. SSH needs no exposed ports or TLS certs:

docker context create gcp-vm --docker "host=ssh://USER@VM_HOST"

Select it per-process via the environment that runs your app (preferred over docker context use, which mutates global state):

DOCKER_CONTEXT=gcp-vm node your-app.ts

DeepAgents shells out to the docker CLI without overriding the host, so it inherits this context automatically. Make sure DOCKER_HOST is unset — it would override the context.

6. Attach the volume

import { createDockerSandbox } from '@deepagents/context';

const sandbox = await createDockerSandbox({
  image: 'node:lts-alpine',
  volumes: [
    {
      type: 'volume',
      name: 'agent-storage',
      containerPath: '/workspace/storage',
      readOnly: false,
      lifecycle: 'external', // the volume already exists on the VM; DeepAgents only inspects it
    },
  ],
});

await sandbox.executeCommand('echo "result" > /workspace/storage/output.txt');

The file written at /workspace/storage/output.txt becomes an object in gcs:YOUR_BUCKET.

Same-host daemon: gcsfuse + gcs()

When your app process and the Docker daemon run on the same Linux host, you can skip the rclone plugin: mount the bucket on the host with gcsfuse and bind that mountpoint in with the gcs() helper. The host does all the FUSE work; the sandbox gets a plain bind mount with no capabilities and no in-container FUSE.

gcs() returns a bind volume, so its hostPath is validated on the machine running your app — which must therefore be the daemon host. For a remote daemon use the named-volume approach above instead, and on macOS/Docker Desktop neither host-side option works because the daemon host is a hidden VM.

On the daemon host

# 1. Install gcsfuse (Debian/Ubuntu shown; see the gcsfuse docs for other distros)
export GCSFUSE_REPO=gcsfuse-$(lsb_release -c -s)
echo "deb https://packages.cloud.google.com/apt $GCSFUSE_REPO main" \
  | sudo tee /etc/apt/sources.list.d/gcsfuse.list
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
sudo apt-get update && sudo apt-get install -y gcsfuse

# 2. Make credentials available to the host (ADC): a service account key, or on GCE the metadata server.
gcloud auth application-default login   # dev; on GCE the attached SA is used automatically

# 3. Mount the bucket as root so the container (any uid) can read it via allow_other.
sudo mkdir -p /mnt/gcs
sudo gcsfuse -o allow_other --implicit-dirs YOUR_BUCKET /mnt/gcs

allow_other is what lets a container process — frequently not the mounting user — read the mount. As root (sudo) it works directly; if you mount as a non-root user you must first uncomment user_allow_other in /etc/fuse.conf. Docker bind mounts are recursive, so binding /mnt/gcs captures the gcsfuse mount that already lives there (mount the bucket before creating the sandbox).

Unmounting is the host's job too: fusermount -u /mnt/gcs. The sandbox does not manage the host mount lifecycle. As with rclone, gcsfuse is FUSE over object storage — whole-file create-and-close writes are the reliable pattern, and a freshly written object may take a moment to appear.

In your app

import { createDockerSandbox, gcs } from '@deepagents/context';

const sandbox = await createDockerSandbox({
  volumes: [gcs({ hostPath: '/mnt/gcs', mountPath: '/workspace/storage' })],
});

await sandbox.executeCommand('echo "result" > /workspace/storage/output.txt');

gcs() is just a typed bind volume over the host mountpoint — the library never learns about GCS, gcsfuse, or credentials.

Things to know

  • Writes are eventually consistent. With vfs-cache-mode=writes, writes land in a local cache and upload asynchronously after the file closes and the --vfs-write-back delay elapses (rclone default 5s; the volume above uses 1s), with a final flush on unmount. The upload is async even at 0s, so a reader on the GCS side must poll for the object or read through the mount. A low vfs-write-back costs extra uploads when the same file is re-written rapidly (no coalescing) — fine for whole-file create-and-close writes.
  • One app process targets one daemon. DeepAgents resolves the daemon globally per docker invocation, so a single app process sends all sandboxes to whichever VM the context points at. To spread sandboxes across multiple VMs, run one app instance per VM, each with its own DOCKER_CONTEXT.
  • This is FUSE over object storage. Tools that do in-place random writes can misbehave; whole-file create-and-close writes are the reliable pattern.
  • No GCS coupling in the library. @deepagents/context never learns about GCS, rclone, or credentials — it only attaches ordinary Docker storage primitives (named volumes or bind mounts). The same recipe works for any rclone backend (S3, Azure, SFTP) by changing the remote.

On this page