Container class

Last updated Sep 30, 2026 The Container class ↗︎ from @cloudflare/containers ↗︎ provides lifecycle helpers for container instances. For direct lifecycle control, use the Durable Object Container API.

Container extends DurableObject. The Durable Object manages routing, persistent state, and lifecycle hooks, while the container process runs your image inside a Linux VM. Because your subclass is a Durable Object, you have access to the full Durable Object API — including this.ctx.storage for persistent SQLite-backed storage and this.ctx.id for the unique instance identifier. Use Durable Object storage to persist state that should survive container restarts, such as configuration, user data, or task results.

Note

The Container class does not support applications that use the durable_object scheduling policy. It is available only with the default scheduling policy. For durable_object applications, use the Durable Object Container API.

To move an existing application to direct control, refer to Migrate to the Durable Object Container API.

Use the Container class

Start by installing the @cloudflare/containers package:

npmyarnpnpmbun

npm i @cloudflare/containers
yarn add @cloudflare/containers
pnpm add @cloudflare/containers
bun add @cloudflare/containers

Then, define a class that extends Container and set the shared properties on the class:

import { Container, getContainer } from "@cloudflare/containers";

export class SandboxContainer extends Container {
    defaultPort = 8080;
    requiredPorts = [8080, 9222];
    sleepAfter = "5m";
    envVars = {
        NODE_ENV: "production",
        LOG_LEVEL: "info",
    };
    entrypoint = ["npm", "run", "start"];
    enableInternet = false;
    pingEndpoint = "localhost/ready";
}

export default {
    async fetch(request, env) {
        return getContainer(env.SANDBOX_CONTAINER, "workspace-123").fetch(request);
    },
};
import { Container, getContainer } from "@cloudflare/containers";

export class SandboxContainer extends Container {
    defaultPort = 8080;
    requiredPorts = [8080, 9222];
    sleepAfter = "5m";
    envVars = {
        NODE_ENV: "production",
        LOG_LEVEL: "info",
    };
    entrypoint = ["npm", "run", "start"];
    enableInternet = false;
    pingEndpoint = "localhost/ready";
}

export default {
    async fetch(request: Request, env) {
        return getContainer(env.SANDBOX_CONTAINER, "workspace-123").fetch(request);
    },
};

The Container class extends DurableObject, so all Durable Object functionality is available — including SQLite storage, alarms, and RPC methods. Container disk is ephemeral by default, but Durable Object storage persists across container restarts.

import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    defaultPort = 8080;

async runAndPersist() {
        const res = await this.containerFetch("/run-task");
        const body = await res.text();
        this.ctx.storage.sql.exec(
            "INSERT OR REPLACE INTO results (value) VALUES (?)",
            body,
        );
        return body;
    }
}
import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    defaultPort = 8080;

async runAndPersist() {
        const res = await this.containerFetch("/run-task");
        const body = await res.text();
        this.ctx.storage.sql.exec(
            "INSERT OR REPLACE INTO results (value) VALUES (?)",
            body,
        );
        return body;
    }
}

Execute commands

Use this.ctx.container.exec() to start another process inside a running Container. Refer to Execute commands for startup, streaming, output, and process-control examples.

Properties

Configure these as class fields on your subclass. They apply to every instance of the container.

  • defaultPort ( number, optional) — the port your container process listens on. fetch() and containerFetch() forward requests here unless you specify a different port via switchPort() or the port argument to containerFetch(). Most subclasses set this.
  • requiredPorts ( number[], optional) — ports that must be accepting connections before the container is considered ready. Used by startAndWaitForPorts() when no ports argument is passed. Set this when your container runs multiple services that all need to be healthy before serving traffic.
  • sleepAfter ( string | number, default: "10m") — how long to keep the container alive without activity before shutting it down. Accepts a number of seconds or a duration string such as "30s", "5m", or "1h". Activity resets the timer — see renewActivityTimeout() for manual resets.
  • envVars ( Record<string, string>, default: {}) — environment variables passed to the container on every start. For per-instance variables, pass envVars through startAndWaitForPorts() instead.
  • entrypoint ( string[], optional) — overrides the image's default entrypoint. Useful when you want to run a different command without rebuilding the image, such as a dev server or a one-off task.
  • enableInternet ( boolean, default: true) — controls whether the container can make outbound HTTP requests. Set to false for sandboxed environments where you want to intercept or block all outbound traffic. For more information, refer to Handle outbound traffic.
  • pingEndpoint ( string, default: "ping") — the host and path the class uses to health-check the container during startup. Most users do not need to change this.

