A class for interacting with Containers on Cloudflare Workers.
- HTTP request proxying and WebSocket forwarding
- Outbound request interception by host or catch-all handler, with HTTPS support
- Host allow/deny lists with glob pattern matching
- Simple container lifecycle management (starting and stopping containers)
- Event hooks for container lifecycle events (onStart, onStop, onError)
- Configurable sleep timeout that renews on requests
- Load balancing utilities
npm install @cloudflare/containersimport { Container, getContainer, getRandom } from '@cloudflare/containers';
export class MyContainer extends Container {
// Configure default port for the container
defaultPort = 8080;
// After 1 minute of no new activity, shutdown the container
sleepAfter = '1m';
}
export default {
async fetch(request, env) {
const pathname = new URL(request.url).pathname;
// If you want to route requests to a specific container,
// pass a unique container identifier to .get()
if (pathname.startsWith('/specific/')) {
// In this case, each unique pathname will spawn a new container
const container = env.MY_CONTAINER.getByName(pathname);
return await container.fetch(request);
}
// Note: this is a temporary method until built-in autoscaling and load balancing are added.
// If you want to route to one of many containers (in this case 5), use the getRandom helper.
// This load balances incoming requests across these container instances.
let container = await getRandom(env.MY_CONTAINER, 5);
return await container.fetch(request);
},
};The Container class that extends a container-enbled Durable Object to provide additional container-specific functionality.
-
defaultPort?Optional default port to use when communicating with the container. If this is not set, or you want to target a specific port on your container, you can specify the port with
fetch(switchPort(req, 8080))orcontainerFetch(req, 8080). -
requiredPorts?Array of ports that should be checked for availability during container startup. Used by
startAndWaitForPortswhen no specific ports are provided. -
sleepAfterHow long to keep the container alive without activity (format: number for seconds, or string like "5m", "30s", "1h").
Defaults to "10m", meaning that after the Container class Durable Object receives no requests for 10 minutes, it will shut down the container.
The following properties are used to set defaults when starting the container, but can be overriden on a per-instance basis by passing in values to startAndWaitForPorts() or start().
-
env?: Record<string, string>Environment variables to pass to the container when starting up.
-
entrypoint?: string[]Specify an entrypoint to override image default.
-
enableInternet: booleanWhether to enable internet access for the container.
Defaults to
true. -
interceptHttps: booleanWhen
true, outbound HTTPS traffic is also intercepted through the same handler chain as HTTP. The container must trust the Cloudflare-provided CA certificate at/etc/cloudflare/certs/cloudflare-containers-ca.crt. See Egress docs for setup instructions per distribution.Defaults to
false. -
allowedHosts: string[]A whitelist of hostname patterns. When non-empty, only matching hosts can proceed past the allow check; everything else is blocked (HTTP 520). Supports simple glob patterns where
*matches any sequence of characters (e.g.'*.example.com'). -
deniedHosts: string[]A blocklist of hostname patterns. Matching hosts are blocked unconditionally (HTTP 520), overriding everything else in the handler chain including per-host handlers. Supports the same glob patterns as
allowedHosts. -
pingEndpoint: stringSpecify an endpoint the container class will hit to check if the underlying instance started. This does not need to be set by the majority of people, only use it if you would like the container supervisor to hit another endpoint in your container when it starts it. Observe that
pingEndpointcan include both the hostname and the path. You can setcontainer/health, meaning"container"will be the value passed along theHostheader, and"/health"the path.Defaults to
ping.
These lifecycle methods are automatically called when the container state transitions. Override these methods to use these hooks.
See this example.
-
onStart()Called when container starts successfully.
- called when states transition from
stopped->running,running->healthy
- called when states transition from
-
onStop()Called when container shuts down.
-
onError(error)Called when container encounters an error, and by default logs and throws the error.
-
onActivityExpired()Called when the activity is expired. The container will run continue to run for some time after the last activity - this length of time is configured by
sleepAfter. By default, this stops the container with aSIGTERM, but you can override this behaviour, as with other lifecycle hooks. However, if you don't stop the container here, the activity tracker will be renewed, and this lifecycle hook will be called again when the timer re-expires.
-
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>Forwards HTTP requests to the container.
If you want to target a specific port on the container, rather than the default port, you should use
switchPortlike so:const container = env.MY_CONTAINER.getByName('id'); await container.fetch(switchPort(request, 8080));
Make sure you provide a port with switchPort or specify a port with the
defaultPortproperty.You must use
fetchrather thancontainerFetchif you want to forward websockets.Note that when you call any of the fetch functions, the activity will be automatically renewed (sleepAfter time starts after last activity), and the container will be started if not already running.
-
containerFetch(...)Note:
containerFetchdoes not work with websockets.Sends an HTTP request to the container. Supports both standard fetch API signatures:
containerFetch(request, port?): Traditional signature with Request objectcontainerFetch(url, init?, port?): Standard fetch-like signature with URL string/object and RequestInit options
-
startAndWaitForPorts(args: StartAndWaitForPortsOptions): Promise<void>Starts the container and then waits for specified ports to be ready. Prioritises
portspassed in to the function, thenrequiredPortsif set, thendefaultPort.interface StartAndWaitForPortsOptions { startOptions?: { /** Environment variables to pass to the container */ envVars?: Record<string, string>; /** Custom entrypoint to override container default */ entrypoint?: string[]; /** Whether to enable internet access for the container */ enableInternet?: boolean; }; /** Ports to check */ ports?: number | number[]; cancellationOptions?: { /** Abort signal to cancel start and port checking */ abort?: AbortSignal; /** Max time to wait for container to start, in milliseconds */ instanceGetTimeoutMS?: number; /** Max time to wait for ports to be ready, in milliseconds */ portReadyTimeoutMS?: number; /** Polling interval for checking container has started or ports are ready, in milliseconds */ waitInterval?: number; }; }
-
start(startOptions?: ContainerStartConfigOptions, waitOptions?: WaitOptions)Starts the container, without waiting for any ports to be ready.
You might want to use this instead of
startAndWaitForPortsif you want to:- Start a container without blocking until a port is available
- Initialize a container that doesn't expose ports
- Perform custom port availability checks separately
Options:
interface ContainerStartConfigOptions { /** Environment variables to pass to the container */ envVars?: Record<string, string>; /** Custom entrypoint to override container default */ entrypoint?: string[]; /** Whether to enable internet access for the container */ enableInternet?: boolean; } interface WaitOptions { /** The port number to check for readiness */ portToCheck: number; /** Optional AbortSignal, use this to abort waiting for ports */ signal?: AbortSignal; /** Number of attempts to wait for port to be ready */ retries?: number; /** Time to wait in between polling port for readiness, in milliseconds */ waitInterval?: number; }
-
stop(signal = SIGTERM): Promise<void>Sends the specified signal to the container. Triggers
onStop. -
destroy(): Promise<void>Forcefully destroys the container (sends
SIGKILL). TriggersonStop. -
getState(): Promise<State>Get the current container state.
type State = { lastChange: number; } & ( | { // 'running' means that the container is trying to start and is transitioning to a healthy status. // onStop might be triggered if there is an exit code, and it will transition to 'stopped'. status: 'running' | 'stopping' | 'stopped' | 'healthy'; } | { status: 'stopped_with_code'; exitCode?: number; } );
-
renewActivityTimeout()Manually renews the container activity timeout (extends container lifetime).
Use outbound interception when you want to control what the container can reach, or proxy outbound requests through Worker code.
-
setOutboundByHost(host: string, method: string): Promise<void>Routes a specific hostname to a named handler from
static outboundHandlers. -
setOutboundByHosts(handlers: Record<string, string>): Promise<void>Replaces all runtime host-specific overrides at once.
-
removeOutboundByHost(host: string): Promise<void>Removes a runtime host-specific override.
-
setOutboundHandler(method: string): Promise<void>Sets the catch-all outbound handler to a named handler from
static outboundHandlers. -
setAllowedHosts(hosts: string[]): Promise<void>Replaces the allowed hosts list at runtime.
-
setDeniedHosts(hosts: string[]): Promise<void>Replaces the denied hosts list at runtime.
-
allowHost(hostname: string): Promise<void>Adds a single host to the allowed list.
-
denyHost(hostname: string): Promise<void>Adds a single host to the denied list.
-
removeAllowedHost(hostname: string): Promise<void>Removes a host from the allowed list.
-
removeDeniedHost(hostname: string): Promise<void>Removes a host from the denied list.
To configure interception on the class itself:
-
static outbound = (req, env, ctx) => ResponseCatch-all handler for outbound requests.
-
static outboundByHost = { [host]: handler }Per-host handlers for exact hostname matches such as
google.comor an IP address. -
static outboundHandlers = { [name]: handler }Named handlers that can be selected at runtime with
setOutboundHandlerandsetOutboundByHost.
Processing order (first match wins):
- Denied hosts — if the hostname matches any
deniedHostspattern, the request is blocked (HTTP 520). Overrides everything else. - Allowed hosts gate — if
allowedHostsis non-empty and the hostname does not match, the request is blocked (HTTP 520). WhenallowedHostsis empty this step is skipped. - Runtime per-host handler —
setOutboundByHost()override - Static per-host handler —
outboundByHost - Runtime catch-all handler —
setOutboundHandler()override - Static catch-all handler —
outbound - Allowed host internet fallback — if the hostname matched
allowedHostsbut no handler above handled it, the request is forwarded to the internet (even ifenableInternetisfalse). enableInternetfallback — iftrue, the request goes to the internet.- Default deny — the request is blocked (HTTP 520).
-
schedule<T = string>(when: Date | number, callback: string, payload?: T): Promise<Schedule<T>>Options:
when: When to execute the task (Date object or number of seconds delay)callback: Name of the function to call as a stringpayload: Data to pass to the callback
Instead of using the default alarm handler, use
schedule()instead. The default alarm handler is in charge of renewing the container activity and keeping the durable object alive. You can overridealarm(), but because its functionality is currently vital to managing the container lifecycle, we recommend callingscheduleto schedule tasks instead.
-
getRandom(binding, instances?: number)Get a random container instances across N instances. This is useful for load balancing. Returns a stub for the container. See example.
-
getContainer(binding, name?: string)Helper to get a particular container instance stub.e.g.
const container = getContainer(env.CONTAINER, "unique-id")If no name is provided, "cf-singleton-container" is used.
import { Container } from '@cloudflare/containers';
export class MyContainer extends Container {
// Configure default port for the container
defaultPort = 8080;
// Set how long the container should stay active without requests
// Supported formats: "10m" (minutes), "30s" (seconds), "1h" (hours), or a number (seconds)
sleepAfter = '10m';
// Lifecycle method called when container starts
override onStart(): void {
console.log('Container started!');
}
// Lifecycle method called when container shuts down
override onStop(): void {
console.log('Container stopped!');
// you can also call startAndWaitForPorts() again
// this.startAndWaitForPorts();
}
// Lifecycle method called on errors
override onError(error: unknown): any {
console.error('Container error:', error);
throw error;
}
// Lifecycle method when the container class considers the activity to be expired
override onActivityExpired() {
console.log('Container activity expired');
await this.destroy();
}
// Custom method that will extend the container's lifetime
async performBackgroundTask(): Promise<void> {
// Do some work...
// Renew the container's activity timeout
await this.renewActivityTimeout();
console.log('Container activity timeout extended');
}
// Additional methods can be implemented as needed
}The Container class automatically supports proxying WebSocket connections to your container. WebSocket connections are bi-directionally proxied, with messages forwarded in both directions. The Container also automatically renews the activity timeout when WebSocket messages are sent or received.
// Connect to a WebSocket on port 9000
const response = await container.fetch(switchPort(request, 9000));Note websockets are not supported with containerFetch.
You can configure defaults for how the container starts by setting the instance properties for environment variables, entrypoint, and network access:
import { Container } from '@cloudflare/containers';
export class ConfiguredContainer extends Container {
// Default port for the container
defaultPort = 9000;
// Set the timeout for sleeping the container after inactivity
sleepAfter = '2h';
// Environment variables to pass to the container
envVars = {
NODE_ENV: 'production',
LOG_LEVEL: 'info',
APP_PORT: '9000',
};
// Custom entrypoint to run in the container
entrypoint = ['node', 'server.js', '--config', 'production.json'];
// Enable internet access for the container
enableInternet = true;
// These configuration properties will be used automatically
// when the container starts
}You can also set these on a per-instance basis with start or startAndWaitForPorts
This lets you intercept requests the container makes to the outside world.
You must export ContainerProxy from your Worker entrypoint for outbound interception to work:
export { ContainerProxy } from '@cloudflare/containers';import { Container, OutboundHandlerContext } from '@cloudflare/containers';
export class MyContainer extends Container {
defaultPort = 8080;
enableInternet = false;
interceptHttps = true; // also intercept HTTPS outbound traffic
// Only allow these hosts through the egress chain
allowedHosts = ['google.com', 'github.com', 'api.stripe.com'];
// Block these hosts unconditionally, even if they appear in allowedHosts
deniedHosts = ['evil.com', '*.malware.net'];
static outboundByHost = {
'google.com': (_req: Request, _env: unknown, ctx: OutboundHandlerContext) => {
return new Response('hi ' + ctx.containerId + ' i am google');
},
};
static outboundHandlers = {
async github(_req: Request, _env: unknown, _ctx: OutboundHandlerContext) {
return new Response('i am github');
},
};
static outbound = (req: Request) => {
return new Response(`Hi ${req.url}, I can't handle you`);
};
async routeGithubThroughHandler(): Promise<void> {
await this.setOutboundByHost('github.com', 'github');
}
async makeEverythingUseGithubHandler(): Promise<void> {
await this.setOutboundHandler('github');
}
}Use outboundByHost for fixed host rules, outbound for a default catch-all, and outboundHandlers for reusable named handlers you want to switch on at runtime. Use allowedHosts and deniedHosts for glob-based host filtering (e.g. '*.example.com'). See the full egress documentation for detailed interception strategy and CA certificate setup for HTTPS interception.
You can create a container that doesn't use a default port and instead routes traffic to different ports based on request path or other factors:
import { Container } from '@cloudflare/containers';
export class MultiPortContainer extends Container {
// No defaultPort defined - we'll handle port specification manually
constructor(ctx: any, env: any) {
super(ctx, env);
}
/**
* Process an incoming request and route to different ports based on path
*/
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
try {
if (url.pathname.startsWith('/api')) {
// API server runs on port 3000
return await this.containerFetch(request, 3000);
} else if (url.pathname.startsWith('/admin')) {
// Admin interface runs on port 8080
return await this.containerFetch(request, 8080);
} else {
// Public website runs on port 80
return await this.containerFetch(request, 80);
}
} catch (error) {
return new Response(`Error: ${error instanceof Error ? error.message : String(error)}`, {
status: 500,
});
}
}
}You can use the containerFetch method with standard fetch API syntax:
import { Container } from '@cloudflare/containers';
export class FetchStyleContainer extends Container {
defaultPort = 8080;
async customHandler(): Promise<Response> {
try {
// Using the new fetch-style syntax
const response = await this.containerFetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: 'example' }),
});
// You can also specify a port with this syntax
const adminResponse = await this.containerFetch(
'https://example.com/admin',
{ method: 'GET' },
3000 // port
);
return response;
} catch (error) {
return new Response(`Error: ${error instanceof Error ? error.message : String(error)}`, {
status: 500,
});
}
}
}The Container class includes an automatic idle timeout feature that will shut down the container after a period of inactivity. This helps save resources when containers are not in use.
import { Container } from '@cloudflare/containers';
export class TimeoutContainer extends Container {
// Configure default port for the container
defaultPort = 8080;
// Set timeout to 30 minutes of inactivity
sleepAfter = '30m'; // Supports "30s", "5m", "1h" formats, or a number in seconds
// Custom method that will extend the container's lifetime
async performBackgroundTask(data: any): Promise<void> {
console.log('Performing background task...');
// Manually renew the activity timeout, even though
// you have not made a request to the container
await this.renewActivityTimeout();
console.log('Container activity timeout renewed');
}
// Activity timeout is automatically renewed on fetch requests
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
// Example endpoint to trigger background task
if (url.pathname === '/task') {
await this.performBackgroundTask();
return new Response(
JSON.stringify({
success: true,
message: 'Background task executed',
nextStop: `Container will shut down after ${this.sleepAfter} of inactivity`,
}),
{ headers: { 'Content-Type': 'application/json' } }
);
}
// For all other requests, forward to the container
// This will automatically renew the activity timeout
return this.containerFetch(request);
}
}This package includes a getRandom helper which routes requests to one of N instances.
In the future, this will be automatically handled with resource-aware load balancing
when autoscaling is set to true, but it is not yet implemented.
import { Container, getContainer, getRandom } from '@cloudflare/containers';
export class MyContainer extends Container {
defaultPort = 8080;
}
export default {
async fetch(request: Request, env: any) {
const url = new URL(request.url);
// Example: Load balance across 5 container instances
if (url.pathname === '/api') {
const containerInstance = await getRandom(env.MY_CONTAINER, 5);
return containerInstance.fetch(request);
}
// Example: Direct request to a specific container
if (url.pathname.startsWith('/specific/')) {
const id = url.pathname.split('/')[2] || 'default';
const containerInstance = getContainer(env.MY_CONTAINER, id);
return containerInstance.fetch(request);
}
return new Response('Not found', { status: 404 });
},
};