Each container is managed and proxied by a Durable Object. The Durable Object manages routing and persistent state. The container process runs your image inside a Linux VM.
The API documented on this page is available on this.ctx.container inside any Durable Object class that has a container binding. Use it for direct control over the container process.
Your Durable Object also has access to SQLite storage through this.ctx.storage, alarms, and all other Durable Object APIs. Use storage to preserve configuration and state across container restarts. Use alarms to schedule work without keeping the container running.
import { DurableObject } from "cloudflare:workers";
export class MyDurableObject extends DurableObject {
async start() {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
// With the `default` scheduling policy, call `container.start()` without options.
container.start({
image: container.images.base,
enableInternet: false,
});
await this.ctx.storage.put("lastStartedAt", Date.now());
}
}
async scheduleStart(timestamp) {
await this.ctx.storage.setAlarm(timestamp);
}
async alarm() {
await this.start();
}
}import { DurableObject } from "cloudflare:workers";
interface Env {}
export class MyDurableObject extends DurableObject<Env> {
async start(): Promise<void> {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
// With the `default` scheduling policy, call `container.start()` without options.
container.start({
image: container.images.base,
enableInternet: false,
});
await this.ctx.storage.put("lastStartedAt", Date.now());
}
}
async scheduleStart(timestamp: number): Promise<void> {
await this.ctx.storage.setAlarm(timestamp);
}
async alarm(): Promise<void> {
await this.start();
}
}images is a read-only map generated from the images field in the application's Wrangler configuration. Wrangler builds or resolves each named image and exposes its digest-pinned reference under the same key. This attribute is available only for applications that use the durable_object scheduling policy.
const image = this.ctx.container.images.base;Pass a value from this map as the image option to start().
running is true when the container is running. It does not confirm that the container is ready to accept requests.
this.ctx.container.running;start() boots a container. It returns before the container is ready to accept requests. Confirm readiness before sending traffic.
start() returns once it validates its options. To catch later errors, including a container that fails to start, use monitor().
The required options depend on the application's scheduling policy.
// `default` scheduling policy:
// The image and instance size come from Wrangler configuration.
this.ctx.container.start();
// `durable_object` scheduling policy:
// Pass an image and choose whether to allow outbound Internet access.
// The instance size is optional and defaults to "lite".
this.ctx.container.start({
image: this.ctx.container.images.base,
enableInternet: true,
});options(object, conditionally required): Startup options. Required for applications that use thedurable_objectscheduling policy and optional for applications that use thedefaultscheduling policy.env(Record<string, string>, optional): Environment variables to pass to the container. Processes started withexec()do not receive these variables, exceptPATH.entrypoint(string[], optional): Command and arguments to run in the container.enableInternet(boolean, required): Whether to allow outbound Internet access. Required when you passoptions.image(string, conditionally required) durable_object policy only: Image reference to start. Required unless you passcontainerSnapshot. Pass thecloudflare/debian-trixiemanaged image or a value fromctx.container.images.instance(string | object, optional) durable_object policy only: Instance size. Pass"lite","standard-1","standard-2","standard-3","standard-4", or a custom object withvcpu,memoryMib, anddiskMbproperties.memoryMibis in mebibytes anddiskMbis in megabytes. Both must be positive integers. Defaults to"lite".containerSnapshot(ContainerSnapshotRestoreParams, optional) durable_object policy only: Snapshot handle to restore before startup. Pass theContainerSnapshotreturned bysnapshotContainer(), or an object containing itsid. You cannot pass bothcontainerSnapshotandimage.labels(Record<string, string>, optional): Up to 10 labels thatinspect()returns. Label names must contain 1 to 16 bytes. Label values can contain up to 64 bytes. Names and values cannot contain control characters.
monitor() rejects when a container that uses the durable_object scheduling policy starts without image or containerSnapshot.
void: No return value.
start()throws anErrorwhen the container is already running.start()throws aTypeErrorwhen bothimageandcontainerSnapshotare set, when either is empty, or wheninstancenames an unknown type.start()throws aRangeErrorwhen a custominstancevalue is missing or invalid.start()throws anErrorwhenlabelsexceed the label limits, when an environment variable name contains=, or when an environment variable name or value contains a null character.
inspect() returns the image and labels for a running container. It returns null when no container is running.
const containerInfo = await this.ctx.container.inspect();- None.
Promise<ContainerInfo | null>: Resolves withnullwhen no container is running, including after a failed start or afterdestroy(). Otherwise, it resolves with aContainerInfoobject containing:
exec() starts another process inside an already-running container. It does not start a stopped container. It waits for a container that is still starting.
exec(
cmd: string[],
options?: ContainerExecOptions,
): Promise<ExecProcess>exec() starts the executable directly with the provided arguments. It does not start a shell or interpret pipes, redirects, expansion, or other shell syntax. Invoke Bash explicitly with ["bash", "-lc", "<COMMAND>"] when Bash exists in the image. Use ["sh", "-c", "<COMMAND>"] for images with only a Portable Operating System Interface (POSIX) shell.
The following RPC method checks that the container is running before executing a command:
import { DurableObject } from "cloudflare:workers";
export class MyDurableObject extends DurableObject {
async runCommand() {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
throw new Error("Container is not running");
}
const process = await container.exec(["node", "--version"]);
const output = await process.output();
return {
pid: process.pid,
exitCode: output.exitCode,
stdout: new TextDecoder().decode(output.stdout),
};
}
}import { DurableObject } from "cloudflare:workers";
interface Env {}
export class MyDurableObject extends DurableObject<Env> {
async runCommand() {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
throw new Error("Container is not running");
}
const process = await container.exec(["node", "--version"]);
const output = await process.output();
return {
pid: process.pid,
exitCode: output.exitCode,
stdout: new TextDecoder().decode(output.stdout),
};
}
}cmd(string[]): Executable followed by its arguments.options(ContainerExecOptions, optional): Process configuration:stdin(ReadableStream | "pipe", optional): Source for standard input. Use"pipe"to write through the returnedstdinstream. When omitted, standard input closes and sends end-of-file (EOF).stdout("pipe" | "ignore", optional, default"pipe"): Captures or discards standard output.stderr("pipe" | "ignore" | "combined", optional, default"pipe"): Captures, discards, or merges standard error into standard output. The"combined"value requiresstdout: "pipe". Combined output does not guarantee ordering between its source streams.cwd(string, optional): Working directory for the process.env(Record<string, string>, optional): Environment variables for the process. Other variables fromstart()or the image are not inherited, except forPATH. Unless overridden here,PATHuses the container's startup value, including any override passed tostart().user(string, optional): Numeric Linux user and group IDs for the process, asuid:gid. When omitted, the process runs as the image user.signal(AbortSignal, optional): Aborting the signal sendsSIGKILLto the process. An already-aborted signal makesexec()throw anAbortError.pty(boolean | { cols?: number; rows?: number }, optional): Runs the process attached to a pseudo-terminal.trueuses 80 columns and 24 rows.
The following call runs a command as user 1000 and group 1000. The file it creates belongs to 1000:1000:
const process = await this.ctx.container.exec(["touch", "/tmp/notes.txt"], {
user: "1000:1000",
});With pty, stdin defaults to "pipe", and stderr defaults to "combined" and cannot take another value. Standard output and standard error arrive together on stdout. The terminal converts line endings to \r\n.
Promise<ExecProcess>: Resolves with anExecProcessfor the started process.
An ExecProcess has these fields and methods:
stdin(WritableStream | undefined): Writable standard input whenstdinis"pipe".stdout(ReadableStream | undefined): Readable standard output when piped.stderr(ReadableStream | undefined): Readable standard error when piped separately.pid(number): Process identifier.isPty(boolean): Whether the process runs attached to a pseudo-terminal.exitCode(Promise<number>): Resolves when the process exits. Nonzero codes resolve normally instead of rejecting.output()(Promise<ExecOutput>): Reads buffered output once.ExecOutputcontainsstdout(ArrayBuffer),stderr(ArrayBuffer), andexitCode(number). Ignored streams produce empty buffers. UseTextDecoderto decode text.kill(signal?: number)(void): Queues a signal for the process. The default isSIGTERM, signal15. The signal must be from1through64.resize(cols: number, rows: number)(void): Changes the pseudo-terminal size. Both values must be from1through65535.
A stream that the process does not provide is undefined. For example, with stderr: "combined" or pty, stderr is undefined on ExecProcess and an empty ArrayBuffer on ExecOutput. Read both output channels from stdout.
The Workers TypeScript types declare stdin, stdout, and stderr as | null. Test for a stream, such as if (process.stderr), instead of comparing with null.
output() throws a TypeError when called more than once or after either readable stream starts being consumed.
output() holds all of the output in the memory of the Durable Object, where it counts toward the memory limit. When output can be large, read both streams concurrently instead. For more information, refer to Stream large output.
exec() has no built-in timeout. Use kill() to request termination, then observe completion through exitCode. A process can handle or ignore a signal, so this does not enforce a hard deadline. Do not infer a specific exit code from the signal.
kill() and signal reach only the process that exec() started. Processes that this process starts, such as the commands in a bash -c script, keep running. output() waits until they exit. To stop them too, refer to Stop the processes a command starts.
Canceling the request that started a process does not stop the process. If the client retries, a second copy of the command runs.
To stop the process when the client disconnects, turn on the enable_request_signal compatibility flag in the Worker that defines the Durable Object, and call the Durable Object through fetch() on its stub. Then abort the exec() signal when request.signal aborts. RPC methods do not receive a request signal.
export class MyContainer extends DurableObject {
async fetch(request) {
const container = this.ctx.container;
if (!container?.running) {
return new Response("The container is not running", { status: 503 });
}
const controller = new AbortController();
const abort = () => controller.abort();
if (request.signal.aborted) {
abort();
} else {
request.signal.addEventListener("abort", abort, { once: true });
}
const process = await container.exec(["sleep", "60"], {
signal: controller.signal,
});
// Stop forwarding the abort once the process exits.
void process.exitCode.then(() =>
request.signal.removeEventListener("abort", abort),
);
const output = await process.output();
return new Response(`Exit code: ${output.exitCode}`);
}
}export class MyContainer extends DurableObject {
async fetch(request: Request) {
const container = this.ctx.container;
if (!container?.running) {
return new Response("The container is not running", { status: 503 });
}
const controller = new AbortController();
const abort = () => controller.abort();
if (request.signal.aborted) {
abort();
} else {
request.signal.addEventListener("abort", abort, { once: true });
}
const process = await container.exec(["sleep", "60"], {
signal: controller.signal,
});
// Stop forwarding the abort once the process exits.
void process.exitCode.then(() =>
request.signal.removeEventListener("abort", abort),
);
const output = await process.output();
return new Response(`Exit code: ${output.exitCode}`);
}
}exec()throws when the container is not running.exec()throws aTypeErrorwhencmdis empty, an option mode is invalid,stderr: "combined"is used withstdout: "ignore", orptyis used with astderrvalue other than"combined".exec()throws anAbortErrorwhensignalis already aborted.exec()rejects if the runtime cannot create or start the process.- Environment variable names cannot contain
=or null characters. Environment values,cwd, andusercannot contain null characters. kill()throws aRangeErrorwhen the signal is outside the supported range.resize()throws aTypeErrorwhen the process does not have a pseudo-terminal, and aRangeErrorwhen a dimension is out of range.
For task-oriented examples, refer to Execute commands.
snapshotContainer() creates a point-in-time snapshot of the writable root filesystem of a running container. It does not capture memory, running processes, or separately mounted filesystems. A container started from the snapshot runs its entrypoint again. This method is only supported for applications that use the durable_object scheduling policy.
const snapshot = await this.ctx.container.snapshotContainer({
name: "before-upgrade",
});options(ContainerSnapshotOptions): Snapshot configuration. Pass{}when you do not set a name:name(string, optional): Human-readable name for the snapshot.
Promise<ContainerSnapshot>: Resolves with an opaque handle for the stored filesystem snapshot. The snapshot data is not returned to the Worker. The handle contains:id(string): Unique snapshot identifier. Pass the returnedContainerSnapshottostart()ascontainerSnapshot, or store it for a later restore.size(number): Snapshot size in bytes.name(string, optional): Human-readable name supplied inoptions.
Container snapshots are immutable. Snapshot handles have an implicit 30-day time-to-live that refreshes when you restore them. For the complete save and restore flow, refer to Use snapshots.
snapshotContainer()throws anErrorwhen the container is not running.snapshotContainer()throws aTypeErrorwhenoptionsis omitted.
destroy() stops the container and can include an optional reason for the operation.
await this.ctx.container.destroy("Manually Destroyed");error(any, optional): Optional reason associated with the destroy operation. A string is commonly used for logging or debugging.
Promise<void>: Resolves when the container is destroyed.
signal() sends an inter-process communication (IPC) signal to the container, such as SIGKILL or SIGTERM. Use it to stop the container gracefully or forcefully.
const SIGTERM = 15;
this.ctx.container.signal(SIGTERM);signal(number): POSIX signal number to send to the container, such asSIGTERM(15) orSIGKILL(9).
void: No return value.
setInactivityTimeout() sets how long a container keeps running after its Durable Object becomes inactive. If the Durable Object handles another request before the timeout ends, the same container is still running. Otherwise, Cloudflare stops the container. Without a timeout, Cloudflare stops the container shortly after the Durable Object becomes inactive.
Each Durable Object instance sets its own timeout. A Durable Object that restarts, for example after a deploy, starts without one. Set the timeout after start(), and set it again in the constructor when the container is already running.
setInactivityTimeout(durationMs: number | bigint): Promise<void>import { DurableObject } from "cloudflare:workers";
const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
export class MyDurableObject extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
const container = ctx.container;
if (container?.running) {
void ctx.blockConcurrencyWhile(() =>
container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS),
);
}
}
async ensureRunning() {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
container.start({
// `cloudflare/debian-trixie` requires the `durable_object` scheduling policy.
image: "cloudflare/debian-trixie",
entrypoint: ["sleep", "infinity"],
enableInternet: false,
});
await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
}
}
}import { DurableObject } from "cloudflare:workers";
interface Env {}
const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
const container = ctx.container;
if (container?.running) {
void ctx.blockConcurrencyWhile(() =>
container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS),
);
}
}
async ensureRunning() {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
container.start({
// `cloudflare/debian-trixie` requires the `durable_object` scheduling policy.
image: "cloudflare/debian-trixie",
entrypoint: ["sleep", "infinity"],
enableInternet: false,
});
await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
}
}
}durationMs(number | bigint): Inactivity timeout in milliseconds. The value must be greater than0and at most 6 hours (21600000).
Promise<void>: Resolves after the timeout is set.
setInactivityTimeout()throws aTypeErrorwhendurationMsis0or less.- The returned promise rejects when
durationMsis greater than 6 hours.
Await the call inside a try block to handle both.
getTcpPort() returns a TCP port from the container. Use it to communicate with the container over TCP or HTTP.
const port = this.ctx.container.getTcpPort(8080);
const res = await port.fetch("http://container/set-state", {
body: initialState,
method: "POST",
});const conn = this.ctx.container.getTcpPort(8080).connect("10.0.0.1:8080");
await conn.opened;
try {
if (request.body) {
await request.body.pipeTo(conn.writable);
}
return new Response(conn.readable);
} catch (error) {
console.error("Request body piping failed:", error);
return new Response("Failed to proxy request body", { status: 502 });
}port(number): TCP port number to use for communication with the container.
Fetcher: Object used to send HTTP requests or TCP connections to the container port.
monitor() returns a promise that resolves when a container exits and rejects if the container errors. Use it to handle container status changes in your Workers code.
A pending monitor() call prevents eviction until the container exits or for up to 15 minutes, whichever comes first. Starting another operation later can extend the Durable Object's time in memory.
import { DurableObject } from "cloudflare:workers";
class MyDurableObject extends DurableObject {
startAndMonitor() {
const container = this.ctx.container;
// With the `default` scheduling policy, call `container.start()` without options.
container.start({
image: container.images.base,
enableInternet: false,
});
this.ctx.waitUntil(
container
.monitor()
.then(() => console.log("Container exited"))
.catch((error) => console.error("Container errored", error)),
);
}
}import { DurableObject } from "cloudflare:workers";
interface Env {}
class MyDurableObject extends DurableObject<Env> {
startAndMonitor() {
const container = this.ctx.container;
// With the `default` scheduling policy, call `container.start()` without options.
container.start({
image: container.images.base,
enableInternet: false,
});
this.ctx.waitUntil(
container
.monitor()
.then(() => console.log("Container exited"))
.catch((error) => console.error("Container errored", error)),
);
}
}A monitor() promise does not carry over when the Durable Object restarts, for example after a deploy. If running is true when the Durable Object starts, call monitor() again to observe the container.
- None.
Promise<void>: Settles when the container stops, including a container that stopped before the call. The outcome depends on how the container stopped:- It resolves when the main process exits with code
0, or whendestroy()stops the container without an error value. - It rejects with an
Errorthat has anexitCodeproperty when the main process exits with another code. - It rejects with the value passed to
destroy(), when there is one. - It rejects with an error that describes the failure when the container stops for another reason.
- It resolves when the main process exits with code
monitor()throws anErrorwhen this Durable Object instance has no container. An instance has a container after it callsstart(), or when a container was already running as the instance started.
interceptOutboundHttp() routes outbound HTTP requests matching a hostname, hostname glob, IP address, IP:port, or CIDR range through a Fetcher. Call it before or after starting the container. An intercept lasts until the container stops, so register intercepts again for each new container. Registering a target again replaces its handler, and open connections use the new handler without being dropped.
const worker = this.ctx.exports.MyWorker({ props: { message: "hello" } });
// Match a specific hostname
await this.ctx.container.interceptOutboundHttp("api.example.com", worker);
// Match a hostname glob pattern
await this.ctx.container.interceptOutboundHttp("*.example.com", worker);
// Match an IP:port
await this.ctx.container.interceptOutboundHttp("15.0.0.1:80", worker);
// Match an IPv4 CIDR range. Register an IPv6 range separately.
await this.ctx.container.interceptOutboundHttp("203.0.113.0/24", worker);In a container started with enableInternet: false, no public resolver answers DNS lookups. Lookups resolve as follows:
AandAAAAlookups for an intercepted hostname return a placeholder address that routes the request to the intercept.interceptAllOutboundHttp()and a*hostname target make every hostname resolve, including hostnames that do not exist.- IP address and CIDR targets add no DNS answers, except for
0.0.0.0/0and::/0, which make every hostname resolve. - Every other lookup times out, including a lookup made before you register an intercept.
A container has 128 intercept entries, which interceptOutboundHttp(), interceptOutboundHttps(), and interceptAllOutboundHttp() share. Each new target uses entries as follows:
- A hostname or hostname glob, including
*, uses two entries, one for IPv4 and one for IPv6. - An IP address, IP:port, or CIDR range uses one entry.
- The
interceptAllOutboundHttp()rule uses two entries.
A hostname intercepted for both HTTP and HTTPS is two targets and uses four entries. A container can hold up to 64 hostname targets, or up to 128 IP address and CIDR targets. Registering an existing target again uses no more entries.
You cannot remove an intercept while the container runs. To route a changing set of hostnames, register interceptAllOutboundHttp() or the * target once. Then choose the behavior for each hostname inside the WorkerEntrypoint, for example from its props. To change the props, register the rule again with new props.
When a new target does not fit, awaiting the call throws an Error with the message You can't configure more than 128 egress interceptors. Intercepts registered earlier keep working.
addr(string): Hostname, hostname glob (for example,*.example.com), IP address, IP:port, or CIDR range to match.binding(Fetcher): Worker entrypoint or service binding that handles matching requests.
Promise<void>: Resolves when the intercept rule is installed.
- Awaiting
interceptOutboundHttp()throws anErrorwhenaddris new and the container does not have enough free entries for it. For more information, refer to Intercept limit.
interceptAllOutboundHttp() routes outbound HTTP requests on port 80 from the container through a Fetcher, regardless of destination.
This rule does not intercept other ports, including HTTPS on port 443. To intercept HTTPS as well, also register interceptOutboundHttps("*"). In a container started with enableInternet: true, connections that no intercept covers reach the Internet directly. To block them, start the container with enableInternet: false.
Register hostname intercepts before interceptAllOutboundHttp(). A hostname intercept that interceptOutboundHttp() registers first keeps receiving its requests, even after you register this rule again. A hostname intercept registered after this rule receives no requests.
await this.ctx.container.interceptAllOutboundHttp(worker);binding(Fetcher): Worker entrypoint or service binding that handles all outbound HTTP requests.
Promise<void>: Resolves when the intercept rule is installed.
- Awaiting
interceptAllOutboundHttp()throws anErrorwhen the rule is not registered yet and the container does not have two free entries. For more information, refer to Intercept limit.
interceptOutboundHttps() routes outbound HTTPS requests matching a hostname or hostname glob, with an optional port, through a Fetcher. It works like interceptOutboundHttp() but handles HTTPS traffic. The container must trust the CA certificate at /etc/cloudflare/certs/cloudflare-containers-ca.crt for HTTPS interception.
Hostname globs support * to match any sequence of characters.
const worker = this.ctx.exports.MyWorker({ props: {} });
// Match a specific hostname
await this.ctx.container.interceptOutboundHttps("api.example.com", worker);
// Match a hostname glob pattern
await this.ctx.container.interceptOutboundHttps("*.example.com", worker);
// Intercept all HTTPS traffic
await this.ctx.container.interceptOutboundHttps("*", worker);interceptOutboundHttps() intercepts HTTPS on port 443 by default. To use another port, include it in the target, for example "api.example.com:8443". It matches the hostname that the client sends. In a container started with enableInternet: false, HTTPS requests to a bare IP address fail while a * intercept is registered.
addr(string): Hostname or hostname glob pattern to match, with an optional port such asapi.example.com:8443. Use*to intercept all HTTPS traffic on port443.binding(Fetcher): Worker entrypoint or service binding that handles matching requests.
Promise<void>: Resolves when the intercept rule is installed.
- Awaiting
interceptOutboundHttps()throws anErrorwhenaddris new and the container does not have enough free entries for it. For more information, refer to Intercept limit.
- Containers APIs: Compare direct runtime control with the
Containerclass. - Container class reference: Reference for existing
Containerclass applications. - Containers overview: Understand how Cloudflare Containers work.
- Get started with Containers: Deploy your first container.
- SQLite storage API: Persist state across container restarts.
- Durable Objects: The underlying platform that powers Containers.
- Snapshots: Save and restore container filesystems.