Skip to content

Container lifecycle

A Container runs only when you start it. You start it, stop it, and restore it from the Durable Object that owns it.

The examples use this container:

src/Sandbox.ts
export class Sandbox extends Cloudflare.Container<Sandbox>()("Sandbox", {
schedulingPolicy: "durable_object",
images: { node: { image: "node:24-slim" } },
}) {}

yield* Sandbox in a Durable Object’s Construction phase gives you the container’s handle:

export default class Workspace extends Cloudflare.DurableObject<Workspace>()(
"Workspace",
Effect.gen(function* () {
const sandbox = yield* Sandbox;
return Effect.gen(function* () {
return {};
});
}),
) {}

Binding does not start the container.

Call start at the top of each operation that needs the container:

const start = sandbox.start({
entrypoint: ["sleep", "infinity"],
enableInternet: false,
});
return {
run: (cmd: string[]) =>
Effect.gen(function* () {
yield* start;
// the container is running
}),
};

start does nothing when the container is already running, and concurrent calls start it once. A container that stopped or crashed since the last request starts again on the next one.

The options only apply when start actually starts the container. To run with different options, stop the container first.

A container with schedulingPolicy: "durable_object" picks its image and size when it starts:

const images = yield* sandbox.images;
yield* sandbox.start({
image: images.node,
instance: "standard-1",
entrypoint: ["sleep", "infinity"],
enableInternet: false,
});

images holds one reference per image you declared, typed by name.

A Layer that depends on the container starts it the same way:

export const ShellLive = Layer.effect(
Shell,
Effect.gen(function* () {
const sandbox = yield* Sandbox;
const start = sandbox.start({ enableInternet: false });
return {
run: (cmd: string[]) =>
start.pipe(
Effect.andThen(sandbox.exec(cmd)),
Effect.flatMap((child) => child.output()),
Effect.scoped,
),
};
}),
);

Every Layer in the Durable Object shares one container, so a Layer never stops it.

getTcpPort waits until the server inside the container accepts connections:

const { fetch } = yield* sandbox.getTcpPort(8080);
const health = Effect.gen(function* () {
yield* start;
const response = yield* fetch(HttpClientRequest.get("http://container/health"));
return yield* response.text;
});

A port never starts the container. If the container is stopped, the request fails with ContainerNotRunningError.

exec runs a command in a started container:

const output = yield* Effect.scoped(
Effect.gen(function* () {
const child = yield* sandbox.exec(["npm", "test"]);
return yield* child.output();
}),
);

exec runs the command without a shell. Pass ["sh", "-c", command] when you need one. Closing the scope kills a process that is still running.

if (yield* sandbox.running) {
// the process is up; its ports may not be ready yet
}

destroy stops the container immediately:

yield* sandbox.destroy();

signal sends a signal to its main process instead, so it can shut down on its own:

yield* sandbox.signal(15); // SIGTERM

When nothing calls the Durable Object, it goes idle and its container stops. Change how long it waits with setInactivityTimeout:

yield* sandbox.setInactivityTimeout(10 * 60 * 1000); // 10 minutes

The next operation that calls start starts the container again.

monitor waits until the container exits:

yield* sandbox.monitor().pipe(
Effect.catchTag("ContainerError", (error) =>
Effect.logError(`container exited: ${error.message}`),
),
);

A crash fails with ContainerError; a clean exit succeeds.

snapshotContainer saves the container’s filesystem:

const snapshot = yield* sandbox.snapshotContainer({ name: "workspace" });
yield* state.storage.put("snapshot", snapshot);

Store the snapshot in the Durable Object’s storage so you can restore it after the container stops. A snapshot holds the writable root filesystem: files, installed packages, build caches. It does not hold memory, running processes, or mounted filesystems.

Snapshots need schedulingPolicy: "durable_object".

Pass the snapshot to start to begin from that filesystem:

const snapshot = yield* state.storage.get<Cloudflare.Containers.ContainerSnapshot>("snapshot");
yield* sandbox.start({
...(snapshot ? { containerSnapshot: snapshot } : { image: images.node }),
entrypoint: ["sleep", "infinity"],
enableInternet: false,
});

The entrypoint starts fresh. A snapshot can start any number of containers, and expires after 30 days without a restore.

Save the filesystem, then stop the container, so the next start picks up where this one left off:

const suspend = Effect.gen(function* () {
const snapshot = yield* sandbox.snapshotContainer({ name: "workspace" });
yield* state.storage.put("snapshot", snapshot);
yield* sandbox.destroy();
});