Mount buckets
Last updated Sep 30, 2026 Note
This page documents Sandbox SDK 0.x for existing applications. For new applications, refer to Mount an R2 bucket. To move an existing application to @cloudflare/sandbox 1.0, refer to Migrate from Sandbox SDK 0.x.
Mount S3-compatible object storage buckets as local filesystem paths. Access object storage using standard file operations. For Cloudflare R2 in production, you can also mount by Worker R2 binding name so credentials stay in the Worker runtime.
Mounting `/workspace`
Mounting a bucket at /workspace or a subpath under it can be confusing in app or project setups. In production, the mount overlays that path instead of merging with files already in the image or template. If you want /workspace itself to persist over time, Backup and restore is often a better fit.
S3-compatible providers
The SDK works with any S3-compatible object storage provider. Examples include Cloudflare R2, Amazon S3, Google Cloud Storage, Backblaze B2, MinIO, and many others ↗︎. The SDK automatically detects and optimizes for R2, S3, and GCS.
Production prerequisites for R2 binding mounts
To mount an R2 bucket in production without passing credentials into the container, add an R2 binding and export ContainerProxy from your Worker entrypoint.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"r2_buckets": [
{
"binding": "MY_BUCKET",
"bucket_name": "my-r2-bucket"
}
]
}
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-r2-bucket"
import { ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };
import { ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };
When you omit endpoint, the first argument to mountBucket() must be the Worker R2 binding name, such as MY_BUCKET.
When to mount buckets
Mount S3-compatible buckets when you need:
- Persistent data - Data survives sandbox destruction
- Large datasets - Process data without downloading
- Shared storage - Multiple sandboxes access the same data
- Cost-effective persistence - Cheaper than keeping sandboxes alive
Mount an R2 bucket
import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "data-processor");
// Mount R2 bucket by Worker binding name
await sandbox.mountBucket("MY_BUCKET", "/data");
// Access bucket with standard filesystem operations
await sandbox.exec("ls", { args: ["/data"] });
await sandbox.writeFile("/data/results.json", JSON.stringify(results));
// Use from Python
await sandbox.exec("python", {
args: [
"-c",
`
import pandas as pd
df = pd.read_csv('/data/input.csv')
df.describe().to_csv('/data/summary.csv')
`,
],
});
import { getSandbox } from '@cloudflare/sandbox';
const sandbox = getSandbox(env.Sandbox, 'data-processor');
// Mount R2 bucket by Worker binding name
await sandbox.mountBucket('MY_BUCKET', '/data');
// Access bucket with standard filesystem operations
await sandbox.exec('ls', { args: ['/data'] });
await sandbox.writeFile('/data/results.json', JSON.stringify(results));
// Use from Python
await sandbox.exec('python', { args: ['-c', `
import pandas as pd
df = pd.read_csv('/data/input.csv')
df.describe().to_csv('/data/summary.csv')
`] });
In this example, MY_BUCKET is the binding name from wrangler.toml. It does not have to match the bucket's dashboard name, although many projects use matching names.
Mounting affects entire sandbox
Mounted buckets are visible across all sessions since they share the filesystem. Mount once per sandbox.
Credentials
R2 binding mounts do not require credentials. Remote endpoint mounts remain supported for Cloudflare R2 and other S3-compatible providers, and those flows can still use automatic credential detection or explicit credentials.
Automatic detection
When you include an endpoint, set credentials as Worker secrets and the SDK automatically detects them:
npx wrangler secret put R2_ACCESS_KEY_ID
npx wrangler secret put R2_SECRET_ACCESS_KEY
R2 credentials
We also automatically detect AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY for compatibility with other S3-compatible providers.
// Credentials automatically detected from environment for remote endpoint mounts
await sandbox.mountBucket("my-r2-bucket", "/data", {
endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",
});
// Credentials automatically detected from environment for remote endpoint mounts
await sandbox.mountBucket('my-r2-bucket', '/data', {
endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com'
});
Explicit credentials
Pass credentials directly when needed:
await sandbox.mountBucket("my-r2-bucket", "/data", {
endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",
credentials: {
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
},
});
await sandbox.mountBucket('my-r2-bucket', '/data', {
endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',
credentials: {
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY
}
});
Credential proxy
When you mount with explicit credentials, s3fs writes those credentials to a password file on the container's disk. A compromised container process can read and exfiltrate the credentials, or use them to access storage outside the intended bucket scope.
Set credentialProxy: true to keep credentials out of the container entirely. Instead of passing real credentials into the container, the Durable Object intercepts all outbound S3 requests at the network layer, re-signs them with the real credentials, and forwards them upstream. The container only ever holds dummy credentials that are useless outside the proxy.
This works with AWS SigV4 ↗︎ signing for S3-compatible endpoints (including R2) and HMAC signing for Google Cloud Storage. It is recommended to set credentialProxy: true for all endpoint mounts. The option defaults to false for backwards compatibility and will become the default in a future version of the Sandbox SDK.
await sandbox.mountBucket("my-bucket", "/data", {
endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",
provider: "r2",
credentials: {
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
},
credentialProxy: true,
});
await sandbox.mountBucket('my-bucket', '/data', {
endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',
provider: 'r2',
credentials: {
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY
},
credentialProxy: true
});
ContainerProxy export required
Credential proxy mounts use egress interception and require ContainerProxy to be exported from your Worker entrypoint. Without this export, the proxy cannot intercept requests and the mount will fail.
import { ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };
Mount bucket subdirectories
Mount a specific subdirectory within a bucket using the prefix option. Only contents under the prefix are visible at the mount point:
// Mount only the /uploads/images/ subdirectory
await sandbox.mountBucket("MY_BUCKET", "/images", {
prefix: "/uploads/images/",
});
// Files appear at mount point without the prefix
// Bound bucket: my-r2-bucket/uploads/images/photo.jpg
// Mounted path: /images/photo.jpg
await sandbox.exec("ls", { args: ["/images"] });
// Write to subdirectory
await sandbox.writeFile("/images/photo.jpg", imageData);
// Creates my-r2-bucket/uploads/images/photo.jpg
// Mount different prefixes to different paths
await sandbox.mountBucket("MY_BUCKET", "/training-data", {
prefix: "/ml/training/",
});
await sandbox.mountBucket("MY_BUCKET", "/test-data", {
prefix: "/ml/testing/",
});
// Mount only the /uploads/images/ subdirectory
await sandbox.mountBucket('MY_BUCKET', '/images', {
prefix: '/uploads/images/'
});
// Files appear at mount point without the prefix
// Bound bucket: my-r2-bucket/uploads/images/photo.jpg
// Mounted path: /images/photo.jpg
await sandbox.exec('ls', { args: ['/images'] });
// Write to subdirectory
await sandbox.writeFile('/images/photo.jpg', imageData);
// Creates my-r2-bucket/uploads/images/photo.jpg
// Mount different prefixes to different paths
await sandbox.mountBucket('MY_BUCKET', '/training-data', {
prefix: '/ml/training/'
});
await sandbox.mountBucket('MY_BUCKET', '/test-data', {
prefix: '/ml/testing/'
});
Prefix format
The prefix must start with / (for example, /data or /logs/2024/).
Read-only mounts
Protect data by mounting buckets in read-only mode:
await sandbox.mountBucket("MY_BUCKET", "/data", {
readOnly: true,
});
// Reads work
await sandbox.exec("cat", { args: ["/data/dataset.csv"] });
// Writes fail
await sandbox.writeFile("/data/new-file.txt", "data"); // Error: Read-only filesystem
await sandbox.mountBucket('MY_BUCKET', '/data', {
readOnly: true
});
// Reads work
await sandbox.exec('cat', { args: ['/data/dataset.csv'] });
// Writes fail
await sandbox.writeFile('/data/new-file.txt', 'data'); // Error: Read-only filesystem
Local development
You can also mount R2 buckets during local development with wrangler dev by passing the localBucket option. Production R2 binding mounts and local localBucket mounts both avoid explicit credentials, but they are different execution paths. Production uses credential-less egress interception and overlays the target path. Local development uses periodic synchronization with the R2 binding.
await sandbox.mountBucket("MY_BUCKET", "/data", {
localBucket: true,
});
// Access files using standard operations
await sandbox.exec("ls", { args: ["/data"] });
await sandbox.writeFile("/data/results.json", JSON.stringify(results));
await sandbox.mountBucket('MY_BUCKET', '/data', {
localBucket: true
});
// Access files using standard operations
await sandbox.exec('ls', { args: ['/data'] });
await sandbox.writeFile('/data/results.json', JSON.stringify(results));
Note
You can use an environment variable to toggle localBucket between local development and production. Set an environment variable such as LOCAL_DEV in your Wrangler configuration using vars for local development, then reference it in your code:
const mountOptions = env.LOCAL_DEV ? { localBucket: true } : {};
await sandbox.mountBucket('MY_BUCKET', '/data', mountOptions);
When localBucket is true, the SDK uses local R2 binding synchronization. When localBucket is false or omitted and endpoint is also omitted, the SDK uses the production R2 binding mount path. For more information on setting environment variables, refer to Environment variables in Wrangler configuration.
The readOnly and prefix options work the same way in local mode:
