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/contextside uses the standardtype: 'volume'primitive withlifecycle: '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
hostPathis validated on the app server and resolved by the daemon on the VM — two different machines. Atype: 'volume'withlifecycle: '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_PROJECTThe 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_PROJECT2. 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-permissions3. 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 rcloneEnable 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=1sThe 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=writesApp 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.tsDeepAgents 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 itshostPathis 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_otheris 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 uncommentuser_allow_otherin/etc/fuse.conf. Docker bind mounts are recursive, so binding/mnt/gcscaptures 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-backdelay elapses (rclone default5s; the volume above uses1s), with a final flush on unmount. The upload is async even at0s, so a reader on the GCS side must poll for the object or read through the mount. A lowvfs-write-backcosts 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
dockerinvocation, 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 ownDOCKER_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/contextnever 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 theremote.