Mount buckets
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.
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.
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
import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "data-processor");
// Mount R2 bucket by Worker binding nameawait sandbox.mountBucket("MY_BUCKET", "/data");
// Access bucket with standard filesystem operationsawait sandbox.exec("ls", { args: ["/data"] });await sandbox.writeFile("/data/results.json", JSON.stringify(results));
// Use from Pythonawait sandbox.exec("python", { args: [ "-c", `import pandas as pddf = 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 nameawait sandbox.mountBucket('MY_BUCKET', '/data');
// Access bucket with standard filesystem operationsawait sandbox.exec('ls', { args: ['/data'] });await sandbox.writeFile('/data/results.json', JSON.stringify(results));
// Use from Pythonawait sandbox.exec('python', { args: ['-c', `import pandas as pddf = 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.
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.
When you include an endpoint, set credentials as Worker secrets and the SDK automatically detects them:
npx wrangler secret put R2_ACCESS_KEY_IDnpx wrangler secret put R2_SECRET_ACCESS_KEY// Credentials automatically detected from environment for remote endpoint mountsawait sandbox.mountBucket("my-r2-bucket", "/data", { endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",});// Credentials automatically detected from environment for remote endpoint mountsawait sandbox.mountBucket('my-r2-bucket', '/data', { endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com'});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 }});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/ subdirectoryawait 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.jpgawait sandbox.exec("ls", { args: ["/images"] });
// Write to subdirectoryawait sandbox.writeFile("/images/photo.jpg", imageData);// Creates my-r2-bucket/uploads/images/photo.jpg
// Mount different prefixes to different pathsawait sandbox.mountBucket("MY_BUCKET", "/training-data", { prefix: "/ml/training/",});
await sandbox.mountBucket("MY_BUCKET", "/test-data", { prefix: "/ml/testing/",});// Mount only the /uploads/images/ subdirectoryawait 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.jpgawait sandbox.exec('ls', { args: ['/images'] });
// Write to subdirectoryawait sandbox.writeFile('/images/photo.jpg', imageData);// Creates my-r2-bucket/uploads/images/photo.jpg
// Mount different prefixes to different pathsawait sandbox.mountBucket('MY_BUCKET', '/training-data', { prefix: '/ml/training/'});
await sandbox.mountBucket('MY_BUCKET', '/test-data', { prefix: '/ml/testing/'});Protect data by mounting buckets in read-only mode:
await sandbox.mountBucket("MY_BUCKET", "/data", { readOnly: true,});
// Reads workawait sandbox.exec("cat", { args: ["/data/dataset.csv"] });
// Writes failawait sandbox.writeFile("/data/new-file.txt", "data"); // Error: Read-only filesystemawait sandbox.mountBucket('MY_BUCKET', '/data', { readOnly: true});
// Reads workawait sandbox.exec('cat', { args: ['/data/dataset.csv'] });
// Writes failawait sandbox.writeFile('/data/new-file.txt', 'data'); // Error: Read-only filesystemYou 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 operationsawait 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 operationsawait sandbox.exec('ls', { args: ['/data'] });await sandbox.writeFile('/data/results.json', JSON.stringify(results));The readOnly and prefix options work the same way in local mode:
// Read-only local mountawait sandbox.mountBucket("MY_BUCKET", "/data", { localBucket: true, readOnly: true,});
// Mount a subdirectoryawait sandbox.mountBucket("MY_BUCKET", "/images", { localBucket: true, prefix: "/uploads/images/",});// Read-only local mountawait sandbox.mountBucket('MY_BUCKET', '/data', { localBucket: true, readOnly: true});
// Mount a subdirectoryawait sandbox.mountBucket('MY_BUCKET', '/images', { localBucket: true, prefix: '/uploads/images/'});During local development, files are synchronized between R2 and the container using a periodic sync process rather than a direct filesystem mount. Keep the following in mind:
- Synchronization window - A brief delay exists between when a file is written and when it appears on the other side. For example, if you upload a file to R2 and then immediately read it from the mounted path in the container, the file may not yet be available. Allow a short window for synchronization to complete before reading recently written data.
- High-frequency writes - Rapid successive writes to the same file path may take slightly longer to fully propagate. For best results, avoid writing to the same file from both R2 and the container at the same time.
- Bidirectional sync - Changes made in the container are synced to R2, and changes made in R2 are synced to the container. Both directions follow the same periodic sync model.
// Mount for processingawait sandbox.mountBucket("MY_BUCKET", "/data");
// Do workawait sandbox.exec("python process_data.py");
// Clean upawait sandbox.unmountBucket("/data");// Mount for processingawait sandbox.mountBucket('MY_BUCKET', '/data');
// Do workawait sandbox.exec('python process_data.py');
// Clean upawait sandbox.unmountBucket('/data');The SDK supports any S3-compatible object storage. Here are examples for common providers:
await sandbox.mountBucket("my-s3-bucket", "/data", { endpoint: "https://s3.us-west-2.amazonaws.com", credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY, },});await sandbox.mountBucket('my-s3-bucket', '/data', { endpoint: 'https://s3.us-west-2.amazonaws.com', credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY }});await sandbox.mountBucket("my-gcs-bucket", "/data", { endpoint: "https://storage.googleapis.com", credentials: { accessKeyId: env.GCS_ACCESS_KEY_ID, secretAccessKey: env.GCS_SECRET_ACCESS_KEY, },});await sandbox.mountBucket('my-gcs-bucket', '/data', { endpoint: 'https://storage.googleapis.com', credentials: { accessKeyId: env.GCS_ACCESS_KEY_ID, secretAccessKey: env.GCS_SECRET_ACCESS_KEY }});For providers like Backblaze B2, MinIO, Wasabi, or others, use the standard mount pattern:
await sandbox.mountBucket("my-bucket", "/data", { endpoint: "https://s3.us-west-000.backblazeb2.com", credentials: { accessKeyId: env.ACCESS_KEY_ID, secretAccessKey: env.SECRET_ACCESS_KEY, },});await sandbox.mountBucket('my-bucket', '/data', { endpoint: 'https://s3.us-west-000.backblazeb2.com', credentials: { accessKeyId: env.ACCESS_KEY_ID, secretAccessKey: env.SECRET_ACCESS_KEY }});For provider-specific configuration, see the s3fs-fuse wiki ↗ for supported providers and recommended flags.
Error: R2 binding "MY_BUCKET" not found in Worker env
Solution: Ensure your Worker has an r2_buckets binding and that mountBucket() uses the binding name, not the bucket's dashboard name:
{ "$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"Solution: Ensure your Worker entrypoint exports ContainerProxy. If you are using an older Wrangler version, you may also need the enable_ctx_exports compatibility flag.
Error: MissingCredentialsError: No credentials found
Solution: This error only applies when you mount a remote S3-compatible endpoint by setting endpoint. Set credentials as Worker secrets:
npx wrangler secret put R2_ACCESS_KEY_IDnpx wrangler secret put R2_SECRET_ACCESS_KEYor
npx wrangler secret put AWS_ACCESS_KEY_IDnpx wrangler secret put AWS_SECRET_ACCESS_KEYError: S3FSMountError: mount failed
Common causes:
- Incorrect endpoint URL
- Invalid credentials
- Missing
ContainerProxyexport, or on older Wrangler versions missingenable_ctx_exports - Bucket does not exist
- Network connectivity issues
Verify your binding or endpoint configuration:
try { await sandbox.mountBucket("MY_BUCKET", "/data");} catch (error) { console.error("Mount failed:", error.message); // Check binding name, ContainerProxy export, or remote endpoint configuration}try { await sandbox.mountBucket('MY_BUCKET', '/data');} catch (error) { console.error('Mount failed:', error.message); // Check binding name, ContainerProxy export, or remote endpoint configuration}Error: InvalidMountConfigError: Mount path already in use
Solution: Unmount first or use a different path:
// Unmount existingawait sandbox.unmountBucket("/data");
// Or use different pathawait sandbox.mountBucket("bucket2", "/storage", { endpoint: "..." });// Unmount existingawait sandbox.unmountBucket('/data');
// Or use different pathawait sandbox.mountBucket('bucket2', '/storage', { endpoint: '...' });File operations on mounted buckets are slower than local filesystem due to network latency.
Solution: Copy frequently accessed files locally:
// Copy to local filesystemawait sandbox.exec("cp", { args: ["/data/large-dataset.csv", "/workspace/dataset.csv"],});
// Work with local copy (faster)await sandbox.exec("python", { args: ["process.py", "/workspace/dataset.csv"],});
// Save results back to bucketawait sandbox.exec("cp", { args: ["/workspace/results.json", "/data/results/output.json"],});// Copy to local filesystemawait sandbox.exec('cp', { args: ['/data/large-dataset.csv', '/workspace/dataset.csv'] });
// Work with local copy (faster)await sandbox.exec('python', { args: ['process.py', '/workspace/dataset.csv'] });
// Save results back to bucketawait sandbox.exec('cp', { args: ['/workspace/results.json', '/data/results/output.json'] });- Mount early - Mount buckets at sandbox initialization
- Choose the right mount mode - Use R2 binding mounts when you want Worker-managed R2 access, or use
endpointfor explicit R2, S3, GCS, and other S3-compatible providers - Secure credentials - Always use Worker secrets, never hardcode
- Read-only when possible - Protect data with read-only mounts
- Mount the narrowest path - Use prefixes to expose only the data a sandbox needs
- Mount paths - Prefer
/data,/storage, or/mnt/*; if you mount under/workspace, account for the mount overlaying that path in production - Handle errors - Wrap mount operations in
try...catchblocks - Optimize access - Copy frequently accessed files locally
- Persistent storage tutorial - Complete R2 example
- Storage API reference - Full method documentation
- Environment variables - Credential configuration for remote endpoint mounts
- Wrangler configuration - Configure R2 bindings and compatibility flags
- R2 documentation - Learn about Cloudflare R2
- Outbound traffic - Learn how
ContainerProxyand outbound interception work