Move files in and out of a sandbox
Last updated Oct 22, 2026
This guide streams an uploaded file into a sandbox, compresses it with gzip, and streams the compressed file back to the caller.
The Files class from @cloudflare/sandbox reads and writes files in a running Linux instance, and exec() runs the command. Files does not start the instance, so your Durable Object starts it before each file operation.
Prerequisites
- A Worker project from Run a Linux command. This project uses a Durable Object that starts a container with the Durable Object scheduling policy.
You must have Docker running locally when you run wrangler deploy. For most people, the best way to install Docker is to follow the docs for installing Docker Desktop ↗︎. Other tools like Colima ↗︎ may also work.
You can check that Docker is running properly by running the docker info command in your terminal. If Docker is running, the command will succeed. If Docker is not running, the docker info command will hang or return an error including the message "Cannot connect to the Docker daemon".
Run a command on an uploaded file
- Install the package:npmyarnpnpmbun
npm i @cloudflare/sandbox
yarn add @cloudflare/sandbox
pnpm add @cloudflare/sandbox
bun add @cloudflare/sandbox
- To give
Filesits helper binary, create aDockerfilein the project root. If you already have one, add theCOPYline to it:
Dockerfiledockerfile
```dockerfile FROM node:24-trixie-slim
COPY --from=docker.io/cloudflare/sandbox:1.0.1 /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim
RUN mkdir /workspace CMD ["sleep", "infinity"] ```
The cloudflare/sandbox tag must match the installed package version. A mismatch causes SandboxProtocolError. Replace the FROM line with the base image your commands need.
3. To build the image, add the nodejs_compat flag to wrangler.jsonc and replace the containers entry:
jsonc
{
"compatibility_flags": ["nodejs_compat"],
"containers": [
{
"class_name": "MyContainer",
"scheduling_policy": "durable_object",
"images": {
"workspace": {
"dockerfile": "./Dockerfile",
},
},
},
],
}
```toml compatibility_flags = [ "nodejs_compat" ]
[[containers]] class_name = "MyContainer" scheduling_policy = "durable_object"
[containers.images.workspace] dockerfile = "./Dockerfile" ```
The workspace key names the image. Your code starts it as container.images.workspace.
4. To start an instance before each file operation, replace src/index.ts with a MyContainer class that has a startSandbox() method:
src/index.jsjs
```js import { Files } from "@cloudflare/sandbox"; import { DurableObject } from "cloudflare:workers";
const workspace = "/workspace"; const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
export class MyContainer extends DurableObject { container; files;
constructor(ctx, env) {
super(ctx, env);
const container = ctx.container;
if (!container) {
throw new Error("No container is configured");
}
this.container = container;
this.files = new Files(container);
// A restarted Durable Object sets the timeout again.
if (container.running) {
void ctx.blockConcurrencyWhile(() =>
container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS),
);
}
}
// Files does not start an instance, so start one before each operation.
async startSandbox() {
if (!this.container.running) {
this.container.start({
image: this.container.images.workspace,
enableInternet: false,
});
await this.container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
}
}
} ```
src/index.tsts
```ts import { Files } from "@cloudflare/sandbox"; import { DurableObject } from "cloudflare:workers";
const workspace = "/workspace"; const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
export class MyContainer extends DurableObject
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
const container = ctx.container;
if (!container) {
throw new Error("No container is configured");
}
this.container = container;
this.files = new Files(container);
// A restarted Durable Object sets the timeout again.
if (container.running) {
void ctx.blockConcurrencyWhile(() =>
container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS),
);
}
}
// Files does not start an instance, so start one before each operation.
private async startSandbox(): Promise<void> {
if (!this.container.running) {
this.container.start({
image: this.container.images.workspace,
enableInternet: false,
});
await this.container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
}
}
} ```
The constructor creates one Files object for the Durable Object, because this.ctx.container stays the same object for as long as the Durable Object runs. startSandbox() starts the workspace image when no instance is running, and sets the inactivity timeout after start(). For more information, refer to Sandbox lifetime.
5. To write the upload, run the command, and return the output file, add a compress() method to MyContainer:
src/index.tsts
```ts
export class MyContainer extends DurableObject
async compress(upload: ReadableStream<Uint8Array>): Promise<Response> {
await this.startSandbox();
const name = crypto.randomUUID();
await this.files.writeFile(name, upload, { cwd: workspace });
const process = await this.container.exec(["gzip", "--force", name], {
cwd: workspace,
});
const output = await process.output();
if (output.exitCode !== 0) {
throw new Error(new TextDecoder().decode(output.stderr));
}
return this.files.readFile(`${name}.gz`, { cwd: workspace });
}
} ```
writeFile() streams the upload into the file, and readFile() returns a Response that streams the output file back. Neither method holds a whole file in the memory of the Durable Object. output() holds only what gzip prints, and that output counts toward the memory limit.
A Linux failure, such as a full disk, throws SandboxFileError. Its code holds the Linux error name, such as ENOSPC. To handle other errors, refer to Files errors.
6. To send uploads to compress(), add a default export to src/index.ts:
src/index.jsjs
```js export default { async fetch(request, env) { const url = new URL(request.url); const match = /^\/sandboxes\/(a-z0-9?)\/compress$/.exec( url.pathname, );
if (!match || request.method !== "POST" || !request.body) {
return new Response("Not found", { status: 404 });
}
const sandbox = env.MY_CONTAINER.getByName(match[1]);
return sandbox.compress(request.body);
},
}; ```
src/index.tsts
```ts
export default {
async fetch(request: Request, env: Env): Promise
if (!match || request.method !== "POST" || !request.body) {
return new Response("Not found", { status: 404 });
}
const sandbox = env.MY_CONTAINER.getByName(match[1]);
return sandbox.compress(request.body);
},
} satisfies ExportedHandler
Caution
This route reads the sandbox name from the URL, so any caller can reach any sandbox. In production, derive the name from the authenticated caller. For more information, refer to Sandbox security. 7. Deploy your Worker:npmyarnpnpm
npx wrangler deploy
yarn wrangler deploy
pnpm wrangler deploy
- To test the Worker, upload a file to the sandbox named
adaand save the response. Replace the hostname with theworkers.devURL that Wrangler prints:
sh
printf 'Ada Lovelace\nGrace Hopper\n' | curl \
https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/sandboxes/ada/compress \
--data-binary @- \
--fail-with-body \
--output names.txt.gz
- Decompress the response:
sh
gunzip --stdout names.txt.gz
txt
Ada Lovelace
Grace Hopper
List, rename, and remove files
Files has methods for other file operations. Each method accepts the same cwd option as writeFile() and readFile():
| Method | Use it to |
|---|---|
readDirectory() |
List directory entries |
stat() |
Read the type, size, owner, and timestamps of a path |
mkdir() |
Create a directory |
rename() |
Rename or move a file or directory |
remove() |
Delete a file, or a directory with recursive: true |
Compressed files stay in /workspace until the instance stops. To delete a file sooner, call remove() after the caller has read the response.
For more information, refer to Files API.
Accept paths from callers
Some applications let callers choose paths, such as a file browser. Files does not restrict which paths it opens. To keep a request inside one directory, check each path before you pass it to Files.
This example adds a download() method that accepts only relative paths without .., and returns 404 for a missing file:
src/index.jsjs
import { SandboxFileError } from "@cloudflare/sandbox";
import { DurableObject } from "cloudflare:workers";
const workspace = "/workspace";
// Accept only relative paths that stay inside cwd.
function workspacePath(path) {
if (
path === "" ||
path.startsWith("/") ||
path.includes("\0") ||
path.split("/").includes("..")
) {
return null;
}
return path;
}
export class MyContainer extends DurableObject {
// ...
async download(path) {
const relativePath = workspacePath(path);
if (!relativePath) {
return new Response("Invalid path", { status: 400 });
}
await this.startSandbox();
try {
return await this.files.readFile(relativePath, { cwd: workspace });
} catch (error) {
if (SandboxFileError.is(error) && error.code === "ENOENT") {
return new Response("Not found", { status: 404 });
}
throw error;
}
}
}
src/index.tsts
import { SandboxFileError } from "@cloudflare/sandbox";
import { DurableObject } from "cloudflare:workers";
const workspace = "/workspace";
// Accept only relative paths that stay inside cwd.
function workspacePath(path: string): string | null {
if (
path === "" ||
path.startsWith("/") ||
path.includes("\0") ||
path.split("/").includes("..")
) {
return null;
}
return path;
}
export class MyContainer extends DurableObject<Env> {
// ...
async download(path: string): Promise<Response> {
const relativePath = workspacePath(path);
if (!relativePath) {
return new Response("Invalid path", { status: 400 });
}
await this.startSandbox();
try {
return await this.files.readFile(relativePath, { cwd: workspace });
} catch (error) {
if (SandboxFileError.is(error) && error.code === "ENOENT") {
return new Response("Not found", { status: 404 });
}
throw error;
}
}
}
Caution
The check applies only to the path in the request. It does not isolate files inside the sandbox. Code in the sandbox can still create a symbolic link in /workspace that points elsewhere in the sandbox.
The user option, such as user: "1000:1000", does not narrow access either. It sets the owner of files that Files creates. The helper can still open files that the user has no permission for.
For more information, refer to Sandbox security.
Related resources
- Files API: every method, including
readDirectory(),rename(), andremove(). - Save and restore a sandbox with snapshots: keep the files after the instance stops.
- Mount an R2 bucket: write files that other systems read.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/files/manage-files/#page","headline":"Move files in and out of a sandbox","description":"Stream a file from your Worker into a Linux sandbox, run a command on it, and stream the output file back to the caller.","url":"https://developers.cloudflare.com/sandbox/files/manage-files/","inLanguage":"en","image":"https://developers.cloudflare.com/sandbox/files/manage-files/og.png?v=145e8247eda37df9","dateModified":"2026-10-22","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Mount a prefix of an R2 bucket in a Linux sandbox without giving the sandbox your credentials. title: Mount an R2 bucket image: https://developers.cloudflare.com/sandbox/files/mount-an-r2-bucket/og.png?v=b6b9fbc310367588
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt
Use this file to discover all available pages before exploring further.
Docs