Lifecycle hooks

Override these methods to run Worker code when the container changes state. Refer to the status hooks example for a full example.

onStart

Run Worker code after the container has started.

onStart(): void | Promise<void>

Returns: void | Promise<void>. Resolve after any startup logic finishes.

Use this to log startup, seed data, or schedule recurring tasks with schedule().

import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    defaultPort = 8080;

async onStart() {
        await this.containerFetch("http://localhost/bootstrap", {
            method: "POST",
        });
    }
}
import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    defaultPort = 8080;

override async onStart() {
        await this.containerFetch("http://localhost/bootstrap", {
            method: "POST",
        });
    }

}

onStop

Run Worker code after the container process exits.

onStop(params: StopParams): void | Promise<void>

Parameters:

  • params.exitCode - Container process exit code.
  • params.reason - Why the container stopped: 'exit' when the process exited on its own, or 'runtime_signal' when the runtime signalled it.

Returns: void | Promise<void>. Resolve after your shutdown logic finishes.

Use this to log, alert, or restart the container.

import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    onStop({ exitCode, reason }) {
        console.log("Container stopped", { exitCode, reason });
    }
}
import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    override onStop({ exitCode, reason }) {
        console.log("Container stopped", { exitCode, reason });
    }
}

onError

Handle startup and port-checking errors.

onError(error: unknown): any

Parameters:

  • error - The error thrown during startup or port checks.

Returns: any. The default implementation logs the error and re-throws it.

Override this to suppress errors, notify an external service, or attempt a restart.

import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    onError(error) {
        console.error("Container failed to start", error);
        throw error;
    }
}
import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    override onError(error: unknown) {
        console.error("Container failed to start", error);
        throw error;
    }
}

onActivityExpired

Run Worker code when the sleepAfter timer expires.

onActivityExpired(): Promise<void>

Returns: Promise<void>. Resolve after your idle-time logic finishes.

Called when the sleepAfter timeout expires with no incoming requests. The default implementation calls stop().

Caution

If you override onActivityExpired(), call await this.stop() or await this.destroy(). Otherwise, the container does not go to sleep.

If you override this method without stopping the container, the timer renews and the hook fires again on the next expiry.

import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    sleepAfter = "2m";

async onActivityExpired() {
        const state = await this.getState();
        console.log("Container is idle, stopping it now", state.status);

await this.stop();
    }
}
import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    sleepAfter = "2m";

override async onActivityExpired() {
        const state = await this.getState();
        console.log("Container is idle, stopping it now", state.status);

await this.stop();
    }

}

Request methods

fetch

Handle incoming HTTP or WebSocket requests.

fetch(request: Request): Promise<Response>

Parameters:

  • request - The incoming request to proxy to the container.

Returns: Promise<Response> from the container or from your custom routing logic.

By default, fetch forwards the request to the container process at defaultPort. The container is started automatically if it is not already running.

Override fetch when you need routing logic, authentication, or other middleware before forwarding to the container. Inside the override, call this.containerFetch() rather than this.fetch() to avoid infinite recursion:

import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    defaultPort = 8080;

async fetch(request) {
        const url = new URL(request.url);

if (url.pathname === "/health") {
            return new Response("ok");
        }

return this.containerFetch(request);
    }
}
import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    defaultPort = 8080;

override async fetch(request: Request): Promise<Response> {
        const url = new URL(request.url);

if (url.pathname === "/health") {
            return new Response("ok");
        }

return this.containerFetch(request);
    }

}

fetch is the only method that supports WebSocket proxying. Refer to the WebSocket example for a full example.

containerFetch

Send an HTTP request directly to the container process. Generally, users should prefer to use fetch unless it has been overridden.

