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.
- 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".
-
Install the package:
npm i @cloudflare/sandboxyarn add @cloudflare/sandboxpnpm add @cloudflare/sandboxbun add @cloudflare/sandbox -
To give
Filesits helper binary, create aDockerfilein the project root. If you already have one, add theCOPYline to it:Dockerfiledockerfile FROM node:24-trixie-slim COPY --from=docker.io/cloudflare/sandbox:1.0.0 /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim RUN mkdir /workspace CMD ["sleep", "infinity"]The
cloudflare/sandboxtag must match the installed package version. A mismatch causesSandboxProtocolError. Replace theFROMline with the base image your commands need. -
To build the image, add the
nodejs_compatflag towrangler.jsoncand replace thecontainersentry:{ "compatibility_flags": ["nodejs_compat"], "containers": [ { "class_name": "MyContainer", "scheduling_policy": "durable_object", "images": { "workspace": { "dockerfile": "./Dockerfile", }, }, }, ], }compatibility_flags = [ "nodejs_compat" ] [[containers]] class_name = "MyContainer" scheduling_policy = "durable_object" [containers.images.workspace] dockerfile = "./Dockerfile"The
workspacekey names the image. Your code starts it ascontainer.images.workspace. -
To start an instance before each file operation, replace
src/index.tswith aMyContainerclass that has astartSandbox()method:src/index.jsjs 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("The container binding is not 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 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<Env> { private readonly container: Container; private readonly files: Files; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); const container = ctx.container; if (!container) { throw new Error("The container binding is not 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
Filesobject for the Durable Object, becausethis.ctx.containerstays the same object for as long as the Durable Object runs.startSandbox()starts theworkspaceimage when no instance is running, and sets the inactivity timeout afterstart(). For more information, refer to Sandbox lifetime. -
To write the upload, run the command, and return the output file, add a
compress()method toMyContainer:src/index.tsts export class MyContainer extends DurableObject<Env> { // ... 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, andreadFile()returns aResponsethat streams the output file back. Neither method holds a whole file in the memory of the Durable Object.output()holds only whatgzipprints, and that output counts toward the memory limit.A Linux failure, such as a full disk, throws
SandboxFileError. Itscodeholds the Linux error name, such asENOSPC. To handle other errors, refer to Files errors. -
To send uploads to
compress(), add a default export tosrc/index.ts:src/index.jsjs export default { async fetch(request, env) { const url = new URL(request.url); const match = /^\/sandboxes\/([a-z0-9](?:[a-z0-9-]{0,61}[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 export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); const match = /^\/sandboxes\/([a-z0-9](?:[a-z0-9-]{0,61}[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); }, } satisfies ExportedHandler<Env>; -
Deploy your Worker:
npx wrangler deployyarn wrangler deploypnpm 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: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:
gunzip --stdout names.txt.gzAda Lovelace Grace Hopper
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.
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:
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;
}
}
}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;
}
}
}- 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.