Containers
A class for interacting with Containers on Cloudflare Workers.
Features
- HTTP request proxying and WebSocket forwarding
- Outbound request interception by host or catch-all handler
- 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
Installation
npm install @cloudflare/containers
Basic Example
import { Container, getContainer, getRandom } from '@cloudflare/containers';
export class MyContainer extends Container {
defaultPort = 8080;
sleepAfter = '1m';
}
export default {
async fetch(request, env) {
const pathname = new URL(request.url).pathname;
if (pathname.startsWith('/specific/')) {
const container = env.MY_CONTAINER.getByName(pathname);
return await container.fetch(request);
}
let container = await getRandom(env.MY_CONTAINER, 5);
return await container.fetch(request);
},
};
API Reference
Container Class
The Container class that extends a container-enbled Durable Object to provide additional container-specific functionality.
Properties
-
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)) or containerFetch(req, 8080).
-
requiredPorts?
Array of ports that should be checked for availability during container startup. Used by startAndWaitForPorts when no specific ports are provided.
-
sleepAfter
How 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: boolean
Whether to enable internet access for the container.
Defaults to true.
-
pingEndpoint: string
Specify 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 pingEndpoint can include both the hostname and the path. You can
set container/health, meaning "container" will be the value passed along the Host header, and
"/health" the path.
Defaults to ping.
Methods
Lifecycle Hooks
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
-
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 a SIGTERM, 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.
Container Methods
-
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 switchPort like 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 defaultPort property.
You must use fetch rather than containerFetch if 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: containerFetch does not work with websockets.
Sends an HTTP request to the container. Supports both standard fetch API signatures:
containerFetch(request, port?): Traditional signature with Request object
containerFetch(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 ports passed in to the function, then requiredPorts if set, then defaultPort.
interface StartAndWaitForPortsOptions {
startOptions?: {
envVars?: Record<string, string>;
entrypoint?: string[];
enableInternet?: boolean;
};
ports?: number | number[];
cancellationOptions?: {
abort?: AbortSignal;
instanceGetTimeoutMS?: number;
portReadyTimeoutMS?: number;
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 startAndWaitForPorts if 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 {
envVars?: Record<string, string>;
entrypoint?: string[];
enableInternet?: boolean;
}
interface WaitOptions {
portToCheck: number;
signal?: AbortSignal;
retries?: number;
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). Triggers onStop.
-
getState(): Promise<State>
Get the current container state.
type State = {
lastChange: number;
} & (
| {
status: 'running' | 'stopping' | 'stopped' | 'healthy';
}
| {
status: 'stopped_with_code';
exitCode?: number;
}
);
-
renewActivityTimeout()
Manually renews the container activity timeout (extends container lifetime).
Outbound Interception
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.
To configure interception on the class itself:
-
static outbound = (req, env, ctx) => Response
Catch-all handler for outbound requests.
-
static outboundByHost = { [host]: handler }
Per-host handlers for exact hostname matches such as google.com or an IP address.
-
static outboundHandlers = { [name]: handler }
Named handlers that can be selected at runtime with setOutboundHandler and setOutboundByHost.
Matching order is:
- Runtime
setOutboundByHost override
- Static
outboundByHost
- Runtime
setOutboundHandler catch-all
- Static
outbound
If nothing matches, the request goes out normally when enableInternet is true, and is blocked when enableInternet is false.
-
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 string
payload: 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 override alarm(), but because its functionality is currently vital to managing the container lifecycle, we recommend calling schedule to schedule tasks instead.
Utility Functions
-
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.
Examples
HTTP Example with Lifecycle Hooks
import { Container } from '@cloudflare/containers';
export class MyContainer extends Container {
defaultPort = 8080;
sleepAfter = '10m';
override onStart(): void {
console.log('Container started!');
}
override onStop(): void {
console.log('Container stopped!');
}
override onError(error: unknown): any {
console.error('Container error:', error);
throw error;
}
override onActivityExpired() {
console.log('Container activity expired');
await this.destroy();
}
async performBackgroundTask(): Promise<void> {
await this.renewActivityTimeout();
console.log('Container activity timeout extended');
}
}
WebSocket Support
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.
const response = await container.fetch(switchPort(request, 9000));
Note websockets are not supported with containerFetch.
Container Configuration Example
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 {
defaultPort = 9000;
sleepAfter = '2h';
envVars = {
NODE_ENV: 'production',
LOG_LEVEL: 'info',
APP_PORT: '9000',
};
entrypoint = ['node', 'server.js', '--config', 'production.json'];
enableInternet = true;
}
You can also set these on a per-instance basis with start or startAndWaitForPorts
Outbound Interception
This lets you intercept requests the container makes to the outside world.
import { Container, OutboundHandlerContext } from '@cloudflare/containers';
export class MyContainer extends Container {
defaultPort = 8080;
enableInternet = false;
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.
Multiple Ports and Custom Routing
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 {
constructor(ctx: any, env: any) {
super(ctx, env);
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
try {
if (url.pathname.startsWith('/api')) {
return await this.containerFetch(request, 3000);
} else if (url.pathname.startsWith('/admin')) {
return await this.containerFetch(request, 8080);
} else {
return await this.containerFetch(request, 80);
}
} catch (error) {
return new Response(`Error: ${error instanceof Error ? error.message : String(error)}`, {
status: 500,
});
}
}
}
Using Standard Fetch API Syntax
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 {
const response = await this.containerFetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: 'example' }),
});
const adminResponse = await this.containerFetch(
'https://example.com/admin',
{ method: 'GET' },
3000
);
return response;
} catch (error) {
return new Response(`Error: ${error instanceof Error ? error.message : String(error)}`, {
status: 500,
});
}
}
}
Managing Container Idle Timeout
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 {
defaultPort = 8080;
sleepAfter = '30m';
async performBackgroundTask(data: any): Promise<void> {
console.log('Performing background task...');
await this.renewActivityTimeout();
console.log('Container activity timeout renewed');
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
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' } }
);
}
return this.containerFetch(request);
}
}
Using Load Balancing
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);
if (url.pathname === '/api') {
const containerInstance = await getRandom(env.MY_CONTAINER, 5);
return containerInstance.fetch(request);
}
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 });
},
};