Replace the 0.12 Sandbox class with a Durable Object class that you write. In 0.12, your Worker calls package methods through getSandbox(). In 1.0, your class starts the container through this.ctx.container, and your Worker calls the methods that your class defines.
Your application runs @cloudflare/sandbox 0.12.10. This page replaces the Sandbox class, whether you export it from the package or extend it. It also replaces getSandbox() and its options, class fields such as sleepAfter and envVars, and the hooks onStart(), onStop(), onError(), and onActivityExpired().
The page ends with the Worker running under wrangler dev. You cannot undo the deploy that switches your sandboxes to 1.0. Before you deploy, follow Plan the move to Sandbox SDK 1.0.
The steps keep your class name, which moves every sandbox in place. To move sandboxes side by side, write the class under a new name, such as SandboxV1, and add its Wrangler configuration as Move sandboxes side by side describes.
-
Install version 1 of the package:
npm i @cloudflare/sandboxyarn add @cloudflare/sandboxpnpm add @cloudflare/sandboxbun add @cloudflare/sandbox -
Replace the
FROM docker.io/cloudflare/sandboxline in yourDockerfilewith a base image that has the tools your commands use, and copy thesandbox-shimhelper into it:Dockerfiledockerfile FROM node:24-trixie-slim RUN apt-get update \ && apt-get install -y --no-install-recommends \ ca-certificates git python3 \ && rm -rf /var/lib/apt/lists/* COPY --from=docker.io/cloudflare/sandbox:1.0.0 /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim WORKDIR /workspace CMD ["sleep", "infinity"]The 0.12 image included Python, Node.js, and Git. Keep the ones your commands use.
FilesandS3Mountneedsandbox-shim. Match its tag to the@cloudflare/sandboxversion that you installed. RemoveEXPOSElines, which 1.0 does not use. Commands thatexec()runs do not seeENVlines, so pass those values in theenvoption instead. -
Change the
containersentry for your class in the Wrangler configuration. Keep your class name and its binding, so each name reaches the Durable Object and storage it reached in 0.12. Replacemy-worker-mysandbox-v2with a name that your account does not use yet:{ "containers": [ { "class_name": "MySandbox", "name": "my-worker-mysandbox-v2", "scheduling_policy": "durable_object", "images": { "sandbox": { "dockerfile": "./Dockerfile", }, }, }, ], }[[containers]] class_name = "MySandbox" name = "my-worker-mysandbox-v2" scheduling_policy = "durable_object" [containers.images.sandbox] dockerfile = "./Dockerfile"The new entry makes these changes:
scheduling_policy: "durable_object"lets each Durable Object start its own container. This policy does not acceptimage,instance_type, ormax_instances, so remove them. TheDockerfilemoves toimages, and the instance type moves tostart().- The container application gets a new
name. Without one, the new application takes the name that Wrangler generated for the old application, and the deploy fails after the new Worker version is already live. - Leave
durable_objectsand yourmigrationsorexportsas they are. The class keeps its name, so it needs no class migration.
Also remove
SANDBOX_TRANSPORT,SANDBOX_INSTANCE_TIMEOUT_MS,SANDBOX_PORT_TIMEOUT_MS, andSANDBOX_POLL_INTERVAL_MSfromvars, and keepnodejs_compat, which 1.0 requires. -
Check your compatibility date.
-
Generate types for the new configuration:
npx wrangler typesyarn wrangler typespnpm wrangler types -
Write the class. In this example, the 0.12 subclass sets a lifetime and environment variables, and overrides
onStart():src/index.ts (0.12)ts import { getSandbox, Sandbox } from "@cloudflare/sandbox"; export class MySandbox extends Sandbox<Env> { sleepAfter = "30m"; envVars = { NODE_ENV: "test" }; override async onStart() { await this.ctx.storage.put("startedAt", Date.now()); } }In 1.0, the class extends
DurableObjectand starts the container itself:src/index.ts (1.0)ts import { DurableObject } from "cloudflare:workers"; // Replaces sleepAfter and envVars. const INACTIVITY_TIMEOUT_MS = 30 * 60 * 1000; const ENV = { NODE_ENV: "test" }; export class MySandbox extends DurableObject<Env> { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); const container = ctx.container; // A restarted Durable Object sets the timeout again. if (container?.running) { void ctx.blockConcurrencyWhile(() => container.setInactivityTimeout( INACTIVITY_TIMEOUT_MS, ), ); } } private get container(): Container { const container = this.ctx.container; if (!container) { throw new Error( "The container binding is not configured", ); } return container; } private setup: Promise<void> | null = null; private ensureRunning(): Promise<void> { // Set up each new container, and a running container // after this Durable Object restarts. if (this.setup === null || !this.container.running) { this.setup = this.startContainer().catch((error) => { this.setup = null; throw error; }); } return this.setup; } private async startContainer(): Promise<void> { const newContainer = !this.container.running; if (newContainer) { this.container.start({ image: this.container.images.sandbox, instance: "standard-1", env: ENV, // 0.12 allowed Internet access by default. enableInternet: true, }); } try { if (newContainer) { // Replaces onStart(). await this.ctx.storage.put("startedAt", Date.now()); } await this.container.setInactivityTimeout( INACTIVITY_TIMEOUT_MS, ); } catch (error) { // The next request starts a new container. await this.container.destroy(); throw error; } } }Call
ensureRunning()before each use of the container. It runsstartContainer()once for each new container, and once more when a restarted Durable Object finds the container running. A deploy restarts the Durable Object and can stop setup partway, so every step afterstart()must be safe to run again.runningistrueas soon asstart()returns, so requests that arrive during setup wait for the samestartContainer()call. If a setup step throws,startContainer()stops the container, so no request runs commands in a container that is not set up.start()returns before the container is ready, and the firstexec()waits for it. The inactivity timeout does not survive a Durable Object restart. The constructor sets it again when a restarted Durable Object finds the container running. The timeout accepts at most 6 hours. If your commands do not need the Internet, setenableInternettofalse. -
Replace each class field and
getSandbox()option that your code sets:0.12 1.0 sleepAftersetInactivityTimeout()afterstart(), and again in the constructor.keepAliveAn alarm that runs more often than the inactivity timeout while work remains. envVars,setEnvVars()envonstart()for the main process, andenvon eachexec()call. Commands do not inherit thestart()variables, exceptPATH.entrypointentrypointonstart(), orCMDin the image.enableInternetenableInternetonstart(), which every call must pass. 0.12 defaulted totrue.labels,setLabels()labelsonstart().defaultPort,requiredPortsthis.container.getTcpPort(port). Refer to Move preview URLs.allowedHosts,deniedHosts,outboundByHost,outbound,interceptHttpsRefer to Move outbound rules. normalizeIdLowercase the name before getByName().containerTimeoutsAn AbortSignalon the calls that need a deadline. Refer to Replace timeouts.enableDefaultSessionNothing. Each exec()call is its own process. Refer to Replace sessions.transport,pingEndpointNothing. Remove them. -
Replace the hooks your subclass overrides:
onStart(): run the code instartContainer(), insideif (newContainer), as in the class from the previous step.onStop()andonError(): watch the container withmonitor(), and catch errors where you callstart()andexec().onActivityExpired(): no code runs before the inactivity timeout stops the container. To run code first, keep your own idle deadline in an alarm, and callthis.container.destroy()when it passes. For an example that saves the sandbox before it stops, refer to Save a sandbox automatically.
To watch the container, call a method like this one in the
tryblock ofstartContainer(). A restarted Durable Object runsstartContainer()again, so it watches the running container too:src/index.tsts export class MySandbox extends DurableObject<Env> { // ... private watch(): void { this.container.monitor().then( () => console.log("Exited with code 0"), (error: unknown) => console.error("Stopped", error), ); } }monitor()resolves when the main process exits with code0ordestroy()stops the container without a reason. It rejects when the process fails ordestroy()passes a reason. The call ends when the Durable Object restarts, so it misses a stop that happens before the next request. While the call waits, the inactivity timeout does not start, so a watched container keeps running past the timeout. For more information, refer to Sandbox lifetime. For a record of every stop, refer to Run code when a sandbox stops. -
Move the calls your Worker makes into methods on the class. In 0.12, the Worker calls package methods on the stub that
getSandbox()returns:src/index.ts (0.12)ts export default { async fetch(request: Request, env: Env): Promise<Response> { const id = new URL(request.url).pathname.slice(1); const sandbox = getSandbox(env.MY_SANDBOX, id, { normalizeId: true, }); const result = await sandbox.exec("npm test", { cwd: "/workspace/app", }); return Response.json({ exitCode: result.exitCode, output: result.stdout, }); }, };In 1.0, the stub has only the methods your class defines. Add a method to
MySandbox, and call it from the Worker:src/index.ts (1.0)ts export class MySandbox extends DurableObject<Env> { // ... async test(): Promise<{ exitCode: number; output: string }> { await this.ensureRunning(); const process = await this.container.exec(["npm", "test"], { cwd: "/workspace/app", env: ENV, }); const result = await process.output(); return { exitCode: result.exitCode, output: new TextDecoder().decode(result.stdout), }; } }src/index.ts (1.0)ts export default { async fetch(request: Request, env: Env): Promise<Response> { const id = new URL(request.url).pathname.slice(1); // Replaces normalizeId: true. const sandbox = env.MY_SANDBOX.getByName(id.toLowerCase()); return Response.json(await sandbox.test()); }, } satisfies ExportedHandler<Env>;Move the methods that your 0.12 subclass defines into the 1.0 class. Your Worker calls them on the stub as it did in 0.12. Inside those methods, replace
this.exec()and other package calls with calls onthis.container. For each kind of call, refer to Change command calls and the feature pages in Migrate from Sandbox SDK 0.x.
Start your Worker locally:
npx wrangler devyarn wrangler devpnpm wrangler devWrangler builds the image when it starts. Send a request to each route that uses a sandbox. The first request for each name starts a container and waits for it. Each route returns what it returned on 0.12.
- To deploy the new class, follow Plan the move to Sandbox SDK 1.0.
- To change
exec()and sessions, refer to Change command calls. - To change file calls, refer to Change file calls.
- For the methods on
this.ctx.container, refer to the Container API.