Skip to content

Replace the Sandbox class

Last updated View as MarkdownAgent setup

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.

Before you start

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.

Replace the class

  1. Install version 1 of the package:

    npm i @cloudflare/sandbox
  2. Replace the FROM docker.io/cloudflare/sandbox line in your Dockerfile with a base image that has the tools your commands use, and copy the sandbox-shim helper 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. Files and S3Mount need sandbox-shim. Match its tag to the @cloudflare/sandbox version that you installed. Remove EXPOSE lines, which 1.0 does not use. Commands that exec() runs do not see ENV lines, so pass those values in the env option instead.

  3. Change the containers entry 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. Replace my-worker-mysandbox-v2 with 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 accept image, instance_type, or max_instances, so remove them. The Dockerfile moves to images, and the instance type moves to start().
    • 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_objects and your migrations or exports as 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, and SANDBOX_POLL_INTERVAL_MS from vars, and keep nodejs_compat, which 1.0 requires.

  4. Check your compatibility date.

  5. Generate types for the new configuration:

    npx wrangler types
  6. 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 DurableObject and 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 runs startContainer() 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 after start() must be safe to run again. running is true as soon as start() returns, so requests that arrive during setup wait for the same startContainer() 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 first exec() 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, set enableInternet to false.

  7. Replace each class field and getSandbox() option that your code sets:

    0.12 1.0
    sleepAfter setInactivityTimeout() after start(), and again in the constructor.
    keepAlive An alarm that runs more often than the inactivity timeout while work remains.
    envVars, setEnvVars() env on start() for the main process, and env on each exec() call. Commands do not inherit the start() variables, except PATH.
    entrypoint entrypoint on start(), or CMD in the image.
    enableInternet enableInternet on start(), which every call must pass. 0.12 defaulted to true.
    labels, setLabels() labels on start().
    defaultPort, requiredPorts this.container.getTcpPort(port). Refer to Move preview URLs.
    allowedHosts, deniedHosts, outboundByHost, outbound, interceptHttps Refer to Move outbound rules.
    normalizeId Lowercase the name before getByName().
    containerTimeouts An AbortSignal on the calls that need a deadline. Refer to Replace timeouts.
    enableDefaultSession Nothing. Each exec() call is its own process. Refer to Replace sessions.
    transport, pingEndpoint Nothing. Remove them.
  8. Replace the hooks your subclass overrides:

    • onStart(): run the code in startContainer(), inside if (newContainer), as in the class from the previous step.
    • onStop() and onError(): watch the container with monitor(), and catch errors where you call start() and exec().
    • onActivityExpired(): no code runs before the inactivity timeout stops the container. To run code first, keep your own idle deadline in an alarm, and call this.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 try block of startContainer(). A restarted Durable Object runs startContainer() 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 code 0 or destroy() stops the container without a reason. It rejects when the process fails or destroy() 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.

  9. 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 on this.container. For each kind of call, refer to Change command calls and the feature pages in Migrate from Sandbox SDK 0.x.

Check the class

Start your Worker locally:

npx wrangler dev

Wrangler 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.

Next steps

Was this helpful?