containerFetch(request: Request, port?: number): Promise<Response>
containerFetch(url: string | URL, init?: RequestInit, port?: number): Promise<Response>

Parameters:

  • request - Existing Request object to forward.
  • url - URL to request when you are constructing a new request.
  • init - Standard RequestInit options for the URL-based overload.
  • port - Optional target port. If omitted, the class uses defaultPort.

Returns: Promise<Response> from the container.

This is what the default fetch() implementation calls internally, and it is what you should call from within an overridden fetch() method to avoid infinite recursion. It also accepts a standard fetch-style signature with a URL string and RequestInit, which is useful when you are constructing a new request rather than forwarding an existing one.

Does not support WebSockets. Use fetch() with switchPort() for those.

import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    defaultPort = 8080;

async fetch(request) {
        const url = new URL(request.url);

if (url.pathname === "/metrics") {
            return this.containerFetch(
                "http://localhost/internal/metrics",
                {
                    headers: {
                        authorization: request.headers.get("authorization") ?? "",
                    },
                },
                9090,
            );
        }

return this.containerFetch(request);
    }
}
import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
    defaultPort = 8080;

override async fetch(request: Request): Promise<Response> {
        const url = new URL(request.url);

if (url.pathname === "/metrics") {
            return this.containerFetch(
                "http://localhost/internal/metrics",
                {
                    headers: {
                        authorization: request.headers.get("authorization") ?? "",
                    },
                },
                9090,
            );
        }

return this.containerFetch(request);
    }

}

Start and stop

In most cases you do not need to call these methods directly. fetch() and containerFetch() start the container automatically. Call these explicitly when you need to pre-warm a container, run a task on a schedule, or control the lifecycle from within a lifecycle hook.

startAndWaitForPorts

Start the container and wait until the target ports are accepting connections.

startAndWaitForPorts(args?: StartAndWaitForPortsOptions): Promise<void>
startAndWaitForPorts(
  ports?: number | number[],
  cancellationOptions?: CancellationOptions,
  startOptions?: ContainerStartConfigOptions,
): Promise<void>

Parameters:

  • args.ports - Port or ports to wait for. Port resolution order is explicit ports, then requiredPorts, then defaultPort.
  • args.startOptions - Per-instance startup overrides.
  • args.startOptions.envVars - Per-instance environment variables.
  • args.startOptions.entrypoint - Entrypoint override for this start only.
  • args.startOptions.enableInternet - Whether outbound internet access is allowed for this start.
  • args.cancellationOptions.abort - Abort signal to cancel startup.
  • args.cancellationOptions.instanceGetTimeoutMS - Maximum time to get a container instance and issue the start command. Default: 8000.
  • args.cancellationOptions.portReadyTimeoutMS - Maximum time to wait for all ports to become ready. Default: 20000.
  • args.cancellationOptions.waitInterval - Polling interval in milliseconds. Default: 300.

Returns: Promise<void>. Resolves after the target ports are ready and onStart() has run.

This is the safest way to explicitly start a container when you need to be certain it is ready before sending traffic.

This method also supports positional ports, cancellationOptions, and startOptions arguments, but the object form is easier to read.

import { getContainer } from "@cloudflare/containers";

export default {
    async scheduled(_event, env) {
        const container = getContainer(env.API_CONTAINER, "tenant-42");

await container.startAndWaitForPorts({
            ports: [8080, 9222],
            startOptions: {
                envVars: {
                    API_KEY: env.API_KEY,
                    TENANT_ID: "tenant-42",
                },
            },
            cancellationOptions: {
                portReadyTimeoutMS: 30_000,
            },
        });
    },
};

```plaintext import { getContainer } from "@cloudflare/containers";

export default { async scheduled(_event, env) { const container = getContainer(env.API_CONTAINER, "tenant-42");

await container.startAndWaitForPorts({ ports: [8080, 9222], startOptions: { envVars: { API_KEY: env.API_KEY, TENANT_ID: "tenant-42", }, }, cancellationOptions: { portReadyTimeoutMS: 30_000, }, }); },