From 0bfe2f4d39402889f39efc9487522bc63cb76e00 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Fri, 28 Aug 2026 14:33:57 -0500 Subject: [PATCH 01/23] Document raw Containers API alongside Container class --- public/__redirects | 12 +- .../{reference => api}/container-class.mdx | 8 +- .../api/durable-object-container.mdx | 371 ++++++++++++++++++ src/content/docs/containers/api/index.mdx | 108 +++++ .../docs/containers/concepts/architecture.mdx | 57 ++- .../configuration/environment-variables.mdx | 2 +- .../docs/containers/configuration/index.mdx | 2 +- .../configuration/outbound-traffic.mdx | 2 +- .../containers/configuration/rollouts.mdx | 6 +- .../configuration/scaling-and-routing.mdx | 4 +- .../configuration/workers-connections.mdx | 4 +- .../containers/configuration/wrangler.mdx | 73 ++++ .../containers/examples/container-backend.mdx | 70 +++- src/content/docs/containers/examples/cron.mdx | 102 ++++- .../examples/durable-object-interface.mdx | 13 - .../examples/env-vars-and-secrets.mdx | 217 +++++++++- .../docs/containers/examples/index.mdx | 2 +- .../containers/examples/r2-fuse-mount.mdx | 99 ++++- .../docs/containers/examples/stateless.mdx | 67 +++- .../docs/containers/examples/status-hooks.mdx | 77 +++- .../docs/containers/examples/websocket.mdx | 62 ++- src/content/docs/containers/faq.mdx | 4 +- .../docs/containers/get-started/index.mdx | 6 +- .../containers/guides/execute-commands.mdx | 4 +- src/content/docs/containers/index.mdx | 17 +- .../docs/containers/platform/index.mdx | 2 +- .../reference/durable-object-methods.mdx | 11 - .../docs/containers/reference/index.mdx | 4 +- .../docs/durable-objects/api/container.mdx | 346 +--------------- .../ai/enterprise-ai-vibe-coding-platform.mdx | 2 +- .../docs/workers/wrangler/configuration.mdx | 2 +- 31 files changed, 1289 insertions(+), 467 deletions(-) rename src/content/docs/containers/{reference => api}/container-class.mdx (98%) create mode 100644 src/content/docs/containers/api/durable-object-container.mdx create mode 100644 src/content/docs/containers/api/index.mdx create mode 100644 src/content/docs/containers/configuration/wrangler.mdx delete mode 100644 src/content/docs/containers/examples/durable-object-interface.mdx delete mode 100644 src/content/docs/containers/reference/durable-object-methods.mdx diff --git a/public/__redirects b/public/__redirects index 2702c2b0734..2a5f6efe42c 100644 --- a/public/__redirects +++ b/public/__redirects @@ -641,10 +641,14 @@ /constellation/ /workers-ai/ 301 # Containers /containers/beta-info/ /containers/faq/ 301 -/containers/container-package/ /containers/reference/container-class/ 301 -/containers/durable-object-methods/ /durable-objects/api/container/ 301 +# Containers API +/containers/container-package/ /containers/api/container-class/ 301 +/containers/durable-object-methods/ /containers/api/durable-object-container/ 301 +/containers/container-class/ /containers/api/container-class/ 301 +/containers/reference/container-class/ /containers/api/container-class/ 301 +/containers/reference/durable-object-methods/ /containers/api/durable-object-container/ 301 +/durable-objects/api/container/ /containers/api/durable-object-container/ 301 # Containers IA rework: loose pages folded into core sections -/containers/container-class/ /containers/reference/container-class/ 301 /containers/local-dev/ /containers/guides/local-dev/ 301 /containers/deploy/ /containers/guides/deploy/ 301 /containers/execute-commands/ /containers/guides/execute-commands/ 301 @@ -663,7 +667,7 @@ /containers/platform-details/scaling-and-routing/ /containers/configuration/scaling-and-routing/ 301 /containers/platform-details/limits/ /containers/platform/limits/ 301 /containers/platform-details/image-management/ /containers/guides/image-management/ 301 -/containers/platform-details/durable-object-methods/ /durable-objects/api/container/ 301 +/containers/platform-details/durable-object-methods/ /containers/api/durable-object-container/ 301 /containers/platform-details/ /containers/concepts/architecture/ 301 # Containers IA rework: Configuration section + Local Development to Guides + Wrangler pages to Reference /containers/guides/outbound-traffic/ /containers/configuration/outbound-traffic/ 301 diff --git a/src/content/docs/containers/reference/container-class.mdx b/src/content/docs/containers/api/container-class.mdx similarity index 98% rename from src/content/docs/containers/reference/container-class.mdx rename to src/content/docs/containers/api/container-class.mdx index 1c32036aac7..9443c81ee57 100644 --- a/src/content/docs/containers/reference/container-class.mdx +++ b/src/content/docs/containers/api/container-class.mdx @@ -1,16 +1,16 @@ --- pcx_content_type: reference -title: Container Interface +title: Container class sidebar: - order: 1 -description: API reference for the Container interface and utility functions + order: 2 +description: API reference for the higher-level Container class built on Durable Objects. products: - containers --- import { PackageManagers, TypeScriptExample } from "~/components"; -The [`Container` class](https://github.com/cloudflare/containers) from [`@cloudflare/containers`](https://www.npmjs.com/package/@cloudflare/containers) is the most common way to interact with container instances from a Worker. +The [`Container` class](https://github.com/cloudflare/containers) from [`@cloudflare/containers`](https://www.npmjs.com/package/@cloudflare/containers) provides lifecycle helpers for container instances. For direct lifecycle control, use the [Durable Object Container API](/containers/api/durable-object-container/). **`Container` extends [`DurableObject`](/durable-objects/api/base/).** 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`](/durable-objects/api/sqlite-storage-api/) for persistent SQLite-backed storage and [`this.ctx.id`](/durable-objects/api/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. diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx new file mode 100644 index 00000000000..4dd87ce737a --- /dev/null +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -0,0 +1,371 @@ +--- +title: Durable Object Container API +description: Access and manage containers associated with a Durable Object, including start, stop, and interaction methods. +pcx_content_type: concept +sidebar: + order: 1 +products: + - containers + - durable-objects +--- + +import { + Render, + Tabs, + TabItem, + GlossaryTooltip, + Type, + MetaInfo, + TypeScriptExample, +} from "~/components"; + +## Description + +Each [container](/containers/) is managed 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. + +You can instead use the [`Container` class](/containers/api/container-class/) from `@cloudflare/containers`. The class adds routing, readiness checks, lifecycle hooks, activity tracking, and scheduling. To compare both APIs, refer to [Containers APIs](/containers/api/). + +Your Durable Object also has access to [SQLite storage](/durable-objects/api/sqlite-storage-api/) through `this.ctx.storage`, [alarms](/durable-objects/api/alarms/), and all other Durable Object APIs. + + +```ts +import { DurableObject } from "cloudflare:workers"; + +interface Env {} + +export class MyDurableObject extends DurableObject { + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + + ctx.blockConcurrencyWhile(async () => { + if (!ctx.container!.running) { + ctx.container!.start(); + } + }); + } +} +``` + + +## Attributes + +### `running` + +`running` returns `true` if the container is currently running. It does not ensure that the container has fully started and ready to accept requests. + +```js +this.ctx.container.running; +``` + +## Methods + +### `start` + +`start` boots a container. This method does not block until the container is fully started. +You may want to confirm the container is ready to accept requests before using it. + +```js +this.ctx.container.start({ + env: { + FOO: "bar", + }, + enableInternet: false, + entrypoint: ["node", "server.js"], +}); +``` + +#### Parameters + +- `options` (optional): An object with the following properties: + - `env`: An object containing environment variables to pass to the container. This is useful for passing configuration values or secrets to the container. + - `entrypoint`: An array of strings representing the command to run in the container. + - `enableInternet`: A boolean indicating whether to enable internet access for the container. + +#### Return values + +- None. + +### `exec` + +`exec` starts another process inside an already-running Container. It does not start a stopped Container. + +The following example calls `this.ctx.container.exec()` inside a class extending `Container` from `@cloudflare/containers`. In RPC methods, check `this.ctx.container.running` and call `await this.start()` when needed. You can also use the `onStart()` hook to run any series of commands whenever the Container starts. + +```ts +exec( + cmd: string[], + options?: ContainerExecOptions, +): Promise +``` + +The `exec` operation 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", ""]` when Bash exists in the image. Use `["sh", "-c", ""]` for images with only a Portable Operating System Interface (POSIX) shell. + +The following RPC method starts the Container before executing a command: + + +```ts +import { Container } from "@cloudflare/containers"; + +export class MyContainer extends Container { + async runCommand() { + if (!this.ctx.container.running) { + await this.start(); + } + + const process = await this.ctx.container.exec(["node", "--version"]); + const output = await process.output(); + + return { + pid: process.pid, + exitCode: output.exitCode, + stdout: new TextDecoder().decode(output.stdout), + }; + } +} +``` + + +#### Parameters + +- `cmd` (`string[]`) — executable followed by its arguments. +- `options` (`ContainerExecOptions`, optional) — process configuration: + - `stdin` (`ReadableStream | "pipe"`) — source for standard input. Use `"pipe"` to write through the returned `stdin` stream. When omitted, standard input closes and sends end-of-file (EOF). + - `stdout` (`"pipe" | "ignore"`, default `"pipe"`) — captures or discards standard output. + - `stderr` (`"pipe" | "ignore" | "combined"`, default `"pipe"`) — captures, discards, or merges standard error into standard output. The `"combined"` value requires `stdout: "pipe"`. Combined output does not guarantee ordering between its source streams. + - `cwd` (`string`) — working directory for the process. + - `env` (`Record`) — environment additions and overrides. The process inherits existing Container variables. Matching keys use the per-execution value. + - `user` (`string`) — image user for the process. + +#### Return values + +Returns `Promise`. + +An `ExecProcess` has these fields and methods: + +- `stdin` (`WritableStream | null`) — writable standard input when `stdin` is `"pipe"`. +- `stdout` (`ReadableStream | null`) — readable standard output when piped. +- `stderr` (`ReadableStream | null`) — readable standard error when piped separately. +- `pid` (`number`) — process identifier. +- `exitCode` (`Promise`) — resolves when the process exits. Nonzero codes resolve normally instead of rejecting. +- `output()` (`Promise`) — reads buffered output once. `ExecOutput` contains `stdout` (`ArrayBuffer`), `stderr` (`ArrayBuffer`), and `exitCode` (`number`). Ignored streams produce empty buffers. Use `TextDecoder` to decode text. +- `kill(signal?: number)` (`void`) — queues a signal for the process. The default is `SIGTERM`, signal `15`. The signal must be from `1` through `64`. + +With `stderr: "combined"`, `stderr` is `null` on `ExecProcess` and an empty `ArrayBuffer` on `ExecOutput`. Read both output channels from `stdout`. + +`output()` throws a `TypeError` when called more than once or after either readable stream starts being consumed. For large output, consume both readable streams concurrently instead of buffering them with `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. + +#### Exceptions + +- `exec()` throws when the Container is not running. +- `exec()` throws a `TypeError` when `cmd` is empty, an option mode is invalid, or `stderr: "combined"` is used with `stdout: "ignore"`. +- `exec()` rejects if the runtime cannot create or start the process. +- Environment variable names cannot contain `=` or null characters. Environment values, `cwd`, and `user` cannot contain null characters. +- `kill()` throws a `RangeError` when the signal is outside the supported range. + +For task-oriented examples, refer to [Execute commands](/containers/guides/execute-commands/). + +### `destroy` + +`destroy` stops the container and optionally returns a custom error message to the `monitor()` error callback. + +```js +this.ctx.container.destroy("Manually Destroyed"); +``` + +#### Parameters + +- `error` (optional): A string that will be sent to the error handler of the `monitor` method. This is useful for logging or debugging purposes. + +#### Return values + +- A promise that returns once the container is destroyed. + +### `signal` + +`signal` sends an IPC signal to the container, such as SIGKILL or SIGTERM. This is useful for stopping the container gracefully or forcefully. + +```js +const SIGTERM = 15; +this.ctx.container.signal(SIGTERM); +``` + +#### Parameters + +- `signal`: a number representing the signal to send to the container. This is typically a POSIX signal number, such as SIGTERM (15) or SIGKILL (9). + +#### Return values + +- None. + +### `setInactivityTimeout` + +`setInactivityTimeout` sets how long a running container can remain inactive before the runtime stops it. + +```ts +setInactivityTimeout(durationMs: number | bigint): Promise +``` + +```js +await this.ctx.container.setInactivityTimeout(10 * 60 * 1000); +``` + +#### Parameters + +- `durationMs`: Inactivity timeout in milliseconds. + +#### Return values + +- A promise that resolves after the timeout is set. + +### `getTcpPort` + +`getTcpPort` returns a TCP port from the container. This can be used to communicate with the container over TCP and HTTP. + +```js +const port = this.ctx.container.getTcpPort(8080); +const res = await port.fetch("http://container/set-state", { + body: initialState, + method: "POST", +}); +``` + +```js +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 (err) { + console.error("Request body piping failed:", err); + return new Response("Failed to proxy request body", { status: 502 }); +} +``` + +#### Parameters + +- `port` (number): a TCP port number to use for communication with the container. + +#### Return values + +- `TcpPort`: a `TcpPort` object representing the TCP port. This object can be used to send requests to the container over TCP and HTTP. + +### `monitor` + +`monitor` returns a promise that resolves when a container exits and errors if a container errors. This is useful for setting up +callbacks to handle container status changes in your Workers code. + +```js +class MyContainer extends DurableObject { + constructor(ctx, env) { + super(ctx, env); + function onContainerExit() { + console.log("Container exited"); + } + + // the "err" value can be customized by the destroy() method + async function onContainerError(err) { + console.log("Container errored", err); + } + + this.ctx.container.start(); + this.ctx.container.monitor().then(onContainerExit).catch(onContainerError); + } +} +``` + +#### Parameters + +- None + +#### Return values + +- A promise that resolves when the container exits. + +### `interceptOutboundHttp` + +`interceptOutboundHttp` routes outbound HTTP requests matching a hostname, hostname glob, IP address, IP:port, or CIDR range through a `WorkerEntrypoint`. Can be called before or after starting the container. Open connections pick up the new handler without being dropped. + +```js +const worker = this.ctx.exports.MyWorker({ props: { message: "hello" } }); + +// Match a specific hostname +this.ctx.container.interceptOutboundHttp("api.example.com", worker); + +// Match a hostname glob pattern +this.ctx.container.interceptOutboundHttp("*.example.com", worker); + +// Match an IP:port +await this.ctx.container.interceptOutboundHttp("15.0.0.1:80", worker); + +// Match a CIDR range (IPv4 and IPv6) +await this.ctx.container.interceptOutboundHttp("123.123.123.123/23", worker); +``` + +#### Parameters + +- `target` (string): A hostname, hostname glob (for example, `*.example.com`), IP address, IP:port, or CIDR range to match. +- `worker` (WorkerEntrypoint): A `WorkerEntrypoint` instance to handle matching requests. + +#### Return values + +- None. + +### `interceptAllOutboundHttp` + +`interceptAllOutboundHttp` routes all outbound HTTP requests from the container through a `WorkerEntrypoint`, regardless of destination. + +```js +await this.ctx.container.interceptAllOutboundHttp(worker); +``` + +#### Parameters + +- `worker` (WorkerEntrypoint): A `WorkerEntrypoint` instance to handle all outbound HTTP requests. + +#### Return values + +- A promise that resolves once the intercept rule is installed. + +### `interceptOutboundHttps` + +`interceptOutboundHttps` routes outbound HTTPS requests matching a hostname or hostname glob through a `WorkerEntrypoint`. Works the same way as `interceptOutboundHttp` but for HTTPS traffic. The container must trust the CA certificate at `/etc/cloudflare/certs/cloudflare-containers-ca.crt` for HTTPS interception to work. + +Supports glob patterns where `*` matches any sequence of characters. + +```js +const worker = this.ctx.exports.MyWorker({ props: {} }); + +// Match a specific hostname +this.ctx.container.interceptOutboundHttps("api.example.com", worker); + +// Match a hostname glob pattern +this.ctx.container.interceptOutboundHttps("*.example.com", worker); + +// Intercept all HTTPS traffic +this.ctx.container.interceptOutboundHttps("*", worker); +``` + +#### Parameters + +- `target` (string): A hostname or hostname glob pattern to match. Use `*` to intercept all HTTPS traffic. +- `worker` (WorkerEntrypoint): A `WorkerEntrypoint` instance to handle matching requests. + +#### Return values + +- None. + +## Related resources + +- [Containers APIs](/containers/api/) — compare direct runtime control with the `Container` class +- [Container class reference](/containers/api/container-class/) — use higher-level lifecycle helpers +- [Containers overview](/containers/) +- [Get started with Containers](/containers/get-started/) +- [SQLite storage API](/durable-objects/api/sqlite-storage-api/) — persist state across container restarts +- [Durable Objects](/durable-objects/) — the underlying platform that powers Containers diff --git a/src/content/docs/containers/api/index.mdx b/src/content/docs/containers/api/index.mdx new file mode 100644 index 00000000000..2c3addd38f1 --- /dev/null +++ b/src/content/docs/containers/api/index.mdx @@ -0,0 +1,108 @@ +--- +pcx_content_type: navigation +title: API +description: Choose between the Durable Object Container API and the higher-level Container class. +sidebar: + order: 6 +products: + - containers + - durable-objects +--- + +import { CardGrid, LinkTitleCard } from "~/components"; + +Containers provide two APIs for managing a container from a Durable Object. Both APIs address the same container runtime. + +For new applications, use the Durable Object Container API when you need direct lifecycle control. Use the `Container` class when you prefer built-in lifecycle helpers. + + + + + Start, stop, monitor, and connect to a container through `ctx.container`. + + + + Use a higher-level class built on Durable Objects, with routing, readiness + checks, lifecycle hooks, and scheduling. + + + + +## Choose an API + +### Durable Object Container API + +The Durable Object Container API exposes the container runtime through `ctx.container`. Choose it when you need direct control over startup, shutdown, networking, or resource usage. You can add readiness checks, custom request routing, or lifecycle policies when your application needs them. + +### Container class + +The `Container` class builds on Durable Objects and the runtime API. Choose it when you prefer built-in request proxying, readiness checks, lifecycle hooks, and scheduling. These helpers reduce application code, but some features use Durable Object storage and alarms. + +The following table compares both options: + +Methods labeled **Durable Object API** are available through the surrounding Durable Object, not through `ctx.container`. + +| Requirement | Durable Object Container API | `Container` class | +| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Start and stop a container | [`start()`](/containers/api/durable-object-container/#start), [`signal()`](/containers/api/durable-object-container/#signal), and [`destroy()`](/containers/api/durable-object-container/#destroy) | [`start()`](/containers/api/container-class/#start), [`stop()`](/containers/api/container-class/#stop), and [`destroy()`](/containers/api/container-class/#destroy) | +| Send and proxy traffic | [`getTcpPort(port).fetch()`](/containers/api/durable-object-container/#gettcpport) and [`getTcpPort(port).connect()`](/containers/api/durable-object-container/#gettcpport) | [`fetch()`](/containers/api/container-class/#fetch) and [`containerFetch()`](/containers/api/container-class/#containerfetch) | +| Execute another process | [`exec()`](/containers/api/durable-object-container/#exec) | [`ctx.container.exec()`](/containers/api/container-class/#execute-commands) | +| Check port readiness | Use [`getTcpPort()`](/containers/api/durable-object-container/#gettcpport) in application code | [`startAndWaitForPorts()`](/containers/api/container-class/#startandwaitforports) and [`waitForPort()`](/containers/api/container-class/#waitforport) | +| Handle concurrent starts | Coordinate calls to [`start()`](/containers/api/durable-object-container/#start) when needed | Handled by [`start()`](/containers/api/container-class/#start) and [`startAndWaitForPorts()`](/containers/api/container-class/#startandwaitforports) | +| Run lifecycle hooks | [`monitor()`](/containers/api/durable-object-container/#monitor) and application code | [`onStart()`](/containers/api/container-class/#onstart), [`onStop()`](/containers/api/container-class/#onstop), [`onError()`](/containers/api/container-class/#onerror), and [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) | +| Stop inactive containers | [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout) | [`sleepAfter`](/containers/api/container-class/#sleepafter) and [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) | +| Schedule callbacks | **Durable Object API:** [`ctx.storage.setAlarm()`](/durable-objects/api/alarms/#setalarm) | [`schedule()`](/containers/api/container-class/#schedule) | + +Use the Durable Object Container API for latency-sensitive workloads or workloads that need a smaller storage footprint. + +## Use the Durable Object Container API + +The Durable Object Container API is available through `ctx.container` of the Durable Object. It exposes the container runtime without adding lifecycle policy. + +```ts +import { DurableObject } from "cloudflare:workers"; + +export class MyContainer extends DurableObject { + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + ctx.blockConcurrencyWhile(() => + ctx.container.setInactivityTimeout(10 * 60 * 1000), + ); + } + + async fetch(request: Request): Promise { + if (!this.ctx.container.running) { + this.ctx.container.start({ enableInternet: true }); + } + + return this.ctx.container.getTcpPort(8080).fetch(request); + } +} +``` + +The `running` property does not indicate port readiness. Check the required port before routing the first request if your process needs time to start. + +For all methods, refer to the [Durable Object Container API](/containers/api/durable-object-container/). + +## Use the Container class + +The [`Container` class](https://github.com/cloudflare/containers) extends `DurableObject`. It adds default routing, readiness checks, lifecycle hooks, activity tracking, and scheduled callbacks. + +```ts +import { Container } from "@cloudflare/containers"; + +export class MyContainer extends Container { + defaultPort = 8080; + sleepAfter = "10m"; +} +``` + +These helpers reduce application code. They also add lifecycle state and scheduled work to the Durable Object. For all properties and methods, refer to the [Container class API](/containers/api/container-class/). diff --git a/src/content/docs/containers/concepts/architecture.mdx b/src/content/docs/containers/concepts/architecture.mdx index 4b3c7b4da8a..631e67ff529 100644 --- a/src/content/docs/containers/concepts/architecture.mdx +++ b/src/content/docs/containers/concepts/architecture.mdx @@ -17,7 +17,38 @@ times when scaling up the number of concurrent container instances. Worker code goes live on deploy. Container instances update with a [rollout](/containers/configuration/rollouts/). Refer to [Deploy Containers](/containers/guides/deploy/). -## Lifecycle of a Request +## Container instance lifecycle + +```mermaid +flowchart LR + accTitle: Container instance lifecycle + accDescr: A Worker accesses a container through its Durable Object. The container moves from stopped to starting, running but not ready, ready for traffic, stopping, and stopped. + + Worker["Worker"] -->|Durable Object binding| DurableObject["Durable Object"] + DurableObject -->|ctx.container| Container + + subgraph Container["Container instance"] + direction TD + Stopped["Stopped"] + Starting["Starting
start() called"] + Running["Running
Not ready"] + Ready["Ready
Accepting traffic"] + Stopping["Stopping
Stop requested"] + StoppedAgain["Stopped"] + + Stopped --> Starting --> Running --> Ready --> Stopping --> StoppedAgain + end +``` + +A Container can only be accessed through its Durable Object. A Worker sends a request to the Durable Object, which accesses the Container through `ctx.container`. + +An inactivity timeout, `signal()`, `destroy()`, a rollout, or a process exit can stop the instance. If startup fails or the process exits early, the instance returns to the stopped state. + +The `ctx.container.running` property becomes `true` before the process is ready to accept traffic. Check port readiness before you send the first request. + +You can manage this lifecycle through the [Durable Object Container API](/containers/api/durable-object-container/) or the higher-level [`Container` class](/containers/api/container-class/). To compare both options, refer to [Containers APIs](/containers/api/). + +## Lifecycle of a request ### Client to Worker @@ -33,11 +64,9 @@ or UDP from an end-user, please [let us know](https://forms.gle/AGSq54VvUje6kmKu ### Worker to Durable Object -From the Worker, a request passes through a Durable Object instance (the [Container class](/containers/reference/container-class/) extends a Durable Object class). +From the Worker, a request passes through a Durable Object instance. You can extend `DurableObject` and use `ctx.container` directly, or extend the [`Container` class](/containers/api/container-class/). Each Durable Object instance is a globally routable isolate that can execute code and store state. This allows -developers to easily address and route to specific container instances (no matter where they are placed), -define and run hooks on container status changes, execute recurring checks on the instance, and store persistent -state associated with each instance. +developers to address and route to specific container instances, run code when a container exits, and store persistent state associated with each instance. ### Starting a Container @@ -91,9 +120,9 @@ should be built for the `linux/amd64` architecture, and should stay within ### Container shutdown -The Container class sets [`sleepAfter`](/containers/reference/container-class/#sleepafter) to 10 minutes by default. Its default [`onActivityExpired()`](/containers/reference/container-class/#onactivityexpired) implementation signals the container to stop after that period without activity. You can change the duration or override the hook. +With the Durable Object Container API, call [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout) to let the runtime stop an inactive container. You can also stop a container with [`signal()`](/containers/api/durable-object-container/#signal) or [`destroy()`](/containers/api/durable-object-container/#destroy). -You can stop a container instance yourself with [`stop()`](/containers/reference/container-class/#stop) or [`destroy()`](/containers/reference/container-class/#destroy). +The `Container` class sets [`sleepAfter`](/containers/api/container-class/#sleepafter) to 10 minutes by default. Its [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) implementation calls [`stop()`](/containers/api/container-class/#stop). You can change the duration or override the hook. When the platform is about to stop a container instance, it: @@ -103,14 +132,16 @@ When the platform is about to stop a container instance, it: Handle `SIGTERM` in your image if you need cleanup before exit. The same sequence runs when a [rollout](/containers/configuration/rollouts/) replaces a container instance with a new image. -### Lifecycle hooks +### Lifecycle events + +The Durable Object Container API provides [`monitor()`](/containers/api/durable-object-container/#monitor). Its promise resolves when the container exits and rejects when the container errors. -The [`Container` class](/containers/reference/container-class/) provides hooks that run Worker code when the container changes state: +The [`Container` class](/containers/api/container-class/) adds hooks that run Worker code when the container changes state: -- [`onStart()`](/containers/reference/container-class/#onstart) — Runs after the container has started. -- [`onStop()`](/containers/reference/container-class/#onstop) — Runs after the container process exits. Receives the exit code and reason for the stop. -- [`onActivityExpired()`](/containers/reference/container-class/#onactivityexpired) — Runs when the [`sleepAfter`](/containers/reference/container-class/#sleepafter) timer expires with no incoming requests. The default implementation calls `stop()` to shut down the container. You can use this to only stop the container on certain conditions. -- [`onError()`](/containers/reference/container-class/#onerror) — Runs when the container exits with an error. +- [`onStart()`](/containers/api/container-class/#onstart) — Runs after the container has started. +- [`onStop()`](/containers/api/container-class/#onstop) — Runs after the container process exits. Receives the exit code and reason for the stop. +- [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) — Runs when the [`sleepAfter`](/containers/api/container-class/#sleepafter) timer expires with no incoming requests. The default implementation calls `stop()` to shut down the container. You can use this to only stop the container on certain conditions. +- [`onError()`](/containers/api/container-class/#onerror) — Runs when the container exits with an error. Refer to the [status hooks example](/containers/examples/status-hooks/) for a full implementation. diff --git a/src/content/docs/containers/configuration/environment-variables.mdx b/src/content/docs/containers/configuration/environment-variables.mdx index 62612167fa4..ac71f31fa1c 100644 --- a/src/content/docs/containers/configuration/environment-variables.mdx +++ b/src/content/docs/containers/configuration/environment-variables.mdx @@ -3,7 +3,7 @@ pcx_content_type: reference title: Environment Variables description: Runtime and user-defined environment variables available inside Container instances. sidebar: - order: 2 + order: 3 products: - containers --- diff --git a/src/content/docs/containers/configuration/index.mdx b/src/content/docs/containers/configuration/index.mdx index a49102d100a..787c7a1766a 100644 --- a/src/content/docs/containers/configuration/index.mdx +++ b/src/content/docs/containers/configuration/index.mdx @@ -1,7 +1,7 @@ --- pcx_content_type: navigation title: Configuration -description: Configure Containers — connect them to Workers and bindings, set environment variables, tune scaling and routing, and manage rollouts. +description: Configure Containers in Wrangler, connect them to Workers and bindings, set environment variables, tune scaling and routing, and manage rollouts. sidebar: order: 5 group: diff --git a/src/content/docs/containers/configuration/outbound-traffic.mdx b/src/content/docs/containers/configuration/outbound-traffic.mdx index fbe1ee0e430..c102fa967bc 100644 --- a/src/content/docs/containers/configuration/outbound-traffic.mdx +++ b/src/content/docs/containers/configuration/outbound-traffic.mdx @@ -414,4 +414,4 @@ The `Container` class calls these methods automatically when you use the functio - [Connect to Workers bindings](/containers/configuration/workers-connections/) — Access KV, R2, Durable Objects, and other bindings from a container - [Control outbound traffic (Sandboxes)](/sandbox/guides/outbound-traffic/) — Sandbox SDK API for outbound handlers - [Environment variables and secrets](/containers/configuration/environment-variables/) — Configure secrets and environment variables -- [Durable Object interface](/durable-objects/api/container/) — Full `ctx.container` API reference +- [Durable Object Container API](/containers/api/durable-object-container/) — Full `ctx.container` API reference diff --git a/src/content/docs/containers/configuration/rollouts.mdx b/src/content/docs/containers/configuration/rollouts.mdx index d658eb17a03..cb9b73c551c 100644 --- a/src/content/docs/containers/configuration/rollouts.mdx +++ b/src/content/docs/containers/configuration/rollouts.mdx @@ -3,7 +3,7 @@ pcx_content_type: reference title: Rollouts description: How container instances update after a deploy, including step percentages, grace periods, and rollout modes. sidebar: - order: 4 + order: 5 products: - containers --- @@ -53,7 +53,7 @@ When the rollout selects a container instance to update: 2. **Signal stop.** The platform sends `SIGTERM` to the main process in the container so it can stop accepting new work and finish in-flight work. Handle `SIGTERM` in your image if that process needs cleanup before exit. 3. **Drain.** The process has up to 15 minutes to exit after `SIGTERM`. 4. **Force stop if needed.** If the process is still running after 15 minutes, the platform sends `SIGKILL`. -5. **After exit.** The Container class [`onStop`](/containers/reference/container-class/#onstop) hook can run in the Worker once the container process has exited. +5. **After exit.** The Container class [`onStop`](/containers/api/container-class/#onstop) hook can run in the Worker once the container process has exited. 6. **Start a new container instance** with the target image. Disk is [ephemeral](/containers/faq/#is-disk-persistent-what-happens-to-my-disk-when-my-container-sleeps) unless you store data outside the container filesystem. Each selected container instance follows this sequence on its own schedule. The fleet does not restart in a single moment. @@ -160,4 +160,4 @@ Use none when the deploy should not publish a new image or start a container ins - [Deploy Containers](/containers/guides/deploy/) - [Lifecycle of a Container](/containers/concepts/architecture/) - [Image management](/containers/guides/image-management/) -- [Containers configuration](/workers/wrangler/configuration/#containers) +- [Wrangler configuration](/containers/configuration/wrangler/) diff --git a/src/content/docs/containers/configuration/scaling-and-routing.mdx b/src/content/docs/containers/configuration/scaling-and-routing.mdx index 7306d9c20c9..c5f85f46f7c 100644 --- a/src/content/docs/containers/configuration/scaling-and-routing.mdx +++ b/src/content/docs/containers/configuration/scaling-and-routing.mdx @@ -3,7 +3,7 @@ pcx_content_type: reference title: Scaling and Routing description: Scale Container instances using explicit IDs or the getRandom helper for stateless load balancing. sidebar: - order: 3 + order: 4 products: - containers --- @@ -11,7 +11,7 @@ products: ## Scale container instances with explicit IDs :::note -This section uses helpers from the [Container class](/containers/reference/container-class/). +This section uses helpers from the [Container class](/containers/api/container-class/). ::: Today, Containers are scaled manually by getting containers with a unique ID, then diff --git a/src/content/docs/containers/configuration/workers-connections.mdx b/src/content/docs/containers/configuration/workers-connections.mdx index 8cf45a36f38..1aa35122871 100644 --- a/src/content/docs/containers/configuration/workers-connections.mdx +++ b/src/content/docs/containers/configuration/workers-connections.mdx @@ -2,7 +2,7 @@ title: Connect to Workers and Bindings pcx_content_type: concept sidebar: - order: 1 + order: 2 description: Access KV, R2, Durable Objects, and other bindings from a container. products: - containers @@ -59,4 +59,4 @@ The `ctx` argument exposes `containerId`, which lets you interact with the conta - [Handle outbound traffic](/containers/configuration/outbound-traffic/) — Block, allow, and intercept all outbound HTTP from a container - [Environment variables and secrets](/containers/configuration/environment-variables/) — Configure secrets and environment variables -- [Durable Object interface](/durable-objects/api/container/) — Full `ctx.container` API reference +- [Durable Object Container API](/containers/api/durable-object-container/) — Full `ctx.container` API reference diff --git a/src/content/docs/containers/configuration/wrangler.mdx b/src/content/docs/containers/configuration/wrangler.mdx new file mode 100644 index 00000000000..3b5597754c3 --- /dev/null +++ b/src/content/docs/containers/configuration/wrangler.mdx @@ -0,0 +1,73 @@ +--- +title: Wrangler configuration +description: Configure a Container, its Durable Object binding, and its migration in Wrangler. +pcx_content_type: configuration +sidebar: + order: 0 +products: + - containers + - durable-objects +--- + +import { WranglerConfig } from "~/components"; + +Define Containers in the Wrangler configuration file for your Worker. Each Container is associated with a Durable Object class, which provides access to the Container at runtime. + +## Minimal configuration + +A Container application requires a Container definition, a Durable Object binding, and a Durable Object migration: + + + +```jsonc +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "my-container-worker", + "main": "src/index.ts", + "compatibility_date": "$today", + "containers": [ + { + "class_name": "MyContainer", + "image": "./Dockerfile", + "max_instances": 10, + }, + ], + "durable_objects": { + "bindings": [ + { + "name": "MY_CONTAINER", + "class_name": "MyContainer", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["MyContainer"], + }, + ], +} +``` + + + +The configuration uses three sections: + +1. **`containers`** defines the container image and associates it with a Durable Object class through `class_name`. +2. **`durable_objects.bindings`** makes the Durable Object namespace available to Worker code. In this example, access it through `env.MY_CONTAINER`. +3. **`migrations`** creates the SQLite-backed Durable Object class. Use `new_sqlite_classes`, not `new_classes`, for a Container. + +The `class_name` in all three sections must match the exported Durable Object class in your Worker. + +## Container settings + +The `containers` entry can also configure the instance type, maximum number of running instances, image build, placement constraints, rollouts, and SSH access. + +For all available fields and values, refer to the [Containers Wrangler configuration reference](/workers/wrangler/configuration/#containers). + +## Next steps + +- [Deploy Containers](/containers/guides/deploy/) — Build the image and deploy the Worker. +- [Scaling and Routing](/containers/configuration/scaling-and-routing/) — Route requests and scale Container instances. +- [Rollouts](/containers/configuration/rollouts/) — Control how configuration changes reach running instances. +- [Image management](/containers/guides/image-management/) — Use local and remote container images. diff --git a/src/content/docs/containers/examples/container-backend.mdx b/src/content/docs/containers/examples/container-backend.mdx index fc458f7461a..e44a4b4d9c4 100644 --- a/src/content/docs/containers/examples/container-backend.mdx +++ b/src/content/docs/containers/examples/container-backend.mdx @@ -10,7 +10,7 @@ products: - containers --- -import { WranglerConfig, Details } from "~/components"; +import { WranglerConfig, Details, TabItem, Tabs, TypeScriptExample } from "~/components"; A common pattern is to serve a static frontend application (e.g., React, Vue, Svelte) using Static Assets, then pass backend requests to a containerized backend application. @@ -138,6 +138,68 @@ Your Worker needs to be able to both serve static assets and route requests to t In this case, we will pass requests to one of three container instances if the route starts with `/api`, and all other requests will be served as static assets. + + + + +```ts +import { DurableObject } from "cloudflare:workers"; + +const INSTANCE_COUNT = 3; + +interface Env { + ASSETS: Fetcher; + BACKEND: DurableObjectNamespace; +} + +export class Backend extends DurableObject { + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + + ctx.blockConcurrencyWhile(async () => { + const container = ctx.container!; + await container.setInactivityTimeout(2 * 60 * 60 * 1000); + + if (!container.running) { + container.start(); + } + + const port = container.getTcpPort(8080); + let lastError: unknown; + for (let attempt = 0; attempt < 50; attempt++) { + try { + await port.fetch("http://container/"); + return; + } catch (error) { + lastError = error; + await scheduler.wait(100); + } + } + throw lastError; + }); + } + + fetch(request: Request): Promise { + return this.ctx.container!.getTcpPort(8080).fetch(request); + } +} + +export default { + async fetch(request: Request, env: Env): Promise { + if (new URL(request.url).pathname.startsWith("/api")) { + const index = Math.floor(Math.random() * INSTANCE_COUNT); + return env.BACKEND.getByName(`instance-${index}`).fetch(request); + } + + return env.ASSETS.fetch(request); + }, +}; +``` + + + + + ```javascript import { Container, getRandom } from "@cloudflare/containers"; @@ -161,9 +223,11 @@ export default { }; ``` + + + :::note -This example uses `getRandom`, which randomly selects one of a fixed number of Container -instances for each request. +Both examples randomly select one of a fixed number of Container instances for each request. The `Container` class provides `getRandom()` as a helper. In the future, we will provide improved latency-aware load balancing and autoscaling. diff --git a/src/content/docs/containers/examples/cron.mdx b/src/content/docs/containers/examples/cron.mdx index beb5c71d036..9f06cadc5e2 100644 --- a/src/content/docs/containers/examples/cron.mdx +++ b/src/content/docs/containers/examples/cron.mdx @@ -10,7 +10,7 @@ products: - containers --- -import { WranglerConfig } from "~/components"; +import { TabItem, Tabs, TypeScriptExample, WranglerConfig } from "~/components"; To launch a container on a schedule, you can use a Workers [Cron Trigger](/workers/configuration/cron-triggers/). @@ -54,10 +54,81 @@ Use a cron expression in your Wrangler config to specify the schedule: -Then in your Worker, call your Container from the "scheduled" handler: +Then call the Container from the `scheduled()` handler in the Worker. The raw API example expects the Container to expose a `POST /run` endpoint that starts the scheduled task. + + + + ```ts -import { Container, getContainer } from '@cloudflare/containers'; +import { DurableObject } from "cloudflare:workers"; + +interface Env { + CRON_CONTAINER: DurableObjectNamespace; +} + +export class CronContainer extends DurableObject { + private currentRun: Promise | undefined; + + run(startTime: string): Promise { + this.currentRun ??= this.runOnce(startTime).finally(() => { + this.currentRun = undefined; + }); + return this.currentRun; + } + + private async runOnce(startTime: string): Promise { + const container = this.ctx.container!; + await container.setInactivityTimeout(10_000); + + if (!container.running) { + container.start(); + } + + const port = container.getTcpPort(8080); + let lastError: unknown; + for (let attempt = 0; attempt < 50; attempt++) { + try { + await port.fetch("http://container/"); + lastError = undefined; + break; + } catch (error) { + lastError = error; + await scheduler.wait(100); + } + } + if (lastError) { + throw lastError; + } + + const response = await port.fetch("http://container/run", { + method: "POST", + body: JSON.stringify({ startTime }), + headers: { "content-type": "application/json" }, + }); + if (!response.ok) { + throw new Error(`Container returned ${response.status}`); + } + } +} + +export default { + async fetch(): Promise { + return new Response("This Worker runs a scheduled Container task."); + }, + + async scheduled(_controller: ScheduledController, env: Env): Promise { + await env.CRON_CONTAINER.getByName("cron").run(new Date().toISOString()); + }, +}; +``` + + + + + +```ts +import { Container, getContainer } from "@cloudflare/containers"; export class CronContainer extends Container { sleepAfter = '10s'; @@ -72,17 +143,20 @@ export class CronContainer extends Container { } export default { - async fetch(): Promise { - return new Response("This Worker runs a cron job to execute a container on a schedule."); - }, - - async scheduled(_controller: any, env: { CRON_CONTAINER: DurableObjectNamespace }) { - let container = getContainer(env.CRON_CONTAINER); - await container.start({ - envVars: { + async fetch(): Promise { + return new Response("This Worker runs a cron job to execute a container on a schedule."); + }, + + async scheduled(_controller: ScheduledController, env: { CRON_CONTAINER: DurableObjectNamespace }) { + const container = getContainer(env.CRON_CONTAINER); + await container.start({ + envVars: { MESSAGE: "Start Time: " + new Date().toISOString(), - } - }) - }, + }, + }); + }, }; ``` + + + diff --git a/src/content/docs/containers/examples/durable-object-interface.mdx b/src/content/docs/containers/examples/durable-object-interface.mdx deleted file mode 100644 index f957451fa12..00000000000 --- a/src/content/docs/containers/examples/durable-object-interface.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- - -summary: Various examples calling Containers directly from Durable Objects -pcx_content_type: example -title: Using Durable Objects Directly -external_link: https://github.com/cloudflare/containers-demos -sidebar: - order: 10 -description: Various examples calling Containers directly from Durable Objects -reviewed: 2025-06-24 -products: - - containers ---- diff --git a/src/content/docs/containers/examples/env-vars-and-secrets.mdx b/src/content/docs/containers/examples/env-vars-and-secrets.mdx index 00d3117d36b..0fc5aa497ae 100644 --- a/src/content/docs/containers/examples/env-vars-and-secrets.mdx +++ b/src/content/docs/containers/examples/env-vars-and-secrets.mdx @@ -10,10 +10,9 @@ products: - containers --- -import { WranglerConfig, PackageManagers } from "~/components"; +import { PackageManagers, TabItem, Tabs, TypeScriptExample, WranglerConfig } from "~/components"; -Environment variables can be passed into a Container using the `envVars` field -in the [`Container`](/containers/reference/container-class/) class, or by setting manually when the Container starts. +Environment variables can be passed when the Durable Object Container API starts a Container, or through the `envVars` field on the [`Container`](/containers/api/container-class/) class. Secrets can be passed into a Container by using [Worker Secrets](/workers/configuration/secrets/) or the [Secret Store](/secrets-store/integrations/workers/), then passing them into the Container @@ -82,7 +81,8 @@ in Wrangler configuration. { "name": "my-container-worker", "vars": { - "ENV_VAR": "my-env-var" + "ENV_VAR": "my-env-var", + "CONTAINER_IMAGE": "registry.cloudflare.com//my-container:latest" }, "secrets_store_secrets": [ { @@ -109,13 +109,52 @@ added to `env`. Also note that we did not configure anything specific for environment variables, secrets, or KV values in the _container-related_ portion of the Wrangler configuration file. -## Using `envVars` on the Container class +## Set environment variables for every instance -Now, let's pass the env vars and secrets to our container using the `envVars` field in the `Container` class: +Pass synchronous Worker variables and secrets when the Container starts. When using the raw API with startup options, provide a deployed image reference. + + + + + +```ts +import { DurableObject } from "cloudflare:workers"; + +interface Env { + CONTAINER_IMAGE: string; + ENV_VAR: string; + WORKER_SECRET: string; +} + +export class MyContainer extends DurableObject { + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + + ctx.blockConcurrencyWhile(async () => { + if (!ctx.container!.running) { + ctx.container!.start({ + image: env.CONTAINER_IMAGE, + enableInternet: true, + env: { + ENV_VAR: env.ENV_VAR, + WORKER_SECRET: env.WORKER_SECRET, + }, + }); + } + }); + } +} +``` + + + + ```js // https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global import { env } from "cloudflare:workers"; +import { Container } from "@cloudflare/containers"; + export class MyContainer extends Container { defaultPort = 8080; sleepAfter = "10s"; @@ -127,6 +166,9 @@ export class MyContainer extends Container { } ``` + + + Every instance of this `Container` will now have these variables and secrets set as environment variables when it launches. @@ -134,7 +176,77 @@ set as environment variables when it launches. But what if you want to set environment variables on a per-instance basis? -In this case, use the `startAndWaitForPorts()` method to pass in environment variables for each instance. +Pass the values when starting each instance. The raw API example defines a `launch()` RPC method on the Durable Object. The class version uses `startAndWaitForPorts()`. + + + + + +```ts +import { DurableObject } from "cloudflare:workers"; + +interface Env { + CONTAINER_IMAGE: string; + DEMO_KV: KVNamespace; + ENV_VAR: string; + MY_CONTAINER: DurableObjectNamespace; + SECRET_STORE: SecretsStoreSecret; + WORKER_SECRET: string; +} + +export class MyContainer extends DurableObject { + launch(image: string, env: Record): void { + if (this.ctx.container!.running) { + throw new Error("Container is already running"); + } + this.ctx.container!.start({ image, enableInternet: true, env }); + } +} + +function required(value: string | null, name: string): string { + if (value === null) { + throw new Error(`${name} was not found`); + } + return value; +} + +export default { + async fetch(request: Request, env: Env): Promise { + if (new URL(request.url).pathname !== "/launch-instances") { + return new Response("Not found", { status: 404 }); + } + + const secretStoreSecret = await env.SECRET_STORE.get(); + const kvValue = required(await env.DEMO_KV.get("KV_VALUE"), "KV_VALUE"); + const instanceConfig = required( + await env.DEMO_KV.get("instance-bar-config"), + "instance-bar-config", + ); + + await Promise.all([ + env.MY_CONTAINER.getByName("foo").launch(env.CONTAINER_IMAGE, { + ENV_VAR: `${env.ENV_VAR}foo`, + WORKER_SECRET: env.WORKER_SECRET, + SECRET_STORE_SECRET: secretStoreSecret, + KV_VALUE: kvValue, + }), + env.MY_CONTAINER.getByName("bar").launch(env.CONTAINER_IMAGE, { + ENV_VAR: `${env.ENV_VAR}bar`, + WORKER_SECRET: env.WORKER_SECRET, + SECRET_STORE_SECRET: secretStoreSecret, + KV_VALUE: kvValue, + INSTANCE_CONFIG: instanceConfig, + }), + ]); + + return new Response("Container instances launched"); + }, +}; +``` + + + + ```js export class MyContainer extends Container { @@ -181,6 +293,9 @@ export default { }; ``` + + + ## Reading KV values in containers KV values are particularly useful for configuration data that changes infrequently but needs to be accessible to your containers. Since KV operations are asynchronous, you must read the values at runtime when starting containers. @@ -189,6 +304,45 @@ Here are common patterns for using KV with containers: ### Configuration data + + + + +```ts +export default { + async fetch(request: Request, env: Env): Promise { + if (new URL(request.url).pathname !== "/configure-container") { + return new Response("Not found", { status: 404 }); + } + + const config = await env.DEMO_KV.get("container-config", "json"); + const apiEndpoint = required( + await env.DEMO_KV.get("api-endpoint"), + "api-endpoint", + ); + const deploymentEnv = required( + await env.DEMO_KV.get("deployment-env"), + "deployment-env", + ); + + await env.MY_CONTAINER.getByName("configured").launch( + env.CONTAINER_IMAGE, + { + CONFIG_JSON: JSON.stringify(config), + API_ENDPOINT: apiEndpoint, + DEPLOYMENT_ENV: deploymentEnv, + }, + ); + + return new Response("Container configured and launched"); + }, +}; +``` + + + + + ```js export default { async fetch(request, env) { @@ -215,8 +369,54 @@ export default { }; ``` + + + ### Feature flags + + + + +```ts +export default { + async fetch(request: Request, env: Env): Promise { + if (new URL(request.url).pathname !== "/launch-with-features") { + return new Response("Not found", { status: 404 }); + } + + const featureFlags = { + ENABLE_FEATURE_A: required( + await env.DEMO_KV.get("feature-a-enabled"), + "feature-a-enabled", + ), + ENABLE_FEATURE_B: required( + await env.DEMO_KV.get("feature-b-enabled"), + "feature-b-enabled", + ), + DEBUG_MODE: required( + await env.DEMO_KV.get("debug-enabled"), + "debug-enabled", + ), + }; + + await env.MY_CONTAINER.getByName("features").launch( + env.CONTAINER_IMAGE, + { + ...featureFlags, + CONTAINER_VERSION: "1.2.3", + }, + ); + + return new Response("Container launched with feature flags"); + }, +}; +``` + + + + + ```js export default { async fetch(request, env) { @@ -245,6 +445,9 @@ export default { }; ``` + + + ## Build-time environment variables Finally, you can also set build-time environment variables that are only available when building the container image via the `image_vars` field in the Wrangler configuration. diff --git a/src/content/docs/containers/examples/index.mdx b/src/content/docs/containers/examples/index.mdx index 532425ee67a..337286b2dd3 100644 --- a/src/content/docs/containers/examples/index.mdx +++ b/src/content/docs/containers/examples/index.mdx @@ -3,7 +3,7 @@ pcx_content_type: navigation title: Examples description: Code examples showing how to use Containers with Workers for stateless routing, cron jobs, WebSockets, and more. sidebar: - order: 6 + order: 7 group: hideIndex: true products: diff --git a/src/content/docs/containers/examples/r2-fuse-mount.mdx b/src/content/docs/containers/examples/r2-fuse-mount.mdx index 90901640222..c179f5f39d8 100644 --- a/src/content/docs/containers/examples/r2-fuse-mount.mdx +++ b/src/content/docs/containers/examples/r2-fuse-mount.mdx @@ -11,7 +11,7 @@ products: - r2 --- -import { Details, TypeScriptExample } from "~/components"; +import { Details, TabItem, Tabs, TypeScriptExample } from "~/components"; FUSE (Filesystem in Userspace) allows you to mount [R2 buckets](/r2/) as filesystems within Containers. Applications can then interact with R2 using standard filesystem operations rather than object storage APIs. @@ -81,33 +81,95 @@ The startup script creates a mount point, starts tigrisfs in the background to m ### Passing credentials to the container -Your Container needs [R2 credentials](/r2/api/tokens/) and configuration passed as environment variables. Store credentials as [Worker secrets](/workers/configuration/secrets/), then pass them through the `envVars` property: +Your Container needs [R2 credentials](/r2/api/tokens/) and configuration passed as environment variables. Store credentials as [Worker secrets](/workers/configuration/secrets/), then pass them when the Container starts. + + + + + +```ts +import { DurableObject } from "cloudflare:workers"; + +interface Env { + FUSE_DEMO: DurableObjectNamespace; + FUSE_IMAGE: string; + AWS_ACCESS_KEY_ID: string; + AWS_SECRET_ACCESS_KEY: string; + R2_BUCKET_NAME: string; + R2_ACCOUNT_ID: string; +} + +export class FUSEDemo extends DurableObject { + private currentRun: Promise | undefined; + + run(): Promise { + this.currentRun ??= this.runOnce().finally(() => { + this.currentRun = undefined; + }); + return this.currentRun; + } + + private async runOnce(): Promise { + const container = this.ctx.container!; + if (container.running) { + throw new Error("Container is already running"); + } + + container.start({ + image: this.env.FUSE_IMAGE, + enableInternet: true, + env: { + AWS_ACCESS_KEY_ID: this.env.AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY: this.env.AWS_SECRET_ACCESS_KEY, + R2_BUCKET_NAME: this.env.R2_BUCKET_NAME, + R2_ACCOUNT_ID: this.env.R2_ACCOUNT_ID, + }, + }); + + await container.monitor(); + } +} + +export default { + async fetch(_request: Request, env: Env): Promise { + await env.FUSE_DEMO.getByName("default").run(); + return new Response("FUSE task completed"); + }, +}; +``` + + + + ```ts -import { Container, getContainer } from "@cloudflare/containers"; +import { Container } from "@cloudflare/containers"; interface Env { - FUSEDemo: DurableObjectNamespace; - AWS_ACCESS_KEY_ID: string; - AWS_SECRET_ACCESS_KEY: string; - R2_BUCKET_NAME: string; - R2_ACCOUNT_ID: string; + FUSE_DEMO: DurableObjectNamespace; + AWS_ACCESS_KEY_ID: string; + AWS_SECRET_ACCESS_KEY: string; + R2_BUCKET_NAME: string; + R2_ACCOUNT_ID: string; } export class FUSEDemo extends Container { - defaultPort = 8080; - sleepAfter = "10m"; - envVars = { - AWS_ACCESS_KEY_ID: this.env.AWS_ACCESS_KEY_ID, - AWS_SECRET_ACCESS_KEY: this.env.AWS_SECRET_ACCESS_KEY, - R2_BUCKET_NAME: this.env.R2_BUCKET_NAME, - R2_ACCOUNT_ID: this.env.R2_ACCOUNT_ID, - }; + defaultPort = 8080; + sleepAfter = "10m"; + envVars = { + AWS_ACCESS_KEY_ID: this.env.AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY: this.env.AWS_SECRET_ACCESS_KEY, + R2_BUCKET_NAME: this.env.R2_BUCKET_NAME, + R2_ACCOUNT_ID: this.env.R2_ACCOUNT_ID, + }; } ``` + + + The `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` should be stored as secrets, while `R2_BUCKET_NAME` and `R2_ACCOUNT_ID` can be configured as variables in your `wrangler.jsonc`: :::note[Creating your R2 AWS API keys] @@ -116,8 +178,9 @@ To get your `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, [head to your R2 da ```json { "vars": { - "R2_BUCKET_NAME": "my-bucket", - "R2_ACCOUNT_ID": "your-account-id" + "FUSE_IMAGE": "registry.cloudflare.com//r2-fuse:latest", + "R2_BUCKET_NAME": "my-bucket", + "R2_ACCOUNT_ID": "your-account-id" } } ``` diff --git a/src/content/docs/containers/examples/stateless.mdx b/src/content/docs/containers/examples/stateless.mdx index 8cf6275a0df..37c1fc4e6d2 100644 --- a/src/content/docs/containers/examples/stateless.mdx +++ b/src/content/docs/containers/examples/stateless.mdx @@ -10,7 +10,66 @@ products: - containers --- -To simply proxy requests to one of multiple instances of a container, you can use the `getRandom` function: +import { TabItem, Tabs, TypeScriptExample } from "~/components"; + +To proxy requests across a fixed number of Container instances, select an instance name and forward the request through its Durable Object. + + + + + +```ts +import { DurableObject } from "cloudflare:workers"; + +const INSTANCE_COUNT = 3; + +interface Env { + BACKEND: DurableObjectNamespace; +} + +export class Backend extends DurableObject { + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + + ctx.blockConcurrencyWhile(async () => { + const container = ctx.container!; + await container.setInactivityTimeout(2 * 60 * 60 * 1000); + + if (!container.running) { + container.start(); + } + + const port = container.getTcpPort(8080); + let lastError: unknown; + for (let attempt = 0; attempt < 50; attempt++) { + try { + await port.fetch("http://container/"); + return; + } catch (error) { + lastError = error; + await scheduler.wait(100); + } + } + throw lastError; + }); + } + + fetch(request: Request): Promise { + return this.ctx.container!.getTcpPort(8080).fetch(request); + } +} + +export default { + async fetch(request: Request, env: Env): Promise { + const index = Math.floor(Math.random() * INSTANCE_COUNT); + return env.BACKEND.getByName(`instance-${index}`).fetch(request); + }, +}; +``` + + + + ```ts import { Container, getRandom } from "@cloudflare/containers"; @@ -30,9 +89,11 @@ export default { }; ``` + + + :::note -This example uses `getRandom`, which randomly selects one of a fixed number of Container -instances for each request. +Both examples randomly select one of a fixed number of Container instances for each request. The `Container` class provides `getRandom()` as a helper. In the future, we will provide improved latency-aware load balancing and autoscaling. diff --git a/src/content/docs/containers/examples/status-hooks.mdx b/src/content/docs/containers/examples/status-hooks.mdx index 1a0e219a1e5..e8b59689eea 100644 --- a/src/content/docs/containers/examples/status-hooks.mdx +++ b/src/content/docs/containers/examples/status-hooks.mdx @@ -11,8 +11,76 @@ products: - workers --- -When a Container starts, stops, becomes idle, and errors, it can trigger code execution in a Worker -that has defined status hooks on the `Container` class. Refer to the [Container class lifecycle hooks](/containers/reference/container-class/#lifecycle-hooks) for more details. +import { TabItem, Tabs, TypeScriptExample } from "~/components"; + +Use `monitor()` with the Durable Object Container API to run code after the Container exits or errors. The `Container` class adds named lifecycle hooks and an inactivity callback. + + + + + +```ts +import { DurableObject } from "cloudflare:workers"; + +interface Env {} + +export class MyContainer extends DurableObject { + private ready: Promise | undefined; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + ctx.blockConcurrencyWhile(() => + ctx.container!.setInactivityTimeout(5 * 60 * 1000), + ); + } + + async fetch(request: Request): Promise { + const container = this.ctx.container!; + if (!container.running) { + this.ready = undefined; + } + this.ready ??= this.startAndMonitor().catch((error: unknown) => { + this.ready = undefined; + throw error; + }); + await this.ready; + + return container.getTcpPort(4000).fetch(request); + } + + private async startAndMonitor(): Promise { + const container = this.ctx.container!; + if (!container.running) { + container.start(); + } + + this.ctx.waitUntil( + container + .monitor() + .then(() => console.log("Container stopped")) + .catch((error: unknown) => console.error("Container error:", error)), + ); + + const port = container.getTcpPort(4000); + let lastError: unknown; + for (let attempt = 0; attempt < 50; attempt++) { + try { + await port.fetch("http://container/"); + console.log("Container successfully started"); + return; + } catch (error) { + lastError = error; + await scheduler.wait(100); + } + } + throw lastError; + } +} +``` + + + + ```ts import { Container } from "@cloudflare/containers"; @@ -45,3 +113,8 @@ export class MyContainer extends Container { } } ``` + + + + +The `monitor()` promise in the raw API does not include an exit code or stop reason. The `setInactivityTimeout()` method does not invoke a callback when the timeout expires. Use the [`Container` class lifecycle hooks](/containers/api/container-class/#lifecycle-hooks) when you need those higher-level events. diff --git a/src/content/docs/containers/examples/websocket.mdx b/src/content/docs/containers/examples/websocket.mdx index 66a5968d448..b8a92a1955f 100644 --- a/src/content/docs/containers/examples/websocket.mdx +++ b/src/content/docs/containers/examples/websocket.mdx @@ -10,8 +10,63 @@ products: - containers --- -WebSocket requests are automatically forwarded to a container using the default `fetch` -method on the `Container` class: +import { TabItem, Tabs, TypeScriptExample } from "~/components"; + +Forward an incoming WebSocket upgrade request through the Durable Object to the listening port on the Container. + + + + + +```ts +import { DurableObject } from "cloudflare:workers"; + +interface Env { + MY_CONTAINER: DurableObjectNamespace; +} + +export class MyContainer extends DurableObject { + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + + ctx.blockConcurrencyWhile(async () => { + const container = ctx.container!; + await container.setInactivityTimeout(2 * 60 * 1000); + + if (!container.running) { + container.start(); + } + + const port = container.getTcpPort(8080); + let lastError: unknown; + for (let attempt = 0; attempt < 50; attempt++) { + try { + await port.fetch("http://container/"); + return; + } catch (error) { + lastError = error; + await scheduler.wait(100); + } + } + throw lastError; + }); + } + + fetch(request: Request): Promise { + return this.ctx.container!.getTcpPort(8080).fetch(request); + } +} + +export default { + fetch(request: Request, env: Env): Promise { + return env.MY_CONTAINER.getByName("default").fetch(request); + }, +}; +``` + + + + ```js import { Container, getContainer } from "@cloudflare/containers"; @@ -29,5 +84,8 @@ export default { }; ``` + + + View a full example in the [Container class repository](https://github.com/cloudflare/containers/tree/main/examples/websocket). {/* TODO: Add more advanced examples - like kicking off a WS request then passing messages to container from the WS */} diff --git a/src/content/docs/containers/faq.mdx b/src/content/docs/containers/faq.mdx index 982cdc3fe55..94ddc8be3f1 100644 --- a/src/content/docs/containers/faq.mdx +++ b/src/content/docs/containers/faq.mdx @@ -123,7 +123,9 @@ Containers do not use swap memory. ## How long can instances run for? What happens when a host server is shut down? -Cloudflare does not stop a container instance after a fixed maximum runtime. The Container class sets [`sleepAfter`](/containers/reference/container-class/#sleepafter) to 10 minutes by default, and its default [`onActivityExpired()`](/containers/reference/container-class/#onactivityexpired) implementation signals the container to stop after that period without activity. You can change the duration or override the hook. Even if your hook keeps the instance running, another platform event can stop it. One of those cases is a host server restart, which happens on an irregular cadence. Cloudflare does not guarantee that any container instance will run for any set period of time. +Cloudflare does not stop a container instance after a fixed maximum runtime. With the Durable Object Container API, call [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout) to stop an inactive container. The `Container` class sets [`sleepAfter`](/containers/api/container-class/#sleepafter) to 10 minutes by default. Its [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) implementation signals the container to stop after that period without activity. You can change the duration or override the hook. + +Another platform event can stop an active container. For example, a host server restart happens on an irregular cadence. Cloudflare does not guarantee that any container instance will run for a set period. When the platform is about to stop a container instance (including before a host moves work off a server), it: diff --git a/src/content/docs/containers/get-started/index.mdx b/src/content/docs/containers/get-started/index.mdx index b8c6fdeeae9..c33f6ed0d61 100644 --- a/src/content/docs/containers/get-started/index.mdx +++ b/src/content/docs/containers/get-started/index.mdx @@ -15,6 +15,8 @@ In this example, each container runs a small webserver written in Go. This example Worker should give you a sense for simple Container use, and provide a starting point for more complex use cases. +This guide uses the higher-level `Container` class. You can also manage containers directly through `ctx.container`. To compare both options, refer to [Containers APIs](/containers/api/). + ## Prerequisites ### Ensure Docker is running locally @@ -72,7 +74,7 @@ Now that you've deployed your first container, let's explain what is happening i ### Configuration -Your [Wrangler configuration file](/workers/wrangler/configuration/) defines the configuration for both your Worker and your container: +Your [Wrangler configuration file](/containers/configuration/wrangler/) defines the configuration for both your Worker and your container: @@ -171,7 +173,7 @@ This defines basic configuration for the container: The `Container` class itself extends [`DurableObject`](/durable-objects/), so your subclass has access to the full Durable Object API. The Durable Object handles routing, lifecycle, and persistent state, while the container process runs your image inside a Linux VM. This means you can use [`this.ctx.storage`](/durable-objects/api/sqlite-storage-api/) to persist data that survives container restarts and resides close to the container itself. -Refer to the [Container class reference](/containers/reference/container-class/) and the [low-level Durable Object container API](/durable-objects/api/container/) for more details. +For all properties and methods, refer to the [Container class API](/containers/api/container-class/). #### Routing to Containers diff --git a/src/content/docs/containers/guides/execute-commands.mdx b/src/content/docs/containers/guides/execute-commands.mdx index c5160e3cba6..7b05288a936 100644 --- a/src/content/docs/containers/guides/execute-commands.mdx +++ b/src/content/docs/containers/guides/execute-commands.mdx @@ -11,7 +11,7 @@ reviewed: 2026-06-18 import { TypeScriptExample } from "~/components"; -Use `exec()` to start another process inside a running [Container](/containers/reference/container-class/). The examples call `this.ctx.container.exec()` inside a class extending `Container` from `@cloudflare/containers`. +Use `exec()` to start another process inside a running [Container](/containers/api/container-class/). The examples call `this.ctx.container.exec()` inside a class extending `Container` from `@cloudflare/containers`. `exec()` does not start a stopped Container. In remote procedure call (RPC) methods, check `this.ctx.container.running` and call `await this.start()` when needed. You can also use the `onStart()` hook to run any series of commands whenever the Container starts. @@ -490,4 +490,4 @@ export class MyContainer extends Container { ``` -For all fields and return types, refer to the [`exec()` API contract](/durable-objects/api/container/#exec). +For all fields and return types, refer to the [`exec()` API contract](/containers/api/durable-object-container/#exec). diff --git a/src/content/docs/containers/index.mdx b/src/content/docs/containers/index.mdx index a2cf34aff99..4c875d13b88 100644 --- a/src/content/docs/containers/index.mdx +++ b/src/content/docs/containers/index.mdx @@ -170,11 +170,7 @@ Ship from your machine or Workers Builds, and confirm the deploy. How a container is scheduled, started, routed, and shut down. - + Instance counts, image size, and other platform limits. @@ -186,12 +182,8 @@ Ship from your machine or Workers Builds, and confirm the deploy. CLI commands for images and containers. - - Start, stop, and talk to the container process from a Durable Object. + + Choose between direct runtime control and higher-level lifecycle helpers. @@ -203,7 +195,8 @@ Ship from your machine or Workers Builds, and confirm the deploy. href="https://discord.cloudflare.com" icon="discord" > - Ask questions, show what you are building, and talk with other Containers developers. + Ask questions, show what you are building, and talk with other Containers + developers. diff --git a/src/content/docs/containers/platform/index.mdx b/src/content/docs/containers/platform/index.mdx index 3a596df5460..581b8f1d3c1 100644 --- a/src/content/docs/containers/platform/index.mdx +++ b/src/content/docs/containers/platform/index.mdx @@ -3,7 +3,7 @@ pcx_content_type: navigation title: Platform description: Product-wide information for Containers, including pricing and limits. sidebar: - order: 8 + order: 9 group: hideIndex: true products: diff --git a/src/content/docs/containers/reference/durable-object-methods.mdx b/src/content/docs/containers/reference/durable-object-methods.mdx deleted file mode 100644 index 8889c8cc6c5..00000000000 --- a/src/content/docs/containers/reference/durable-object-methods.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -pcx_content_type: navigation -title: Durable Object Interface -description: API reference for the low-level Durable Object methods that control Container instances. -external_link: /durable-objects/api/container/ -sidebar: - order: 2 -products: - - containers - - durable-objects ---- diff --git a/src/content/docs/containers/reference/index.mdx b/src/content/docs/containers/reference/index.mdx index 866ffe34560..8df5a63ad4a 100644 --- a/src/content/docs/containers/reference/index.mdx +++ b/src/content/docs/containers/reference/index.mdx @@ -1,9 +1,9 @@ --- pcx_content_type: navigation title: Reference -description: Lookup details for the Containers platform, including the Container class, the Durable Object interface, and Wrangler configuration and commands. +description: Lookup Wrangler configuration and commands for Containers. sidebar: - order: 7 + order: 8 group: hideIndex: true products: diff --git a/src/content/docs/durable-objects/api/container.mdx b/src/content/docs/durable-objects/api/container.mdx index e335a576411..cfc46405ca5 100644 --- a/src/content/docs/durable-objects/api/container.mdx +++ b/src/content/docs/durable-objects/api/container.mdx @@ -1,345 +1,11 @@ --- -title: Durable Object Container -description: Access and manage containers associated with a Durable Object, including start, stop, and interaction methods. -pcx_content_type: concept +pcx_content_type: navigation +title: Durable Object Container API +description: Manage a container directly through its Durable Object. +external_link: /containers/api/durable-object-container/ sidebar: - order: 1 + order: 7.5 products: + - containers - durable-objects --- - -import { - Render, - Tabs, - TabItem, - GlossaryTooltip, - Type, - MetaInfo, - TypeScriptExample, -} from "~/components"; - -## Description - -Each [container](/containers/) is managed by a Durable Object. The [`Container` class](/containers/reference/container-class/) from `@cloudflare/containers` extends `DurableObject` and handles lifecycle management, port readiness, and sleep timeouts for you. The Durable Object manages routing, persistent state, and lifecycle hooks, while the container process runs your image inside a Linux VM. - -The low-level API documented on this page is available on `this.ctx.container` inside any Durable Object class that has a container binding. Use it when you need direct control over the container process or cannot use the `Container` class. - -Because the `Container` class extends `DurableObject`, you also have access to [SQLite storage](/durable-objects/api/sqlite-storage-api/) via `this.ctx.storage`, [alarms](/durable-objects/api/alarms/), and all other Durable Object APIs. - - -```ts -export class MyDurableObject extends DurableObject { - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - - // boot the container when starting the DO - this.ctx.blockConcurrencyWhile(async () => { - this.ctx.container.start(); - }); - } - -} - -```` - - - -## Attributes - -### `running` - -`running` returns `true` if the container is currently running. It does not ensure that the container has fully started and ready to accept requests. - -```js - this.ctx.container.running; -```` - -## Methods - -### `start` - -`start` boots a container. This method does not block until the container is fully started. -You may want to confirm the container is ready to accept requests before using it. - -```js -this.ctx.container.start({ - env: { - FOO: "bar", - }, - enableInternet: false, - entrypoint: ["node", "server.js"], -}); -``` - -#### Parameters - -- `options` (optional): An object with the following properties: - - `env`: An object containing environment variables to pass to the container. This is useful for passing configuration values or secrets to the container. - - `entrypoint`: An array of strings representing the command to run in the container. - - `enableInternet`: A boolean indicating whether to enable internet access for the container. - -#### Return values - -- None. - -### `exec` - -`exec` starts another process inside an already-running Container. It does not start a stopped Container. - -The following example calls `this.ctx.container.exec()` inside a class extending `Container` from `@cloudflare/containers`. In RPC methods, check `this.ctx.container.running` and call `await this.start()` when needed. You can also use the `onStart()` hook to run any series of commands whenever the Container starts. - -```ts -exec( - cmd: string[], - options?: ContainerExecOptions, -): Promise -``` - -The `exec` operation 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", ""]` when Bash exists in the image. Use `["sh", "-c", ""]` for images with only a Portable Operating System Interface (POSIX) shell. - -The following RPC method starts the Container before executing a command: - - -```ts -import { Container } from "@cloudflare/containers"; - -export class MyContainer extends Container { - async runCommand() { - if (!this.ctx.container.running) { - await this.start(); - } - - const process = await this.ctx.container.exec(["node", "--version"]); - const output = await process.output(); - - return { - pid: process.pid, - exitCode: output.exitCode, - stdout: new TextDecoder().decode(output.stdout), - }; - } -} -``` - - -#### Parameters - -- `cmd` (`string[]`) — executable followed by its arguments. -- `options` (`ContainerExecOptions`, optional) — process configuration: - - `stdin` (`ReadableStream | "pipe"`) — source for standard input. Use `"pipe"` to write through the returned `stdin` stream. When omitted, standard input closes and sends end-of-file (EOF). - - `stdout` (`"pipe" | "ignore"`, default `"pipe"`) — captures or discards standard output. - - `stderr` (`"pipe" | "ignore" | "combined"`, default `"pipe"`) — captures, discards, or merges standard error into standard output. The `"combined"` value requires `stdout: "pipe"`. Combined output does not guarantee ordering between its source streams. - - `cwd` (`string`) — working directory for the process. - - `env` (`Record`) — environment additions and overrides. The process inherits existing Container variables. Matching keys use the per-execution value. - - `user` (`string`) — image user for the process. - -#### Return values - -Returns `Promise`. - -An `ExecProcess` has these fields and methods: - -- `stdin` (`WritableStream | null`) — writable standard input when `stdin` is `"pipe"`. -- `stdout` (`ReadableStream | null`) — readable standard output when piped. -- `stderr` (`ReadableStream | null`) — readable standard error when piped separately. -- `pid` (`number`) — process identifier. -- `exitCode` (`Promise`) — resolves when the process exits. Nonzero codes resolve normally instead of rejecting. -- `output()` (`Promise`) — reads buffered output once. `ExecOutput` contains `stdout` (`ArrayBuffer`), `stderr` (`ArrayBuffer`), and `exitCode` (`number`). Ignored streams produce empty buffers. Use `TextDecoder` to decode text. -- `kill(signal?: number)` (`void`) — queues a signal for the process. The default is `SIGTERM`, signal `15`. The signal must be from `1` through `64`. - -With `stderr: "combined"`, `stderr` is `null` on `ExecProcess` and an empty `ArrayBuffer` on `ExecOutput`. Read both output channels from `stdout`. - -`output()` throws a `TypeError` when called more than once or after either readable stream starts being consumed. For large output, consume both readable streams concurrently instead of buffering them with `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. - -#### Exceptions - -- `exec()` throws when the Container is not running. -- `exec()` throws a `TypeError` when `cmd` is empty, an option mode is invalid, or `stderr: "combined"` is used with `stdout: "ignore"`. -- `exec()` rejects if the runtime cannot create or start the process. -- Environment variable names cannot contain `=` or null characters. Environment values, `cwd`, and `user` cannot contain null characters. -- `kill()` throws a `RangeError` when the signal is outside the supported range. - -For task-oriented examples, refer to [Execute commands](/containers/guides/execute-commands/). - -### `destroy` - -`destroy` stops the container and optionally returns a custom error message to the `monitor()` error callback. - -```js -this.ctx.container.destroy("Manually Destroyed"); -``` - -#### Parameters - -- `error` (optional): A string that will be sent to the error handler of the `monitor` method. This is useful for logging or debugging purposes. - -#### Return values - -- A promise that returns once the container is destroyed. - -### `signal` - -`signal` sends an IPC signal to the container, such as SIGKILL or SIGTERM. This is useful for stopping the container gracefully or forcefully. - -```js -const SIGTERM = 15; -this.ctx.container.signal(SIGTERM); -``` - -#### Parameters - -- `signal`: a number representing the signal to send to the container. This is typically a POSIX signal number, such as SIGTERM (15) or SIGKILL (9). - -#### Return values - -- None. - -### `getTcpPort` - -`getTcpPort` returns a TCP port from the container. This can be used to communicate with the container over TCP and HTTP. - -```js -const port = this.ctx.container.getTcpPort(8080); -const res = await port.fetch("http://container/set-state", { - body: initialState, - method: "POST", -}); -``` - -```js -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 (err) { - console.error("Request body piping failed:", err); - return new Response("Failed to proxy request body", { status: 502 }); -} -``` - -#### Parameters - -- `port` (number): a TCP port number to use for communication with the container. - -#### Return values - -- `TcpPort`: a `TcpPort` object representing the TCP port. This object can be used to send requests to the container over TCP and HTTP. - -### `monitor` - -`monitor` returns a promise that resolves when a container exits and errors if a container errors. This is useful for setting up -callbacks to handle container status changes in your Workers code. - -```js -class MyContainer extends DurableObject { - constructor(ctx, env) { - super(ctx, env); - function onContainerExit() { - console.log("Container exited"); - } - - // the "err" value can be customized by the destroy() method - async function onContainerError(err) { - console.log("Container errored", err); - } - - this.ctx.container.start(); - this.ctx.container.monitor().then(onContainerExit).catch(onContainerError); - } -} -``` - -#### Parameters - -- None - -#### Return values - -- A promise that resolves when the container exits. - -### `interceptOutboundHttp` - -`interceptOutboundHttp` routes outbound HTTP requests matching a hostname, hostname glob, IP address, IP:port, or CIDR range through a `WorkerEntrypoint`. Can be called before or after starting the container. Open connections pick up the new handler without being dropped. - -```js -const worker = this.ctx.exports.MyWorker({ props: { message: "hello" } }); - -// Match a specific hostname -this.ctx.container.interceptOutboundHttp("api.example.com", worker); - -// Match a hostname glob pattern -this.ctx.container.interceptOutboundHttp("*.example.com", worker); - -// Match an IP:port -await this.ctx.container.interceptOutboundHttp("15.0.0.1:80", worker); - -// Match a CIDR range (IPv4 and IPv6) -await this.ctx.container.interceptOutboundHttp("123.123.123.123/23", worker); -``` - -#### Parameters - -- `target` (string): A hostname, hostname glob (for example, `*.example.com`), IP address, IP:port, or CIDR range to match. -- `worker` (WorkerEntrypoint): A `WorkerEntrypoint` instance to handle matching requests. - -#### Return values - -- None. - -### `interceptAllOutboundHttp` - -`interceptAllOutboundHttp` routes all outbound HTTP requests from the container through a `WorkerEntrypoint`, regardless of destination. - -```js -await this.ctx.container.interceptAllOutboundHttp(worker); -``` - -#### Parameters - -- `worker` (WorkerEntrypoint): A `WorkerEntrypoint` instance to handle all outbound HTTP requests. - -#### Return values - -- A promise that resolves once the intercept rule is installed. - -### `interceptOutboundHttps` - -`interceptOutboundHttps` routes outbound HTTPS requests matching a hostname or hostname glob through a `WorkerEntrypoint`. Works the same way as `interceptOutboundHttp` but for HTTPS traffic. The container must trust the CA certificate at `/etc/cloudflare/certs/cloudflare-containers-ca.crt` for HTTPS interception to work. - -Supports glob patterns where `*` matches any sequence of characters. - -```js -const worker = this.ctx.exports.MyWorker({ props: {} }); - -// Match a specific hostname -this.ctx.container.interceptOutboundHttps("api.example.com", worker); - -// Match a hostname glob pattern -this.ctx.container.interceptOutboundHttps("*.example.com", worker); - -// Intercept all HTTPS traffic -this.ctx.container.interceptOutboundHttps("*", worker); -``` - -#### Parameters - -- `target` (string): A hostname or hostname glob pattern to match. Use `*` to intercept all HTTPS traffic. -- `worker` (WorkerEntrypoint): A `WorkerEntrypoint` instance to handle matching requests. - -#### Return values - -- None. - -## Related resources - -- [Container class reference](/containers/reference/container-class/) — the recommended high-level API built on top of this interface -- [Containers overview](/containers/) -- [Get started with Containers](/containers/get-started/) -- [SQLite storage API](/durable-objects/api/sqlite-storage-api/) — persist state across container restarts -- [Durable Objects](/durable-objects/) — the underlying platform that powers Containers diff --git a/src/content/docs/reference-architecture/diagrams/ai/enterprise-ai-vibe-coding-platform.mdx b/src/content/docs/reference-architecture/diagrams/ai/enterprise-ai-vibe-coding-platform.mdx index 17b22409d26..7bb16c681c4 100644 --- a/src/content/docs/reference-architecture/diagrams/ai/enterprise-ai-vibe-coding-platform.mdx +++ b/src/content/docs/reference-architecture/diagrams/ai/enterprise-ai-vibe-coding-platform.mdx @@ -80,4 +80,4 @@ The dispatch worker can set [custom limits](/cloudflare-for-platforms/workers-fo Observability operates at two levels. At the platform level, [Workers Trace Events Logpush](/cloudflare-for-platforms/workers-for-platforms/configuration/observability/) enabled on the dispatch worker covers all user workers in the namespace. The [GraphQL Analytics API](/analytics/graphql-api/) queries by `dispatchNamespaceName` for aggregate metrics across the platform. At the app level, [Tail Workers](/cloudflare-for-platforms/workers-for-platforms/configuration/observability/) can be attached to individual user workers for granular logging. [Workers Analytics Engine](/analytics/analytics-engine/) lets the platform write and query events by script tag, surfacing per-app usage metrics to individual users. -As the number of vibe-coded applications grows, the platform metadata store serves as an application registry, surfacing the owner, team, description, connected data sources, and usage metrics for each deployment. As with the development plane, this metadata can be stored in any preferred data store such as [D1](/d1/), [KV](/kv/), or [R2](/r2/). This allows employees to find existing tools before duplicating efforts. [Resource tagging](/resource-tagging/) applied to the underlying Cloudflare resources links each user worker back to its owner and cost attribution. \ No newline at end of file +As the number of vibe-coded applications grows, the platform metadata store serves as an application registry, surfacing the owner, team, description, connected data sources, and usage metrics for each deployment. As with the development plane, this metadata can be stored in any preferred data store such as [D1](/d1/), [KV](/kv/), or [R2](/r2/). This allows employees to find existing tools before duplicating efforts. [Resource tagging](/resource-tagging/) applied to the underlying Cloudflare resources links each user worker back to its owner and cost attribution. diff --git a/src/content/docs/workers/wrangler/configuration.mdx b/src/content/docs/workers/wrangler/configuration.mdx index 6a444a83343..0a6f3b8b38a 100644 --- a/src/content/docs/workers/wrangler/configuration.mdx +++ b/src/content/docs/workers/wrangler/configuration.mdx @@ -1321,7 +1321,7 @@ The following options are available: build and push the image, or it can be an image reference. Supported registries are the Cloudflare Registry, Docker Hub, Amazon ECR, and Google Artifact Registry. For more information, refer to [Image Management](/containers/guides/image-management/). - `class_name` - The corresponding Durable Object class name. This will make this Durable Object a container-enabled Durable Object - and allow each instance to control a container. See [Durable Object Container Methods](/durable-objects/api/container/) for details. + and allow each instance to control a container. Refer to the [Durable Object Container API](/containers/api/durable-object-container/) for details. - `instance_type` - The instance type of the container. This determines the amount of memory, CPU, and disk given to the container instance. The current options are `"lite"`, `"basic"`, `"standard-1"`, `"standard-2"`, `"standard-3"`, and `"standard-4"`. The default is `"lite"`. For more information, From 9a1b137c16445e81a5c25215ec7063ebc140e225 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Fri, 28 Aug 2026 14:42:45 -0500 Subject: [PATCH 02/23] Clarify Durable Object alarm method --- src/content/docs/containers/api/index.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/content/docs/containers/api/index.mdx b/src/content/docs/containers/api/index.mdx index 2c3addd38f1..dc3c3b0169f 100644 --- a/src/content/docs/containers/api/index.mdx +++ b/src/content/docs/containers/api/index.mdx @@ -48,8 +48,6 @@ The `Container` class builds on Durable Objects and the runtime API. Choose it w The following table compares both options: -Methods labeled **Durable Object API** are available through the surrounding Durable Object, not through `ctx.container`. - | Requirement | Durable Object Container API | `Container` class | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Start and stop a container | [`start()`](/containers/api/durable-object-container/#start), [`signal()`](/containers/api/durable-object-container/#signal), and [`destroy()`](/containers/api/durable-object-container/#destroy) | [`start()`](/containers/api/container-class/#start), [`stop()`](/containers/api/container-class/#stop), and [`destroy()`](/containers/api/container-class/#destroy) | @@ -59,7 +57,7 @@ Methods labeled **Durable Object API** are available through the surrounding Dur | Handle concurrent starts | Coordinate calls to [`start()`](/containers/api/durable-object-container/#start) when needed | Handled by [`start()`](/containers/api/container-class/#start) and [`startAndWaitForPorts()`](/containers/api/container-class/#startandwaitforports) | | Run lifecycle hooks | [`monitor()`](/containers/api/durable-object-container/#monitor) and application code | [`onStart()`](/containers/api/container-class/#onstart), [`onStop()`](/containers/api/container-class/#onstop), [`onError()`](/containers/api/container-class/#onerror), and [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) | | Stop inactive containers | [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout) | [`sleepAfter`](/containers/api/container-class/#sleepafter) and [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) | -| Schedule callbacks | **Durable Object API:** [`ctx.storage.setAlarm()`](/durable-objects/api/alarms/#setalarm) | [`schedule()`](/containers/api/container-class/#schedule) | +| Schedule callbacks | [`ctx.storage.setAlarm()`](/durable-objects/api/alarms/#setalarm) (Durable Object API) | [`schedule()`](/containers/api/container-class/#schedule) | Use the Durable Object Container API for latency-sensitive workloads or workloads that need a smaller storage footprint. From 7ce4abed90720e100f0a31791981e092a8f101ba Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Wed, 23 Sep 2026 17:24:09 -0400 Subject: [PATCH 03/23] [Containers] Separate outbound traffic IA from API docs --- public/__redirects | 3 +-- .../changelog/containers/2026-03-26-outbound-workers.mdx | 2 +- .../2026-04-13-sandbox-outbound-workers-tls-auth.mdx | 2 +- src/content/docs/containers/api/container-class.mdx | 4 ++-- .../docs/containers/configuration/workers-connections.mdx | 4 ++-- src/content/docs/containers/faq.mdx | 2 +- .../containers/{configuration => guides}/outbound-traffic.mdx | 0 .../diagrams/ai/enterprise-ai-vibe-coding-platform.mdx | 4 ++-- src/content/docs/sandbox/guides/outbound-traffic.mdx | 2 +- 9 files changed, 11 insertions(+), 12 deletions(-) rename src/content/docs/containers/{configuration => guides}/outbound-traffic.mdx (100%) diff --git a/public/__redirects b/public/__redirects index 2a5f6efe42c..d099b461567 100644 --- a/public/__redirects +++ b/public/__redirects @@ -660,7 +660,7 @@ # Containers IA rework: platform-details/ dissolved into core sections /containers/platform-details/architecture/ /containers/concepts/architecture/ 301 /containers/platform-details/placement/ /containers/concepts/placement/ 301 -/containers/platform-details/outbound-traffic/ /containers/configuration/outbound-traffic/ 301 +/containers/platform-details/outbound-traffic/ /containers/guides/outbound-traffic/ 301 /containers/platform-details/workers-connections/ /containers/configuration/workers-connections/ 301 /containers/platform-details/environment-variables/ /containers/configuration/environment-variables/ 301 /containers/platform-details/rollouts/ /containers/configuration/rollouts/ 301 @@ -670,7 +670,6 @@ /containers/platform-details/durable-object-methods/ /containers/api/durable-object-container/ 301 /containers/platform-details/ /containers/concepts/architecture/ 301 # Containers IA rework: Configuration section + Local Development to Guides + Wrangler pages to Reference -/containers/guides/outbound-traffic/ /containers/configuration/outbound-traffic/ 301 /containers/reference/local-dev/ /containers/guides/local-dev/ 301 /containers/reference/environment-variables/ /containers/configuration/environment-variables/ 301 /containers/reference/scaling-and-routing/ /containers/configuration/scaling-and-routing/ 301 diff --git a/src/content/changelog/containers/2026-03-26-outbound-workers.mdx b/src/content/changelog/containers/2026-03-26-outbound-workers.mdx index 4d09b7a470f..80efb1f9b20 100644 --- a/src/content/changelog/containers/2026-03-26-outbound-workers.mdx +++ b/src/content/changelog/containers/2026-03-26-outbound-workers.mdx @@ -78,4 +78,4 @@ This provides an easy way to associate state with any container instance, and in Upgrade to `@cloudflare/containers` version 0.2.0 or later, or `@cloudflare/sandbox` version 0.8.0 or later to use outbound Workers. -Refer to [Containers outbound traffic](/containers/configuration/outbound-traffic/) and [Sandboxes outbound traffic](/sandbox/guides/outbound-traffic/) for more details and examples. +Refer to [Containers outbound traffic](/containers/guides/outbound-traffic/) and [Sandboxes outbound traffic](/sandbox/guides/outbound-traffic/) for more details and examples. diff --git a/src/content/changelog/containers/2026-04-13-sandbox-outbound-workers-tls-auth.mdx b/src/content/changelog/containers/2026-04-13-sandbox-outbound-workers-tls-auth.mdx index 7b563931f02..f5fe931784a 100644 --- a/src/content/changelog/containers/2026-04-13-sandbox-outbound-workers-tls-auth.mdx +++ b/src/content/changelog/containers/2026-04-13-sandbox-outbound-workers-tls-auth.mdx @@ -109,4 +109,4 @@ Handlers accept `params`, so you can customize behavior per instance without def Upgrade to `@cloudflare/containers@0.3.0` or `@cloudflare/sandbox@0.8.9` to use these features. -For more details, refer to [Sandbox outbound traffic](/sandbox/guides/outbound-traffic/) and [Container outbound traffic](/containers/configuration/outbound-traffic/). +For more details, refer to [Sandbox outbound traffic](/sandbox/guides/outbound-traffic/) and [Container outbound traffic](/containers/guides/outbound-traffic/). diff --git a/src/content/docs/containers/api/container-class.mdx b/src/content/docs/containers/api/container-class.mdx index 9443c81ee57..bf8c8eebc12 100644 --- a/src/content/docs/containers/api/container-class.mdx +++ b/src/content/docs/containers/api/container-class.mdx @@ -102,7 +102,7 @@ Configure these as class fields on your subclass. They apply to every instance o `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](/containers/configuration/outbound-traffic/). + traffic](/containers/guides/outbound-traffic/). - **`pingEndpoint`** (`string`, default: `"ping"`) — the host and path the class uses to health-check the container @@ -719,7 +719,7 @@ export default { ``` -For more information, refer to [Handle outbound traffic](/containers/configuration/outbound-traffic/). +For more information, refer to [Handle outbound traffic](/containers/guides/outbound-traffic/). ## Utility functions diff --git a/src/content/docs/containers/configuration/workers-connections.mdx b/src/content/docs/containers/configuration/workers-connections.mdx index 1aa35122871..9e7ff7a4d7f 100644 --- a/src/content/docs/containers/configuration/workers-connections.mdx +++ b/src/content/docs/containers/configuration/workers-connections.mdx @@ -8,7 +8,7 @@ products: - containers --- -Containers can access [Workers bindings](/workers/runtime-apis/bindings/) — KV, R2, D1, Durable Objects, and others — through [outbound handlers](/containers/configuration/outbound-traffic/#define-outbound-handlers). An outbound handler intercepts HTTP requests from the container and runs inside the Workers runtime, where all of your configured bindings are available. +Containers can access [Workers bindings](/workers/runtime-apis/bindings/) — KV, R2, D1, Durable Objects, and others — through [outbound handlers](/containers/guides/outbound-traffic/#define-outbound-handlers). An outbound handler intercepts HTTP requests from the container and runs inside the Workers runtime, where all of your configured bindings are available. The container makes a plain HTTP request to a virtual hostname (for example, `http://my.kv/some-key`), and the outbound handler resolves it using the bound resource. No SDK or client library is required inside the container. @@ -57,6 +57,6 @@ The `ctx` argument exposes `containerId`, which lets you interact with the conta ## Related resources -- [Handle outbound traffic](/containers/configuration/outbound-traffic/) — Block, allow, and intercept all outbound HTTP from a container +- [Handle outbound traffic](/containers/guides/outbound-traffic/) — Block, allow, and intercept all outbound HTTP from a container - [Environment variables and secrets](/containers/configuration/environment-variables/) — Configure secrets and environment variables - [Durable Object Container API](/containers/api/durable-object-container/) — Full `ctx.container` API reference diff --git a/src/content/docs/containers/faq.mdx b/src/content/docs/containers/faq.mdx index 94ddc8be3f1..dfca242606f 100644 --- a/src/content/docs/containers/faq.mdx +++ b/src/content/docs/containers/faq.mdx @@ -177,4 +177,4 @@ For a complete working example, see the [Docker-in-Docker Containers example](ht ## How do I allow or disallow egress from my container? -Refer to [Handle outbound traffic](/containers/configuration/outbound-traffic/) for how to control outbound traffic and internet access. +Refer to [Handle outbound traffic](/containers/guides/outbound-traffic/) for how to control outbound traffic and internet access. diff --git a/src/content/docs/containers/configuration/outbound-traffic.mdx b/src/content/docs/containers/guides/outbound-traffic.mdx similarity index 100% rename from src/content/docs/containers/configuration/outbound-traffic.mdx rename to src/content/docs/containers/guides/outbound-traffic.mdx diff --git a/src/content/docs/reference-architecture/diagrams/ai/enterprise-ai-vibe-coding-platform.mdx b/src/content/docs/reference-architecture/diagrams/ai/enterprise-ai-vibe-coding-platform.mdx index 7bb16c681c4..52c4c03ea23 100644 --- a/src/content/docs/reference-architecture/diagrams/ai/enterprise-ai-vibe-coding-platform.mdx +++ b/src/content/docs/reference-architecture/diagrams/ai/enterprise-ai-vibe-coding-platform.mdx @@ -54,7 +54,7 @@ With a local agent harness, developers use CLI-based tools like Cursor, Windsurf All LLM interactions are tracked and managed through [AI Gateway](/ai-gateway/), which provides provider routing, cost controls, prompt logging, and [DLP inspection](/cloudflare-one/data-loss-prevention/). [Cost tracking](/ai-gateway/observability/costs/) attributes usage to projects, teams, departments, and individual users. -All egress from the development environment is controlled at the platform level. For containers, an [outbound handler](/containers/configuration/outbound-traffic/) intercepts HTTP traffic. For Dynamic Workers, [egress control](/dynamic-workers/usage/egress-control/) provides equivalent capabilities. Secrets required for downstream connectivity are stored in [Secrets Store](/secrets-store/) and injected by the outbound handler at the platform level. The sandboxed environment never has direct access to credentials. With this outbound handler, platform administrators can allow or deny specific origin destinations, reroute traffic, apply custom policies on outbound traffic, or connect to other Cloudflare resources through [bindings](/workers/runtime-apis/bindings/). For access to on-premises or internal systems, [Workers VPC](/workers-vpc/) establishes private connectivity without exposing those systems to the Internet. +All egress from the development environment is controlled at the platform level. For containers, an [outbound handler](/containers/guides/outbound-traffic/) intercepts HTTP traffic. For Dynamic Workers, [egress control](/dynamic-workers/usage/egress-control/) provides equivalent capabilities. Secrets required for downstream connectivity are stored in [Secrets Store](/secrets-store/) and injected by the outbound handler at the platform level. The sandboxed environment never has direct access to credentials. With this outbound handler, platform administrators can allow or deny specific origin destinations, reroute traffic, apply custom policies on outbound traffic, or connect to other Cloudflare resources through [bindings](/workers/runtime-apis/bindings/). For access to on-premises or internal systems, [Workers VPC](/workers-vpc/) establishes private connectivity without exposing those systems to the Internet. Additional security controls can be layered into the development container through package version locking and organizational controls baked into the container image. If the harness uses MCP servers, [MCP portals](/cloudflare-one/access-controls/ai-controls/mcp-portals/) provide audit logging of tool invocations, permission management for tool access, and visibility into which tools agents use and what data they access. @@ -80,4 +80,4 @@ The dispatch worker can set [custom limits](/cloudflare-for-platforms/workers-fo Observability operates at two levels. At the platform level, [Workers Trace Events Logpush](/cloudflare-for-platforms/workers-for-platforms/configuration/observability/) enabled on the dispatch worker covers all user workers in the namespace. The [GraphQL Analytics API](/analytics/graphql-api/) queries by `dispatchNamespaceName` for aggregate metrics across the platform. At the app level, [Tail Workers](/cloudflare-for-platforms/workers-for-platforms/configuration/observability/) can be attached to individual user workers for granular logging. [Workers Analytics Engine](/analytics/analytics-engine/) lets the platform write and query events by script tag, surfacing per-app usage metrics to individual users. -As the number of vibe-coded applications grows, the platform metadata store serves as an application registry, surfacing the owner, team, description, connected data sources, and usage metrics for each deployment. As with the development plane, this metadata can be stored in any preferred data store such as [D1](/d1/), [KV](/kv/), or [R2](/r2/). This allows employees to find existing tools before duplicating efforts. [Resource tagging](/resource-tagging/) applied to the underlying Cloudflare resources links each user worker back to its owner and cost attribution. +As the number of vibe-coded applications grows, the platform metadata store serves as an application registry, surfacing the owner, team, description, connected data sources, and usage metrics for each deployment. As with the development plane, this metadata can be stored in any preferred data store such as [D1](/d1/), [KV](/kv/), or [R2](/r2/). This allows employees to find existing tools before duplicating efforts. [Resource tagging](/resource-tagging/) applied to the underlying Cloudflare resources links each user worker back to its owner and cost attribution. \ No newline at end of file diff --git a/src/content/docs/sandbox/guides/outbound-traffic.mdx b/src/content/docs/sandbox/guides/outbound-traffic.mdx index 70b871df68a..e3da306c4a5 100644 --- a/src/content/docs/sandbox/guides/outbound-traffic.mdx +++ b/src/content/docs/sandbox/guides/outbound-traffic.mdx @@ -289,6 +289,6 @@ Requests are evaluated in this order: ## Related resources - [Connect to Workers bindings](/sandbox/guides/workers-connections/) — Access KV, R2, Durable Objects, and other bindings from a sandbox -- [Handle outbound traffic (Containers)](/containers/configuration/outbound-traffic/) — Container SDK API for outbound handlers +- [Handle outbound traffic (Containers)](/containers/guides/outbound-traffic/) — Container SDK API for outbound handlers - [Sandbox options](/sandbox/configuration/sandbox-options/) — Configure sandbox behavior - [Environment variables](/sandbox/configuration/environment-variables/) — Configure secrets and environment variables From 6cfbf88394e1ec597e35ae64fcb6e777bb66a25f Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Wed, 23 Sep 2026 18:09:29 -0400 Subject: [PATCH 04/23] [Containers] Validate API examples and preserve redirects --- public/__redirects | 1 + .../api/durable-object-container.mdx | 99 ++++++++++----- src/content/docs/containers/api/index.mdx | 62 +++++++++- .../docs/containers/concepts/architecture.mdx | 2 +- .../containers/configuration/wrangler.mdx | 2 +- .../containers/examples/container-backend.mdx | 112 +++++++++++++---- src/content/docs/containers/examples/cron.mdx | 16 ++- .../examples/env-vars-and-secrets.mdx | 72 ++++++++++- .../containers/examples/r2-fuse-mount.mdx | 114 ++++++++++-------- .../docs/containers/examples/stateless.mdx | 75 ++++++++---- .../docs/containers/examples/status-hooks.mdx | 30 +++-- .../docs/containers/examples/websocket.mdx | 79 ++++++++---- 12 files changed, 488 insertions(+), 176 deletions(-) diff --git a/public/__redirects b/public/__redirects index d099b461567..1b3be50a78b 100644 --- a/public/__redirects +++ b/public/__redirects @@ -644,6 +644,7 @@ # Containers API /containers/container-package/ /containers/api/container-class/ 301 /containers/durable-object-methods/ /containers/api/durable-object-container/ 301 +/containers/examples/durable-object-interface/ https://github.com/cloudflare/containers-demos 301 /containers/container-class/ /containers/api/container-class/ 301 /containers/reference/container-class/ /containers/api/container-class/ 301 /containers/reference/durable-object-methods/ /containers/api/durable-object-container/ 301 diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index 4dd87ce737a..54e45c708d9 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -9,15 +9,7 @@ products: - durable-objects --- -import { - Render, - Tabs, - TabItem, - GlossaryTooltip, - Type, - MetaInfo, - TypeScriptExample, -} from "~/components"; +import { TypeScriptExample } from "~/components"; ## Description @@ -30,6 +22,7 @@ You can instead use the [`Container` class](/containers/api/container-class/) fr Your Durable Object also has access to [SQLite storage](/durable-objects/api/sqlite-storage-api/) through `this.ctx.storage`, [alarms](/durable-objects/api/alarms/), and all other Durable Object APIs. + ```ts import { DurableObject } from "cloudflare:workers"; @@ -38,7 +31,6 @@ interface Env {} export class MyDurableObject extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); - ctx.blockConcurrencyWhile(async () => { if (!ctx.container!.running) { ctx.container!.start(); @@ -47,6 +39,7 @@ export class MyDurableObject extends DurableObject { } } ``` + ## Attributes @@ -55,10 +48,14 @@ export class MyDurableObject extends DurableObject { `running` returns `true` if the container is currently running. It does not ensure that the container has fully started and ready to accept requests. -```js + + +```ts this.ctx.container.running; ``` + + ## Methods ### `start` @@ -66,8 +63,11 @@ this.ctx.container.running; `start` boots a container. This method does not block until the container is fully started. You may want to confirm the container is ready to accept requests before using it. -```js + + +```ts this.ctx.container.start({ + image: "registry.cloudflare.com//my-container:latest", env: { FOO: "bar", }, @@ -76,12 +76,19 @@ this.ctx.container.start({ }); ``` + + #### Parameters -- `options` (optional): An object with the following properties: - - `env`: An object containing environment variables to pass to the container. This is useful for passing configuration values or secrets to the container. - - `entrypoint`: An array of strings representing the command to run in the container. - - `enableInternet`: A boolean indicating whether to enable internet access for the container. +- `options` (optional): Omit to use the image configured in Wrangler. When provided, set `enableInternet` and either `image` or `containerSnapshot`, but not both: + - `image`: The container image to run. Specify this when selecting an image at runtime. + - `containerSnapshot`: A snapshot to restore instead of starting from an image. Provide its `id`. + - `enableInternet`: Whether the container can access the Internet. + - `env`: Environment variables to pass to the container. + - `entrypoint`: The command and arguments to run in the container. + - `instance`: The instance type (`lite` or `standard-1` through `standard-4`) or a resource configuration with `vcpu`, `memoryMib`, and `diskMb`. + - `labels`: String key-value labels associated with the container. + - `directorySnapshots`: Directory snapshot restore configurations. Each entry accepts a `snapshot` and optional `mountPoint`, or a `mountPoint` alone. #### Return values @@ -93,7 +100,7 @@ this.ctx.container.start({ The following example calls `this.ctx.container.exec()` inside a class extending `Container` from `@cloudflare/containers`. In RPC methods, check `this.ctx.container.running` and call `await this.start()` when needed. You can also use the `onStart()` hook to run any series of commands whenever the Container starts. -```ts +```txt exec( cmd: string[], options?: ContainerExecOptions, @@ -105,6 +112,7 @@ The `exec` operation starts the executable directly with the provided arguments. The following RPC method starts the Container before executing a command: + ```ts import { Container } from "@cloudflare/containers"; @@ -125,6 +133,7 @@ export class MyContainer extends Container { } } ``` + #### Parameters @@ -172,10 +181,14 @@ For task-oriented examples, refer to [Execute commands](/containers/guides/execu `destroy` stops the container and optionally returns a custom error message to the `monitor()` error callback. -```js + + +```ts this.ctx.container.destroy("Manually Destroyed"); ``` + + #### Parameters - `error` (optional): A string that will be sent to the error handler of the `monitor` method. This is useful for logging or debugging purposes. @@ -188,11 +201,15 @@ this.ctx.container.destroy("Manually Destroyed"); `signal` sends an IPC signal to the container, such as SIGKILL or SIGTERM. This is useful for stopping the container gracefully or forcefully. -```js + + +```ts const SIGTERM = 15; this.ctx.container.signal(SIGTERM); ``` + + #### Parameters - `signal`: a number representing the signal to send to the container. This is typically a POSIX signal number, such as SIGTERM (15) or SIGKILL (9). @@ -205,14 +222,18 @@ this.ctx.container.signal(SIGTERM); `setInactivityTimeout` sets how long a running container can remain inactive before the runtime stops it. -```ts +```txt setInactivityTimeout(durationMs: number | bigint): Promise ``` -```js + + +```ts await this.ctx.container.setInactivityTimeout(10 * 60 * 1000); ``` + + #### Parameters - `durationMs`: Inactivity timeout in milliseconds. @@ -225,7 +246,9 @@ await this.ctx.container.setInactivityTimeout(10 * 60 * 1000); `getTcpPort` returns a TCP port from the container. This can be used to communicate with the container over TCP and HTTP. -```js + + +```ts const port = this.ctx.container.getTcpPort(8080); const res = await port.fetch("http://container/set-state", { body: initialState, @@ -233,7 +256,11 @@ const res = await port.fetch("http://container/set-state", { }); ``` -```js + + + + +```ts const conn = this.ctx.container.getTcpPort(8080).connect("10.0.0.1:8080"); await conn.opened; @@ -248,6 +275,8 @@ try { } ``` + + #### Parameters - `port` (number): a TCP port number to use for communication with the container. @@ -261,7 +290,9 @@ try { `monitor` returns a promise that resolves when a container exits and errors if a container errors. This is useful for setting up callbacks to handle container status changes in your Workers code. -```js + + +```ts class MyContainer extends DurableObject { constructor(ctx, env) { super(ctx, env); @@ -280,6 +311,8 @@ class MyContainer extends DurableObject { } ``` + + #### Parameters - None @@ -292,7 +325,9 @@ class MyContainer extends DurableObject { `interceptOutboundHttp` routes outbound HTTP requests matching a hostname, hostname glob, IP address, IP:port, or CIDR range through a `WorkerEntrypoint`. Can be called before or after starting the container. Open connections pick up the new handler without being dropped. -```js + + +```ts const worker = this.ctx.exports.MyWorker({ props: { message: "hello" } }); // Match a specific hostname @@ -308,6 +343,8 @@ await this.ctx.container.interceptOutboundHttp("15.0.0.1:80", worker); await this.ctx.container.interceptOutboundHttp("123.123.123.123/23", worker); ``` + + #### Parameters - `target` (string): A hostname, hostname glob (for example, `*.example.com`), IP address, IP:port, or CIDR range to match. @@ -321,10 +358,14 @@ await this.ctx.container.interceptOutboundHttp("123.123.123.123/23", worker); `interceptAllOutboundHttp` routes all outbound HTTP requests from the container through a `WorkerEntrypoint`, regardless of destination. -```js + + +```ts await this.ctx.container.interceptAllOutboundHttp(worker); ``` + + #### Parameters - `worker` (WorkerEntrypoint): A `WorkerEntrypoint` instance to handle all outbound HTTP requests. @@ -339,7 +380,9 @@ await this.ctx.container.interceptAllOutboundHttp(worker); Supports glob patterns where `*` matches any sequence of characters. -```js + + +```ts const worker = this.ctx.exports.MyWorker({ props: {} }); // Match a specific hostname @@ -352,6 +395,8 @@ this.ctx.container.interceptOutboundHttps("*.example.com", worker); this.ctx.container.interceptOutboundHttps("*", worker); ``` + + #### Parameters - `target` (string): A hostname or hostname glob pattern to match. Use `*` to intercept all HTTPS traffic. diff --git a/src/content/docs/containers/api/index.mdx b/src/content/docs/containers/api/index.mdx index dc3c3b0169f..dac45754255 100644 --- a/src/content/docs/containers/api/index.mdx +++ b/src/content/docs/containers/api/index.mdx @@ -9,7 +9,7 @@ products: - durable-objects --- -import { CardGrid, LinkTitleCard } from "~/components"; +import { CardGrid, LinkTitleCard, TypeScriptExample } from "~/components"; Containers provide two APIs for managing a container from a Durable Object. Both APIs address the same container runtime. @@ -65,28 +65,74 @@ Use the Durable Object Container API for latency-sensitive workloads or workload The Durable Object Container API is available through `ctx.container` of the Durable Object. It exposes the container runtime without adding lifecycle policy. +The following example expects the container to serve `GET /health` on port 8080 and return a successful response once it is ready. + + + ```ts import { DurableObject } from "cloudflare:workers"; +interface Env {} + export class MyContainer extends DurableObject { + private ready: Promise | undefined; + constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); ctx.blockConcurrencyWhile(() => - ctx.container.setInactivityTimeout(10 * 60 * 1000), + ctx.container!.setInactivityTimeout(10 * 60 * 1000), ); } async fetch(request: Request): Promise { - if (!this.ctx.container.running) { - this.ctx.container.start({ enableInternet: true }); + const container = this.ctx.container!; + if (!container.running) { + this.ready = undefined; } + this.ready ??= this.startAndWaitForPort().catch((error: unknown) => { + this.ready = undefined; + throw error; + }); + await this.ready; + + const url = new URL(request.url); + url.protocol = "http:"; + url.host = "container"; + const forwarded = new Request(url, request); + forwarded.headers.delete("host"); + return container.getTcpPort(8080).fetch(forwarded); + } - return this.ctx.container.getTcpPort(8080).fetch(request); + private async startAndWaitForPort(): Promise { + const container = this.ctx.container!; + if (!container.running) { + container.start(); + } + + const port = container.getTcpPort(8080); + let lastError: unknown; + for (let attempt = 0; attempt < 100; attempt++) { + try { + const response = await port.fetch("http://container/health"); + if (!response.ok) { + throw new Error(`Health check returned ${response.status}`); + } + return; + } catch (error) { + lastError = error; + await scheduler.wait(200); + } + } + throw new Error("Container did not become ready on port 8080", { + cause: lastError, + }); } } ``` -The `running` property does not indicate port readiness. Check the required port before routing the first request if your process needs time to start. + + +The `running` property does not indicate port readiness. This example checks the port before routing requests and starts the container again after inactivity stops it. For all methods, refer to the [Durable Object Container API](/containers/api/durable-object-container/). @@ -94,6 +140,8 @@ For all methods, refer to the [Durable Object Container API](/containers/api/dur The [`Container` class](https://github.com/cloudflare/containers) extends `DurableObject`. It adds default routing, readiness checks, lifecycle hooks, activity tracking, and scheduled callbacks. + + ```ts import { Container } from "@cloudflare/containers"; @@ -103,4 +151,6 @@ export class MyContainer extends Container { } ``` + + These helpers reduce application code. They also add lifecycle state and scheduled work to the Durable Object. For all properties and methods, refer to the [Container class API](/containers/api/container-class/). diff --git a/src/content/docs/containers/concepts/architecture.mdx b/src/content/docs/containers/concepts/architecture.mdx index 631e67ff529..345b39d917f 100644 --- a/src/content/docs/containers/concepts/architecture.mdx +++ b/src/content/docs/containers/concepts/architecture.mdx @@ -141,7 +141,7 @@ The [`Container` class](/containers/api/container-class/) adds hooks that run Wo - [`onStart()`](/containers/api/container-class/#onstart) — Runs after the container has started. - [`onStop()`](/containers/api/container-class/#onstop) — Runs after the container process exits. Receives the exit code and reason for the stop. - [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) — Runs when the [`sleepAfter`](/containers/api/container-class/#sleepafter) timer expires with no incoming requests. The default implementation calls `stop()` to shut down the container. You can use this to only stop the container on certain conditions. -- [`onError()`](/containers/api/container-class/#onerror) — Runs when the container exits with an error. +- [`onError()`](/containers/api/container-class/#onerror) — Runs when container startup or port checking fails. Refer to the [status hooks example](/containers/examples/status-hooks/) for a full implementation. diff --git a/src/content/docs/containers/configuration/wrangler.mdx b/src/content/docs/containers/configuration/wrangler.mdx index 3b5597754c3..932f24f8608 100644 --- a/src/content/docs/containers/configuration/wrangler.mdx +++ b/src/content/docs/containers/configuration/wrangler.mdx @@ -57,7 +57,7 @@ The configuration uses three sections: 2. **`durable_objects.bindings`** makes the Durable Object namespace available to Worker code. In this example, access it through `env.MY_CONTAINER`. 3. **`migrations`** creates the SQLite-backed Durable Object class. Use `new_sqlite_classes`, not `new_classes`, for a Container. -The `class_name` in all three sections must match the exported Durable Object class in your Worker. +The `class_name` in `containers` and `durable_objects.bindings`, and the class listed in `migrations.new_sqlite_classes`, must match the exported Durable Object class in your Worker. ## Container settings diff --git a/src/content/docs/containers/examples/container-backend.mdx b/src/content/docs/containers/examples/container-backend.mdx index e44a4b4d9c4..6d9b9011017 100644 --- a/src/content/docs/containers/examples/container-backend.mdx +++ b/src/content/docs/containers/examples/container-backend.mdx @@ -10,7 +10,13 @@ products: - containers --- -import { WranglerConfig, Details, TabItem, Tabs, TypeScriptExample } from "~/components"; +import { + WranglerConfig, + Details, + TabItem, + Tabs, + TypeScriptExample, +} from "~/components"; A common pattern is to serve a static frontend application (e.g., React, Vue, Svelte) using Static Assets, then pass backend requests to a containerized backend application. @@ -25,8 +31,9 @@ For a full example, see the [Static Frontend + Container Backend Template](https ```json { - "name": "cron-container", + "name": "static-frontend-container-backend", "main": "src/index.ts", + "compatibility_date": "$today", "assets": { "directory": "./dist", "binding": "ASSETS" @@ -138,6 +145,8 @@ Your Worker needs to be able to both serve static assets and route requests to t In this case, we will pass requests to one of three container instances if the route starts with `/api`, and all other requests will be served as static assets. +For the direct API example, expose `GET /health` on port 8080. Return a successful response only when the backend is ready. + @@ -153,35 +162,58 @@ interface Env { } export class Backend extends DurableObject { + private ready: Promise | undefined; + constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); + ctx.blockConcurrencyWhile(() => + ctx.container!.setInactivityTimeout(2 * 60 * 60 * 1000), + ); + } - ctx.blockConcurrencyWhile(async () => { - const container = ctx.container!; - await container.setInactivityTimeout(2 * 60 * 60 * 1000); + async fetch(request: Request): Promise { + const container = this.ctx.container!; + if (!container.running) { + this.ready = undefined; + } + this.ready ??= this.startAndWaitForPort().catch((error: unknown) => { + this.ready = undefined; + throw error; + }); + await this.ready; - if (!container.running) { - container.start(); - } + const url = new URL(request.url); + url.protocol = "http:"; + url.host = "container"; + const forwarded = new Request(url, request); + forwarded.headers.delete("host"); + return container.getTcpPort(8080).fetch(forwarded); + } - const port = container.getTcpPort(8080); - let lastError: unknown; - for (let attempt = 0; attempt < 50; attempt++) { - try { - await port.fetch("http://container/"); - return; - } catch (error) { - lastError = error; - await scheduler.wait(100); + private async startAndWaitForPort(): Promise { + const container = this.ctx.container!; + if (!container.running) { + container.start(); + } + + const port = container.getTcpPort(8080); + let lastError: unknown; + for (let attempt = 0; attempt < 100; attempt++) { + try { + const response = await port.fetch("http://container/health"); + if (!response.ok) { + throw new Error(`Health check returned ${response.status}`); } + return; + } catch (error) { + lastError = error; + await scheduler.wait(200); } - throw lastError; + } + throw new Error("Backend did not become ready on port 8080", { + cause: lastError, }); } - - fetch(request: Request): Promise { - return this.ctx.container!.getTcpPort(8080).fetch(request); - } } export default { @@ -200,18 +232,24 @@ export default { -```javascript + +```ts import { Container, getRandom } from "@cloudflare/containers"; const INSTANCE_COUNT = 3; -class Backend extends Container { +interface Env { + ASSETS: Fetcher; + BACKEND: DurableObjectNamespace; +} + +export class Backend extends Container { defaultPort = 8080; // pass requests to port 8080 in the container sleepAfter = "2h"; // only sleep a container if it hasn't gotten requests in 2 hours } export default { - async fetch(request, env) { + async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); if (url.pathname.startsWith("/api")) { const containerInstance = await getRandom(env.BACKEND, INSTANCE_COUNT); @@ -222,6 +260,7 @@ export default { }, }; ``` + @@ -252,7 +291,7 @@ import ( "net/http" ) -func handler(w http.ResponseWriter, r \*http.Request) { +func handler(w http.ResponseWriter, r *http.Request) { widgets := []map[string]interface{}{ {"id": 1, "name": "Widget A"}, {"id": 2, "name": "Sprocket B"}, @@ -266,6 +305,9 @@ func handler(w http.ResponseWriter, r \*http.Request) { } func main() { + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) http.HandleFunc("/api/widgets", handler) log.Fatal(http.ListenAndServe(":8080", nil)) } @@ -273,3 +315,21 @@ func main() { ``` + +The health endpoint lets the Worker wait for the backend before forwarding requests. Build the backend image with this Dockerfile in the project root: + +
+ +```dockerfile +FROM golang:1.25-alpine AS build +WORKDIR /app +COPY server.go . +RUN CGO_ENABLED=0 go build -o /server server.go + +FROM alpine:3.20 +COPY --from=build /server /server +EXPOSE 8080 +CMD ["/server"] +``` + +
diff --git a/src/content/docs/containers/examples/cron.mdx b/src/content/docs/containers/examples/cron.mdx index 9f06cadc5e2..f048ced8708 100644 --- a/src/content/docs/containers/examples/cron.mdx +++ b/src/content/docs/containers/examples/cron.mdx @@ -20,10 +20,11 @@ Use a cron expression in your Wrangler config to specify the schedule: -```json +```jsonc { "name": "cron-container", "main": "src/index.ts", + "compatibility_date": "$today", "triggers": { "crons": [ "*/2 * * * *" // Run every 2 minutes @@ -54,7 +55,7 @@ Use a cron expression in your Wrangler config to specify the schedule: -Then call the Container from the `scheduled()` handler in the Worker. The raw API example expects the Container to expose a `POST /run` endpoint that starts the scheduled task. +Then call the Container from the `scheduled()` handler in the Worker. The raw API example expects the Container to expose a `GET /health` endpoint that returns success when ready and a `POST /run` endpoint that starts the scheduled task. @@ -87,14 +88,17 @@ export class CronContainer extends DurableObject { const port = container.getTcpPort(8080); let lastError: unknown; - for (let attempt = 0; attempt < 50; attempt++) { + for (let attempt = 0; attempt < 100; attempt++) { try { - await port.fetch("http://container/"); + const response = await port.fetch("http://container/health"); + if (!response.ok) { + throw new Error(`Health check returned ${response.status}`); + } lastError = undefined; break; } catch (error) { lastError = error; - await scheduler.wait(100); + await scheduler.wait(200); } } if (lastError) { @@ -127,6 +131,7 @@ export default { + ```ts import { Container, getContainer } from "@cloudflare/containers"; @@ -157,6 +162,7 @@ export default { }, }; ``` + diff --git a/src/content/docs/containers/examples/env-vars-and-secrets.mdx b/src/content/docs/containers/examples/env-vars-and-secrets.mdx index 0fc5aa497ae..e14be3dfe93 100644 --- a/src/content/docs/containers/examples/env-vars-and-secrets.mdx +++ b/src/content/docs/containers/examples/env-vars-and-secrets.mdx @@ -10,9 +10,15 @@ products: - containers --- -import { PackageManagers, TabItem, Tabs, TypeScriptExample, WranglerConfig } from "~/components"; +import { + PackageManagers, + TabItem, + Tabs, + TypeScriptExample, + WranglerConfig, +} from "~/components"; -Environment variables can be passed when the Durable Object Container API starts a Container, or through the `envVars` field on the [`Container`](/containers/api/container-class/) class. +You can pass environment variables when the Durable Object Container API starts a Container, or through the `envVars` field on the [`Container`](/containers/api/container-class/) class. Secrets can be passed into a Container by using [Worker Secrets](/workers/configuration/secrets/) or the [Secret Store](/secrets-store/integrations/workers/), then passing them into the Container @@ -47,10 +53,12 @@ the `"SECRET_STORE_SECRET"` secret to it: args="secrets-store store create demo --remote" /> +Copy the store ID returned by this command. The following commands and the Wrangler configuration require the ID, not the store name. + Next, let's create a KV namespace called `DEMO_KV` and add a key-value pair: @@ -64,9 +72,11 @@ Next, let's create a KV namespace called `DEMO_KV` and add a key-value pair: +Replace `` with the ID returned by `kv namespace create`. The `--namespace-id` flag works before you add the binding to your Wrangler configuration; `--remote` stores the value in the namespace used by the deployed Worker. + For full details on how to create secrets, see the [Workers Secrets documentation](/workers/configuration/secrets/) and the [Secret Store documentation](/secrets-store/integrations/workers/). For KV setup, see the [Workers KV documentation](/kv/). @@ -77,7 +87,7 @@ in Wrangler configuration. -```json +```jsonc { "name": "my-container-worker", "vars": { @@ -87,7 +97,7 @@ in Wrangler configuration. "secrets_store_secrets": [ { "binding": "SECRET_STORE", - "store_id": "demo", + "store_id": "", "secret_name": "SECRET_STORE_SECRET" } ], @@ -249,6 +259,8 @@ export default { ```js +import { Container } from "@cloudflare/containers"; + export class MyContainer extends Container { defaultPort = 8080; sleepAfter = "10s"; @@ -309,6 +321,30 @@ Here are common patterns for using KV with containers: ```ts +import { DurableObject } from "cloudflare:workers"; + +interface Env { + CONTAINER_IMAGE: string; + DEMO_KV: KVNamespace; + MY_CONTAINER: DurableObjectNamespace; +} + +export class MyContainer extends DurableObject { + launch(image: string, env: Record): void { + if (this.ctx.container!.running) { + throw new Error("Container is already running"); + } + this.ctx.container!.start({ image, enableInternet: true, env }); + } +} + +function required(value: string | null, name: string): string { + if (value === null) { + throw new Error(`${name} was not found`); + } + return value; +} + export default { async fetch(request: Request, env: Env): Promise { if (new URL(request.url).pathname !== "/configure-container") { @@ -379,6 +415,30 @@ export default { ```ts +import { DurableObject } from "cloudflare:workers"; + +interface Env { + CONTAINER_IMAGE: string; + DEMO_KV: KVNamespace; + MY_CONTAINER: DurableObjectNamespace; +} + +export class MyContainer extends DurableObject { + launch(image: string, env: Record): void { + if (this.ctx.container!.running) { + throw new Error("Container is already running"); + } + this.ctx.container!.start({ image, enableInternet: true, env }); + } +} + +function required(value: string | null, name: string): string { + if (value === null) { + throw new Error(`${name} was not found`); + } + return value; +} + export default { async fetch(request: Request, env: Env): Promise { if (new URL(request.url).pathname !== "/launch-with-features") { diff --git a/src/content/docs/containers/examples/r2-fuse-mount.mdx b/src/content/docs/containers/examples/r2-fuse-mount.mdx index c179f5f39d8..80815c612bd 100644 --- a/src/content/docs/containers/examples/r2-fuse-mount.mdx +++ b/src/content/docs/containers/examples/r2-fuse-mount.mdx @@ -11,7 +11,7 @@ products: - r2 --- -import { Details, TabItem, Tabs, TypeScriptExample } from "~/components"; +import { Details, TypeScriptExample, WranglerConfig } from "~/components"; FUSE (Filesystem in Userspace) allows you to mount [R2 buckets](/r2/) as filesystems within Containers. Applications can then interact with R2 using standard filesystem operations rather than object storage APIs. @@ -39,20 +39,21 @@ To mount an R2 bucket, install a FUSE adapter in your Dockerfile and configure i This example uses [tigrisfs](https://github.com/tigrisdata/tigrisfs), which supports S3-compatible storage including R2:
+ ```dockerfile FROM alpine:3.20 # Install FUSE and dependencies -RUN apk add --no-cache \ +RUN apk add --no-cache \ --repository http://dl-cdn.alpinelinux.org/alpine/v3.20/main \ ca-certificates fuse curl bash -# Install tigrisfs +# Install the tested tigrisfs release +ARG TIGRISFS_VERSION=v1.2.2 RUN ARCH=$(uname -m) && \ if [ "$ARCH" = "x86_64" ]; then ARCH="amd64"; fi && \ if [ "$ARCH" = "aarch64" ]; then ARCH="arm64"; fi && \ - VERSION=$(curl -s https://api.github.com/repos/tigrisdata/tigrisfs/releases/latest | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4) && \ - curl -L "https://github.com/tigrisdata/tigrisfs/releases/download/${VERSION}/tigrisfs_${VERSION#v}_linux_${ARCH}.tar.gz" -o /tmp/tigrisfs.tar.gz && \ + curl -fL "https://github.com/tigrisdata/tigrisfs/releases/download/${TIGRISFS_VERSION}/tigrisfs_${TIGRISFS_VERSION#v}_linux_${ARCH}.tar.gz" -o /tmp/tigrisfs.tar.gz && \ tar -xzf /tmp/tigrisfs.tar.gz -C /usr/local/bin/ && \ rm /tmp/tigrisfs.tar.gz && \ chmod +x /usr/local/bin/tigrisfs @@ -61,12 +62,26 @@ RUN ARCH=$(uname -m) && \ RUN printf '#!/bin/sh\n\ set -e\n\ \n\ + : "${R2_ACCOUNT_ID:?R2_ACCOUNT_ID is required}"\n\ + : "${R2_BUCKET_NAME:?R2_BUCKET_NAME is required}"\n\ + : "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is required}"\n\ + : "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY is required}"\n\ + \n\ mkdir -p /mnt/r2\n\ \n\ R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"\n\ echo "Mounting bucket ${R2_BUCKET_NAME}..."\n\ /usr/local/bin/tigrisfs --endpoint "${R2_ENDPOINT}" -f "${R2_BUCKET_NAME}" /mnt/r2 &\n\ - sleep 3\n\ + mount_pid=$!\n\ + attempts=0\n\ + until mountpoint -q /mnt/r2; do\n\ + if ! kill -0 "$mount_pid" 2>/dev/null || [ "$attempts" -ge 30 ]; then\n\ + echo "R2 mount failed" >&2\n\ + exit 1\n\ + fi\n\ + attempts=$((attempts + 1))\n\ + sleep 1\n\ + done\n\ \n\ echo "Contents of mounted bucket:"\n\ ls -lah /mnt/r2\n\ @@ -75,24 +90,22 @@ RUN printf '#!/bin/sh\n\ EXPOSE 8080 CMD ["/startup.sh"] ``` +
-The startup script creates a mount point, starts tigrisfs in the background to mount the bucket, and then lists the mounted directory contents. +The startup script checks the required credentials, starts tigrisfs, and waits for the mount to be ready before listing the mounted directory. It exits with an error if mounting fails. ### Passing credentials to the container Your Container needs [R2 credentials](/r2/api/tokens/) and configuration passed as environment variables. Store credentials as [Worker secrets](/workers/configuration/secrets/), then pass them when the Container starts. - - - + ```ts import { DurableObject } from "cloudflare:workers"; interface Env { FUSE_DEMO: DurableObjectNamespace; - FUSE_IMAGE: string; AWS_ACCESS_KEY_ID: string; AWS_SECRET_ACCESS_KEY: string; R2_BUCKET_NAME: string; @@ -116,7 +129,6 @@ export class FUSEDemo extends DurableObject { } container.start({ - image: this.env.FUSE_IMAGE, enableInternet: true, env: { AWS_ACCESS_KEY_ID: this.env.AWS_ACCESS_KEY_ID, @@ -137,54 +149,44 @@ export default { }, }; ``` - - - - - -```ts -import { Container } from "@cloudflare/containers"; - -interface Env { - FUSE_DEMO: DurableObjectNamespace; - AWS_ACCESS_KEY_ID: string; - AWS_SECRET_ACCESS_KEY: string; - R2_BUCKET_NAME: string; - R2_ACCOUNT_ID: string; -} - -export class FUSEDemo extends Container { - defaultPort = 8080; - sleepAfter = "10m"; - envVars = { - AWS_ACCESS_KEY_ID: this.env.AWS_ACCESS_KEY_ID, - AWS_SECRET_ACCESS_KEY: this.env.AWS_SECRET_ACCESS_KEY, - R2_BUCKET_NAME: this.env.R2_BUCKET_NAME, - R2_ACCOUNT_ID: this.env.R2_ACCOUNT_ID, - }; -} -``` - - +This is a one-shot task: the Worker waits for the mount-and-list process to exit. A long-running service that keeps the mount available can instead use the [`Container` class](/containers/api/container-class/), but it must also provide the listening port that the class checks during startup. -The `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` should be stored as secrets, while `R2_BUCKET_NAME` and `R2_ACCOUNT_ID` can be configured as variables in your `wrangler.jsonc`: +Store `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` as secrets. Configure `R2_BUCKET_NAME` and `R2_ACCOUNT_ID` as variables in `wrangler.jsonc`. The container uses the image from this Wrangler configuration: :::note[Creating your R2 AWS API keys] To get your `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, [head to your R2 dashboard](https://dash.cloudflare.com/?to=/:account/r2/overview) and create a new R2 Access API key. Use the generated the `Access Key ID` as your `AWS_ACCESS_KEY_ID` and `Secret Access Key` is the `AWS_SECRET_ACCESS_KEY`. ::: -```json + + + +```jsonc { - "vars": { - "FUSE_IMAGE": "registry.cloudflare.com//r2-fuse:latest", - "R2_BUCKET_NAME": "my-bucket", - "R2_ACCOUNT_ID": "your-account-id" - } + "name": "r2-fuse-demo", + "main": "src/index.ts", + "compatibility_date": "$today", + "containers": [ + { + "class_name": "FUSEDemo", + "image": "./Dockerfile", + "max_instances": 1, + }, + ], + "durable_objects": { + "bindings": [{ "name": "FUSE_DEMO", "class_name": "FUSEDemo" }], + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["FUSEDemo"] }], + "vars": { + "R2_BUCKET_NAME": "my-bucket", + "R2_ACCOUNT_ID": "your-account-id", + }, } ``` + + ### Other S3-compatible storage providers Other S3-compatible storage providers, including AWS S3 and Google Cloud Storage, can be mounted using the same approach as R2. You will need to provide the appropriate endpoint URL and access credentials for the storage provider. @@ -203,7 +205,13 @@ RUN printf '#!/bin/sh\n\ \n\ R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"\n\ /usr/local/bin/tigrisfs --endpoint "${R2_ENDPOINT}" -f "${R2_BUCKET_NAME}" /mnt/r2 &\n\ - sleep 3\n\ + mount_pid=$!\n\ + attempts=0\n\ + until mountpoint -q /mnt/r2; do\n\ + if ! kill -0 "$mount_pid" 2>/dev/null || [ "$attempts" -ge 30 ]; then exit 1; fi\n\ + attempts=$((attempts + 1))\n\ + sleep 1\n\ + done\n\ \n\ echo "Accessing prefix: ${BUCKET_PREFIX}"\n\ ls -lah "/mnt/r2/${BUCKET_PREFIX}"\n\ @@ -224,7 +232,13 @@ RUN printf '#!/bin/sh\n\ \n\ R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"\n\ /usr/local/bin/tigrisfs --endpoint "${R2_ENDPOINT}" -o ro -f "${R2_BUCKET_NAME}" /mnt/r2 &\n\ - sleep 3\n\ + mount_pid=$!\n\ + attempts=0\n\ + until mountpoint -q /mnt/r2; do\n\ + if ! kill -0 "$mount_pid" 2>/dev/null || [ "$attempts" -ge 30 ]; then exit 1; fi\n\ + attempts=$((attempts + 1))\n\ + sleep 1\n\ + done\n\ \n\ ls -lah /mnt/r2\n\ ' > /startup.sh && chmod +x /startup.sh diff --git a/src/content/docs/containers/examples/stateless.mdx b/src/content/docs/containers/examples/stateless.mdx index 37c1fc4e6d2..c154f1c7198 100644 --- a/src/content/docs/containers/examples/stateless.mdx +++ b/src/content/docs/containers/examples/stateless.mdx @@ -14,6 +14,8 @@ import { TabItem, Tabs, TypeScriptExample } from "~/components"; To proxy requests across a fixed number of Container instances, select an instance name and forward the request through its Durable Object. +For the direct API example, expose a `GET /health` endpoint on port 8080 that returns a successful response when the application is ready. + @@ -28,35 +30,58 @@ interface Env { } export class Backend extends DurableObject { + private ready: Promise | undefined; + constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); + ctx.blockConcurrencyWhile(() => + ctx.container!.setInactivityTimeout(2 * 60 * 60 * 1000), + ); + } - ctx.blockConcurrencyWhile(async () => { - const container = ctx.container!; - await container.setInactivityTimeout(2 * 60 * 60 * 1000); - - if (!container.running) { - container.start(); - } + async fetch(request: Request): Promise { + const container = this.ctx.container!; + if (!container.running) { + this.ready = undefined; + } + this.ready ??= this.startAndWaitForPort().catch((error: unknown) => { + this.ready = undefined; + throw error; + }); + await this.ready; + + const url = new URL(request.url); + url.protocol = "http:"; + url.host = "container"; + const forwarded = new Request(url, request); + forwarded.headers.delete("host"); + return container.getTcpPort(8080).fetch(forwarded); + } - const port = container.getTcpPort(8080); - let lastError: unknown; - for (let attempt = 0; attempt < 50; attempt++) { - try { - await port.fetch("http://container/"); - return; - } catch (error) { - lastError = error; - await scheduler.wait(100); + private async startAndWaitForPort(): Promise { + const container = this.ctx.container!; + if (!container.running) { + container.start(); + } + + const port = container.getTcpPort(8080); + let lastError: unknown; + for (let attempt = 0; attempt < 100; attempt++) { + try { + const response = await port.fetch("http://container/health"); + if (!response.ok) { + throw new Error(`Health check returned ${response.status}`); } + return; + } catch (error) { + lastError = error; + await scheduler.wait(200); } - throw lastError; + } + throw new Error("Container did not become ready on port 8080", { + cause: lastError, }); } - - fetch(request: Request): Promise { - return this.ctx.container!.getTcpPort(8080).fetch(request); - } } export default { @@ -71,12 +96,17 @@ export default { + ```ts import { Container, getRandom } from "@cloudflare/containers"; const INSTANCE_COUNT = 3; -class Backend extends Container { +interface Env { + BACKEND: DurableObjectNamespace; +} + +export class Backend extends Container { defaultPort = 8080; sleepAfter = "2h"; } @@ -88,6 +118,7 @@ export default { }, }; ``` + diff --git a/src/content/docs/containers/examples/status-hooks.mdx b/src/content/docs/containers/examples/status-hooks.mdx index e8b59689eea..a41814fd15f 100644 --- a/src/content/docs/containers/examples/status-hooks.mdx +++ b/src/content/docs/containers/examples/status-hooks.mdx @@ -15,6 +15,8 @@ import { TabItem, Tabs, TypeScriptExample } from "~/components"; Use `monitor()` with the Durable Object Container API to run code after the Container exits or errors. The `Container` class adds named lifecycle hooks and an inactivity callback. +For the direct API example, expose `GET /health` on port 4000. Return a successful response once the application is ready. + @@ -45,7 +47,12 @@ export class MyContainer extends DurableObject { }); await this.ready; - return container.getTcpPort(4000).fetch(request); + const url = new URL(request.url); + url.protocol = "http:"; + url.host = "container"; + const forwarded = new Request(url, request); + forwarded.headers.delete("host"); + return container.getTcpPort(4000).fetch(forwarded); } private async startAndMonitor(): Promise { @@ -63,17 +70,22 @@ export class MyContainer extends DurableObject { const port = container.getTcpPort(4000); let lastError: unknown; - for (let attempt = 0; attempt < 50; attempt++) { + for (let attempt = 0; attempt < 100; attempt++) { try { - await port.fetch("http://container/"); + const response = await port.fetch("http://container/health"); + if (!response.ok) { + throw new Error(`Health check returned ${response.status}`); + } console.log("Container successfully started"); return; } catch (error) { lastError = error; - await scheduler.wait(100); + await scheduler.wait(200); } } - throw lastError; + throw new Error("Container did not become ready on port 4000", { + cause: lastError, + }); } } ``` @@ -82,8 +94,9 @@ export class MyContainer extends DurableObject { + ```ts -import { Container } from "@cloudflare/containers"; +import { Container, type StopParams } from "@cloudflare/containers"; export class MyContainer extends Container { defaultPort = 4000; @@ -93,7 +106,7 @@ export class MyContainer extends Container { console.log("Container successfully started"); } - override onStop(stopParams) { + override onStop(stopParams: StopParams) { if (stopParams.exitCode === 0) { console.log("Container stopped gracefully"); } else { @@ -108,11 +121,12 @@ export class MyContainer extends Container { await this.stop(); } - override onError(error: string) { + override onError(error: unknown) { console.log("Container error:", error); } } ``` + diff --git a/src/content/docs/containers/examples/websocket.mdx b/src/content/docs/containers/examples/websocket.mdx index b8a92a1955f..ab4477fc983 100644 --- a/src/content/docs/containers/examples/websocket.mdx +++ b/src/content/docs/containers/examples/websocket.mdx @@ -14,6 +14,8 @@ import { TabItem, Tabs, TypeScriptExample } from "~/components"; Forward an incoming WebSocket upgrade request through the Durable Object to the listening port on the Container. +For the direct API example, expose a `GET /health` endpoint on port 8080 that returns a successful response when the application is ready. Keep the WebSocket endpoint separate from the health check. + @@ -26,35 +28,58 @@ interface Env { } export class MyContainer extends DurableObject { + private ready: Promise | undefined; + constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); + ctx.blockConcurrencyWhile(() => + ctx.container!.setInactivityTimeout(2 * 60 * 1000), + ); + } - ctx.blockConcurrencyWhile(async () => { - const container = ctx.container!; - await container.setInactivityTimeout(2 * 60 * 1000); - - if (!container.running) { - container.start(); - } + async fetch(request: Request): Promise { + const container = this.ctx.container!; + if (!container.running) { + this.ready = undefined; + } + this.ready ??= this.startAndWaitForPort().catch((error: unknown) => { + this.ready = undefined; + throw error; + }); + await this.ready; + + const url = new URL(request.url); + url.protocol = "http:"; + url.host = "container"; + const forwarded = new Request(url, request); + forwarded.headers.delete("host"); + return container.getTcpPort(8080).fetch(forwarded); + } - const port = container.getTcpPort(8080); - let lastError: unknown; - for (let attempt = 0; attempt < 50; attempt++) { - try { - await port.fetch("http://container/"); - return; - } catch (error) { - lastError = error; - await scheduler.wait(100); + private async startAndWaitForPort(): Promise { + const container = this.ctx.container!; + if (!container.running) { + container.start(); + } + + const port = container.getTcpPort(8080); + let lastError: unknown; + for (let attempt = 0; attempt < 100; attempt++) { + try { + const response = await port.fetch("http://container/health"); + if (!response.ok) { + throw new Error(`Health check returned ${response.status}`); } + return; + } catch (error) { + lastError = error; + await scheduler.wait(200); } - throw lastError; + } + throw new Error("Container did not become ready on port 8080", { + cause: lastError, }); } - - fetch(request: Request): Promise { - return this.ctx.container!.getTcpPort(8080).fetch(request); - } } export default { @@ -68,21 +93,27 @@ export default { -```js + +```ts import { Container, getContainer } from "@cloudflare/containers"; -export class MyContainer extends Container { +interface Env { + MY_CONTAINER: DurableObjectNamespace; +} + +export class MyContainer extends Container { defaultPort = 8080; sleepAfter = "2m"; } export default { - async fetch(request, env) { + async fetch(request: Request, env: Env): Promise { // gets default instance and forwards websocket from outside Worker return getContainer(env.MY_CONTAINER).fetch(request); }, }; ``` + From 7616d6a32fe5c1d70ac149bbd79884aa4f1b2707 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Wed, 23 Sep 2026 18:22:25 -0400 Subject: [PATCH 05/23] [Containers] Clarify recommended API guidance --- src/content/docs/containers/api/index.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/content/docs/containers/api/index.mdx b/src/content/docs/containers/api/index.mdx index dac45754255..669e201a379 100644 --- a/src/content/docs/containers/api/index.mdx +++ b/src/content/docs/containers/api/index.mdx @@ -13,7 +13,7 @@ import { CardGrid, LinkTitleCard, TypeScriptExample } from "~/components"; Containers provide two APIs for managing a container from a Durable Object. Both APIs address the same container runtime. -For new applications, use the Durable Object Container API when you need direct lifecycle control. Use the `Container` class when you prefer built-in lifecycle helpers. +For new applications, we recommend the Durable Object Container API. It gives you direct control over the container lifecycle and access to Durable Object features such as storage, alarms, and request routing. Use the `Container` class when you prefer built-in lifecycle helpers. @@ -40,11 +40,11 @@ For new applications, use the Durable Object Container API when you need direct ### Durable Object Container API -The Durable Object Container API exposes the container runtime through `ctx.container`. Choose it when you need direct control over startup, shutdown, networking, or resource usage. You can add readiness checks, custom request routing, or lifecycle policies when your application needs them. +The Durable Object Container API is recommended for new applications. Inside a Durable Object, use `ctx.container` to control the container runtime directly. You can manage startup, shutdown, networking, and resource usage and add readiness checks, custom request routing, or lifecycle policies when needed. ### Container class -The `Container` class builds on Durable Objects and the runtime API. Choose it when you prefer built-in request proxying, readiness checks, lifecycle hooks, and scheduling. These helpers reduce application code, but some features use Durable Object storage and alarms. +The `Container` class builds on Durable Objects and the runtime API. Choose it when you prefer built-in request proxying, readiness checks, lifecycle hooks, and scheduling. These helpers reduce application code by handling common Durable Object lifecycle tasks for you. Some features still use Durable Object storage and alarms. The following table compares both options: From 74539ca7b9331ec08d458fb6aa47cee6b1c5410d Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Wed, 23 Sep 2026 18:25:41 -0400 Subject: [PATCH 06/23] [Containers] Remove redundant API description heading --- src/content/docs/containers/api/durable-object-container.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index 54e45c708d9..e8d36760f41 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -11,8 +11,6 @@ products: import { TypeScriptExample } from "~/components"; -## Description - Each [container](/containers/) is managed 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. From fc9b549186de7f2f51f3e6c13514e179af292fd4 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Wed, 23 Sep 2026 18:28:15 -0400 Subject: [PATCH 07/23] [Containers] Clarify Durable Object proxying --- src/content/docs/containers/api/durable-object-container.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index e8d36760f41..6e71870a7f7 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -11,7 +11,7 @@ products: import { TypeScriptExample } from "~/components"; -Each [container](/containers/) is managed by a Durable Object. The Durable Object manages routing and persistent state. The container process runs your image inside a Linux VM. +Each [container](/containers/) 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. From 8cbeb180cb4d4eefeea17e54e91b2b222c98ccb1 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Wed, 23 Sep 2026 18:30:43 -0400 Subject: [PATCH 08/23] [Containers] Highlight recommended API in note --- src/content/docs/containers/api/durable-object-container.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index 6e71870a7f7..dc084b56120 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -15,7 +15,9 @@ Each [container](/containers/) is managed and proxied by a Durable Object. The D 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. -You can instead use the [`Container` class](/containers/api/container-class/) from `@cloudflare/containers`. The class adds routing, readiness checks, lifecycle hooks, activity tracking, and scheduling. To compare both APIs, refer to [Containers APIs](/containers/api/). +:::note +We recommend starting new applications with the Durable Object Container API. If you prefer built-in lifecycle helpers, use the [`Container` class](/containers/api/container-class/) from `@cloudflare/containers`. The class adds routing, readiness checks, lifecycle hooks, activity tracking, and scheduling. To compare both APIs, refer to [Containers APIs](/containers/api/). +::: Your Durable Object also has access to [SQLite storage](/durable-objects/api/sqlite-storage-api/) through `this.ctx.storage`, [alarms](/durable-objects/api/alarms/), and all other Durable Object APIs. From e5f530576a45b1f9c772f74a46bfc6bf37f9de57 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Wed, 23 Sep 2026 18:33:12 -0400 Subject: [PATCH 09/23] [Containers] Explain Durable Object benefits for containers --- src/content/docs/containers/api/durable-object-container.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index dc084b56120..0fc4df1ee18 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -21,6 +21,8 @@ We recommend starting new applications with the Durable Object Container API. If Your Durable Object also has access to [SQLite storage](/durable-objects/api/sqlite-storage-api/) through `this.ctx.storage`, [alarms](/durable-objects/api/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. + ```ts From e2a233030e18e7f5bcd44aaae81bd8354fa9f583 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Thu, 24 Sep 2026 20:51:46 -0400 Subject: [PATCH 10/23] [Containers] Focus API docs on migration and navigation --- .../docs/containers/api/container-class.mdx | 2 + .../api/durable-object-container.mdx | 20 +--- src/content/docs/containers/api/index.mdx | 106 +++++------------- .../examples/env-vars-and-secrets.mdx | 40 +++---- ...igrate-to-durable-object-container-api.mdx | 49 ++++++++ src/content/docs/sandbox/index.mdx | 2 + 6 files changed, 99 insertions(+), 120 deletions(-) create mode 100644 src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx diff --git a/src/content/docs/containers/api/container-class.mdx b/src/content/docs/containers/api/container-class.mdx index bf8c8eebc12..dd8c1f6eaa7 100644 --- a/src/content/docs/containers/api/container-class.mdx +++ b/src/content/docs/containers/api/container-class.mdx @@ -12,6 +12,8 @@ import { PackageManagers, TypeScriptExample } from "~/components"; The [`Container` class](https://github.com/cloudflare/containers) from [`@cloudflare/containers`](https://www.npmjs.com/package/@cloudflare/containers) provides lifecycle helpers for container instances. For direct lifecycle control, use the [Durable Object Container API](/containers/api/durable-object-container/). +To move an existing application to direct control, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). + **`Container` extends [`DurableObject`](/durable-objects/api/base/).** 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`](/durable-objects/api/sqlite-storage-api/) for persistent SQLite-backed storage and [`this.ctx.id`](/durable-objects/api/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. diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index 0fc4df1ee18..a4ebc439edf 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -15,6 +15,8 @@ Each [container](/containers/) is managed and proxied by a Durable Object. The D 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. +If your application uses the `Container` class or Sandbox SDK, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). + :::note We recommend starting new applications with the Durable Object Container API. If you prefer built-in lifecycle helpers, use the [`Container` class](/containers/api/container-class/) from `@cloudflare/containers`. The class adds routing, readiness checks, lifecycle hooks, activity tracking, and scheduling. To compare both APIs, refer to [Containers APIs](/containers/api/). ::: @@ -68,29 +70,17 @@ You may want to confirm the container is ready to accept requests before using i ```ts -this.ctx.container.start({ - image: "registry.cloudflare.com//my-container:latest", - env: { - FOO: "bar", - }, - enableInternet: false, - entrypoint: ["node", "server.js"], -}); +this.ctx.container.start(); ``` #### Parameters -- `options` (optional): Omit to use the image configured in Wrangler. When provided, set `enableInternet` and either `image` or `containerSnapshot`, but not both: - - `image`: The container image to run. Specify this when selecting an image at runtime. - - `containerSnapshot`: A snapshot to restore instead of starting from an image. Provide its `id`. - - `enableInternet`: Whether the container can access the Internet. +- `options` (optional): An object with the existing startup options: - `env`: Environment variables to pass to the container. - `entrypoint`: The command and arguments to run in the container. - - `instance`: The instance type (`lite` or `standard-1` through `standard-4`) or a resource configuration with `vcpu`, `memoryMib`, and `diskMb`. - - `labels`: String key-value labels associated with the container. - - `directorySnapshots`: Directory snapshot restore configurations. Each entry accepts a `snapshot` and optional `mountPoint`, or a `mountPoint` alone. + - `enableInternet`: Whether to allow outbound Internet access. #### Return values diff --git a/src/content/docs/containers/api/index.mdx b/src/content/docs/containers/api/index.mdx index 669e201a379..55a391fcd47 100644 --- a/src/content/docs/containers/api/index.mdx +++ b/src/content/docs/containers/api/index.mdx @@ -42,30 +42,7 @@ For new applications, we recommend the Durable Object Container API. It gives yo The Durable Object Container API is recommended for new applications. Inside a Durable Object, use `ctx.container` to control the container runtime directly. You can manage startup, shutdown, networking, and resource usage and add readiness checks, custom request routing, or lifecycle policies when needed. -### Container class - -The `Container` class builds on Durable Objects and the runtime API. Choose it when you prefer built-in request proxying, readiness checks, lifecycle hooks, and scheduling. These helpers reduce application code by handling common Durable Object lifecycle tasks for you. Some features still use Durable Object storage and alarms. - -The following table compares both options: - -| Requirement | Durable Object Container API | `Container` class | -| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Start and stop a container | [`start()`](/containers/api/durable-object-container/#start), [`signal()`](/containers/api/durable-object-container/#signal), and [`destroy()`](/containers/api/durable-object-container/#destroy) | [`start()`](/containers/api/container-class/#start), [`stop()`](/containers/api/container-class/#stop), and [`destroy()`](/containers/api/container-class/#destroy) | -| Send and proxy traffic | [`getTcpPort(port).fetch()`](/containers/api/durable-object-container/#gettcpport) and [`getTcpPort(port).connect()`](/containers/api/durable-object-container/#gettcpport) | [`fetch()`](/containers/api/container-class/#fetch) and [`containerFetch()`](/containers/api/container-class/#containerfetch) | -| Execute another process | [`exec()`](/containers/api/durable-object-container/#exec) | [`ctx.container.exec()`](/containers/api/container-class/#execute-commands) | -| Check port readiness | Use [`getTcpPort()`](/containers/api/durable-object-container/#gettcpport) in application code | [`startAndWaitForPorts()`](/containers/api/container-class/#startandwaitforports) and [`waitForPort()`](/containers/api/container-class/#waitforport) | -| Handle concurrent starts | Coordinate calls to [`start()`](/containers/api/durable-object-container/#start) when needed | Handled by [`start()`](/containers/api/container-class/#start) and [`startAndWaitForPorts()`](/containers/api/container-class/#startandwaitforports) | -| Run lifecycle hooks | [`monitor()`](/containers/api/durable-object-container/#monitor) and application code | [`onStart()`](/containers/api/container-class/#onstart), [`onStop()`](/containers/api/container-class/#onstop), [`onError()`](/containers/api/container-class/#onerror), and [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) | -| Stop inactive containers | [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout) | [`sleepAfter`](/containers/api/container-class/#sleepafter) and [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) | -| Schedule callbacks | [`ctx.storage.setAlarm()`](/durable-objects/api/alarms/#setalarm) (Durable Object API) | [`schedule()`](/containers/api/container-class/#schedule) | - -Use the Durable Object Container API for latency-sensitive workloads or workloads that need a smaller storage footprint. - -## Use the Durable Object Container API - -The Durable Object Container API is available through `ctx.container` of the Durable Object. It exposes the container runtime without adding lifecycle policy. - -The following example expects the container to serve `GET /health` on port 8080 and return a successful response once it is ready. +This Durable Object starts its configured container when it receives a request. Starting a container does not mean its ports are ready. @@ -75,70 +52,24 @@ import { DurableObject } from "cloudflare:workers"; interface Env {} export class MyContainer extends DurableObject { - private ready: Promise | undefined; - - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - ctx.blockConcurrencyWhile(() => - ctx.container!.setInactivityTimeout(10 * 60 * 1000), - ); - } - - async fetch(request: Request): Promise { + fetch(): Response { const container = this.ctx.container!; - if (!container.running) { - this.ready = undefined; + if (container.running) { + return new Response("Container is running"); } - this.ready ??= this.startAndWaitForPort().catch((error: unknown) => { - this.ready = undefined; - throw error; - }); - await this.ready; - - const url = new URL(request.url); - url.protocol = "http:"; - url.host = "container"; - const forwarded = new Request(url, request); - forwarded.headers.delete("host"); - return container.getTcpPort(8080).fetch(forwarded); - } - - private async startAndWaitForPort(): Promise { - const container = this.ctx.container!; - if (!container.running) { - container.start(); - } - - const port = container.getTcpPort(8080); - let lastError: unknown; - for (let attempt = 0; attempt < 100; attempt++) { - try { - const response = await port.fetch("http://container/health"); - if (!response.ok) { - throw new Error(`Health check returned ${response.status}`); - } - return; - } catch (error) { - lastError = error; - await scheduler.wait(200); - } - } - throw new Error("Container did not become ready on port 8080", { - cause: lastError, - }); + container.start(); + return new Response("Container is starting", { status: 202 }); } } ``` -The `running` property does not indicate port readiness. This example checks the port before routing requests and starts the container again after inactivity stops it. - -For all methods, refer to the [Durable Object Container API](/containers/api/durable-object-container/). +For request routing with a readiness check, refer to the [stateless application example](/containers/examples/stateless/). For all methods, refer to the [Durable Object Container API](/containers/api/durable-object-container/). -## Use the Container class +### Container class -The [`Container` class](https://github.com/cloudflare/containers) extends `DurableObject`. It adds default routing, readiness checks, lifecycle hooks, activity tracking, and scheduled callbacks. +The `Container` class builds on Durable Objects and the runtime API. Choose it when you prefer built-in request proxying, readiness checks, lifecycle hooks, and scheduling. These helpers reduce application code by handling common Durable Object lifecycle tasks for you. Some features still use Durable Object storage and alarms. @@ -153,4 +84,21 @@ export class MyContainer extends Container { -These helpers reduce application code. They also add lifecycle state and scheduled work to the Durable Object. For all properties and methods, refer to the [Container class API](/containers/api/container-class/). +For all properties and methods, refer to the [Container class API](/containers/api/container-class/). + +The following table compares both options: + +| Requirement | Durable Object Container API | `Container` class | +| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Start and stop a container | [`start()`](/containers/api/durable-object-container/#start), [`signal()`](/containers/api/durable-object-container/#signal), and [`destroy()`](/containers/api/durable-object-container/#destroy) | [`start()`](/containers/api/container-class/#start), [`stop()`](/containers/api/container-class/#stop), and [`destroy()`](/containers/api/container-class/#destroy) | +| Send and proxy traffic | [`getTcpPort(port).fetch()`](/containers/api/durable-object-container/#gettcpport) and [`getTcpPort(port).connect()`](/containers/api/durable-object-container/#gettcpport) | [`fetch()`](/containers/api/container-class/#fetch) and [`containerFetch()`](/containers/api/container-class/#containerfetch) | +| Execute another process | [`exec()`](/containers/api/durable-object-container/#exec) | [`ctx.container.exec()`](/containers/api/container-class/#execute-commands) | +| Check port readiness | Use [`getTcpPort()`](/containers/api/durable-object-container/#gettcpport) in application code | [`startAndWaitForPorts()`](/containers/api/container-class/#startandwaitforports) and [`waitForPort()`](/containers/api/container-class/#waitforport) | +| Handle concurrent starts | Coordinate calls to [`start()`](/containers/api/durable-object-container/#start) when needed | Handled by [`start()`](/containers/api/container-class/#start) and [`startAndWaitForPorts()`](/containers/api/container-class/#startandwaitforports) | +| Run lifecycle hooks | [`monitor()`](/containers/api/durable-object-container/#monitor) and application code | [`onStart()`](/containers/api/container-class/#onstart), [`onStop()`](/containers/api/container-class/#onstop), [`onError()`](/containers/api/container-class/#onerror), and [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) | +| Stop inactive containers | [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout) | [`sleepAfter`](/containers/api/container-class/#sleepafter) and [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) | +| Schedule callbacks | [`ctx.storage.setAlarm()`](/durable-objects/api/alarms/#setalarm) (Durable Object API) | [`schedule()`](/containers/api/container-class/#schedule) | + +Use the Durable Object Container API for latency-sensitive workloads or workloads that need a smaller storage footprint. + +For an existing application, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/) for paths from the `Container` class and Sandbox SDK. diff --git a/src/content/docs/containers/examples/env-vars-and-secrets.mdx b/src/content/docs/containers/examples/env-vars-and-secrets.mdx index e14be3dfe93..7a6db6258a3 100644 --- a/src/content/docs/containers/examples/env-vars-and-secrets.mdx +++ b/src/content/docs/containers/examples/env-vars-and-secrets.mdx @@ -91,8 +91,7 @@ in Wrangler configuration. { "name": "my-container-worker", "vars": { - "ENV_VAR": "my-env-var", - "CONTAINER_IMAGE": "registry.cloudflare.com//my-container:latest" + "ENV_VAR": "my-env-var" }, "secrets_store_secrets": [ { @@ -121,7 +120,7 @@ secrets, or KV values in the _container-related_ portion of the Wrangler configu ## Set environment variables for every instance -Pass synchronous Worker variables and secrets when the Container starts. When using the raw API with startup options, provide a deployed image reference. +Pass synchronous Worker variables and secrets when the Container starts. @@ -131,7 +130,6 @@ Pass synchronous Worker variables and secrets when the Container starts. When us import { DurableObject } from "cloudflare:workers"; interface Env { - CONTAINER_IMAGE: string; ENV_VAR: string; WORKER_SECRET: string; } @@ -143,7 +141,6 @@ export class MyContainer extends DurableObject { ctx.blockConcurrencyWhile(async () => { if (!ctx.container!.running) { ctx.container!.start({ - image: env.CONTAINER_IMAGE, enableInternet: true, env: { ENV_VAR: env.ENV_VAR, @@ -196,7 +193,6 @@ Pass the values when starting each instance. The raw API example defines a `laun import { DurableObject } from "cloudflare:workers"; interface Env { - CONTAINER_IMAGE: string; DEMO_KV: KVNamespace; ENV_VAR: string; MY_CONTAINER: DurableObjectNamespace; @@ -205,11 +201,11 @@ interface Env { } export class MyContainer extends DurableObject { - launch(image: string, env: Record): void { + launch(env: Record): void { if (this.ctx.container!.running) { throw new Error("Container is already running"); } - this.ctx.container!.start({ image, enableInternet: true, env }); + this.ctx.container!.start({ enableInternet: true, env }); } } @@ -234,13 +230,13 @@ export default { ); await Promise.all([ - env.MY_CONTAINER.getByName("foo").launch(env.CONTAINER_IMAGE, { + env.MY_CONTAINER.getByName("foo").launch({ ENV_VAR: `${env.ENV_VAR}foo`, WORKER_SECRET: env.WORKER_SECRET, SECRET_STORE_SECRET: secretStoreSecret, KV_VALUE: kvValue, }), - env.MY_CONTAINER.getByName("bar").launch(env.CONTAINER_IMAGE, { + env.MY_CONTAINER.getByName("bar").launch({ ENV_VAR: `${env.ENV_VAR}bar`, WORKER_SECRET: env.WORKER_SECRET, SECRET_STORE_SECRET: secretStoreSecret, @@ -324,17 +320,16 @@ Here are common patterns for using KV with containers: import { DurableObject } from "cloudflare:workers"; interface Env { - CONTAINER_IMAGE: string; DEMO_KV: KVNamespace; MY_CONTAINER: DurableObjectNamespace; } export class MyContainer extends DurableObject { - launch(image: string, env: Record): void { + launch(env: Record): void { if (this.ctx.container!.running) { throw new Error("Container is already running"); } - this.ctx.container!.start({ image, enableInternet: true, env }); + this.ctx.container!.start({ enableInternet: true, env }); } } @@ -361,14 +356,11 @@ export default { "deployment-env", ); - await env.MY_CONTAINER.getByName("configured").launch( - env.CONTAINER_IMAGE, - { + await env.MY_CONTAINER.getByName("configured").launch({ CONFIG_JSON: JSON.stringify(config), API_ENDPOINT: apiEndpoint, DEPLOYMENT_ENV: deploymentEnv, - }, - ); + }); return new Response("Container configured and launched"); }, @@ -418,17 +410,16 @@ export default { import { DurableObject } from "cloudflare:workers"; interface Env { - CONTAINER_IMAGE: string; DEMO_KV: KVNamespace; MY_CONTAINER: DurableObjectNamespace; } export class MyContainer extends DurableObject { - launch(image: string, env: Record): void { + launch(env: Record): void { if (this.ctx.container!.running) { throw new Error("Container is already running"); } - this.ctx.container!.start({ image, enableInternet: true, env }); + this.ctx.container!.start({ enableInternet: true, env }); } } @@ -460,13 +451,10 @@ export default { ), }; - await env.MY_CONTAINER.getByName("features").launch( - env.CONTAINER_IMAGE, - { + await env.MY_CONTAINER.getByName("features").launch({ ...featureFlags, CONTAINER_VERSION: "1.2.3", - }, - ); + }); return new Response("Container launched with feature flags"); }, diff --git a/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx b/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx new file mode 100644 index 00000000000..a561d308556 --- /dev/null +++ b/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx @@ -0,0 +1,49 @@ +--- +title: Migrate to the Durable Object Container API +description: Move an application from the Container class or Sandbox SDK to direct container control inside a Durable Object. +pcx_content_type: how-to +sidebar: + order: 7 +products: + - containers + - durable-objects + - sandbox +--- + +import { Steps } from "~/components"; + +Use the Durable Object Container API when you want direct control over a container's lifecycle. The API is available as `this.ctx.container` inside a Durable Object with a container binding. It does not provide all the helpers of the `Container` class or Sandbox SDK. + +Before changing code, identify the features your application uses. Keep the existing implementation if you depend on a helper that you cannot replace yet. For an API comparison, refer to [Choose an API](/containers/api/#choose-an-api). + +## Migrate from the Container class + +The `Container` class extends `DurableObject`. Replace its inherited lifecycle and routing helpers with application code that uses `ctx.container`. + + + +1. In your Worker, change the class to extend `DurableObject` from `cloudflare:workers`. Keep the exported class name if you want to retain its existing container definition and Durable Object binding. Do not add a new Durable Object migration solely because you changed the base class. Refer to [Wrangler configuration](/containers/configuration/wrangler/). +2. Replace `start()` and `stop()` calls with [`ctx.container.start()`](/containers/api/durable-object-container/#start) and [`signal()`](/containers/api/durable-object-container/#signal) or [`destroy()`](/containers/api/durable-object-container/#destroy), as appropriate. Do not assume `start()` waits for a port to become ready. +3. Replace `defaultPort`, `containerFetch()`, and automatic `fetch()` routing with [`getTcpPort(port).fetch()`](/containers/api/durable-object-container/#gettcpport) and your own request routing. Check port readiness before forwarding requests. Refer to the [stateless application example](/containers/examples/stateless/) for a complete pattern. +4. Replace `sleepAfter` with [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout). Replace lifecycle hooks and `schedule()` with application code, [`monitor()`](/containers/api/durable-object-container/#monitor), and [Durable Object alarms](/durable-objects/api/alarms/) where appropriate. +5. Test startup, concurrent requests, readiness, idle shutdown, and recovery after a container restart. Then remove `@cloudflare/containers` only if no other code imports it. + + + +Keep the same image in the `containers` section of your Wrangler configuration unless you intend to change the container application itself. The direct API still runs the image associated with its Durable Object class. + +## Migrate from the Sandbox SDK + +The Sandbox SDK builds higher-level execution, filesystem, process, and service APIs on Containers. Moving to the direct API changes your application architecture. This is not the [Sandbox SDK 1.0 migration](/sandbox/1-0-preview/migrate/), which keeps your application on the SDK. + + + +1. Inventory the Sandbox SDK methods your Worker uses. In particular, identify command execution, file operations, sessions, exposed services, and persistence or backup features. Refer to the [Sandbox SDK API](/sandbox/api/) for their current behavior. +2. Replace the exported SDK `Sandbox` class with a Durable Object class of your own. Update the class names in both the `containers` definition and `durable_objects.bindings` in Wrangler. Preserve an existing migration record; follow [Durable Object migration rules](/durable-objects/reference/durable-objects-migrations/) when renaming or replacing a class. Do not assume a new class inherits existing Sandbox Durable Object storage. +3. Replace `getSandbox()` calls with a Durable Object namespace lookup, such as `env.MY_CONTAINER.getByName(id)`. Keep the same application-level instance identifiers if their mapping matters to your users. Route requests or call your own Durable Object RPC methods instead of SDK methods. +4. Start the configured image with [`ctx.container.start()`](/containers/api/durable-object-container/#start). Use [`getTcpPort()`](/containers/api/durable-object-container/#gettcpport) for network traffic and [`exec()`](/containers/api/durable-object-container/#exec) for processes inside a running container. The direct API does not provide SDK filesystem, session, interpreter, or backup methods; implement the behavior you need in your Worker or container before removing the SDK. +5. Verify command results, service readiness, data recovery, and per-user isolation in a separate test environment. Remove `@cloudflare/sandbox` only after you have replaced every SDK feature your application needs. + + + +For a working direct-API request-routing example, refer to [Stateless instances](/containers/examples/stateless/). For process handling and output, refer to [Execute commands](/containers/guides/execute-commands/) and the [`exec()` reference](/containers/api/durable-object-container/#exec). diff --git a/src/content/docs/sandbox/index.mdx b/src/content/docs/sandbox/index.mdx index 9346194a010..a30589aec17 100644 --- a/src/content/docs/sandbox/index.mdx +++ b/src/content/docs/sandbox/index.mdx @@ -41,6 +41,8 @@ We recommend starting new projects on the preview, and migrating existing apps w The Sandbox SDK enables you to run untrusted code securely in isolated environments. Built on [Containers](/containers/), Sandbox SDK provides a simple API for executing commands, managing files, running background processes, and exposing services — all from your [Workers](/workers/) applications. +To move away from the Sandbox SDK entirely, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). For a version upgrade that keeps the SDK, use the [Sandbox SDK 1.0 migration](/sandbox/1-0-preview/migrate/) instead. + Sandboxes are ideal for building AI agents that need to execute code, interactive development environments, data analysis platforms, CI/CD systems, and any application that needs secure code execution at the edge. Each sandbox runs in its own isolated container with a full Linux environment, providing strong security boundaries while maintaining performance. With Sandbox, you can execute Python scripts, run Node.js applications, analyze data, compile code, and perform complex computations — all with a simple TypeScript API and no infrastructure to manage. From 741f4a92af1756c79ad330d64fd1eb84d5741d15 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Thu, 24 Sep 2026 21:49:18 -0400 Subject: [PATCH 11/23] [Containers] Separate API examples from navigation changes --- public/__redirects | 1 - .../api/durable-object-container.mdx | 39 ++- src/content/docs/containers/api/index.mdx | 9 +- .../docs/containers/concepts/architecture.mdx | 4 +- .../containers/examples/container-backend.mdx | 140 +-------- src/content/docs/containers/examples/cron.mdx | 110 +------ .../examples/durable-object-interface.mdx | 13 + .../examples/env-vars-and-secrets.mdx | 271 +----------------- .../containers/examples/r2-fuse-mount.mdx | 139 ++------- .../docs/containers/examples/stateless.mdx | 100 +------ .../docs/containers/examples/status-hooks.mdx | 97 +------ .../docs/containers/examples/websocket.mdx | 99 +------ ...igrate-to-durable-object-container-api.mdx | 27 +- src/content/docs/sandbox/index.mdx | 2 - 14 files changed, 123 insertions(+), 928 deletions(-) create mode 100644 src/content/docs/containers/examples/durable-object-interface.mdx diff --git a/public/__redirects b/public/__redirects index 1b3be50a78b..d099b461567 100644 --- a/public/__redirects +++ b/public/__redirects @@ -644,7 +644,6 @@ # Containers API /containers/container-package/ /containers/api/container-class/ 301 /containers/durable-object-methods/ /containers/api/durable-object-container/ 301 -/containers/examples/durable-object-interface/ https://github.com/cloudflare/containers-demos 301 /containers/container-class/ /containers/api/container-class/ 301 /containers/reference/container-class/ /containers/api/container-class/ 301 /containers/reference/durable-object-methods/ /containers/api/durable-object-container/ 301 diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index a4ebc439edf..024e2510fb3 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -15,7 +15,7 @@ Each [container](/containers/) is managed and proxied by a Durable Object. The D 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. -If your application uses the `Container` class or Sandbox SDK, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). +If your application uses the `Container` class, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). :::note We recommend starting new applications with the Durable Object Container API. If you prefer built-in lifecycle helpers, use the [`Container` class](/containers/api/container-class/) from `@cloudflare/containers`. The class adds routing, readiness checks, lifecycle hooks, activity tracking, and scheduling. To compare both APIs, refer to [Containers APIs](/containers/api/). @@ -33,13 +33,14 @@ import { DurableObject } from "cloudflare:workers"; interface Env {} export class MyDurableObject extends DurableObject { - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - ctx.blockConcurrencyWhile(async () => { - if (!ctx.container!.running) { - ctx.container!.start(); - } - }); + start(): void { + const container = this.ctx.container; + if (!container) { + throw new Error("No container is configured for this Durable Object"); + } + if (!container.running) { + container.start(); + } } } ``` @@ -286,19 +287,15 @@ callbacks to handle container status changes in your Workers code. ```ts class MyContainer extends DurableObject { - constructor(ctx, env) { - super(ctx, env); - function onContainerExit() { - console.log("Container exited"); - } - - // the "err" value can be customized by the destroy() method - async function onContainerError(err) { - console.log("Container errored", err); - } - - this.ctx.container.start(); - this.ctx.container.monitor().then(onContainerExit).catch(onContainerError); + startAndMonitor() { + const container = this.ctx.container; + container.start(); + this.ctx.waitUntil( + container + .monitor() + .then(() => console.log("Container exited")) + .catch((error) => console.error("Container errored", error)), + ); } } ``` diff --git a/src/content/docs/containers/api/index.mdx b/src/content/docs/containers/api/index.mdx index 55a391fcd47..bb37f13ea1a 100644 --- a/src/content/docs/containers/api/index.mdx +++ b/src/content/docs/containers/api/index.mdx @@ -53,7 +53,10 @@ interface Env {} export class MyContainer extends DurableObject { fetch(): Response { - const container = this.ctx.container!; + const container = this.ctx.container; + if (!container) { + return new Response("No container is configured", { status: 500 }); + } if (container.running) { return new Response("Container is running"); } @@ -65,7 +68,7 @@ export class MyContainer extends DurableObject { -For request routing with a readiness check, refer to the [stateless application example](/containers/examples/stateless/). For all methods, refer to the [Durable Object Container API](/containers/api/durable-object-container/). +For request routing, use [`getTcpPort()`](/containers/api/durable-object-container/#gettcpport) after checking port readiness. For all methods, refer to the [Durable Object Container API](/containers/api/durable-object-container/). ### Container class @@ -101,4 +104,4 @@ The following table compares both options: Use the Durable Object Container API for latency-sensitive workloads or workloads that need a smaller storage footprint. -For an existing application, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/) for paths from the `Container` class and Sandbox SDK. +If your application uses the `Container` class, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). diff --git a/src/content/docs/containers/concepts/architecture.mdx b/src/content/docs/containers/concepts/architecture.mdx index 345b39d917f..646028addb2 100644 --- a/src/content/docs/containers/concepts/architecture.mdx +++ b/src/content/docs/containers/concepts/architecture.mdx @@ -40,7 +40,7 @@ flowchart LR end ``` -A Container can only be accessed through its Durable Object. A Worker sends a request to the Durable Object, which accesses the Container through `ctx.container`. +A Container can only be accessed through its [Durable Object](/durable-objects/). A Worker sends a request to the Durable Object, which accesses the Container through `ctx.container`. An inactivity timeout, `signal()`, `destroy()`, a rollout, or a process exit can stop the instance. If startup fails or the process exits early, the instance returns to the stopped state. @@ -120,7 +120,7 @@ should be built for the `linux/amd64` architecture, and should stay within ### Container shutdown -With the Durable Object Container API, call [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout) to let the runtime stop an inactive container. You can also stop a container with [`signal()`](/containers/api/durable-object-container/#signal) or [`destroy()`](/containers/api/durable-object-container/#destroy). +With the Durable Object Container API, call [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout) to let the runtime stop the container after the Durable Object becomes inactive. The Durable Object becomes inactive when it stops receiving requests; the timeout can keep the container available while the Durable Object sleeps. You can also stop a container with [`signal()`](/containers/api/durable-object-container/#signal) or [`destroy()`](/containers/api/durable-object-container/#destroy). The `Container` class sets [`sleepAfter`](/containers/api/container-class/#sleepafter) to 10 minutes by default. Its [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) implementation calls [`stop()`](/containers/api/container-class/#stop). You can change the duration or override the hook. diff --git a/src/content/docs/containers/examples/container-backend.mdx b/src/content/docs/containers/examples/container-backend.mdx index 6d9b9011017..fc458f7461a 100644 --- a/src/content/docs/containers/examples/container-backend.mdx +++ b/src/content/docs/containers/examples/container-backend.mdx @@ -10,13 +10,7 @@ products: - containers --- -import { - WranglerConfig, - Details, - TabItem, - Tabs, - TypeScriptExample, -} from "~/components"; +import { WranglerConfig, Details } from "~/components"; A common pattern is to serve a static frontend application (e.g., React, Vue, Svelte) using Static Assets, then pass backend requests to a containerized backend application. @@ -31,9 +25,8 @@ For a full example, see the [Static Frontend + Container Backend Template](https ```json { - "name": "static-frontend-container-backend", + "name": "cron-container", "main": "src/index.ts", - "compatibility_date": "$today", "assets": { "directory": "./dist", "binding": "ASSETS" @@ -145,111 +138,18 @@ Your Worker needs to be able to both serve static assets and route requests to t In this case, we will pass requests to one of three container instances if the route starts with `/api`, and all other requests will be served as static assets. -For the direct API example, expose `GET /health` on port 8080. Return a successful response only when the backend is ready. - - - - - -```ts -import { DurableObject } from "cloudflare:workers"; - -const INSTANCE_COUNT = 3; - -interface Env { - ASSETS: Fetcher; - BACKEND: DurableObjectNamespace; -} - -export class Backend extends DurableObject { - private ready: Promise | undefined; - - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - ctx.blockConcurrencyWhile(() => - ctx.container!.setInactivityTimeout(2 * 60 * 60 * 1000), - ); - } - - async fetch(request: Request): Promise { - const container = this.ctx.container!; - if (!container.running) { - this.ready = undefined; - } - this.ready ??= this.startAndWaitForPort().catch((error: unknown) => { - this.ready = undefined; - throw error; - }); - await this.ready; - - const url = new URL(request.url); - url.protocol = "http:"; - url.host = "container"; - const forwarded = new Request(url, request); - forwarded.headers.delete("host"); - return container.getTcpPort(8080).fetch(forwarded); - } - - private async startAndWaitForPort(): Promise { - const container = this.ctx.container!; - if (!container.running) { - container.start(); - } - - const port = container.getTcpPort(8080); - let lastError: unknown; - for (let attempt = 0; attempt < 100; attempt++) { - try { - const response = await port.fetch("http://container/health"); - if (!response.ok) { - throw new Error(`Health check returned ${response.status}`); - } - return; - } catch (error) { - lastError = error; - await scheduler.wait(200); - } - } - throw new Error("Backend did not become ready on port 8080", { - cause: lastError, - }); - } -} - -export default { - async fetch(request: Request, env: Env): Promise { - if (new URL(request.url).pathname.startsWith("/api")) { - const index = Math.floor(Math.random() * INSTANCE_COUNT); - return env.BACKEND.getByName(`instance-${index}`).fetch(request); - } - - return env.ASSETS.fetch(request); - }, -}; -``` - - - - - - -```ts +```javascript import { Container, getRandom } from "@cloudflare/containers"; const INSTANCE_COUNT = 3; -interface Env { - ASSETS: Fetcher; - BACKEND: DurableObjectNamespace; -} - -export class Backend extends Container { +class Backend extends Container { defaultPort = 8080; // pass requests to port 8080 in the container sleepAfter = "2h"; // only sleep a container if it hasn't gotten requests in 2 hours } export default { - async fetch(request: Request, env: Env): Promise { + async fetch(request, env) { const url = new URL(request.url); if (url.pathname.startsWith("/api")) { const containerInstance = await getRandom(env.BACKEND, INSTANCE_COUNT); @@ -260,13 +160,10 @@ export default { }, }; ``` - - - - :::note -Both examples randomly select one of a fixed number of Container instances for each request. The `Container` class provides `getRandom()` as a helper. +This example uses `getRandom`, which randomly selects one of a fixed number of Container +instances for each request. In the future, we will provide improved latency-aware load balancing and autoscaling. @@ -291,7 +188,7 @@ import ( "net/http" ) -func handler(w http.ResponseWriter, r *http.Request) { +func handler(w http.ResponseWriter, r \*http.Request) { widgets := []map[string]interface{}{ {"id": 1, "name": "Widget A"}, {"id": 2, "name": "Sprocket B"}, @@ -305,9 +202,6 @@ func handler(w http.ResponseWriter, r *http.Request) { } func main() { - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) http.HandleFunc("/api/widgets", handler) log.Fatal(http.ListenAndServe(":8080", nil)) } @@ -315,21 +209,3 @@ func main() { ``` - -The health endpoint lets the Worker wait for the backend before forwarding requests. Build the backend image with this Dockerfile in the project root: - -
- -```dockerfile -FROM golang:1.25-alpine AS build -WORKDIR /app -COPY server.go . -RUN CGO_ENABLED=0 go build -o /server server.go - -FROM alpine:3.20 -COPY --from=build /server /server -EXPOSE 8080 -CMD ["/server"] -``` - -
diff --git a/src/content/docs/containers/examples/cron.mdx b/src/content/docs/containers/examples/cron.mdx index f048ced8708..beb5c71d036 100644 --- a/src/content/docs/containers/examples/cron.mdx +++ b/src/content/docs/containers/examples/cron.mdx @@ -10,7 +10,7 @@ products: - containers --- -import { TabItem, Tabs, TypeScriptExample, WranglerConfig } from "~/components"; +import { WranglerConfig } from "~/components"; To launch a container on a schedule, you can use a Workers [Cron Trigger](/workers/configuration/cron-triggers/). @@ -20,11 +20,10 @@ Use a cron expression in your Wrangler config to specify the schedule: -```jsonc +```json { "name": "cron-container", "main": "src/index.ts", - "compatibility_date": "$today", "triggers": { "crons": [ "*/2 * * * *" // Run every 2 minutes @@ -55,85 +54,10 @@ Use a cron expression in your Wrangler config to specify the schedule: -Then call the Container from the `scheduled()` handler in the Worker. The raw API example expects the Container to expose a `GET /health` endpoint that returns success when ready and a `POST /run` endpoint that starts the scheduled task. +Then in your Worker, call your Container from the "scheduled" handler: - - - - ```ts -import { DurableObject } from "cloudflare:workers"; - -interface Env { - CRON_CONTAINER: DurableObjectNamespace; -} - -export class CronContainer extends DurableObject { - private currentRun: Promise | undefined; - - run(startTime: string): Promise { - this.currentRun ??= this.runOnce(startTime).finally(() => { - this.currentRun = undefined; - }); - return this.currentRun; - } - - private async runOnce(startTime: string): Promise { - const container = this.ctx.container!; - await container.setInactivityTimeout(10_000); - - if (!container.running) { - container.start(); - } - - const port = container.getTcpPort(8080); - let lastError: unknown; - for (let attempt = 0; attempt < 100; attempt++) { - try { - const response = await port.fetch("http://container/health"); - if (!response.ok) { - throw new Error(`Health check returned ${response.status}`); - } - lastError = undefined; - break; - } catch (error) { - lastError = error; - await scheduler.wait(200); - } - } - if (lastError) { - throw lastError; - } - - const response = await port.fetch("http://container/run", { - method: "POST", - body: JSON.stringify({ startTime }), - headers: { "content-type": "application/json" }, - }); - if (!response.ok) { - throw new Error(`Container returned ${response.status}`); - } - } -} - -export default { - async fetch(): Promise { - return new Response("This Worker runs a scheduled Container task."); - }, - - async scheduled(_controller: ScheduledController, env: Env): Promise { - await env.CRON_CONTAINER.getByName("cron").run(new Date().toISOString()); - }, -}; -``` - - - - - - -```ts -import { Container, getContainer } from "@cloudflare/containers"; +import { Container, getContainer } from '@cloudflare/containers'; export class CronContainer extends Container { sleepAfter = '10s'; @@ -148,21 +72,17 @@ export class CronContainer extends Container { } export default { - async fetch(): Promise { - return new Response("This Worker runs a cron job to execute a container on a schedule."); - }, - - async scheduled(_controller: ScheduledController, env: { CRON_CONTAINER: DurableObjectNamespace }) { - const container = getContainer(env.CRON_CONTAINER); - await container.start({ - envVars: { + async fetch(): Promise { + return new Response("This Worker runs a cron job to execute a container on a schedule."); + }, + + async scheduled(_controller: any, env: { CRON_CONTAINER: DurableObjectNamespace }) { + let container = getContainer(env.CRON_CONTAINER); + await container.start({ + envVars: { MESSAGE: "Start Time: " + new Date().toISOString(), - }, - }); - }, + } + }) + }, }; ``` - - - - diff --git a/src/content/docs/containers/examples/durable-object-interface.mdx b/src/content/docs/containers/examples/durable-object-interface.mdx new file mode 100644 index 00000000000..f957451fa12 --- /dev/null +++ b/src/content/docs/containers/examples/durable-object-interface.mdx @@ -0,0 +1,13 @@ +--- + +summary: Various examples calling Containers directly from Durable Objects +pcx_content_type: example +title: Using Durable Objects Directly +external_link: https://github.com/cloudflare/containers-demos +sidebar: + order: 10 +description: Various examples calling Containers directly from Durable Objects +reviewed: 2025-06-24 +products: + - containers +--- diff --git a/src/content/docs/containers/examples/env-vars-and-secrets.mdx b/src/content/docs/containers/examples/env-vars-and-secrets.mdx index 7a6db6258a3..00d3117d36b 100644 --- a/src/content/docs/containers/examples/env-vars-and-secrets.mdx +++ b/src/content/docs/containers/examples/env-vars-and-secrets.mdx @@ -10,15 +10,10 @@ products: - containers --- -import { - PackageManagers, - TabItem, - Tabs, - TypeScriptExample, - WranglerConfig, -} from "~/components"; +import { WranglerConfig, PackageManagers } from "~/components"; -You can pass environment variables when the Durable Object Container API starts a Container, or through the `envVars` field on the [`Container`](/containers/api/container-class/) class. +Environment variables can be passed into a Container using the `envVars` field +in the [`Container`](/containers/reference/container-class/) class, or by setting manually when the Container starts. Secrets can be passed into a Container by using [Worker Secrets](/workers/configuration/secrets/) or the [Secret Store](/secrets-store/integrations/workers/), then passing them into the Container @@ -53,12 +48,10 @@ the `"SECRET_STORE_SECRET"` secret to it: args="secrets-store store create demo --remote" /> -Copy the store ID returned by this command. The following commands and the Wrangler configuration require the ID, not the store name. - Next, let's create a KV namespace called `DEMO_KV` and add a key-value pair: @@ -72,11 +65,9 @@ Next, let's create a KV namespace called `DEMO_KV` and add a key-value pair: -Replace `` with the ID returned by `kv namespace create`. The `--namespace-id` flag works before you add the binding to your Wrangler configuration; `--remote` stores the value in the namespace used by the deployed Worker. - For full details on how to create secrets, see the [Workers Secrets documentation](/workers/configuration/secrets/) and the [Secret Store documentation](/secrets-store/integrations/workers/). For KV setup, see the [Workers KV documentation](/kv/). @@ -87,7 +78,7 @@ in Wrangler configuration. -```jsonc +```json { "name": "my-container-worker", "vars": { @@ -96,7 +87,7 @@ in Wrangler configuration. "secrets_store_secrets": [ { "binding": "SECRET_STORE", - "store_id": "", + "store_id": "demo", "secret_name": "SECRET_STORE_SECRET" } ], @@ -118,50 +109,13 @@ added to `env`. Also note that we did not configure anything specific for environment variables, secrets, or KV values in the _container-related_ portion of the Wrangler configuration file. -## Set environment variables for every instance - -Pass synchronous Worker variables and secrets when the Container starts. - - - - - -```ts -import { DurableObject } from "cloudflare:workers"; - -interface Env { - ENV_VAR: string; - WORKER_SECRET: string; -} - -export class MyContainer extends DurableObject { - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - - ctx.blockConcurrencyWhile(async () => { - if (!ctx.container!.running) { - ctx.container!.start({ - enableInternet: true, - env: { - ENV_VAR: env.ENV_VAR, - WORKER_SECRET: env.WORKER_SECRET, - }, - }); - } - }); - } -} -``` - +## Using `envVars` on the Container class - - +Now, let's pass the env vars and secrets to our container using the `envVars` field in the `Container` class: ```js // https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global import { env } from "cloudflare:workers"; -import { Container } from "@cloudflare/containers"; - export class MyContainer extends Container { defaultPort = 8080; sleepAfter = "10s"; @@ -173,9 +127,6 @@ export class MyContainer extends Container { } ``` - - - Every instance of this `Container` will now have these variables and secrets set as environment variables when it launches. @@ -183,80 +134,9 @@ set as environment variables when it launches. But what if you want to set environment variables on a per-instance basis? -Pass the values when starting each instance. The raw API example defines a `launch()` RPC method on the Durable Object. The class version uses `startAndWaitForPorts()`. - - - - - -```ts -import { DurableObject } from "cloudflare:workers"; - -interface Env { - DEMO_KV: KVNamespace; - ENV_VAR: string; - MY_CONTAINER: DurableObjectNamespace; - SECRET_STORE: SecretsStoreSecret; - WORKER_SECRET: string; -} - -export class MyContainer extends DurableObject { - launch(env: Record): void { - if (this.ctx.container!.running) { - throw new Error("Container is already running"); - } - this.ctx.container!.start({ enableInternet: true, env }); - } -} - -function required(value: string | null, name: string): string { - if (value === null) { - throw new Error(`${name} was not found`); - } - return value; -} - -export default { - async fetch(request: Request, env: Env): Promise { - if (new URL(request.url).pathname !== "/launch-instances") { - return new Response("Not found", { status: 404 }); - } - - const secretStoreSecret = await env.SECRET_STORE.get(); - const kvValue = required(await env.DEMO_KV.get("KV_VALUE"), "KV_VALUE"); - const instanceConfig = required( - await env.DEMO_KV.get("instance-bar-config"), - "instance-bar-config", - ); - - await Promise.all([ - env.MY_CONTAINER.getByName("foo").launch({ - ENV_VAR: `${env.ENV_VAR}foo`, - WORKER_SECRET: env.WORKER_SECRET, - SECRET_STORE_SECRET: secretStoreSecret, - KV_VALUE: kvValue, - }), - env.MY_CONTAINER.getByName("bar").launch({ - ENV_VAR: `${env.ENV_VAR}bar`, - WORKER_SECRET: env.WORKER_SECRET, - SECRET_STORE_SECRET: secretStoreSecret, - KV_VALUE: kvValue, - INSTANCE_CONFIG: instanceConfig, - }), - ]); - - return new Response("Container instances launched"); - }, -}; -``` - - - - +In this case, use the `startAndWaitForPorts()` method to pass in environment variables for each instance. ```js -import { Container } from "@cloudflare/containers"; - export class MyContainer extends Container { defaultPort = 8080; sleepAfter = "10s"; @@ -301,9 +181,6 @@ export default { }; ``` - - - ## Reading KV values in containers KV values are particularly useful for configuration data that changes infrequently but needs to be accessible to your containers. Since KV operations are asynchronous, you must read the values at runtime when starting containers. @@ -312,65 +189,6 @@ Here are common patterns for using KV with containers: ### Configuration data - - - - -```ts -import { DurableObject } from "cloudflare:workers"; - -interface Env { - DEMO_KV: KVNamespace; - MY_CONTAINER: DurableObjectNamespace; -} - -export class MyContainer extends DurableObject { - launch(env: Record): void { - if (this.ctx.container!.running) { - throw new Error("Container is already running"); - } - this.ctx.container!.start({ enableInternet: true, env }); - } -} - -function required(value: string | null, name: string): string { - if (value === null) { - throw new Error(`${name} was not found`); - } - return value; -} - -export default { - async fetch(request: Request, env: Env): Promise { - if (new URL(request.url).pathname !== "/configure-container") { - return new Response("Not found", { status: 404 }); - } - - const config = await env.DEMO_KV.get("container-config", "json"); - const apiEndpoint = required( - await env.DEMO_KV.get("api-endpoint"), - "api-endpoint", - ); - const deploymentEnv = required( - await env.DEMO_KV.get("deployment-env"), - "deployment-env", - ); - - await env.MY_CONTAINER.getByName("configured").launch({ - CONFIG_JSON: JSON.stringify(config), - API_ENDPOINT: apiEndpoint, - DEPLOYMENT_ENV: deploymentEnv, - }); - - return new Response("Container configured and launched"); - }, -}; -``` - - - - - ```js export default { async fetch(request, env) { @@ -397,74 +215,8 @@ export default { }; ``` - - - ### Feature flags - - - - -```ts -import { DurableObject } from "cloudflare:workers"; - -interface Env { - DEMO_KV: KVNamespace; - MY_CONTAINER: DurableObjectNamespace; -} - -export class MyContainer extends DurableObject { - launch(env: Record): void { - if (this.ctx.container!.running) { - throw new Error("Container is already running"); - } - this.ctx.container!.start({ enableInternet: true, env }); - } -} - -function required(value: string | null, name: string): string { - if (value === null) { - throw new Error(`${name} was not found`); - } - return value; -} - -export default { - async fetch(request: Request, env: Env): Promise { - if (new URL(request.url).pathname !== "/launch-with-features") { - return new Response("Not found", { status: 404 }); - } - - const featureFlags = { - ENABLE_FEATURE_A: required( - await env.DEMO_KV.get("feature-a-enabled"), - "feature-a-enabled", - ), - ENABLE_FEATURE_B: required( - await env.DEMO_KV.get("feature-b-enabled"), - "feature-b-enabled", - ), - DEBUG_MODE: required( - await env.DEMO_KV.get("debug-enabled"), - "debug-enabled", - ), - }; - - await env.MY_CONTAINER.getByName("features").launch({ - ...featureFlags, - CONTAINER_VERSION: "1.2.3", - }); - - return new Response("Container launched with feature flags"); - }, -}; -``` - - - - - ```js export default { async fetch(request, env) { @@ -493,9 +245,6 @@ export default { }; ``` - - - ## Build-time environment variables Finally, you can also set build-time environment variables that are only available when building the container image via the `image_vars` field in the Wrangler configuration. diff --git a/src/content/docs/containers/examples/r2-fuse-mount.mdx b/src/content/docs/containers/examples/r2-fuse-mount.mdx index 80815c612bd..90901640222 100644 --- a/src/content/docs/containers/examples/r2-fuse-mount.mdx +++ b/src/content/docs/containers/examples/r2-fuse-mount.mdx @@ -11,7 +11,7 @@ products: - r2 --- -import { Details, TypeScriptExample, WranglerConfig } from "~/components"; +import { Details, TypeScriptExample } from "~/components"; FUSE (Filesystem in Userspace) allows you to mount [R2 buckets](/r2/) as filesystems within Containers. Applications can then interact with R2 using standard filesystem operations rather than object storage APIs. @@ -39,21 +39,20 @@ To mount an R2 bucket, install a FUSE adapter in your Dockerfile and configure i This example uses [tigrisfs](https://github.com/tigrisdata/tigrisfs), which supports S3-compatible storage including R2:
- ```dockerfile FROM alpine:3.20 # Install FUSE and dependencies -RUN apk add --no-cache \ +RUN apk add --no-cache \ --repository http://dl-cdn.alpinelinux.org/alpine/v3.20/main \ ca-certificates fuse curl bash -# Install the tested tigrisfs release -ARG TIGRISFS_VERSION=v1.2.2 +# Install tigrisfs RUN ARCH=$(uname -m) && \ if [ "$ARCH" = "x86_64" ]; then ARCH="amd64"; fi && \ if [ "$ARCH" = "aarch64" ]; then ARCH="arm64"; fi && \ - curl -fL "https://github.com/tigrisdata/tigrisfs/releases/download/${TIGRISFS_VERSION}/tigrisfs_${TIGRISFS_VERSION#v}_linux_${ARCH}.tar.gz" -o /tmp/tigrisfs.tar.gz && \ + VERSION=$(curl -s https://api.github.com/repos/tigrisdata/tigrisfs/releases/latest | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4) && \ + curl -L "https://github.com/tigrisdata/tigrisfs/releases/download/${VERSION}/tigrisfs_${VERSION#v}_linux_${ARCH}.tar.gz" -o /tmp/tigrisfs.tar.gz && \ tar -xzf /tmp/tigrisfs.tar.gz -C /usr/local/bin/ && \ rm /tmp/tigrisfs.tar.gz && \ chmod +x /usr/local/bin/tigrisfs @@ -62,26 +61,12 @@ RUN ARCH=$(uname -m) && \ RUN printf '#!/bin/sh\n\ set -e\n\ \n\ - : "${R2_ACCOUNT_ID:?R2_ACCOUNT_ID is required}"\n\ - : "${R2_BUCKET_NAME:?R2_BUCKET_NAME is required}"\n\ - : "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is required}"\n\ - : "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY is required}"\n\ - \n\ mkdir -p /mnt/r2\n\ \n\ R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"\n\ echo "Mounting bucket ${R2_BUCKET_NAME}..."\n\ /usr/local/bin/tigrisfs --endpoint "${R2_ENDPOINT}" -f "${R2_BUCKET_NAME}" /mnt/r2 &\n\ - mount_pid=$!\n\ - attempts=0\n\ - until mountpoint -q /mnt/r2; do\n\ - if ! kill -0 "$mount_pid" 2>/dev/null || [ "$attempts" -ge 30 ]; then\n\ - echo "R2 mount failed" >&2\n\ - exit 1\n\ - fi\n\ - attempts=$((attempts + 1))\n\ - sleep 1\n\ - done\n\ + sleep 3\n\ \n\ echo "Contents of mounted bucket:"\n\ ls -lah /mnt/r2\n\ @@ -90,103 +75,53 @@ RUN printf '#!/bin/sh\n\ EXPOSE 8080 CMD ["/startup.sh"] ``` -
-The startup script checks the required credentials, starts tigrisfs, and waits for the mount to be ready before listing the mounted directory. It exits with an error if mounting fails. +The startup script creates a mount point, starts tigrisfs in the background to mount the bucket, and then lists the mounted directory contents. ### Passing credentials to the container -Your Container needs [R2 credentials](/r2/api/tokens/) and configuration passed as environment variables. Store credentials as [Worker secrets](/workers/configuration/secrets/), then pass them when the Container starts. +Your Container needs [R2 credentials](/r2/api/tokens/) and configuration passed as environment variables. Store credentials as [Worker secrets](/workers/configuration/secrets/), then pass them through the `envVars` property: - ```ts -import { DurableObject } from "cloudflare:workers"; +import { Container, getContainer } from "@cloudflare/containers"; interface Env { - FUSE_DEMO: DurableObjectNamespace; - AWS_ACCESS_KEY_ID: string; - AWS_SECRET_ACCESS_KEY: string; - R2_BUCKET_NAME: string; - R2_ACCOUNT_ID: string; + FUSEDemo: DurableObjectNamespace; + AWS_ACCESS_KEY_ID: string; + AWS_SECRET_ACCESS_KEY: string; + R2_BUCKET_NAME: string; + R2_ACCOUNT_ID: string; } -export class FUSEDemo extends DurableObject { - private currentRun: Promise | undefined; - - run(): Promise { - this.currentRun ??= this.runOnce().finally(() => { - this.currentRun = undefined; - }); - return this.currentRun; - } - - private async runOnce(): Promise { - const container = this.ctx.container!; - if (container.running) { - throw new Error("Container is already running"); - } - - container.start({ - enableInternet: true, - env: { - AWS_ACCESS_KEY_ID: this.env.AWS_ACCESS_KEY_ID, - AWS_SECRET_ACCESS_KEY: this.env.AWS_SECRET_ACCESS_KEY, - R2_BUCKET_NAME: this.env.R2_BUCKET_NAME, - R2_ACCOUNT_ID: this.env.R2_ACCOUNT_ID, - }, - }); - - await container.monitor(); - } +export class FUSEDemo extends Container { + defaultPort = 8080; + sleepAfter = "10m"; + envVars = { + AWS_ACCESS_KEY_ID: this.env.AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY: this.env.AWS_SECRET_ACCESS_KEY, + R2_BUCKET_NAME: this.env.R2_BUCKET_NAME, + R2_ACCOUNT_ID: this.env.R2_ACCOUNT_ID, + }; } - -export default { - async fetch(_request: Request, env: Env): Promise { - await env.FUSE_DEMO.getByName("default").run(); - return new Response("FUSE task completed"); - }, -}; ``` - -This is a one-shot task: the Worker waits for the mount-and-list process to exit. A long-running service that keeps the mount available can instead use the [`Container` class](/containers/api/container-class/), but it must also provide the listening port that the class checks during startup. - -Store `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` as secrets. Configure `R2_BUCKET_NAME` and `R2_ACCOUNT_ID` as variables in `wrangler.jsonc`. The container uses the image from this Wrangler configuration: +The `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` should be stored as secrets, while `R2_BUCKET_NAME` and `R2_ACCOUNT_ID` can be configured as variables in your `wrangler.jsonc`: :::note[Creating your R2 AWS API keys] To get your `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, [head to your R2 dashboard](https://dash.cloudflare.com/?to=/:account/r2/overview) and create a new R2 Access API key. Use the generated the `Access Key ID` as your `AWS_ACCESS_KEY_ID` and `Secret Access Key` is the `AWS_SECRET_ACCESS_KEY`. ::: - - - -```jsonc +```json { - "name": "r2-fuse-demo", - "main": "src/index.ts", - "compatibility_date": "$today", - "containers": [ - { - "class_name": "FUSEDemo", - "image": "./Dockerfile", - "max_instances": 1, - }, - ], - "durable_objects": { - "bindings": [{ "name": "FUSE_DEMO", "class_name": "FUSEDemo" }], - }, - "migrations": [{ "tag": "v1", "new_sqlite_classes": ["FUSEDemo"] }], - "vars": { - "R2_BUCKET_NAME": "my-bucket", - "R2_ACCOUNT_ID": "your-account-id", - }, + "vars": { + "R2_BUCKET_NAME": "my-bucket", + "R2_ACCOUNT_ID": "your-account-id" + } } ``` - - ### Other S3-compatible storage providers Other S3-compatible storage providers, including AWS S3 and Google Cloud Storage, can be mounted using the same approach as R2. You will need to provide the appropriate endpoint URL and access credentials for the storage provider. @@ -205,13 +140,7 @@ RUN printf '#!/bin/sh\n\ \n\ R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"\n\ /usr/local/bin/tigrisfs --endpoint "${R2_ENDPOINT}" -f "${R2_BUCKET_NAME}" /mnt/r2 &\n\ - mount_pid=$!\n\ - attempts=0\n\ - until mountpoint -q /mnt/r2; do\n\ - if ! kill -0 "$mount_pid" 2>/dev/null || [ "$attempts" -ge 30 ]; then exit 1; fi\n\ - attempts=$((attempts + 1))\n\ - sleep 1\n\ - done\n\ + sleep 3\n\ \n\ echo "Accessing prefix: ${BUCKET_PREFIX}"\n\ ls -lah "/mnt/r2/${BUCKET_PREFIX}"\n\ @@ -232,13 +161,7 @@ RUN printf '#!/bin/sh\n\ \n\ R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"\n\ /usr/local/bin/tigrisfs --endpoint "${R2_ENDPOINT}" -o ro -f "${R2_BUCKET_NAME}" /mnt/r2 &\n\ - mount_pid=$!\n\ - attempts=0\n\ - until mountpoint -q /mnt/r2; do\n\ - if ! kill -0 "$mount_pid" 2>/dev/null || [ "$attempts" -ge 30 ]; then exit 1; fi\n\ - attempts=$((attempts + 1))\n\ - sleep 1\n\ - done\n\ + sleep 3\n\ \n\ ls -lah /mnt/r2\n\ ' > /startup.sh && chmod +x /startup.sh diff --git a/src/content/docs/containers/examples/stateless.mdx b/src/content/docs/containers/examples/stateless.mdx index c154f1c7198..8cf6275a0df 100644 --- a/src/content/docs/containers/examples/stateless.mdx +++ b/src/content/docs/containers/examples/stateless.mdx @@ -10,103 +10,14 @@ products: - containers --- -import { TabItem, Tabs, TypeScriptExample } from "~/components"; +To simply proxy requests to one of multiple instances of a container, you can use the `getRandom` function: -To proxy requests across a fixed number of Container instances, select an instance name and forward the request through its Durable Object. - -For the direct API example, expose a `GET /health` endpoint on port 8080 that returns a successful response when the application is ready. - - - - - -```ts -import { DurableObject } from "cloudflare:workers"; - -const INSTANCE_COUNT = 3; - -interface Env { - BACKEND: DurableObjectNamespace; -} - -export class Backend extends DurableObject { - private ready: Promise | undefined; - - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - ctx.blockConcurrencyWhile(() => - ctx.container!.setInactivityTimeout(2 * 60 * 60 * 1000), - ); - } - - async fetch(request: Request): Promise { - const container = this.ctx.container!; - if (!container.running) { - this.ready = undefined; - } - this.ready ??= this.startAndWaitForPort().catch((error: unknown) => { - this.ready = undefined; - throw error; - }); - await this.ready; - - const url = new URL(request.url); - url.protocol = "http:"; - url.host = "container"; - const forwarded = new Request(url, request); - forwarded.headers.delete("host"); - return container.getTcpPort(8080).fetch(forwarded); - } - - private async startAndWaitForPort(): Promise { - const container = this.ctx.container!; - if (!container.running) { - container.start(); - } - - const port = container.getTcpPort(8080); - let lastError: unknown; - for (let attempt = 0; attempt < 100; attempt++) { - try { - const response = await port.fetch("http://container/health"); - if (!response.ok) { - throw new Error(`Health check returned ${response.status}`); - } - return; - } catch (error) { - lastError = error; - await scheduler.wait(200); - } - } - throw new Error("Container did not become ready on port 8080", { - cause: lastError, - }); - } -} - -export default { - async fetch(request: Request, env: Env): Promise { - const index = Math.floor(Math.random() * INSTANCE_COUNT); - return env.BACKEND.getByName(`instance-${index}`).fetch(request); - }, -}; -``` - - - - - - ```ts import { Container, getRandom } from "@cloudflare/containers"; const INSTANCE_COUNT = 3; -interface Env { - BACKEND: DurableObjectNamespace; -} - -export class Backend extends Container { +class Backend extends Container { defaultPort = 8080; sleepAfter = "2h"; } @@ -118,13 +29,10 @@ export default { }, }; ``` - - - - :::note -Both examples randomly select one of a fixed number of Container instances for each request. The `Container` class provides `getRandom()` as a helper. +This example uses `getRandom`, which randomly selects one of a fixed number of Container +instances for each request. In the future, we will provide improved latency-aware load balancing and autoscaling. diff --git a/src/content/docs/containers/examples/status-hooks.mdx b/src/content/docs/containers/examples/status-hooks.mdx index a41814fd15f..1a0e219a1e5 100644 --- a/src/content/docs/containers/examples/status-hooks.mdx +++ b/src/content/docs/containers/examples/status-hooks.mdx @@ -11,92 +11,11 @@ products: - workers --- -import { TabItem, Tabs, TypeScriptExample } from "~/components"; +When a Container starts, stops, becomes idle, and errors, it can trigger code execution in a Worker +that has defined status hooks on the `Container` class. Refer to the [Container class lifecycle hooks](/containers/reference/container-class/#lifecycle-hooks) for more details. -Use `monitor()` with the Durable Object Container API to run code after the Container exits or errors. The `Container` class adds named lifecycle hooks and an inactivity callback. - -For the direct API example, expose `GET /health` on port 4000. Return a successful response once the application is ready. - - - - - -```ts -import { DurableObject } from "cloudflare:workers"; - -interface Env {} - -export class MyContainer extends DurableObject { - private ready: Promise | undefined; - - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - ctx.blockConcurrencyWhile(() => - ctx.container!.setInactivityTimeout(5 * 60 * 1000), - ); - } - - async fetch(request: Request): Promise { - const container = this.ctx.container!; - if (!container.running) { - this.ready = undefined; - } - this.ready ??= this.startAndMonitor().catch((error: unknown) => { - this.ready = undefined; - throw error; - }); - await this.ready; - - const url = new URL(request.url); - url.protocol = "http:"; - url.host = "container"; - const forwarded = new Request(url, request); - forwarded.headers.delete("host"); - return container.getTcpPort(4000).fetch(forwarded); - } - - private async startAndMonitor(): Promise { - const container = this.ctx.container!; - if (!container.running) { - container.start(); - } - - this.ctx.waitUntil( - container - .monitor() - .then(() => console.log("Container stopped")) - .catch((error: unknown) => console.error("Container error:", error)), - ); - - const port = container.getTcpPort(4000); - let lastError: unknown; - for (let attempt = 0; attempt < 100; attempt++) { - try { - const response = await port.fetch("http://container/health"); - if (!response.ok) { - throw new Error(`Health check returned ${response.status}`); - } - console.log("Container successfully started"); - return; - } catch (error) { - lastError = error; - await scheduler.wait(200); - } - } - throw new Error("Container did not become ready on port 4000", { - cause: lastError, - }); - } -} -``` - - - - - - ```ts -import { Container, type StopParams } from "@cloudflare/containers"; +import { Container } from "@cloudflare/containers"; export class MyContainer extends Container { defaultPort = 4000; @@ -106,7 +25,7 @@ export class MyContainer extends Container { console.log("Container successfully started"); } - override onStop(stopParams: StopParams) { + override onStop(stopParams) { if (stopParams.exitCode === 0) { console.log("Container stopped gracefully"); } else { @@ -121,14 +40,8 @@ export class MyContainer extends Container { await this.stop(); } - override onError(error: unknown) { + override onError(error: string) { console.log("Container error:", error); } } ``` - - - - - -The `monitor()` promise in the raw API does not include an exit code or stop reason. The `setInactivityTimeout()` method does not invoke a callback when the timeout expires. Use the [`Container` class lifecycle hooks](/containers/api/container-class/#lifecycle-hooks) when you need those higher-level events. diff --git a/src/content/docs/containers/examples/websocket.mdx b/src/content/docs/containers/examples/websocket.mdx index ab4477fc983..66a5968d448 100644 --- a/src/content/docs/containers/examples/websocket.mdx +++ b/src/content/docs/containers/examples/websocket.mdx @@ -10,113 +10,24 @@ products: - containers --- -import { TabItem, Tabs, TypeScriptExample } from "~/components"; +WebSocket requests are automatically forwarded to a container using the default `fetch` +method on the `Container` class: -Forward an incoming WebSocket upgrade request through the Durable Object to the listening port on the Container. - -For the direct API example, expose a `GET /health` endpoint on port 8080 that returns a successful response when the application is ready. Keep the WebSocket endpoint separate from the health check. - - - - - -```ts -import { DurableObject } from "cloudflare:workers"; - -interface Env { - MY_CONTAINER: DurableObjectNamespace; -} - -export class MyContainer extends DurableObject { - private ready: Promise | undefined; - - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - ctx.blockConcurrencyWhile(() => - ctx.container!.setInactivityTimeout(2 * 60 * 1000), - ); - } - - async fetch(request: Request): Promise { - const container = this.ctx.container!; - if (!container.running) { - this.ready = undefined; - } - this.ready ??= this.startAndWaitForPort().catch((error: unknown) => { - this.ready = undefined; - throw error; - }); - await this.ready; - - const url = new URL(request.url); - url.protocol = "http:"; - url.host = "container"; - const forwarded = new Request(url, request); - forwarded.headers.delete("host"); - return container.getTcpPort(8080).fetch(forwarded); - } - - private async startAndWaitForPort(): Promise { - const container = this.ctx.container!; - if (!container.running) { - container.start(); - } - - const port = container.getTcpPort(8080); - let lastError: unknown; - for (let attempt = 0; attempt < 100; attempt++) { - try { - const response = await port.fetch("http://container/health"); - if (!response.ok) { - throw new Error(`Health check returned ${response.status}`); - } - return; - } catch (error) { - lastError = error; - await scheduler.wait(200); - } - } - throw new Error("Container did not become ready on port 8080", { - cause: lastError, - }); - } -} - -export default { - fetch(request: Request, env: Env): Promise { - return env.MY_CONTAINER.getByName("default").fetch(request); - }, -}; -``` - - - - - - -```ts +```js import { Container, getContainer } from "@cloudflare/containers"; -interface Env { - MY_CONTAINER: DurableObjectNamespace; -} - -export class MyContainer extends Container { +export class MyContainer extends Container { defaultPort = 8080; sleepAfter = "2m"; } export default { - async fetch(request: Request, env: Env): Promise { + async fetch(request, env) { // gets default instance and forwards websocket from outside Worker return getContainer(env.MY_CONTAINER).fetch(request); }, }; ``` - - - - View a full example in the [Container class repository](https://github.com/cloudflare/containers/tree/main/examples/websocket). {/* TODO: Add more advanced examples - like kicking off a WS request then passing messages to container from the WS */} diff --git a/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx b/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx index a561d308556..24faa1ef75f 100644 --- a/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx +++ b/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx @@ -1,22 +1,21 @@ --- -title: Migrate to the Durable Object Container API -description: Move an application from the Container class or Sandbox SDK to direct container control inside a Durable Object. +title: Migrate from the Container class to the Durable Object Container API +description: Replace Container class lifecycle and routing helpers with direct container control inside a Durable Object. pcx_content_type: how-to sidebar: order: 7 products: - containers - durable-objects - - sandbox --- import { Steps } from "~/components"; -Use the Durable Object Container API when you want direct control over a container's lifecycle. The API is available as `this.ctx.container` inside a Durable Object with a container binding. It does not provide all the helpers of the `Container` class or Sandbox SDK. +Use the Durable Object Container API when you want direct control over a container's lifecycle. The API is available as `this.ctx.container` inside a Durable Object with a container binding. It does not provide all the helpers of the `Container` class. Before changing code, identify the features your application uses. Keep the existing implementation if you depend on a helper that you cannot replace yet. For an API comparison, refer to [Choose an API](/containers/api/#choose-an-api). -## Migrate from the Container class +## Replace Container class helpers The `Container` class extends `DurableObject`. Replace its inherited lifecycle and routing helpers with application code that uses `ctx.container`. @@ -24,7 +23,7 @@ The `Container` class extends `DurableObject`. Replace its inherited lifecycle a 1. In your Worker, change the class to extend `DurableObject` from `cloudflare:workers`. Keep the exported class name if you want to retain its existing container definition and Durable Object binding. Do not add a new Durable Object migration solely because you changed the base class. Refer to [Wrangler configuration](/containers/configuration/wrangler/). 2. Replace `start()` and `stop()` calls with [`ctx.container.start()`](/containers/api/durable-object-container/#start) and [`signal()`](/containers/api/durable-object-container/#signal) or [`destroy()`](/containers/api/durable-object-container/#destroy), as appropriate. Do not assume `start()` waits for a port to become ready. -3. Replace `defaultPort`, `containerFetch()`, and automatic `fetch()` routing with [`getTcpPort(port).fetch()`](/containers/api/durable-object-container/#gettcpport) and your own request routing. Check port readiness before forwarding requests. Refer to the [stateless application example](/containers/examples/stateless/) for a complete pattern. +3. Replace `defaultPort`, `containerFetch()`, and automatic `fetch()` routing with [`getTcpPort(port).fetch()`](/containers/api/durable-object-container/#gettcpport) and your own request routing. Check port readiness before forwarding requests. 4. Replace `sleepAfter` with [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout). Replace lifecycle hooks and `schedule()` with application code, [`monitor()`](/containers/api/durable-object-container/#monitor), and [Durable Object alarms](/durable-objects/api/alarms/) where appropriate. 5. Test startup, concurrent requests, readiness, idle shutdown, and recovery after a container restart. Then remove `@cloudflare/containers` only if no other code imports it. @@ -32,18 +31,4 @@ The `Container` class extends `DurableObject`. Replace its inherited lifecycle a Keep the same image in the `containers` section of your Wrangler configuration unless you intend to change the container application itself. The direct API still runs the image associated with its Durable Object class. -## Migrate from the Sandbox SDK - -The Sandbox SDK builds higher-level execution, filesystem, process, and service APIs on Containers. Moving to the direct API changes your application architecture. This is not the [Sandbox SDK 1.0 migration](/sandbox/1-0-preview/migrate/), which keeps your application on the SDK. - - - -1. Inventory the Sandbox SDK methods your Worker uses. In particular, identify command execution, file operations, sessions, exposed services, and persistence or backup features. Refer to the [Sandbox SDK API](/sandbox/api/) for their current behavior. -2. Replace the exported SDK `Sandbox` class with a Durable Object class of your own. Update the class names in both the `containers` definition and `durable_objects.bindings` in Wrangler. Preserve an existing migration record; follow [Durable Object migration rules](/durable-objects/reference/durable-objects-migrations/) when renaming or replacing a class. Do not assume a new class inherits existing Sandbox Durable Object storage. -3. Replace `getSandbox()` calls with a Durable Object namespace lookup, such as `env.MY_CONTAINER.getByName(id)`. Keep the same application-level instance identifiers if their mapping matters to your users. Route requests or call your own Durable Object RPC methods instead of SDK methods. -4. Start the configured image with [`ctx.container.start()`](/containers/api/durable-object-container/#start). Use [`getTcpPort()`](/containers/api/durable-object-container/#gettcpport) for network traffic and [`exec()`](/containers/api/durable-object-container/#exec) for processes inside a running container. The direct API does not provide SDK filesystem, session, interpreter, or backup methods; implement the behavior you need in your Worker or container before removing the SDK. -5. Verify command results, service readiness, data recovery, and per-user isolation in a separate test environment. Remove `@cloudflare/sandbox` only after you have replaced every SDK feature your application needs. - - - -For a working direct-API request-routing example, refer to [Stateless instances](/containers/examples/stateless/). For process handling and output, refer to [Execute commands](/containers/guides/execute-commands/) and the [`exec()` reference](/containers/api/durable-object-container/#exec). +For process handling and output, refer to [Execute commands](/containers/guides/execute-commands/) and the [`exec()` reference](/containers/api/durable-object-container/#exec). diff --git a/src/content/docs/sandbox/index.mdx b/src/content/docs/sandbox/index.mdx index a30589aec17..9346194a010 100644 --- a/src/content/docs/sandbox/index.mdx +++ b/src/content/docs/sandbox/index.mdx @@ -41,8 +41,6 @@ We recommend starting new projects on the preview, and migrating existing apps w The Sandbox SDK enables you to run untrusted code securely in isolated environments. Built on [Containers](/containers/), Sandbox SDK provides a simple API for executing commands, managing files, running background processes, and exposing services — all from your [Workers](/workers/) applications. -To move away from the Sandbox SDK entirely, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). For a version upgrade that keeps the SDK, use the [Sandbox SDK 1.0 migration](/sandbox/1-0-preview/migrate/) instead. - Sandboxes are ideal for building AI agents that need to execute code, interactive development environments, data analysis platforms, CI/CD systems, and any application that needs secure code execution at the edge. Each sandbox runs in its own isolated container with a full Linux environment, providing strong security boundaries while maintaining performance. With Sandbox, you can execute Python scripts, run Node.js applications, analyze data, compile code, and perform complex computations — all with a simple TypeScript API and no infrastructure to manage. From ce48c61b9217e097f6be1e6bca4e39ca7f373be8 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Thu, 24 Sep 2026 22:53:18 -0400 Subject: [PATCH 12/23] [Containers] Recommend direct API for new applications --- .../docs/containers/api/container-class.mdx | 2 +- .../containers/api/durable-object-container.mdx | 4 ++-- src/content/docs/containers/api/index.mdx | 14 +++++++------- .../docs/containers/concepts/architecture.mdx | 2 +- src/content/docs/containers/get-started/index.mdx | 2 +- .../migrate-to-durable-object-container-api.mdx | 2 +- .../docs/containers/guides/outbound-traffic.mdx | 2 +- src/content/docs/containers/index.mdx | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/content/docs/containers/api/container-class.mdx b/src/content/docs/containers/api/container-class.mdx index dd8c1f6eaa7..bdc689ec0bf 100644 --- a/src/content/docs/containers/api/container-class.mdx +++ b/src/content/docs/containers/api/container-class.mdx @@ -3,7 +3,7 @@ pcx_content_type: reference title: Container class sidebar: order: 2 -description: API reference for the higher-level Container class built on Durable Objects. +description: API reference for the Container class and its built-in lifecycle helpers. products: - containers --- diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index 024e2510fb3..a35a23fa35d 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -18,7 +18,7 @@ The API documented on this page is available on `this.ctx.container` inside any If your application uses the `Container` class, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). :::note -We recommend starting new applications with the Durable Object Container API. If you prefer built-in lifecycle helpers, use the [`Container` class](/containers/api/container-class/) from `@cloudflare/containers`. The class adds routing, readiness checks, lifecycle hooks, activity tracking, and scheduling. To compare both APIs, refer to [Containers APIs](/containers/api/). +We recommend starting new applications with the Durable Object Container API. It lets you run container workloads alongside the Durable Object's persistent storage, alarms, and request handling. Use `ctx.container` to control when the container starts, receives traffic, and stops, while the Durable Object manages application state and coordination. For existing applications that use the `Container` class, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). ::: Your Durable Object also has access to [SQLite storage](/durable-objects/api/sqlite-storage-api/) through `this.ctx.storage`, [alarms](/durable-objects/api/alarms/), and all other Durable Object APIs. @@ -398,7 +398,7 @@ this.ctx.container.interceptOutboundHttps("*", worker); ## Related resources - [Containers APIs](/containers/api/) — compare direct runtime control with the `Container` class -- [Container class reference](/containers/api/container-class/) — use higher-level lifecycle helpers +- [Container class reference](/containers/api/container-class/) — reference for existing `Container` class applications - [Containers overview](/containers/) - [Get started with Containers](/containers/get-started/) - [SQLite storage API](/durable-objects/api/sqlite-storage-api/) — persist state across container restarts diff --git a/src/content/docs/containers/api/index.mdx b/src/content/docs/containers/api/index.mdx index bb37f13ea1a..d8a0349aee4 100644 --- a/src/content/docs/containers/api/index.mdx +++ b/src/content/docs/containers/api/index.mdx @@ -1,7 +1,7 @@ --- pcx_content_type: navigation title: API -description: Choose between the Durable Object Container API and the higher-level Container class. +description: Use the Durable Object Container API for new applications and compare it with the Container class. sidebar: order: 6 products: @@ -13,7 +13,7 @@ import { CardGrid, LinkTitleCard, TypeScriptExample } from "~/components"; Containers provide two APIs for managing a container from a Durable Object. Both APIs address the same container runtime. -For new applications, we recommend the Durable Object Container API. It gives you direct control over the container lifecycle and access to Durable Object features such as storage, alarms, and request routing. Use the `Container` class when you prefer built-in lifecycle helpers. +For new applications, we recommend the Durable Object Container API. It lets you combine container workloads with the Durable Object's persistent storage, alarms, and request handling. Use `ctx.container` to control the container lifecycle while your Durable Object coordinates application state. The `Container` class remains documented for existing applications. @@ -30,8 +30,8 @@ For new applications, we recommend the Durable Object Container API. It gives yo href="/containers/api/container-class/" icon="seti:typescript" > - Use a higher-level class built on Durable Objects, with routing, readiness - checks, lifecycle hooks, and scheduling. + Reference the class and its routing, readiness checks, lifecycle hooks, + and scheduling for existing applications.
@@ -40,7 +40,7 @@ For new applications, we recommend the Durable Object Container API. It gives yo ### Durable Object Container API -The Durable Object Container API is recommended for new applications. Inside a Durable Object, use `ctx.container` to control the container runtime directly. You can manage startup, shutdown, networking, and resource usage and add readiness checks, custom request routing, or lifecycle policies when needed. +Inside a Durable Object, use `ctx.container` to control the container runtime directly. You can manage startup, shutdown, networking, and resource usage while using Durable Object storage and alarms for state and coordination. Add readiness checks, custom request routing, or lifecycle policies when needed. This Durable Object starts its configured container when it receives a request. Starting a container does not mean its ports are ready. @@ -72,7 +72,7 @@ For request routing, use [`getTcpPort()`](/containers/api/durable-object-contain ### Container class -The `Container` class builds on Durable Objects and the runtime API. Choose it when you prefer built-in request proxying, readiness checks, lifecycle hooks, and scheduling. These helpers reduce application code by handling common Durable Object lifecycle tasks for you. Some features still use Durable Object storage and alarms. +The `Container` class builds on Durable Objects and the runtime API. Existing applications may use its request proxying, readiness checks, lifecycle hooks, and scheduling. These helpers handle common Durable Object lifecycle tasks. Some features still use Durable Object storage and alarms. @@ -102,6 +102,6 @@ The following table compares both options: | Stop inactive containers | [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout) | [`sleepAfter`](/containers/api/container-class/#sleepafter) and [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) | | Schedule callbacks | [`ctx.storage.setAlarm()`](/durable-objects/api/alarms/#setalarm) (Durable Object API) | [`schedule()`](/containers/api/container-class/#schedule) | -Use the Durable Object Container API for latency-sensitive workloads or workloads that need a smaller storage footprint. +For new applications, use the Durable Object Container API to combine direct container control with Durable Object storage and coordination. It also supports latency-sensitive workloads and applications that need a smaller storage footprint. If your application uses the `Container` class, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). diff --git a/src/content/docs/containers/concepts/architecture.mdx b/src/content/docs/containers/concepts/architecture.mdx index 646028addb2..a14e16274a3 100644 --- a/src/content/docs/containers/concepts/architecture.mdx +++ b/src/content/docs/containers/concepts/architecture.mdx @@ -46,7 +46,7 @@ An inactivity timeout, `signal()`, `destroy()`, a rollout, or a process exit can The `ctx.container.running` property becomes `true` before the process is ready to accept traffic. Check port readiness before you send the first request. -You can manage this lifecycle through the [Durable Object Container API](/containers/api/durable-object-container/) or the higher-level [`Container` class](/containers/api/container-class/). To compare both options, refer to [Containers APIs](/containers/api/). +For new applications, manage this lifecycle through the [Durable Object Container API](/containers/api/durable-object-container/). It lets the Durable Object coordinate container compute with persistent state and alarms. Existing applications may use the [`Container` class](/containers/api/container-class/). To compare both APIs, refer to [Containers APIs](/containers/api/). ## Lifecycle of a request diff --git a/src/content/docs/containers/get-started/index.mdx b/src/content/docs/containers/get-started/index.mdx index c33f6ed0d61..b7e9708d893 100644 --- a/src/content/docs/containers/get-started/index.mdx +++ b/src/content/docs/containers/get-started/index.mdx @@ -15,7 +15,7 @@ In this example, each container runs a small webserver written in Go. This example Worker should give you a sense for simple Container use, and provide a starting point for more complex use cases. -This guide uses the higher-level `Container` class. You can also manage containers directly through `ctx.container`. To compare both options, refer to [Containers APIs](/containers/api/). +This guide uses the `Container` class and its built-in lifecycle helpers. You can also manage containers directly through `ctx.container`. To compare both options, refer to [Containers APIs](/containers/api/). ## Prerequisites diff --git a/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx b/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx index 24faa1ef75f..423860e1636 100644 --- a/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx +++ b/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx @@ -11,7 +11,7 @@ products: import { Steps } from "~/components"; -Use the Durable Object Container API when you want direct control over a container's lifecycle. The API is available as `this.ctx.container` inside a Durable Object with a container binding. It does not provide all the helpers of the `Container` class. +The Durable Object Container API lets your Durable Object coordinate container compute with persistent storage, alarms, and request handling. Access it as `this.ctx.container` inside a Durable Object with a container binding. When migrating from the `Container` class, replace its helpers with application code where needed. Before changing code, identify the features your application uses. Keep the existing implementation if you depend on a helper that you cannot replace yet. For an API comparison, refer to [Choose an API](/containers/api/#choose-an-api). diff --git a/src/content/docs/containers/guides/outbound-traffic.mdx b/src/content/docs/containers/guides/outbound-traffic.mdx index c102fa967bc..f20ad6875bd 100644 --- a/src/content/docs/containers/guides/outbound-traffic.mdx +++ b/src/content/docs/containers/guides/outbound-traffic.mdx @@ -357,7 +357,7 @@ Requests are evaluated in this order: 5. Instance-level handlers set with `setOutboundHandler()` are checked before the class-level `outbound` handler. 6. If no handler matches, the request can still egress to the public internet when it matched `allowedHosts` or `enableInternet = true`. Otherwise, it is denied. -## Low-level API +## Direct API To configure outbound interception directly on `ctx.container`, use `interceptOutboundHttp` for a specific hostname glob, IP, or CIDR range, or `interceptAllOutboundHttp` for all traffic. Both accept a `WorkerEntrypoint`. diff --git a/src/content/docs/containers/index.mdx b/src/content/docs/containers/index.mdx index 4c875d13b88..88f9cf20278 100644 --- a/src/content/docs/containers/index.mdx +++ b/src/content/docs/containers/index.mdx @@ -183,7 +183,7 @@ Ship from your machine or Workers Builds, and confirm the deploy. - Choose between direct runtime control and higher-level lifecycle helpers. + Use the Durable Object Container API or reference the Container class. From feaeda3de4c874587d1676b10ac4afee0c848ce6 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Mon, 28 Sep 2026 17:00:47 -0400 Subject: [PATCH 13/23] [Containers] Clarify direct API reference --- .../api/durable-object-container.mdx | 187 +++++++++--------- src/content/docs/containers/api/index.mdx | 6 +- 2 files changed, 100 insertions(+), 93 deletions(-) diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index a35a23fa35d..19d8e3936fb 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -15,15 +15,11 @@ Each [container](/containers/) is managed and proxied by a Durable Object. The D 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. -If your application uses the `Container` class, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). - :::note We recommend starting new applications with the Durable Object Container API. It lets you run container workloads alongside the Durable Object's persistent storage, alarms, and request handling. Use `ctx.container` to control when the container starts, receives traffic, and stops, while the Durable Object manages application state and coordination. For existing applications that use the `Container` class, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). ::: -Your Durable Object also has access to [SQLite storage](/durable-objects/api/sqlite-storage-api/) through `this.ctx.storage`, [alarms](/durable-objects/api/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. +Your Durable Object also has access to [SQLite storage](/durable-objects/api/sqlite-storage-api/) through `this.ctx.storage`, [alarms](/durable-objects/api/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. @@ -33,15 +29,24 @@ import { DurableObject } from "cloudflare:workers"; interface Env {} export class MyDurableObject extends DurableObject { - start(): void { + async start(): Promise { const container = this.ctx.container; if (!container) { throw new Error("No container is configured for this Durable Object"); } if (!container.running) { container.start(); + await this.ctx.storage.put("lastStartedAt", Date.now()); } } + + async scheduleStart(timestamp: number): Promise { + await this.ctx.storage.setAlarm(timestamp); + } + + async alarm(): Promise { + await this.start(); + } } ``` @@ -51,9 +56,9 @@ export class MyDurableObject extends DurableObject { ### `running` -`running` returns `true` if the container is currently running. It does not ensure that the container has fully started and ready to accept requests. +`running` is `true` when the container is running. It does not confirm that the container is ready to accept requests. - + ```ts this.ctx.container.running; @@ -65,10 +70,9 @@ this.ctx.container.running; ### `start` -`start` boots a container. This method does not block until the container is fully started. -You may want to confirm the container is ready to accept requests before using it. +`start()` boots a container. It returns before the container is ready to accept requests. Confirm readiness before sending traffic. - + ```ts this.ctx.container.start(); @@ -78,20 +82,18 @@ this.ctx.container.start(); #### Parameters -- `options` (optional): An object with the existing startup options: - - `env`: Environment variables to pass to the container. - - `entrypoint`: The command and arguments to run in the container. - - `enableInternet`: Whether to allow outbound Internet access. +- `options` (`object`, optional): Container startup options: + - `env` (`Record`, optional): Environment variables to pass to the container. + - `entrypoint` (`string[]`, optional): Command and arguments to run in the container. + - `enableInternet` (`boolean`, optional): Whether to allow outbound Internet access. #### Return values -- None. +- `void`: No return value. ### `exec` -`exec` starts another process inside an already-running Container. It does not start a stopped Container. - -The following example calls `this.ctx.container.exec()` inside a class extending `Container` from `@cloudflare/containers`. In RPC methods, check `this.ctx.container.running` and call `await this.start()` when needed. You can also use the `onStart()` hook to run any series of commands whenever the Container starts. +`exec()` starts another process inside an already-running container. It does not start a stopped container. ```txt exec( @@ -100,22 +102,28 @@ exec( ): Promise ``` -The `exec` operation 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", ""]` when Bash exists in the image. Use `["sh", "-c", ""]` for images with only a Portable Operating System Interface (POSIX) shell. +`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", ""]` when Bash exists in the image. Use `["sh", "-c", ""]` for images with only a Portable Operating System Interface (POSIX) shell. -The following RPC method starts the Container before executing a command: +The following RPC method checks that the container is running before executing a command: ```ts -import { Container } from "@cloudflare/containers"; +import { DurableObject } from "cloudflare:workers"; + +interface Env {} -export class MyContainer extends Container { +export class MyDurableObject extends DurableObject { async runCommand() { - if (!this.ctx.container.running) { - await this.start(); + 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 this.ctx.container.exec(["node", "--version"]); + const process = await container.exec(["node", "--version"]); const output = await process.output(); return { @@ -131,38 +139,38 @@ export class MyContainer extends Container { #### Parameters -- `cmd` (`string[]`) — executable followed by its arguments. -- `options` (`ContainerExecOptions`, optional) — process configuration: - - `stdin` (`ReadableStream | "pipe"`) — source for standard input. Use `"pipe"` to write through the returned `stdin` stream. When omitted, standard input closes and sends end-of-file (EOF). - - `stdout` (`"pipe" | "ignore"`, default `"pipe"`) — captures or discards standard output. - - `stderr` (`"pipe" | "ignore" | "combined"`, default `"pipe"`) — captures, discards, or merges standard error into standard output. The `"combined"` value requires `stdout: "pipe"`. Combined output does not guarantee ordering between its source streams. - - `cwd` (`string`) — working directory for the process. - - `env` (`Record`) — environment additions and overrides. The process inherits existing Container variables. Matching keys use the per-execution value. - - `user` (`string`) — image user for the process. +- `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 returned `stdin` stream. 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 requires `stdout: "pipe"`. Combined output does not guarantee ordering between its source streams. + - `cwd` (`string`, optional): Working directory for the process. + - `env` (`Record`, optional): Environment additions and overrides. The process inherits existing container variables. Matching keys use the per-execution value. + - `user` (`string`, optional): Image user for the process. #### Return values -Returns `Promise`. +- `Promise`: Resolves with an `ExecProcess` for the started process. An `ExecProcess` has these fields and methods: -- `stdin` (`WritableStream | null`) — writable standard input when `stdin` is `"pipe"`. -- `stdout` (`ReadableStream | null`) — readable standard output when piped. -- `stderr` (`ReadableStream | null`) — readable standard error when piped separately. -- `pid` (`number`) — process identifier. -- `exitCode` (`Promise`) — resolves when the process exits. Nonzero codes resolve normally instead of rejecting. -- `output()` (`Promise`) — reads buffered output once. `ExecOutput` contains `stdout` (`ArrayBuffer`), `stderr` (`ArrayBuffer`), and `exitCode` (`number`). Ignored streams produce empty buffers. Use `TextDecoder` to decode text. -- `kill(signal?: number)` (`void`) — queues a signal for the process. The default is `SIGTERM`, signal `15`. The signal must be from `1` through `64`. +- `stdin` (`WritableStream | null`): Writable standard input when `stdin` is `"pipe"`. +- `stdout` (`ReadableStream | null`): Readable standard output when piped. +- `stderr` (`ReadableStream | null`): Readable standard error when piped separately. +- `pid` (`number`): Process identifier. +- `exitCode` (`Promise`): Resolves when the process exits. Nonzero codes resolve normally instead of rejecting. +- `output()` (`Promise`): Reads buffered output once. `ExecOutput` contains `stdout` (`ArrayBuffer`), `stderr` (`ArrayBuffer`), and `exitCode` (`number`). Ignored streams produce empty buffers. Use `TextDecoder` to decode text. +- `kill(signal?: number)` (`void`): Queues a signal for the process. The default is `SIGTERM`, signal `15`. The signal must be from `1` through `64`. With `stderr: "combined"`, `stderr` is `null` on `ExecProcess` and an empty `ArrayBuffer` on `ExecOutput`. Read both output channels from `stdout`. `output()` throws a `TypeError` when called more than once or after either readable stream starts being consumed. For large output, consume both readable streams concurrently instead of buffering them with `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. +`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. #### Exceptions -- `exec()` throws when the Container is not running. +- `exec()` throws when the container is not running. - `exec()` throws a `TypeError` when `cmd` is empty, an option mode is invalid, or `stderr: "combined"` is used with `stdout: "ignore"`. - `exec()` rejects if the runtime cannot create or start the process. - Environment variable names cannot contain `=` or null characters. Environment values, `cwd`, and `user` cannot contain null characters. @@ -172,9 +180,9 @@ For task-oriented examples, refer to [Execute commands](/containers/guides/execu ### `destroy` -`destroy` stops the container and optionally returns a custom error message to the `monitor()` error callback. +`destroy()` stops the container and can pass a custom error message to the `monitor()` error callback. - + ```ts this.ctx.container.destroy("Manually Destroyed"); @@ -184,17 +192,17 @@ this.ctx.container.destroy("Manually Destroyed"); #### Parameters -- `error` (optional): A string that will be sent to the error handler of the `monitor` method. This is useful for logging or debugging purposes. +- `error` (`string`, optional): Error message passed to the `monitor()` error handler for logging or debugging. #### Return values -- A promise that returns once the container is destroyed. +- `Promise`: Resolves when the container is destroyed. ### `signal` -`signal` sends an IPC signal to the container, such as SIGKILL or SIGTERM. This is useful for stopping the container gracefully or forcefully. +`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. - + ```ts const SIGTERM = 15; @@ -205,21 +213,21 @@ this.ctx.container.signal(SIGTERM); #### Parameters -- `signal`: a number representing the signal to send to the container. This is typically a POSIX signal number, such as SIGTERM (15) or SIGKILL (9). +- `signal` (`number`): POSIX signal number to send to the container, such as `SIGTERM` (`15`) or `SIGKILL` (`9`). #### Return values -- None. +- `void`: No return value. ### `setInactivityTimeout` -`setInactivityTimeout` sets how long a running container can remain inactive before the runtime stops it. +`setInactivityTimeout()` sets how long a running container can remain inactive before the runtime stops it. ```txt setInactivityTimeout(durationMs: number | bigint): Promise ``` - + ```ts await this.ctx.container.setInactivityTimeout(10 * 60 * 1000); @@ -229,17 +237,17 @@ await this.ctx.container.setInactivityTimeout(10 * 60 * 1000); #### Parameters -- `durationMs`: Inactivity timeout in milliseconds. +- `durationMs` (`number | bigint`): Inactivity timeout in milliseconds. #### Return values -- A promise that resolves after the timeout is set. +- `Promise`: Resolves after the timeout is set. ### `getTcpPort` -`getTcpPort` returns a TCP port from the container. This can be used to communicate with the container over TCP and HTTP. +`getTcpPort()` returns a TCP port from the container. Use it to communicate with the container over TCP or HTTP. - + ```ts const port = this.ctx.container.getTcpPort(8080); @@ -251,7 +259,7 @@ const res = await port.fetch("http://container/set-state", { - + ```ts const conn = this.ctx.container.getTcpPort(8080).connect("10.0.0.1:8080"); @@ -262,8 +270,8 @@ try { await request.body.pipeTo(conn.writable); } return new Response(conn.readable); -} catch (err) { - console.error("Request body piping failed:", err); +} catch (error) { + console.error("Request body piping failed:", error); return new Response("Failed to proxy request body", { status: 502 }); } ``` @@ -272,21 +280,20 @@ try { #### Parameters -- `port` (number): a TCP port number to use for communication with the container. +- `port` (`number`): TCP port number to use for communication with the container. #### Return values -- `TcpPort`: a `TcpPort` object representing the TCP port. This object can be used to send requests to the container over TCP and HTTP. +- `TcpPort`: Object used to send requests to the container over TCP or HTTP. ### `monitor` -`monitor` returns a promise that resolves when a container exits and errors if a container errors. This is useful for setting up -callbacks to handle container status changes in your Workers code. +`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. - + ```ts -class MyContainer extends DurableObject { +class MyDurableObject extends DurableObject { startAndMonitor() { const container = this.ctx.container; container.start(); @@ -304,17 +311,17 @@ class MyContainer extends DurableObject { #### Parameters -- None +- None. #### Return values -- A promise that resolves when the container exits. +- `Promise`: Resolves when the container exits. ### `interceptOutboundHttp` -`interceptOutboundHttp` routes outbound HTTP requests matching a hostname, hostname glob, IP address, IP:port, or CIDR range through a `WorkerEntrypoint`. Can be called before or after starting the container. Open connections pick up the new handler without being dropped. +`interceptOutboundHttp()` routes outbound HTTP requests matching a hostname, hostname glob, IP address, IP:port, or CIDR range through a `WorkerEntrypoint`. Call it before or after starting the container. Open connections use the new handler without being dropped. - + ```ts const worker = this.ctx.exports.MyWorker({ props: { message: "hello" } }); @@ -336,18 +343,18 @@ await this.ctx.container.interceptOutboundHttp("123.123.123.123/23", worker); #### Parameters -- `target` (string): A hostname, hostname glob (for example, `*.example.com`), IP address, IP:port, or CIDR range to match. -- `worker` (WorkerEntrypoint): A `WorkerEntrypoint` instance to handle matching requests. +- `target` (`string`): Hostname, hostname glob (for example, `*.example.com`), IP address, IP:port, or CIDR range to match. +- `worker` (`WorkerEntrypoint`): `WorkerEntrypoint` instance that handles matching requests. #### Return values -- None. +- `void`: No return value. ### `interceptAllOutboundHttp` -`interceptAllOutboundHttp` routes all outbound HTTP requests from the container through a `WorkerEntrypoint`, regardless of destination. +`interceptAllOutboundHttp()` routes all outbound HTTP requests from the container through a `WorkerEntrypoint`, regardless of destination. - + ```ts await this.ctx.container.interceptAllOutboundHttp(worker); @@ -357,19 +364,19 @@ await this.ctx.container.interceptAllOutboundHttp(worker); #### Parameters -- `worker` (WorkerEntrypoint): A `WorkerEntrypoint` instance to handle all outbound HTTP requests. +- `worker` (`WorkerEntrypoint`): `WorkerEntrypoint` instance that handles all outbound HTTP requests. #### Return values -- A promise that resolves once the intercept rule is installed. +- `Promise`: Resolves when the intercept rule is installed. ### `interceptOutboundHttps` -`interceptOutboundHttps` routes outbound HTTPS requests matching a hostname or hostname glob through a `WorkerEntrypoint`. Works the same way as `interceptOutboundHttp` but for HTTPS traffic. The container must trust the CA certificate at `/etc/cloudflare/certs/cloudflare-containers-ca.crt` for HTTPS interception to work. +`interceptOutboundHttps()` routes outbound HTTPS requests matching a hostname or hostname glob through a `WorkerEntrypoint`. 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. -Supports glob patterns where `*` matches any sequence of characters. +Hostname globs support `*` to match any sequence of characters. - + ```ts const worker = this.ctx.exports.MyWorker({ props: {} }); @@ -388,18 +395,18 @@ this.ctx.container.interceptOutboundHttps("*", worker); #### Parameters -- `target` (string): A hostname or hostname glob pattern to match. Use `*` to intercept all HTTPS traffic. -- `worker` (WorkerEntrypoint): A `WorkerEntrypoint` instance to handle matching requests. +- `target` (`string`): Hostname or hostname glob pattern to match. Use `*` to intercept all HTTPS traffic. +- `worker` (`WorkerEntrypoint`): `WorkerEntrypoint` instance that handles matching requests. #### Return values -- None. +- `void`: No return value. ## Related resources -- [Containers APIs](/containers/api/) — compare direct runtime control with the `Container` class -- [Container class reference](/containers/api/container-class/) — reference for existing `Container` class applications -- [Containers overview](/containers/) -- [Get started with Containers](/containers/get-started/) -- [SQLite storage API](/durable-objects/api/sqlite-storage-api/) — persist state across container restarts -- [Durable Objects](/durable-objects/) — the underlying platform that powers Containers +- [Containers APIs](/containers/api/): Compare direct runtime control with the `Container` class. +- [Container class reference](/containers/api/container-class/): Reference for existing `Container` class applications. +- [Containers overview](/containers/): Understand how Cloudflare Containers work. +- [Get started with Containers](/containers/get-started/): Deploy your first container. +- [SQLite storage API](/durable-objects/api/sqlite-storage-api/): Persist state across container restarts. +- [Durable Objects](/durable-objects/): The underlying platform that powers Containers. diff --git a/src/content/docs/containers/api/index.mdx b/src/content/docs/containers/api/index.mdx index d8a0349aee4..e69721de3b4 100644 --- a/src/content/docs/containers/api/index.mdx +++ b/src/content/docs/containers/api/index.mdx @@ -30,8 +30,8 @@ For new applications, we recommend the Durable Object Container API. It lets you href="/containers/api/container-class/" icon="seti:typescript" > - Reference the class and its routing, readiness checks, lifecycle hooks, - and scheduling for existing applications. + Reference the class and its routing, readiness checks, lifecycle hooks, and + scheduling for existing applications. @@ -72,7 +72,7 @@ For request routing, use [`getTcpPort()`](/containers/api/durable-object-contain ### Container class -The `Container` class builds on Durable Objects and the runtime API. Existing applications may use its request proxying, readiness checks, lifecycle hooks, and scheduling. These helpers handle common Durable Object lifecycle tasks. Some features still use Durable Object storage and alarms. +The `Container` class extends `DurableObject` and wraps the container runtime API with convenience methods. Existing applications can use these methods for request proxying, readiness checks, lifecycle hooks, and scheduling. Durable Object storage and alarms remain available when needed. From b387f20e70f4cb3d6ba7f36d0d8e283f95499e23 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Mon, 28 Sep 2026 17:05:40 -0400 Subject: [PATCH 14/23] [Containers] Use exports in Wrangler configuration --- .../containers/configuration/wrangler.mdx | 24 ++++++++++++------- .../docs/containers/get-started/index.mdx | 12 +++++----- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/content/docs/containers/configuration/wrangler.mdx b/src/content/docs/containers/configuration/wrangler.mdx index 932f24f8608..06a783c35a8 100644 --- a/src/content/docs/containers/configuration/wrangler.mdx +++ b/src/content/docs/containers/configuration/wrangler.mdx @@ -1,6 +1,6 @@ --- title: Wrangler configuration -description: Configure a Container, its Durable Object binding, and its migration in Wrangler. +description: Configure a Container, its Durable Object binding, and its class export in Wrangler. pcx_content_type: configuration sidebar: order: 0 @@ -15,7 +15,7 @@ Define Containers in the Wrangler configuration file for your Worker. Each Conta ## Minimal configuration -A Container application requires a Container definition, a Durable Object binding, and a Durable Object migration: +A new Container application requires a Container definition, a Durable Object binding, and a Durable Object class export: @@ -40,12 +40,12 @@ A Container application requires a Container definition, a Durable Object bindin }, ], }, - "migrations": [ - { - "tag": "v1", - "new_sqlite_classes": ["MyContainer"], + "exports": { + "MyContainer": { + "type": "durable-object", + "storage": "sqlite", }, - ], + }, } ``` @@ -55,9 +55,15 @@ The configuration uses three sections: 1. **`containers`** defines the container image and associates it with a Durable Object class through `class_name`. 2. **`durable_objects.bindings`** makes the Durable Object namespace available to Worker code. In this example, access it through `env.MY_CONTAINER`. -3. **`migrations`** creates the SQLite-backed Durable Object class. Use `new_sqlite_classes`, not `new_classes`, for a Container. +3. **`exports`** declares the Durable Object class and provisions it with SQLite storage. + +The `class_name` in `containers` and `durable_objects.bindings`, and the key in `exports`, must match the exported Durable Object class in your Worker. + +:::note[Existing applications] + +Existing applications that use the legacy `migrations` array can continue to use it. Do not configure `exports` and `migrations` together. To switch an existing application to `exports`, refer to [Migrate from the legacy `migrations` flow](/durable-objects/reference/durable-objects-migrations/#migrate-from-the-legacy-migrations-flow). -The `class_name` in `containers` and `durable_objects.bindings`, and the class listed in `migrations.new_sqlite_classes`, must match the exported Durable Object class in your Worker. +::: ## Container settings diff --git a/src/content/docs/containers/get-started/index.mdx b/src/content/docs/containers/get-started/index.mdx index b7e9708d893..1c9b1dac719 100644 --- a/src/content/docs/containers/get-started/index.mdx +++ b/src/content/docs/containers/get-started/index.mdx @@ -95,12 +95,12 @@ Your [Wrangler configuration file](/containers/configuration/wrangler/) defines }, ], }, - "migrations": [ - { - "tag": "v1", - "new_sqlite_classes": ["MyContainer"], + "exports": { + "MyContainer": { + "type": "durable-object", + "storage": "sqlite", }, - ], + }, } ``` @@ -112,7 +112,7 @@ Important points about this config: - `class_name` must be a [Durable Object class name](/durable-objects/api/base/). - `max_instances` declares the maximum number of simultaneously running container instances that will run. -- The Durable Object must use [`new_sqlite_classes`](/durable-objects/best-practices/access-durable-objects-storage/#create-sqlite-backed-durable-object-class) not `new_classes`. +- The `exports` entry declares the Durable Object class with SQLite storage. Existing applications that use the legacy `migrations` array can continue to use it, but `exports` and `migrations` cannot be combined. ### The Container Image From 484ad633ee73c4a47cbb5dd2604af29904b3321d Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Mon, 28 Sep 2026 17:20:48 -0400 Subject: [PATCH 15/23] [Containers] Address API documentation review --- src/content/docs/containers/api/durable-object-container.mdx | 2 +- src/content/docs/containers/concepts/architecture.mdx | 2 +- src/content/docs/containers/examples/env-vars-and-secrets.mdx | 2 +- src/content/docs/containers/examples/status-hooks.mdx | 2 +- .../guides/migrate-to-durable-object-container-api.mdx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index 19d8e3936fb..304b1d1312c 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -1,7 +1,7 @@ --- title: Durable Object Container API description: Access and manage containers associated with a Durable Object, including start, stop, and interaction methods. -pcx_content_type: concept +pcx_content_type: reference sidebar: order: 1 products: diff --git a/src/content/docs/containers/concepts/architecture.mdx b/src/content/docs/containers/concepts/architecture.mdx index a14e16274a3..dbdd81beb46 100644 --- a/src/content/docs/containers/concepts/architecture.mdx +++ b/src/content/docs/containers/concepts/architecture.mdx @@ -120,7 +120,7 @@ should be built for the `linux/amd64` architecture, and should stay within ### Container shutdown -With the Durable Object Container API, call [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout) to let the runtime stop the container after the Durable Object becomes inactive. The Durable Object becomes inactive when it stops receiving requests; the timeout can keep the container available while the Durable Object sleeps. You can also stop a container with [`signal()`](/containers/api/durable-object-container/#signal) or [`destroy()`](/containers/api/durable-object-container/#destroy). +With the Durable Object Container API, call [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout) to let the runtime stop the container after the Durable Object becomes inactive. The Durable Object becomes inactive when it stops receiving requests. The timeout can keep the container available while the Durable Object sleeps. You can also stop a container with [`signal()`](/containers/api/durable-object-container/#signal) or [`destroy()`](/containers/api/durable-object-container/#destroy). The `Container` class sets [`sleepAfter`](/containers/api/container-class/#sleepafter) to 10 minutes by default. Its [`onActivityExpired()`](/containers/api/container-class/#onactivityexpired) implementation calls [`stop()`](/containers/api/container-class/#stop). You can change the duration or override the hook. diff --git a/src/content/docs/containers/examples/env-vars-and-secrets.mdx b/src/content/docs/containers/examples/env-vars-and-secrets.mdx index 00d3117d36b..4dfa0e739bf 100644 --- a/src/content/docs/containers/examples/env-vars-and-secrets.mdx +++ b/src/content/docs/containers/examples/env-vars-and-secrets.mdx @@ -13,7 +13,7 @@ products: import { WranglerConfig, PackageManagers } from "~/components"; Environment variables can be passed into a Container using the `envVars` field -in the [`Container`](/containers/reference/container-class/) class, or by setting manually when the Container starts. +in the [`Container`](/containers/api/container-class/) class, or by setting manually when the Container starts. Secrets can be passed into a Container by using [Worker Secrets](/workers/configuration/secrets/) or the [Secret Store](/secrets-store/integrations/workers/), then passing them into the Container diff --git a/src/content/docs/containers/examples/status-hooks.mdx b/src/content/docs/containers/examples/status-hooks.mdx index 1a0e219a1e5..3d38fcd9e2b 100644 --- a/src/content/docs/containers/examples/status-hooks.mdx +++ b/src/content/docs/containers/examples/status-hooks.mdx @@ -12,7 +12,7 @@ products: --- When a Container starts, stops, becomes idle, and errors, it can trigger code execution in a Worker -that has defined status hooks on the `Container` class. Refer to the [Container class lifecycle hooks](/containers/reference/container-class/#lifecycle-hooks) for more details. +that has defined status hooks on the `Container` class. Refer to the [Container class lifecycle hooks](/containers/api/container-class/#lifecycle-hooks) for more details. ```ts import { Container } from "@cloudflare/containers"; diff --git a/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx b/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx index 423860e1636..ffc89f37185 100644 --- a/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx +++ b/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx @@ -22,7 +22,7 @@ The `Container` class extends `DurableObject`. Replace its inherited lifecycle a 1. In your Worker, change the class to extend `DurableObject` from `cloudflare:workers`. Keep the exported class name if you want to retain its existing container definition and Durable Object binding. Do not add a new Durable Object migration solely because you changed the base class. Refer to [Wrangler configuration](/containers/configuration/wrangler/). -2. Replace `start()` and `stop()` calls with [`ctx.container.start()`](/containers/api/durable-object-container/#start) and [`signal()`](/containers/api/durable-object-container/#signal) or [`destroy()`](/containers/api/durable-object-container/#destroy), as appropriate. Do not assume `start()` waits for a port to become ready. +2. Replace `start()` calls with [`ctx.container.start()`](/containers/api/durable-object-container/#start). Replace `stop()` calls with [`signal()`](/containers/api/durable-object-container/#signal) or [`destroy()`](/containers/api/durable-object-container/#destroy), as appropriate. Do not assume `start()` waits for a port to become ready. 3. Replace `defaultPort`, `containerFetch()`, and automatic `fetch()` routing with [`getTcpPort(port).fetch()`](/containers/api/durable-object-container/#gettcpport) and your own request routing. Check port readiness before forwarding requests. 4. Replace `sleepAfter` with [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout). Replace lifecycle hooks and `schedule()` with application code, [`monitor()`](/containers/api/durable-object-container/#monitor), and [Durable Object alarms](/durable-objects/api/alarms/) where appropriate. 5. Test startup, concurrent requests, readiness, idle shutdown, and recovery after a container restart. Then remove `@cloudflare/containers` only if no other code imports it. From 83a51a4d4e4bb43beb16436a474a2961448253f8 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Mon, 28 Sep 2026 17:26:21 -0400 Subject: [PATCH 16/23] [Containers] Correct Durable Object API types --- .../api/durable-object-container.mdx | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index 304b1d1312c..71384474300 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -15,6 +15,12 @@ Each [container](/containers/) is managed and proxied by a Durable Object. The D 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. +:::note[API coverage] + +This page documents lifecycle, networking, and process-control APIs. Named images, runtime instance sizing, inspection, and snapshot APIs are outside the scope of this page. + +::: + :::note We recommend starting new applications with the Durable Object Container API. It lets you run container workloads alongside the Durable Object's persistent storage, alarms, and request handling. Use `ctx.container` to control when the container starts, receives traffic, and stops, while the Durable Object manages application state and coordination. For existing applications that use the `Container` class, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). ::: @@ -82,10 +88,10 @@ this.ctx.container.start(); #### Parameters -- `options` (`object`, optional): Container startup options: +- `options` (`ContainerStartupOptions`, optional): Container startup options: - `env` (`Record`, optional): Environment variables to pass to the container. - `entrypoint` (`string[]`, optional): Command and arguments to run in the container. - - `enableInternet` (`boolean`, optional): Whether to allow outbound Internet access. + - `enableInternet` (`boolean`, required): Whether to allow outbound Internet access. Required when you pass `options`. #### Return values @@ -147,6 +153,8 @@ export class MyDurableObject extends DurableObject { - `cwd` (`string`, optional): Working directory for the process. - `env` (`Record`, optional): Environment additions and overrides. The process inherits existing container variables. Matching keys use the per-execution value. - `user` (`string`, optional): Image user for the process. + - `signal` (`AbortSignal`, optional): Signal used to abort the process. + - `pty` (`boolean | { cols?: number; rows?: number }`, optional): Enables a pseudoterminal. Set `cols` and `rows` to configure its initial dimensions. #### Return values @@ -158,9 +166,11 @@ An `ExecProcess` has these fields and methods: - `stdout` (`ReadableStream | null`): Readable standard output when piped. - `stderr` (`ReadableStream | null`): Readable standard error when piped separately. - `pid` (`number`): Process identifier. +- `isPty` (`boolean`): Whether the process uses a pseudoterminal. - `exitCode` (`Promise`): Resolves when the process exits. Nonzero codes resolve normally instead of rejecting. - `output()` (`Promise`): Reads buffered output once. `ExecOutput` contains `stdout` (`ArrayBuffer`), `stderr` (`ArrayBuffer`), and `exitCode` (`number`). Ignored streams produce empty buffers. Use `TextDecoder` to decode text. - `kill(signal?: number)` (`void`): Queues a signal for the process. The default is `SIGTERM`, signal `15`. The signal must be from `1` through `64`. +- `resize(cols: number, rows: number)` (`void`): Resizes the pseudoterminal to the specified number of columns and rows. With `stderr: "combined"`, `stderr` is `null` on `ExecProcess` and an empty `ArrayBuffer` on `ExecOutput`. Read both output channels from `stdout`. @@ -180,19 +190,19 @@ For task-oriented examples, refer to [Execute commands](/containers/guides/execu ### `destroy` -`destroy()` stops the container and can pass a custom error message to the `monitor()` error callback. +`destroy()` stops the container and can pass a custom rejection reason to `monitor()`. ```ts -this.ctx.container.destroy("Manually Destroyed"); +await this.ctx.container.destroy("Manually Destroyed"); ``` #### Parameters -- `error` (`string`, optional): Error message passed to the `monitor()` error handler for logging or debugging. +- `error` (`any`, optional): Rejection reason passed to `monitor()`. A string is commonly used for logging or debugging. #### Return values @@ -284,7 +294,7 @@ try { #### Return values -- `TcpPort`: Object used to send requests to the container over TCP or HTTP. +- `Fetcher`: Object used to send HTTP requests or TCP connections to the container port. ### `monitor` @@ -319,7 +329,7 @@ class MyDurableObject extends DurableObject { ### `interceptOutboundHttp` -`interceptOutboundHttp()` routes outbound HTTP requests matching a hostname, hostname glob, IP address, IP:port, or CIDR range through a `WorkerEntrypoint`. Call it before or after starting the container. Open connections use the new handler without being dropped. +`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. Open connections use the new handler without being dropped. @@ -327,10 +337,10 @@ class MyDurableObject extends DurableObject { const worker = this.ctx.exports.MyWorker({ props: { message: "hello" } }); // Match a specific hostname -this.ctx.container.interceptOutboundHttp("api.example.com", worker); +await this.ctx.container.interceptOutboundHttp("api.example.com", worker); // Match a hostname glob pattern -this.ctx.container.interceptOutboundHttp("*.example.com", worker); +await this.ctx.container.interceptOutboundHttp("*.example.com", worker); // Match an IP:port await this.ctx.container.interceptOutboundHttp("15.0.0.1:80", worker); @@ -343,16 +353,16 @@ await this.ctx.container.interceptOutboundHttp("123.123.123.123/23", worker); #### Parameters -- `target` (`string`): Hostname, hostname glob (for example, `*.example.com`), IP address, IP:port, or CIDR range to match. -- `worker` (`WorkerEntrypoint`): `WorkerEntrypoint` instance that handles matching requests. +- `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. #### Return values -- `void`: No return value. +- `Promise`: Resolves when the intercept rule is installed. ### `interceptAllOutboundHttp` -`interceptAllOutboundHttp()` routes all outbound HTTP requests from the container through a `WorkerEntrypoint`, regardless of destination. +`interceptAllOutboundHttp()` routes all outbound HTTP requests from the container through a `Fetcher`, regardless of destination. @@ -364,7 +374,7 @@ await this.ctx.container.interceptAllOutboundHttp(worker); #### Parameters -- `worker` (`WorkerEntrypoint`): `WorkerEntrypoint` instance that handles all outbound HTTP requests. +- `binding` (`Fetcher`): Worker entrypoint or service binding that handles all outbound HTTP requests. #### Return values @@ -372,7 +382,7 @@ await this.ctx.container.interceptAllOutboundHttp(worker); ### `interceptOutboundHttps` -`interceptOutboundHttps()` routes outbound HTTPS requests matching a hostname or hostname glob through a `WorkerEntrypoint`. 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. +`interceptOutboundHttps()` routes outbound HTTPS requests matching a hostname or hostname glob 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. @@ -382,25 +392,25 @@ Hostname globs support `*` to match any sequence of characters. const worker = this.ctx.exports.MyWorker({ props: {} }); // Match a specific hostname -this.ctx.container.interceptOutboundHttps("api.example.com", worker); +await this.ctx.container.interceptOutboundHttps("api.example.com", worker); // Match a hostname glob pattern -this.ctx.container.interceptOutboundHttps("*.example.com", worker); +await this.ctx.container.interceptOutboundHttps("*.example.com", worker); // Intercept all HTTPS traffic -this.ctx.container.interceptOutboundHttps("*", worker); +await this.ctx.container.interceptOutboundHttps("*", worker); ``` #### Parameters -- `target` (`string`): Hostname or hostname glob pattern to match. Use `*` to intercept all HTTPS traffic. -- `worker` (`WorkerEntrypoint`): `WorkerEntrypoint` instance that handles matching requests. +- `addr` (`string`): Hostname or hostname glob pattern to match. Use `*` to intercept all HTTPS traffic. +- `binding` (`Fetcher`): Worker entrypoint or service binding that handles matching requests. #### Return values -- `void`: No return value. +- `Promise`: Resolves when the intercept rule is installed. ## Related resources From d4a0bc0cc34eab802b24e04e88223858847e3afd Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Mon, 28 Sep 2026 17:32:22 -0400 Subject: [PATCH 17/23] [Containers] Remove API coverage note --- .../docs/containers/api/durable-object-container.mdx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index 71384474300..fe4d589ea18 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -15,12 +15,6 @@ Each [container](/containers/) is managed and proxied by a Durable Object. The D 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. -:::note[API coverage] - -This page documents lifecycle, networking, and process-control APIs. Named images, runtime instance sizing, inspection, and snapshot APIs are outside the scope of this page. - -::: - :::note We recommend starting new applications with the Durable Object Container API. It lets you run container workloads alongside the Durable Object's persistent storage, alarms, and request handling. Use `ctx.container` to control when the container starts, receives traffic, and stops, while the Durable Object manages application state and coordination. For existing applications that use the `Container` class, refer to [Migrate to the Durable Object Container API](/containers/guides/migrate-to-durable-object-container-api/). ::: From 4036e5717a25b5a890e7016488c3fcf0bbdf009a Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Mon, 28 Sep 2026 17:48:45 -0400 Subject: [PATCH 18/23] [Containers] Scope Durable Object API reference --- .../containers/api/durable-object-container.mdx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index fe4d589ea18..a1a024b4f39 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -82,7 +82,7 @@ this.ctx.container.start(); #### Parameters -- `options` (`ContainerStartupOptions`, optional): Container startup options: +- `options` (`object`, optional): Common container startup options: - `env` (`Record`, optional): Environment variables to pass to the container. - `entrypoint` (`string[]`, optional): Command and arguments to run in the container. - `enableInternet` (`boolean`, required): Whether to allow outbound Internet access. Required when you pass `options`. @@ -147,8 +147,6 @@ export class MyDurableObject extends DurableObject { - `cwd` (`string`, optional): Working directory for the process. - `env` (`Record`, optional): Environment additions and overrides. The process inherits existing container variables. Matching keys use the per-execution value. - `user` (`string`, optional): Image user for the process. - - `signal` (`AbortSignal`, optional): Signal used to abort the process. - - `pty` (`boolean | { cols?: number; rows?: number }`, optional): Enables a pseudoterminal. Set `cols` and `rows` to configure its initial dimensions. #### Return values @@ -160,11 +158,9 @@ An `ExecProcess` has these fields and methods: - `stdout` (`ReadableStream | null`): Readable standard output when piped. - `stderr` (`ReadableStream | null`): Readable standard error when piped separately. - `pid` (`number`): Process identifier. -- `isPty` (`boolean`): Whether the process uses a pseudoterminal. - `exitCode` (`Promise`): Resolves when the process exits. Nonzero codes resolve normally instead of rejecting. - `output()` (`Promise`): Reads buffered output once. `ExecOutput` contains `stdout` (`ArrayBuffer`), `stderr` (`ArrayBuffer`), and `exitCode` (`number`). Ignored streams produce empty buffers. Use `TextDecoder` to decode text. - `kill(signal?: number)` (`void`): Queues a signal for the process. The default is `SIGTERM`, signal `15`. The signal must be from `1` through `64`. -- `resize(cols: number, rows: number)` (`void`): Resizes the pseudoterminal to the specified number of columns and rows. With `stderr: "combined"`, `stderr` is `null` on `ExecProcess` and an empty `ArrayBuffer` on `ExecOutput`. Read both output channels from `stdout`. @@ -184,7 +180,7 @@ For task-oriented examples, refer to [Execute commands](/containers/guides/execu ### `destroy` -`destroy()` stops the container and can pass a custom rejection reason to `monitor()`. +`destroy()` stops the container and can include an optional reason for the operation. @@ -196,7 +192,7 @@ await this.ctx.container.destroy("Manually Destroyed"); #### Parameters -- `error` (`any`, optional): Rejection reason passed to `monitor()`. A string is commonly used for logging or debugging. +- `error` (`any`, optional): Optional reason associated with the destroy operation. A string is commonly used for logging or debugging. #### Return values @@ -297,7 +293,11 @@ try { ```ts -class MyDurableObject extends DurableObject { +import { DurableObject } from "cloudflare:workers"; + +interface Env {} + +class MyDurableObject extends DurableObject { startAndMonitor() { const container = this.ctx.container; container.start(); From c5b87567b446ebc20f69dff8af595b9aca2384b8 Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Mon, 28 Sep 2026 17:50:25 -0400 Subject: [PATCH 19/23] [Containers] Add migration guide and workbench --- .../containers/container-api-migration/.npmrc | 2 + .../container-api-migration/README.md | 57 + .../container/Dockerfile | 11 + .../container/server.mjs | 96 ++ .../container-api-migration/package.json | 23 + .../container-api-migration/pnpm-lock.yaml | 1186 +++++++++++++++++ .../container-api-migration/scripts/e2e.mjs | 135 ++ .../container-api-migration/shared/lab.ts | 137 ++ .../shared/workbench.ts | 94 ++ .../stages/1-container-class/src/index.ts | 176 +++ .../stages/1-container-class/wrangler.jsonc | 28 + .../stages/2-bridge/src/index.ts | 345 +++++ .../stages/2-bridge/wrangler.jsonc | 28 + .../stages/3-durable-object-api/src/index.ts | 223 ++++ .../3-durable-object-api/wrangler.jsonc | 28 + .../container-api-migration/tsconfig.json | 14 + src/content/docs/containers/api/index.mdx | 31 +- ...igrate-to-durable-object-container-api.mdx | 16 +- tsconfig.json | 7 +- 19 files changed, 2616 insertions(+), 21 deletions(-) create mode 100644 examples/containers/container-api-migration/.npmrc create mode 100644 examples/containers/container-api-migration/README.md create mode 100644 examples/containers/container-api-migration/container/Dockerfile create mode 100644 examples/containers/container-api-migration/container/server.mjs create mode 100644 examples/containers/container-api-migration/package.json create mode 100644 examples/containers/container-api-migration/pnpm-lock.yaml create mode 100644 examples/containers/container-api-migration/scripts/e2e.mjs create mode 100644 examples/containers/container-api-migration/shared/lab.ts create mode 100644 examples/containers/container-api-migration/shared/workbench.ts create mode 100644 examples/containers/container-api-migration/stages/1-container-class/src/index.ts create mode 100644 examples/containers/container-api-migration/stages/1-container-class/wrangler.jsonc create mode 100644 examples/containers/container-api-migration/stages/2-bridge/src/index.ts create mode 100644 examples/containers/container-api-migration/stages/2-bridge/wrangler.jsonc create mode 100644 examples/containers/container-api-migration/stages/3-durable-object-api/src/index.ts create mode 100644 examples/containers/container-api-migration/stages/3-durable-object-api/wrangler.jsonc create mode 100644 examples/containers/container-api-migration/tsconfig.json diff --git a/examples/containers/container-api-migration/.npmrc b/examples/containers/container-api-migration/.npmrc new file mode 100644 index 00000000000..19c37820f0f --- /dev/null +++ b/examples/containers/container-api-migration/.npmrc @@ -0,0 +1,2 @@ +registry=https://registry.npmjs.org/ +@cloudflare:registry=https://registry.npmjs.org/ diff --git a/examples/containers/container-api-migration/README.md b/examples/containers/container-api-migration/README.md new file mode 100644 index 00000000000..505261b97d1 --- /dev/null +++ b/examples/containers/container-api-migration/README.md @@ -0,0 +1,57 @@ +# Container API migration workbench + +This executable reference migrates one container-enabled Durable Object through three releases: + +1. **Container class** — uses `@cloudflare/containers` helpers. +2. **Bridge** — keeps extending `Container`, but exposes helper and direct `ctx.container` routes on the same Durable Object and container instance. +3. **Durable Object API** — changes the base class to `DurableObject` and replaces the remaining helpers. + +All stages keep the Worker name, exported `MigrationWorkbench` class, `MIGRATION_WORKBENCH` binding, `v1` migration tag, and container image unchanged. Changing the TypeScript base class does not require a new Durable Object migration. + +## Coverage + +The lab exercises two-port readiness, request proxying, `switchPort()`, `getTcpPort()`, environment variables, lifecycle hooks, `monitor()`, `exec()`, inactivity, signals, destruction, outbound interception, scheduling, alarms, and Durable Object storage retained across deployments. + +The E2E run found an important direct-API responsibility: a new instance may be temporarily unavailable immediately after `destroy()`. The direct implementations use a bounded startup retry before checking port readiness. + +## Install and run locally + +```sh +pnpm install --ignore-workspace --frozen-lockfile +pnpm dev:legacy +pnpm dev:bridge +pnpm dev:direct +``` + +Docker must be running. Stop each dev server before starting the next stage so all stages reuse `.wrangler/state`. + +## Deploy and verify + +Deploy each stage over the same Worker and use the URL printed by Wrangler: + +```sh +pnpm deploy:legacy +pnpm test:e2e https://container-api-migration-workbench..workers.dev legacy + +pnpm deploy:bridge +pnpm test:e2e https://container-api-migration-workbench..workers.dev bridge +``` + +Create an alarm through the bridge before the cutover: + +```sh +curl -X POST "https://container-api-migration-workbench..workers.dev/api/schedule-cutover?instance=reference-e2e&mode=helper&delay=120" +``` + +Then replace the base class without changing any Durable Object identifiers: + +```sh +pnpm deploy:direct +pnpm test:e2e https://container-api-migration-workbench..workers.dev direct +``` + +The final suite requires legacy and bridge events to remain in the same Durable Object storage. It also verifies that the direct `alarm()` handler processed the marker created before cutover. + +## Bridge limitation + +The bridge does not install a Durable Object `alarm()` handler. The `Container` class owns that handler until the final cutover. Direct scheduling therefore moves last; other `ctx.container` operations migrate incrementally first. diff --git a/examples/containers/container-api-migration/container/Dockerfile b/examples/containers/container-api-migration/container/Dockerfile new file mode 100644 index 00000000000..5678d731f11 --- /dev/null +++ b/examples/containers/container-api-migration/container/Dockerfile @@ -0,0 +1,11 @@ +FROM node:22-slim + +WORKDIR /workbench +COPY server.mjs ./server.mjs + +ENV PORT=8080 +ENV ALT_PORT=9090 + +EXPOSE 8080 9090 + +CMD ["node", "server.mjs"] diff --git a/examples/containers/container-api-migration/container/server.mjs b/examples/containers/container-api-migration/container/server.mjs new file mode 100644 index 00000000000..7e7bd8c294b --- /dev/null +++ b/examples/containers/container-api-migration/container/server.mjs @@ -0,0 +1,96 @@ +import { createServer } from "node:http"; + +const startedAt = new Date().toISOString(); +let bootstrapCount = 0; +let requestCount = 0; + +async function readBody(request) { + const chunks = []; + for await (const chunk of request) chunks.push(chunk); + return Buffer.concat(chunks).toString("utf8"); +} + +function send(response, status, value) { + response.writeHead(status, { + "content-type": "application/json; charset=utf-8", + }); + response.end(`${JSON.stringify(value, null, 2)}\n`); +} + +function createHandler(port) { + return async (request, response) => { + requestCount += 1; + const url = new URL(request.url ?? "/", `http://container:${port}`); + if (url.pathname === "/ping" || url.pathname === "/health") { + send(response, 200, { ok: true, port, startedAt }); + return; + } + if (url.pathname === "/bootstrap" && request.method === "POST") { + bootstrapCount += 1; + send(response, 200, { bootstrapped: true, bootstrapCount, port }); + return; + } + if (url.pathname === "/echo") { + send(response, 200, { + body: await readBody(request), + bootstrapCount, + environment: { + LAB_MODE: process.env.LAB_MODE ?? null, + LAB_STAGE: process.env.LAB_STAGE ?? null, + }, + method: request.method, + pid: process.pid, + port, + requestCount, + startedAt, + via: url.searchParams.get("via"), + }); + return; + } + if (url.pathname === "/outbound") { + try { + const outbound = await fetch("http://workbench.internal/probe"); + send(response, outbound.status, { + body: await outbound.text(), + intercepted: + outbound.headers.get("x-workbench-intercepted") === "true", + status: outbound.status, + }); + } catch (error) { + send(response, 502, { + error: error instanceof Error ? error.message : String(error), + intercepted: false, + }); + } + return; + } + if (url.pathname === "/scheduled" && request.method === "POST") { + send(response, 200, { + body: await readBody(request), + handledAt: new Date().toISOString(), + port, + }); + return; + } + send(response, 404, { + error: "Unknown container route", + path: url.pathname, + }); + }; +} + +for (const port of [ + Number(process.env.PORT ?? 8080), + Number(process.env.ALT_PORT ?? 9090), +]) { + createServer(createHandler(port)).listen(port, "0.0.0.0", () => { + console.log(`Migration workbench listening on ${port}`); + }); +} + +for (const signal of ["SIGINT", "SIGTERM"]) { + process.on(signal, () => { + console.log(`Migration workbench received ${signal}`); + process.exit(0); + }); +} diff --git a/examples/containers/container-api-migration/package.json b/examples/containers/container-api-migration/package.json new file mode 100644 index 00000000000..e99775cdd2c --- /dev/null +++ b/examples/containers/container-api-migration/package.json @@ -0,0 +1,23 @@ +{ + "name": "container-api-migration-workbench", + "private": true, + "type": "module", + "scripts": { + "check": "tsc --noEmit", + "dev:legacy": "wrangler dev --config stages/1-container-class/wrangler.jsonc --persist-to .wrangler/state", + "dev:bridge": "wrangler dev --config stages/2-bridge/wrangler.jsonc --persist-to .wrangler/state", + "dev:direct": "wrangler dev --config stages/3-durable-object-api/wrangler.jsonc --persist-to .wrangler/state", + "deploy:legacy": "wrangler deploy --config stages/1-container-class/wrangler.jsonc", + "deploy:bridge": "wrangler deploy --config stages/2-bridge/wrangler.jsonc", + "deploy:direct": "wrangler deploy --config stages/3-durable-object-api/wrangler.jsonc", + "test:e2e": "node scripts/e2e.mjs" + }, + "dependencies": { + "@cloudflare/containers": "0.3.7" + }, + "devDependencies": { + "@cloudflare/workers-types": "5.20260914.1", + "typescript": "5.9.3", + "wrangler": "4.131.2" + } +} diff --git a/examples/containers/container-api-migration/pnpm-lock.yaml b/examples/containers/container-api-migration/pnpm-lock.yaml new file mode 100644 index 00000000000..22cba95f58c --- /dev/null +++ b/examples/containers/container-api-migration/pnpm-lock.yaml @@ -0,0 +1,1186 @@ +lockfileVersion: "9.0" + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + .: + dependencies: + "@cloudflare/containers": + specifier: 0.3.7 + version: 0.3.7 + devDependencies: + "@cloudflare/workers-types": + specifier: 5.20260914.1 + version: 5.20260914.1 + typescript: + specifier: 5.9.3 + version: 5.9.3 + wrangler: + specifier: 4.131.2 + version: 4.131.2(@cloudflare/workers-types@5.20260914.1) + +packages: + "@cloudflare/containers@0.3.7": + resolution: + { + integrity: sha512-DM9dm3FnIBSyiSJ1FLavKwl/lk3oAmTaynCzZQ9pZR0ncRPquSxkxd8Nu2MFILxmDDsPkxKsSNEh9mHHMty4Fw==, + } + + "@cloudflare/kv-asset-handler@0.5.0": + resolution: + { + integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==, + } + engines: { node: ">=22.0.0" } + + "@cloudflare/unenv-preset@2.16.1": + resolution: + { + integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==, + } + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: ">1.20260305.0 <2.0.0-0" + peerDependenciesMeta: + workerd: + optional: true + + "@cloudflare/workerd-darwin-64@1.20260911.1": + resolution: + { + integrity: sha512-785eaY1bkR1cm4Z/PCUeteZYmTMe6lre2zz63/GdGGimsoMsKxgl4brFPRukim8iv28EyD1XoCB/VPYF20BERA==, + } + engines: { node: ">=16" } + cpu: [x64] + os: [darwin] + + "@cloudflare/workerd-darwin-arm64@1.20260911.1": + resolution: + { + integrity: sha512-WU4bFqEN0H7ndGWxoedegv95DmNVBtv0ncXcHG9nYFTUI78sxEb0qoT3U6Ga4hyBkzsJFBX/zvVBIGX3qKldGA==, + } + engines: { node: ">=16" } + cpu: [arm64] + os: [darwin] + + "@cloudflare/workerd-linux-64@1.20260911.1": + resolution: + { + integrity: sha512-0Y2gy62oxQxWa38qinSPE6zNL5+JmumJtDY9AWW1HB8KHuATxN71o5MGzmVFfB8PwZsiHfUd2Sv7O22krCOrhw==, + } + engines: { node: ">=16" } + cpu: [x64] + os: [linux] + + "@cloudflare/workerd-linux-arm64@1.20260911.1": + resolution: + { + integrity: sha512-kttNPnx1r2lCqFUoMH62z7CqGV+j4QBbw5fdtaz4pzOrzBv0AWkNATt7onFUe+SwP8zhcepMtbm2F4kKzTf6VA==, + } + engines: { node: ">=16" } + cpu: [arm64] + os: [linux] + + "@cloudflare/workerd-windows-64@1.20260911.1": + resolution: + { + integrity: sha512-5iO/YfoBDOgO3CrHdkiiVP8SL3O2jC+c6Ux3d378TSPKLhU5+CgHjtE/ZSodWQrzr4FzFRqdW8S7n5nbyD1MHQ==, + } + engines: { node: ">=16" } + cpu: [x64] + os: [win32] + + "@cloudflare/workers-types@5.20260914.1": + resolution: + { + integrity: sha512-9xGgkvmG1lw0QdKJmSR5YNXaA221DfefpDCfPLz5PlzdYU0BXzxONQoWakLXTsFIz8xtsNfeUg+mbspIA5P4Xg==, + } + + "@cspotcode/source-map-support@0.8.1": + resolution: + { + integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==, + } + engines: { node: ">=12" } + + "@emnapi/runtime@1.11.3": + resolution: + { + integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==, + } + + "@esbuild/aix-ppc64@0.28.1": + resolution: + { + integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==, + } + engines: { node: ">=18" } + cpu: [ppc64] + os: [aix] + + "@esbuild/android-arm64@0.28.1": + resolution: + { + integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [android] + + "@esbuild/android-arm@0.28.1": + resolution: + { + integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==, + } + engines: { node: ">=18" } + cpu: [arm] + os: [android] + + "@esbuild/android-x64@0.28.1": + resolution: + { + integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [android] + + "@esbuild/darwin-arm64@0.28.1": + resolution: + { + integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [darwin] + + "@esbuild/darwin-x64@0.28.1": + resolution: + { + integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [darwin] + + "@esbuild/freebsd-arm64@0.28.1": + resolution: + { + integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [freebsd] + + "@esbuild/freebsd-x64@0.28.1": + resolution: + { + integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [freebsd] + + "@esbuild/linux-arm64@0.28.1": + resolution: + { + integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [linux] + + "@esbuild/linux-arm@0.28.1": + resolution: + { + integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==, + } + engines: { node: ">=18" } + cpu: [arm] + os: [linux] + + "@esbuild/linux-ia32@0.28.1": + resolution: + { + integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==, + } + engines: { node: ">=18" } + cpu: [ia32] + os: [linux] + + "@esbuild/linux-loong64@0.28.1": + resolution: + { + integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==, + } + engines: { node: ">=18" } + cpu: [loong64] + os: [linux] + + "@esbuild/linux-mips64el@0.28.1": + resolution: + { + integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==, + } + engines: { node: ">=18" } + cpu: [mips64el] + os: [linux] + + "@esbuild/linux-ppc64@0.28.1": + resolution: + { + integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==, + } + engines: { node: ">=18" } + cpu: [ppc64] + os: [linux] + + "@esbuild/linux-riscv64@0.28.1": + resolution: + { + integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==, + } + engines: { node: ">=18" } + cpu: [riscv64] + os: [linux] + + "@esbuild/linux-s390x@0.28.1": + resolution: + { + integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==, + } + engines: { node: ">=18" } + cpu: [s390x] + os: [linux] + + "@esbuild/linux-x64@0.28.1": + resolution: + { + integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [linux] + + "@esbuild/netbsd-arm64@0.28.1": + resolution: + { + integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [netbsd] + + "@esbuild/netbsd-x64@0.28.1": + resolution: + { + integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [netbsd] + + "@esbuild/openbsd-arm64@0.28.1": + resolution: + { + integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [openbsd] + + "@esbuild/openbsd-x64@0.28.1": + resolution: + { + integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [openbsd] + + "@esbuild/openharmony-arm64@0.28.1": + resolution: + { + integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [openharmony] + + "@esbuild/sunos-x64@0.28.1": + resolution: + { + integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [sunos] + + "@esbuild/win32-arm64@0.28.1": + resolution: + { + integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [win32] + + "@esbuild/win32-ia32@0.28.1": + resolution: + { + integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==, + } + engines: { node: ">=18" } + cpu: [ia32] + os: [win32] + + "@esbuild/win32-x64@0.28.1": + resolution: + { + integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [win32] + + "@img/colour@1.1.0": + resolution: + { + integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==, + } + engines: { node: ">=18" } + + "@img/sharp-darwin-arm64@0.35.4": + resolution: + { + integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==, + } + engines: { node: ">=20.9.0" } + cpu: [arm64] + os: [darwin] + + "@img/sharp-darwin-x64@0.35.4": + resolution: + { + integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==, + } + engines: { node: ">=20.9.0" } + cpu: [x64] + os: [darwin] + + "@img/sharp-freebsd-wasm32@0.35.4": + resolution: + { + integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==, + } + engines: { node: ">=20.9.0" } + os: [freebsd] + + "@img/sharp-libvips-darwin-arm64@1.3.3": + resolution: + { + integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==, + } + cpu: [arm64] + os: [darwin] + + "@img/sharp-libvips-darwin-x64@1.3.3": + resolution: + { + integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==, + } + cpu: [x64] + os: [darwin] + + "@img/sharp-libvips-linux-arm64@1.3.3": + resolution: + { + integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==, + } + cpu: [arm64] + os: [linux] + + "@img/sharp-libvips-linux-arm@1.3.3": + resolution: + { + integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==, + } + cpu: [arm] + os: [linux] + + "@img/sharp-libvips-linux-ppc64@1.3.3": + resolution: + { + integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==, + } + cpu: [ppc64] + os: [linux] + + "@img/sharp-libvips-linux-riscv64@1.3.3": + resolution: + { + integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==, + } + cpu: [riscv64] + os: [linux] + + "@img/sharp-libvips-linux-s390x@1.3.3": + resolution: + { + integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==, + } + cpu: [s390x] + os: [linux] + + "@img/sharp-libvips-linux-x64@1.3.3": + resolution: + { + integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==, + } + cpu: [x64] + os: [linux] + + "@img/sharp-libvips-linuxmusl-arm64@1.3.3": + resolution: + { + integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==, + } + cpu: [arm64] + os: [linux] + + "@img/sharp-libvips-linuxmusl-x64@1.3.3": + resolution: + { + integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==, + } + cpu: [x64] + os: [linux] + + "@img/sharp-linux-arm64@0.35.4": + resolution: + { + integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==, + } + engines: { node: ">=20.9.0" } + cpu: [arm64] + os: [linux] + + "@img/sharp-linux-arm@0.35.4": + resolution: + { + integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==, + } + engines: { node: ">=20.9.0" } + cpu: [arm] + os: [linux] + + "@img/sharp-linux-ppc64@0.35.4": + resolution: + { + integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==, + } + engines: { node: ">=20.9.0" } + cpu: [ppc64] + os: [linux] + + "@img/sharp-linux-riscv64@0.35.4": + resolution: + { + integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==, + } + engines: { node: ">=20.9.0" } + cpu: [riscv64] + os: [linux] + + "@img/sharp-linux-s390x@0.35.4": + resolution: + { + integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==, + } + engines: { node: ">=20.9.0" } + cpu: [s390x] + os: [linux] + + "@img/sharp-linux-x64@0.35.4": + resolution: + { + integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==, + } + engines: { node: ">=20.9.0" } + cpu: [x64] + os: [linux] + + "@img/sharp-linuxmusl-arm64@0.35.4": + resolution: + { + integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==, + } + engines: { node: ">=20.9.0" } + cpu: [arm64] + os: [linux] + + "@img/sharp-linuxmusl-x64@0.35.4": + resolution: + { + integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==, + } + engines: { node: ">=20.9.0" } + cpu: [x64] + os: [linux] + + "@img/sharp-wasm32@0.35.4": + resolution: + { + integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==, + } + engines: { node: ">=20.9.0" } + + "@img/sharp-webcontainers-wasm32@0.35.4": + resolution: + { + integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==, + } + engines: { node: ">=20.9.0" } + cpu: [wasm32] + + "@img/sharp-win32-arm64@0.35.4": + resolution: + { + integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==, + } + engines: { node: ">=20.9.0" } + cpu: [arm64] + os: [win32] + + "@img/sharp-win32-ia32@0.35.4": + resolution: + { + integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==, + } + engines: { node: ^20.9.0 } + cpu: [ia32] + os: [win32] + + "@img/sharp-win32-x64@0.35.4": + resolution: + { + integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==, + } + engines: { node: ">=20.9.0" } + cpu: [x64] + os: [win32] + + "@jridgewell/resolve-uri@3.1.2": + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + } + engines: { node: ">=6.0.0" } + + "@jridgewell/sourcemap-codec@1.6.0": + resolution: + { + integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==, + } + + "@jridgewell/trace-mapping@0.3.9": + resolution: + { + integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==, + } + + "@poppinss/colors@4.1.6": + resolution: + { + integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==, + } + + "@poppinss/dumper@0.6.5": + resolution: + { + integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==, + } + + "@poppinss/exception@1.2.3": + resolution: + { + integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==, + } + + "@sindresorhus/is@7.2.0": + resolution: + { + integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==, + } + engines: { node: ">=18" } + + "@speed-highlight/core@1.2.24": + resolution: + { + integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==, + } + + blake3-wasm@2.1.5: + resolution: + { + integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==, + } + + cookie@1.1.1: + resolution: + { + integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==, + } + engines: { node: ">=18" } + + detect-libc@2.1.2: + resolution: + { + integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, + } + engines: { node: ">=8" } + + error-stack-parser-es@1.0.5: + resolution: + { + integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==, + } + + esbuild@0.28.1: + resolution: + { + integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==, + } + engines: { node: ">=18" } + hasBin: true + + fsevents@2.3.3: + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + os: [darwin] + + kleur@4.1.5: + resolution: + { + integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==, + } + engines: { node: ">=6" } + + miniflare@5.20260911.1-alpha: + resolution: + { + integrity: sha512-7IDj9monoYcCPrS8HfcTt90T3pDwKGvNEAR1Y061KbJhBd5JkStOwOpHMUDFBo9PEbjWVDxPICAnXGNNV2LNfQ==, + } + engines: { node: ">=22.0.0" } + + path-to-regexp@6.3.0: + resolution: + { + integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==, + } + + pathe@2.0.3: + resolution: + { + integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, + } + + semver@7.8.5: + resolution: + { + integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==, + } + engines: { node: ">=10" } + hasBin: true + + sharp@0.35.4: + resolution: + { + integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==, + } + engines: { node: ">=20.9.0" } + peerDependencies: + "@types/node": "*" + peerDependenciesMeta: + "@types/node": + optional: true + + supports-color@10.2.2: + resolution: + { + integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==, + } + engines: { node: ">=18" } + + tslib@2.8.1: + resolution: + { + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, + } + + typescript@5.9.3: + resolution: + { + integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, + } + engines: { node: ">=14.17" } + hasBin: true + + undici@7.29.0: + resolution: + { + integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==, + } + engines: { node: ">=20.18.1" } + + unenv@2.0.0-rc.24: + resolution: + { + integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==, + } + + workerd@1.20260911.1: + resolution: + { + integrity: sha512-vRr8QdBxueQOZJO1hRCI73EZlix87IAyBAcSyI3rA1VB+6oxjw3oaqzYnIV8C4IOPtUgihbdMAgzkb5GM4V7DQ==, + } + engines: { node: ">=16" } + hasBin: true + + wrangler@4.131.2: + resolution: + { + integrity: sha512-jmkGE7monbPKyYQr1FPQN+SARVhddqw2fhXOmTKCw4lroqlFGSS6rit/RTvPi/qzNLKrXxkS8DhWXasJnStplg==, + } + engines: { node: ">=22.0.0" } + hasBin: true + peerDependencies: + "@cloudflare/workers-types": ^5.20260911.1 + peerDependenciesMeta: + "@cloudflare/workers-types": + optional: true + + ws@8.21.0: + resolution: + { + integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==, + } + engines: { node: ">=10.0.0" } + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + youch-core@0.3.3: + resolution: + { + integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==, + } + + youch@4.1.0-beta.10: + resolution: + { + integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==, + } + +snapshots: + "@cloudflare/containers@0.3.7": {} + + "@cloudflare/kv-asset-handler@0.5.0": {} + + "@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260911.1)": + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260911.1 + + "@cloudflare/workerd-darwin-64@1.20260911.1": + optional: true + + "@cloudflare/workerd-darwin-arm64@1.20260911.1": + optional: true + + "@cloudflare/workerd-linux-64@1.20260911.1": + optional: true + + "@cloudflare/workerd-linux-arm64@1.20260911.1": + optional: true + + "@cloudflare/workerd-windows-64@1.20260911.1": + optional: true + + "@cloudflare/workers-types@5.20260914.1": {} + + "@cspotcode/source-map-support@0.8.1": + dependencies: + "@jridgewell/trace-mapping": 0.3.9 + + "@emnapi/runtime@1.11.3": + dependencies: + tslib: 2.8.1 + optional: true + + "@esbuild/aix-ppc64@0.28.1": + optional: true + + "@esbuild/android-arm64@0.28.1": + optional: true + + "@esbuild/android-arm@0.28.1": + optional: true + + "@esbuild/android-x64@0.28.1": + optional: true + + "@esbuild/darwin-arm64@0.28.1": + optional: true + + "@esbuild/darwin-x64@0.28.1": + optional: true + + "@esbuild/freebsd-arm64@0.28.1": + optional: true + + "@esbuild/freebsd-x64@0.28.1": + optional: true + + "@esbuild/linux-arm64@0.28.1": + optional: true + + "@esbuild/linux-arm@0.28.1": + optional: true + + "@esbuild/linux-ia32@0.28.1": + optional: true + + "@esbuild/linux-loong64@0.28.1": + optional: true + + "@esbuild/linux-mips64el@0.28.1": + optional: true + + "@esbuild/linux-ppc64@0.28.1": + optional: true + + "@esbuild/linux-riscv64@0.28.1": + optional: true + + "@esbuild/linux-s390x@0.28.1": + optional: true + + "@esbuild/linux-x64@0.28.1": + optional: true + + "@esbuild/netbsd-arm64@0.28.1": + optional: true + + "@esbuild/netbsd-x64@0.28.1": + optional: true + + "@esbuild/openbsd-arm64@0.28.1": + optional: true + + "@esbuild/openbsd-x64@0.28.1": + optional: true + + "@esbuild/openharmony-arm64@0.28.1": + optional: true + + "@esbuild/sunos-x64@0.28.1": + optional: true + + "@esbuild/win32-arm64@0.28.1": + optional: true + + "@esbuild/win32-ia32@0.28.1": + optional: true + + "@esbuild/win32-x64@0.28.1": + optional: true + + "@img/colour@1.1.0": {} + + "@img/sharp-darwin-arm64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-darwin-arm64": 1.3.3 + optional: true + + "@img/sharp-darwin-x64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-darwin-x64": 1.3.3 + optional: true + + "@img/sharp-freebsd-wasm32@0.35.4": + dependencies: + "@img/sharp-wasm32": 0.35.4 + optional: true + + "@img/sharp-libvips-darwin-arm64@1.3.3": + optional: true + + "@img/sharp-libvips-darwin-x64@1.3.3": + optional: true + + "@img/sharp-libvips-linux-arm64@1.3.3": + optional: true + + "@img/sharp-libvips-linux-arm@1.3.3": + optional: true + + "@img/sharp-libvips-linux-ppc64@1.3.3": + optional: true + + "@img/sharp-libvips-linux-riscv64@1.3.3": + optional: true + + "@img/sharp-libvips-linux-s390x@1.3.3": + optional: true + + "@img/sharp-libvips-linux-x64@1.3.3": + optional: true + + "@img/sharp-libvips-linuxmusl-arm64@1.3.3": + optional: true + + "@img/sharp-libvips-linuxmusl-x64@1.3.3": + optional: true + + "@img/sharp-linux-arm64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linux-arm64": 1.3.3 + optional: true + + "@img/sharp-linux-arm@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linux-arm": 1.3.3 + optional: true + + "@img/sharp-linux-ppc64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linux-ppc64": 1.3.3 + optional: true + + "@img/sharp-linux-riscv64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linux-riscv64": 1.3.3 + optional: true + + "@img/sharp-linux-s390x@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linux-s390x": 1.3.3 + optional: true + + "@img/sharp-linux-x64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linux-x64": 1.3.3 + optional: true + + "@img/sharp-linuxmusl-arm64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64": 1.3.3 + optional: true + + "@img/sharp-linuxmusl-x64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64": 1.3.3 + optional: true + + "@img/sharp-wasm32@0.35.4": + dependencies: + "@emnapi/runtime": 1.11.3 + optional: true + + "@img/sharp-webcontainers-wasm32@0.35.4": + dependencies: + "@img/sharp-wasm32": 0.35.4 + optional: true + + "@img/sharp-win32-arm64@0.35.4": + optional: true + + "@img/sharp-win32-ia32@0.35.4": + optional: true + + "@img/sharp-win32-x64@0.35.4": + optional: true + + "@jridgewell/resolve-uri@3.1.2": {} + + "@jridgewell/sourcemap-codec@1.6.0": {} + + "@jridgewell/trace-mapping@0.3.9": + dependencies: + "@jridgewell/resolve-uri": 3.1.2 + "@jridgewell/sourcemap-codec": 1.6.0 + + "@poppinss/colors@4.1.6": + dependencies: + kleur: 4.1.5 + + "@poppinss/dumper@0.6.5": + dependencies: + "@poppinss/colors": 4.1.6 + "@sindresorhus/is": 7.2.0 + supports-color: 10.2.2 + + "@poppinss/exception@1.2.3": {} + + "@sindresorhus/is@7.2.0": {} + + "@speed-highlight/core@1.2.24": {} + + blake3-wasm@2.1.5: {} + + cookie@1.1.1: {} + + detect-libc@2.1.2: {} + + error-stack-parser-es@1.0.5: {} + + esbuild@0.28.1: + optionalDependencies: + "@esbuild/aix-ppc64": 0.28.1 + "@esbuild/android-arm": 0.28.1 + "@esbuild/android-arm64": 0.28.1 + "@esbuild/android-x64": 0.28.1 + "@esbuild/darwin-arm64": 0.28.1 + "@esbuild/darwin-x64": 0.28.1 + "@esbuild/freebsd-arm64": 0.28.1 + "@esbuild/freebsd-x64": 0.28.1 + "@esbuild/linux-arm": 0.28.1 + "@esbuild/linux-arm64": 0.28.1 + "@esbuild/linux-ia32": 0.28.1 + "@esbuild/linux-loong64": 0.28.1 + "@esbuild/linux-mips64el": 0.28.1 + "@esbuild/linux-ppc64": 0.28.1 + "@esbuild/linux-riscv64": 0.28.1 + "@esbuild/linux-s390x": 0.28.1 + "@esbuild/linux-x64": 0.28.1 + "@esbuild/netbsd-arm64": 0.28.1 + "@esbuild/netbsd-x64": 0.28.1 + "@esbuild/openbsd-arm64": 0.28.1 + "@esbuild/openbsd-x64": 0.28.1 + "@esbuild/openharmony-arm64": 0.28.1 + "@esbuild/sunos-x64": 0.28.1 + "@esbuild/win32-arm64": 0.28.1 + "@esbuild/win32-ia32": 0.28.1 + "@esbuild/win32-x64": 0.28.1 + + fsevents@2.3.3: + optional: true + + kleur@4.1.5: {} + + miniflare@5.20260911.1-alpha: + dependencies: + "@cspotcode/source-map-support": 0.8.1 + sharp: 0.35.4 + undici: 7.29.0 + workerd: 1.20260911.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - "@types/node" + - bufferutil + - utf-8-validate + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + semver@7.8.5: {} + + sharp@0.35.4: + dependencies: + "@img/colour": 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + "@img/sharp-darwin-arm64": 0.35.4 + "@img/sharp-darwin-x64": 0.35.4 + "@img/sharp-freebsd-wasm32": 0.35.4 + "@img/sharp-libvips-darwin-arm64": 1.3.3 + "@img/sharp-libvips-darwin-x64": 1.3.3 + "@img/sharp-libvips-linux-arm": 1.3.3 + "@img/sharp-libvips-linux-arm64": 1.3.3 + "@img/sharp-libvips-linux-ppc64": 1.3.3 + "@img/sharp-libvips-linux-riscv64": 1.3.3 + "@img/sharp-libvips-linux-s390x": 1.3.3 + "@img/sharp-libvips-linux-x64": 1.3.3 + "@img/sharp-libvips-linuxmusl-arm64": 1.3.3 + "@img/sharp-libvips-linuxmusl-x64": 1.3.3 + "@img/sharp-linux-arm": 0.35.4 + "@img/sharp-linux-arm64": 0.35.4 + "@img/sharp-linux-ppc64": 0.35.4 + "@img/sharp-linux-riscv64": 0.35.4 + "@img/sharp-linux-s390x": 0.35.4 + "@img/sharp-linux-x64": 0.35.4 + "@img/sharp-linuxmusl-arm64": 0.35.4 + "@img/sharp-linuxmusl-x64": 0.35.4 + "@img/sharp-webcontainers-wasm32": 0.35.4 + "@img/sharp-win32-arm64": 0.35.4 + "@img/sharp-win32-ia32": 0.35.4 + "@img/sharp-win32-x64": 0.35.4 + + supports-color@10.2.2: {} + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + undici@7.29.0: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + workerd@1.20260911.1: + optionalDependencies: + "@cloudflare/workerd-darwin-64": 1.20260911.1 + "@cloudflare/workerd-darwin-arm64": 1.20260911.1 + "@cloudflare/workerd-linux-64": 1.20260911.1 + "@cloudflare/workerd-linux-arm64": 1.20260911.1 + "@cloudflare/workerd-windows-64": 1.20260911.1 + + wrangler@4.131.2(@cloudflare/workers-types@5.20260914.1): + dependencies: + "@cloudflare/kv-asset-handler": 0.5.0 + "@cloudflare/unenv-preset": 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260911.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 5.20260911.1-alpha + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260911.1 + optionalDependencies: + "@cloudflare/workers-types": 5.20260914.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - "@types/node" + - bufferutil + - utf-8-validate + + ws@8.21.0: {} + + youch-core@0.3.3: + dependencies: + "@poppinss/exception": 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + "@poppinss/colors": 4.1.6 + "@poppinss/dumper": 0.6.5 + "@speed-highlight/core": 1.2.24 + cookie: 1.1.1 + youch-core: 0.3.3 diff --git a/examples/containers/container-api-migration/scripts/e2e.mjs b/examples/containers/container-api-migration/scripts/e2e.mjs new file mode 100644 index 00000000000..9e880e614b3 --- /dev/null +++ b/examples/containers/container-api-migration/scripts/e2e.mjs @@ -0,0 +1,135 @@ +import assert from "node:assert/strict"; + +const [baseUrl, stage, instance = "reference-e2e"] = process.argv.slice(2); +if (!baseUrl || !["legacy", "bridge", "direct"].includes(stage)) { + console.error( + "Usage: node scripts/e2e.mjs [instance]", + ); + process.exit(2); +} +const results = []; +async function call(action, mode, options = {}) { + const url = new URL(`/api/${action}`, baseUrl); + url.searchParams.set("instance", instance); + url.searchParams.set("mode", mode); + if (options.delay) url.searchParams.set("delay", String(options.delay)); + const started = Date.now(); + const response = await fetch(url, { + method: action === "status" || action === "events" ? "GET" : "POST", + signal: AbortSignal.timeout(120_000), + }); + const text = await response.text(); + let body; + try { + body = JSON.parse(text); + } catch { + body = text; + } + results.push({ + action, + durationMs: Date.now() - started, + mode, + status: response.status, + }); + return { body, response }; +} +async function startEventually(mode) { + let result; + for (let attempt = 1; attempt <= 8; attempt += 1) { + result = await call("start", mode); + if (result.response.ok) return result; + if (attempt < 8) await new Promise((resolve) => setTimeout(resolve, 5_000)); + } + return result; +} +async function exercise(mode) { + console.log(`\n[${stage}/${mode}] start and readiness`); + let result = await startEventually(mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + result = await call("status", mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + result = await call("echo", mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + assert.equal(result.body.port, 8080); + result = await call("alternate", mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + assert.equal(result.body.port, 9090); + result = await call("switch-port", mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + assert.equal(result.body.port, 9090); + result = await call("exec", mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + assert.equal(result.body.exitCode, 0); + assert.match(result.body.stdout, /^v\d+/); + result = await call("outbound", mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + assert.equal(result.body.intercepted, true, JSON.stringify(result.body)); + result = await call("renew", mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + const scheduleStartedAt = Date.now(); + result = await call("schedule", mode); + if (stage === "bridge" && mode === "direct") + assert.equal(result.response.status, 409, JSON.stringify(result.body)); + else { + assert.equal(result.response.status, 202, JSON.stringify(result.body)); + await new Promise((resolve) => setTimeout(resolve, 4_500)); + } + result = await call("events", mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + assert.ok(Array.isArray(result.body)); + assert.ok(result.body.length > 0); + if (!(stage === "bridge" && mode === "direct")) { + const expectedMessage = + stage === "direct" + ? "Durable Object alarm handled scheduled work" + : "Container.schedule callback"; + assert.ok( + result.body.some( + (event) => + event.stage === stage && + event.message === expectedMessage && + Date.parse(event.at) >= scheduleStartedAt - 1000, + ), + `${expectedMessage} did not run for the current test`, + ); + } + result = await call("stop", mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + await new Promise((resolve) => setTimeout(resolve, 2_000)); + result = await startEventually(mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + result = await call("echo", mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + assert.equal(result.body.port, 8080); + result = await call("outbound", mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + assert.equal(result.body.intercepted, true, JSON.stringify(result.body)); + result = await call("destroy", mode); + assert.equal(result.response.ok, true, JSON.stringify(result.body)); + await new Promise((resolve) => setTimeout(resolve, 4_000)); +} +const modes = + stage === "bridge" + ? ["helper", "direct"] + : [stage === "legacy" ? "helper" : "direct"]; +for (const mode of modes) await exercise(mode); +const ledger = await call("events", modes.at(-1)); +assert.equal(ledger.response.ok, true, JSON.stringify(ledger.body)); +const seenStages = new Set(ledger.body.map((event) => event.stage)); +assert.ok(seenStages.has(stage)); +if (stage === "direct") { + assert.ok(seenStages.has("legacy"), "Legacy events did not survive"); + assert.ok(seenStages.has("bridge"), "Bridge events did not survive"); + assert.ok( + ledger.body.some( + (event) => + event.message === "Durable Object alarm handled scheduled work" && + event.detail?.payload?.stage === "bridge", + ), + "The direct alarm handler did not process the bridge cutover job", + ); +} +console.table(results); +console.log( + `PASS: ${stage} end-to-end suite completed for instance ${instance}`, +); diff --git a/examples/containers/container-api-migration/shared/lab.ts b/examples/containers/container-api-migration/shared/lab.ts new file mode 100644 index 00000000000..742b946eae6 --- /dev/null +++ b/examples/containers/container-api-migration/shared/lab.ts @@ -0,0 +1,137 @@ +export type LabStage = "legacy" | "bridge" | "direct"; +export type LabMode = "helper" | "direct"; + +export interface LabEvent { + at: string; + detail?: unknown; + message: string; + mode: LabMode | "system"; + stage: LabStage; +} + +export interface WorkbenchOptions { + availableModes: LabMode[]; + description: string; + stage: LabStage; +} + +const EVENT_KEY = "lab:events"; + +export async function recordEvent( + storage: DurableObjectStorage, + stage: LabStage, + mode: LabEvent["mode"], + message: string, + detail?: unknown, +): Promise { + const event: LabEvent = { + at: new Date().toISOString(), + detail, + message, + mode, + stage, + }; + const events = (await storage.get(EVENT_KEY)) ?? []; + events.push(event); + await storage.put(EVENT_KEY, events.slice(-100)); + return event; +} + +export async function readEvents( + storage: DurableObjectStorage, +): Promise { + return (await storage.get(EVENT_KEY)) ?? []; +} + +export function json(value: unknown, status = 200): Response { + return Response.json(value, { + status, + headers: { "cache-control": "no-store" }, + }); +} + +export function actionFrom(request: Request): string { + return ( + new URL(request.url).pathname.split("/").filter(Boolean).at(-1) ?? "status" + ); +} + +export function modeFrom(request: Request, fallback: LabMode): LabMode { + return new URL(request.url).searchParams.get("mode") === "direct" + ? "direct" + : fallback; +} + +export function requestForContainer(path: string, init?: RequestInit): Request { + return new Request(`http://container${path}`, init); +} + +export async function startWithRetry( + container: NonNullable, + options: Parameters["start"]>[0], + attempts = 20, +): Promise { + let lastError: unknown; + for (let attempt = 1; attempt <= attempts; attempt += 1) { + if (container.running) return attempt; + try { + container.start(options); + return attempt; + } catch (error) { + lastError = error; + if (attempt < attempts) await scheduler.wait(1000); + } + } + throw new Error( + `Container could not be allocated after ${attempts} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`, + ); +} + +export async function waitForPort( + container: NonNullable, + port: number, + attempts = 40, +): Promise { + let lastError: unknown; + for (let attempt = 1; attempt <= attempts; attempt += 1) { + try { + const response = await container + .getTcpPort(port) + .fetch("http://container/ping"); + if (response.ok) return attempt; + } catch (error) { + lastError = error; + } + await scheduler.wait(250); + } + throw new Error( + `Port ${port} did not become ready: ${lastError instanceof Error ? lastError.message : String(lastError)}`, + ); +} + +export async function routeWorkbench< + T extends Rpc.DurableObjectBranded | undefined, +>( + request: Request, + namespace: DurableObjectNamespace, + options: WorkbenchOptions, + render: (options: WorkbenchOptions) => string, +): Promise { + const url = new URL(request.url); + if (!url.pathname.startsWith("/api/")) { + return new Response(render(options), { + headers: { + "cache-control": "no-store", + "content-type": "text/html; charset=utf-8", + }, + }); + } + const instance = url.searchParams.get("instance")?.trim() || "reference"; + if (!/^[a-zA-Z0-9_-]{1,64}$/.test(instance)) { + return json( + { error: "Instance names may contain letters, numbers, _ and -." }, + 400, + ); + } + return namespace.getByName(instance).fetch(request); +} diff --git a/examples/containers/container-api-migration/shared/workbench.ts b/examples/containers/container-api-migration/shared/workbench.ts new file mode 100644 index 00000000000..e311568a69e --- /dev/null +++ b/examples/containers/container-api-migration/shared/workbench.ts @@ -0,0 +1,94 @@ +import type { WorkbenchOptions } from "./lab"; + +export function renderWorkbench({ + availableModes, + description, + stage, +}: WorkbenchOptions): string { + const stages = [ + ["legacy", "01", "Container class"], + ["bridge", "02", "Bridge release"], + ["direct", "03", "Direct API"], + ] as const; + return ` + + + + + Container API migration workbench + + +
+
Executable migration reference / laboratory 03

Container API
migration workbench

Stage: ${stage}
+ +
Current experiment

${description}

Identity held constant
class MigrationWorkbenchbinding MIGRATION_WORKBENCHimage container/Dockerfile
+
+
+
Control path
${availableModes.map((mode, index) => ``).join("")}
+
${[ + ["start", "Start + readiness"], + ["status", "Read state"], + ["echo", "Proxy request"], + ["alternate", "Alternate port"], + ["switch-port", "Switch port"], + ["exec", "Execute process"], + ["outbound", "Intercept outbound"], + ["renew", "Renew activity"], + ["schedule", "Schedule +3 s"], + ["events", "Read event ledger"], + ["stop", "Graceful stop"], + ["destroy", "Destroy"], + ] + .map( + ([action, label]) => + ``, + ) + .join("")}
+
Runtime responseready
Select an operation to begin.
Event ledger loads here.
+
One image · one class name · one namespaceState ledger persists across stage deployments
+
`; +} diff --git a/examples/containers/container-api-migration/stages/1-container-class/src/index.ts b/examples/containers/container-api-migration/stages/1-container-class/src/index.ts new file mode 100644 index 00000000000..8c8e6e4b344 --- /dev/null +++ b/examples/containers/container-api-migration/stages/1-container-class/src/index.ts @@ -0,0 +1,176 @@ +import { + Container, + ContainerProxy, + getContainer, + switchPort, + type StopParams, +} from "@cloudflare/containers"; +import { + actionFrom, + json, + readEvents, + recordEvent, + requestForContainer, + routeWorkbench, +} from "../../../shared/lab"; +import { renderWorkbench } from "../../../shared/workbench"; + +interface Env { + MIGRATION_WORKBENCH: DurableObjectNamespace; +} +const STAGE = "legacy" as const; + +export class MigrationWorkbench extends Container { + defaultPort = 8080; + requiredPorts = [8080, 9090]; + sleepAfter = "10m"; + envVars = { LAB_MODE: "helper", LAB_STAGE: STAGE }; + enableInternet = false; + allowedHosts = ["workbench.internal"]; + pingEndpoint = "ping"; + + private record(message: string, detail?: unknown) { + return recordEvent(this.ctx.storage, STAGE, "helper", message, detail); + } + override async onStart() { + await this.record("onStart hook"); + await this.containerFetch("http://container/bootstrap", { method: "POST" }); + } + override async onStop(params: StopParams) { + await this.record("onStop hook", params); + } + override onError(error: unknown): never { + void this.record("onError hook", { + message: error instanceof Error ? error.message : String(error), + }); + throw error; + } + override async onActivityExpired() { + await this.record("onActivityExpired hook"); + await this.stop(); + } + + async scheduledProbe(payload: unknown) { + const response = await this.containerFetch( + requestForContainer("/scheduled", { + body: JSON.stringify(payload), + method: "POST", + }), + ); + await this.record("Container.schedule callback", { + payload, + response: await response.json(), + }); + } + + override async fetch(request: Request): Promise { + const action = actionFrom(request); + this.renewActivityTimeout(); + switch (action) { + case "start": + await this.startAndWaitForPorts({ + ports: this.requiredPorts, + startOptions: { envVars: this.envVars, enableInternet: false }, + }); + await this.record("startAndWaitForPorts", { + ports: this.requiredPorts, + }); + return json({ stage: STAGE, state: await this.getState() }); + case "status": + return json({ stage: STAGE, state: await this.getState() }); + case "echo": + return this.containerFetch( + requestForContainer("/echo?via=containerFetch", { + body: JSON.stringify({ hello: "from the Container class" }), + method: "POST", + }), + ); + case "alternate": + return this.containerFetch( + "http://container/echo?via=containerFetch-port-argument", + {}, + 9090, + ); + case "switch-port": + return super.fetch( + switchPort(requestForContainer("/echo?via=switchPort"), 9090), + ); + case "exec": { + await this.startAndWaitForPorts({ ports: 8080 }); + const process = await this.ctx.container?.exec(["node", "--version"]); + if (!process) + return json({ error: "Container runtime unavailable" }, 503); + const result = await process.output(); + return json({ + exitCode: result.exitCode, + stdout: new TextDecoder().decode(result.stdout).trim(), + }); + } + case "outbound": + return this.containerFetch("http://container/outbound"); + case "renew": + this.renewActivityTimeout(); + await this.record("renewActivityTimeout"); + return json({ renewed: true, sleepAfter: this.sleepAfter }); + case "schedule": { + const schedule = await this.schedule(3, "scheduledProbe", { + createdAt: new Date().toISOString(), + stage: STAGE, + }); + await this.record("Container.schedule created", schedule); + return json(schedule, 202); + } + case "events": + return json(await readEvents(this.ctx.storage)); + case "stop": + await this.stop("SIGTERM"); + await this.record("stop helper called"); + return json({ stopping: true }); + case "destroy": + await this.destroy(); + await this.record("destroy helper called"); + return json({ destroyed: true }); + default: + return json({ error: `Unknown action: ${action}` }, 404); + } + } +} + +MigrationWorkbench.outboundByHost = { + "workbench.internal": (request, _env, context) => + new Response( + JSON.stringify({ + containerId: context.containerId, + interceptedBy: "Container.outboundByHost", + url: request.url, + }), + { + headers: { + "content-type": "application/json", + "x-workbench-intercepted": "true", + }, + }, + ), +}; +export { ContainerProxy }; + +export default { + async fetch(request: Request, env: Env) { + if (new URL(request.url).pathname.startsWith("/api/")) { + const instance = + new URL(request.url).searchParams.get("instance") || "reference"; + return getContainer(env.MIGRATION_WORKBENCH, instance).fetch(request); + } + return routeWorkbench( + request, + env.MIGRATION_WORKBENCH, + { + availableModes: ["helper"], + description: + "The baseline uses Container class routing, readiness, lifecycle hooks, scheduling, state, inactivity, port switching, and outbound interception.", + stage: STAGE, + }, + renderWorkbench, + ); + }, +} satisfies ExportedHandler; diff --git a/examples/containers/container-api-migration/stages/1-container-class/wrangler.jsonc b/examples/containers/container-api-migration/stages/1-container-class/wrangler.jsonc new file mode 100644 index 00000000000..81a5d0d29bc --- /dev/null +++ b/examples/containers/container-api-migration/stages/1-container-class/wrangler.jsonc @@ -0,0 +1,28 @@ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "container-api-migration-workbench", + "main": "./src/index.ts", + "compatibility_date": "2026-09-28", + "workers_dev": true, + "containers": [ + { + "class_name": "MigrationWorkbench", + "image": "../../container/Dockerfile", + "max_instances": 5, + }, + ], + "durable_objects": { + "bindings": [ + { + "name": "MIGRATION_WORKBENCH", + "class_name": "MigrationWorkbench", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["MigrationWorkbench"], + }, + ], +} diff --git a/examples/containers/container-api-migration/stages/2-bridge/src/index.ts b/examples/containers/container-api-migration/stages/2-bridge/src/index.ts new file mode 100644 index 00000000000..8e96d151bd0 --- /dev/null +++ b/examples/containers/container-api-migration/stages/2-bridge/src/index.ts @@ -0,0 +1,345 @@ +import { + Container, + ContainerProxy, + getContainer, + switchPort, + type StopParams, +} from "@cloudflare/containers"; +import { WorkerEntrypoint } from "cloudflare:workers"; +import { + actionFrom, + json, + modeFrom, + readEvents, + recordEvent, + requestForContainer, + routeWorkbench, + startWithRetry, + waitForPort, + type LabMode, +} from "../../../shared/lab"; +import { renderWorkbench } from "../../../shared/workbench"; + +interface Env { + MIGRATION_WORKBENCH: DurableObjectNamespace; +} +interface OutboundProps { + stage: string; +} +type InspectorFactory = (options: { props: OutboundProps }) => Fetcher; +const STAGE = "bridge" as const; + +export class OutboundInspector extends WorkerEntrypoint { + override fetch(request: Request) { + return new Response( + JSON.stringify({ + interceptedBy: "ctx.container.interceptOutboundHttp", + stage: this.ctx.props.stage, + url: request.url, + }), + { + headers: { + "content-type": "application/json", + "x-workbench-intercepted": "true", + }, + }, + ); + } +} + +export class MigrationWorkbench extends Container { + defaultPort = 8080; + requiredPorts = [8080, 9090]; + sleepAfter = "10m"; + envVars = { LAB_MODE: "helper", LAB_STAGE: STAGE }; + enableInternet = false; + allowedHosts = ["workbench.internal"]; + pingEndpoint = "ping"; + private directMonitorAttached = false; + private directOutboundInstalled = false; + + private runtime(): NonNullable { + if (!this.ctx.container) throw new Error("Container runtime unavailable"); + return this.ctx.container; + } + private record(mode: LabMode | "system", message: string, detail?: unknown) { + return recordEvent(this.ctx.storage, STAGE, mode, message, detail); + } + private async installDirectOutbound() { + if (this.directOutboundInstalled) return; + const exports = this.ctx.exports as unknown as { + OutboundInspector: InspectorFactory; + }; + await this.runtime().interceptOutboundHttp( + "workbench.internal", + exports.OutboundInspector({ props: { stage: STAGE } }), + ); + this.directOutboundInstalled = true; + await this.record("direct", "interceptOutboundHttp installed"); + } + private attachDirectMonitor() { + if (this.directMonitorAttached) return; + this.directMonitorAttached = true; + this.ctx.waitUntil( + this.runtime() + .monitor() + .then(() => this.record("direct", "monitor resolved: container exited")) + .catch((error: unknown) => + this.record("direct", "monitor rejected", { + message: error instanceof Error ? error.message : String(error), + }), + ) + .finally(() => { + this.directMonitorAttached = false; + this.directOutboundInstalled = false; + }), + ); + } + private async ensureDirect() { + const runtime = this.runtime(); + let startAttempts = 0; + if (!runtime.running) { + startAttempts = await startWithRetry(runtime, { + enableInternet: false, + env: { LAB_MODE: "direct", LAB_STAGE: STAGE }, + }); + await this.record("direct", "ctx.container.start", { startAttempts }); + } + const readinessAttempts = await Promise.all([ + waitForPort(runtime, 8080), + waitForPort(runtime, 9090), + ]); + await this.installDirectOutbound(); + await runtime.setInactivityTimeout(10 * 60 * 1000); + this.attachDirectMonitor(); + return { startAttempts, readinessAttempts }; + } + + override async onStart() { + await this.record("helper", "onStart hook"); + await this.containerFetch("http://container/bootstrap", { method: "POST" }); + } + override async onStop(params: StopParams) { + await this.record("helper", "onStop hook", params); + } + override onError(error: unknown): never { + void this.record("helper", "onError hook", { + message: error instanceof Error ? error.message : String(error), + }); + throw error; + } + override async onActivityExpired() { + await this.record("helper", "onActivityExpired hook"); + await this.stop(); + } + async scheduledProbe(payload: unknown) { + const response = await this.containerFetch( + requestForContainer("/scheduled", { + body: JSON.stringify(payload), + method: "POST", + }), + ); + await this.ctx.storage.delete("lab:pending-cutover"); + await this.record("helper", "Container.schedule callback", { + payload, + response: await response.json(), + }); + } + + private async helperAction( + action: string, + request: Request, + ): Promise { + this.renewActivityTimeout(); + switch (action) { + case "start": + await this.startAndWaitForPorts({ + ports: this.requiredPorts, + startOptions: { envVars: this.envVars, enableInternet: false }, + }); + await this.record("helper", "startAndWaitForPorts", { + ports: this.requiredPorts, + }); + return json({ mode: "helper", state: await this.getState() }); + case "status": + return json({ + classState: await this.getState(), + mode: "helper", + runtimeRunning: this.runtime().running, + }); + case "echo": + return this.containerFetch( + requestForContainer("/echo?via=bridge-containerFetch", { + body: JSON.stringify({ hello: "from the bridge helper path" }), + method: "POST", + }), + ); + case "alternate": + return this.containerFetch( + "http://container/echo?via=bridge-containerFetch-port", + {}, + 9090, + ); + case "switch-port": + return super.fetch( + switchPort(requestForContainer("/echo?via=bridge-switchPort"), 9090), + ); + case "exec": { + await this.startAndWaitForPorts({ ports: 8080 }); + const process = await this.runtime().exec(["node", "--version"]); + const result = await process.output(); + return json({ + exitCode: result.exitCode, + stdout: new TextDecoder().decode(result.stdout).trim(), + }); + } + case "outbound": + return this.containerFetch("http://container/outbound"); + case "renew": + this.renewActivityTimeout(); + await this.record("helper", "renewActivityTimeout"); + return json({ mode: "helper", renewed: true }); + case "schedule": + case "schedule-cutover": { + const rawDelay = new URL(request.url).searchParams.get("delay"); + const requested = rawDelay === null ? Number.NaN : Number(rawDelay); + const delay = Number.isFinite(requested) + ? requested + : action === "schedule-cutover" + ? 120 + : 3; + const payload = { + createdAt: new Date().toISOString(), + delay, + stage: STAGE, + }; + await this.ctx.storage.put("lab:pending-cutover", payload); + const schedule = await this.schedule(delay, "scheduledProbe", payload); + await this.record("helper", "Container.schedule created", schedule); + return json({ ...schedule, cutoverMarker: true }, 202); + } + case "stop": + await this.stop("SIGTERM"); + await this.record("helper", "stop helper called"); + return json({ mode: "helper", stopping: true }); + case "destroy": + await this.destroy(); + await this.record("helper", "destroy helper called"); + return json({ destroyed: true, mode: "helper" }); + default: + return json({ error: `Unknown helper action: ${action}` }, 404); + } + } + + private async directAction(action: string): Promise { + const runtime = this.runtime(); + switch (action) { + case "start": + return json({ + mode: "direct", + running: runtime.running, + ...(await this.ensureDirect()), + }); + case "status": + return json({ + classState: await this.getState(), + mode: "direct", + runtimeRunning: runtime.running, + }); + case "echo": + await this.ensureDirect(); + return runtime.getTcpPort(8080).fetch( + requestForContainer("/echo?via=ctx.container", { + body: JSON.stringify({ hello: "from the bridge direct path" }), + method: "POST", + }), + ); + case "alternate": + case "switch-port": + await this.ensureDirect(); + return runtime + .getTcpPort(9090) + .fetch(`http://container/echo?via=ctx.container-${action}`); + case "exec": { + await this.ensureDirect(); + const process = await runtime.exec(["node", "--version"]); + const result = await process.output(); + return json({ + exitCode: result.exitCode, + stdout: new TextDecoder().decode(result.stdout).trim(), + }); + } + case "outbound": + await this.ensureDirect(); + return runtime.getTcpPort(8080).fetch("http://container/outbound"); + case "renew": + await runtime.setInactivityTimeout(10 * 60 * 1000); + await this.record("direct", "setInactivityTimeout renewed"); + return json({ mode: "direct", renewed: true }); + case "schedule": + return json( + { + error: + "The Container class owns alarm() during the bridge stage. Migrate scheduling during the final cutover.", + }, + 409, + ); + case "stop": + runtime.signal(15); + await this.record("direct", "signal(15) called"); + return json({ mode: "direct", stopping: true }); + case "destroy": + await runtime.destroy("Bridge direct path destroy"); + await this.record("direct", "destroy called"); + return json({ destroyed: true, mode: "direct" }); + default: + return json({ error: `Unknown direct action: ${action}` }, 404); + } + } + + override async fetch(request: Request) { + const action = actionFrom(request); + if (action === "events") return json(await readEvents(this.ctx.storage)); + return modeFrom(request, "helper") === "direct" + ? this.directAction(action) + : this.helperAction(action, request); + } +} + +MigrationWorkbench.outboundByHost = { + "workbench.internal": (request, _env, context) => + new Response( + JSON.stringify({ + containerId: context.containerId, + interceptedBy: "Container.outboundByHost", + url: request.url, + }), + { + headers: { + "content-type": "application/json", + "x-workbench-intercepted": "true", + }, + }, + ), +}; +export { ContainerProxy }; +export default { + async fetch(request: Request, env: Env) { + if (new URL(request.url).pathname.startsWith("/api/")) { + const instance = + new URL(request.url).searchParams.get("instance") || "reference"; + return getContainer(env.MIGRATION_WORKBENCH, instance).fetch(request); + } + return routeWorkbench( + request, + env.MIGRATION_WORKBENCH, + { + availableModes: ["helper", "direct"], + description: + "Both routes control the same Durable Object and container instance. Runtime calls migrate first; alarm ownership remains with the Container class until cutover.", + stage: STAGE, + }, + renderWorkbench, + ); + }, +} satisfies ExportedHandler; diff --git a/examples/containers/container-api-migration/stages/2-bridge/wrangler.jsonc b/examples/containers/container-api-migration/stages/2-bridge/wrangler.jsonc new file mode 100644 index 00000000000..81a5d0d29bc --- /dev/null +++ b/examples/containers/container-api-migration/stages/2-bridge/wrangler.jsonc @@ -0,0 +1,28 @@ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "container-api-migration-workbench", + "main": "./src/index.ts", + "compatibility_date": "2026-09-28", + "workers_dev": true, + "containers": [ + { + "class_name": "MigrationWorkbench", + "image": "../../container/Dockerfile", + "max_instances": 5, + }, + ], + "durable_objects": { + "bindings": [ + { + "name": "MIGRATION_WORKBENCH", + "class_name": "MigrationWorkbench", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["MigrationWorkbench"], + }, + ], +} diff --git a/examples/containers/container-api-migration/stages/3-durable-object-api/src/index.ts b/examples/containers/container-api-migration/stages/3-durable-object-api/src/index.ts new file mode 100644 index 00000000000..ef4597aef8b --- /dev/null +++ b/examples/containers/container-api-migration/stages/3-durable-object-api/src/index.ts @@ -0,0 +1,223 @@ +import { DurableObject, WorkerEntrypoint } from "cloudflare:workers"; +import { + actionFrom, + json, + readEvents, + recordEvent, + requestForContainer, + routeWorkbench, + startWithRetry, + waitForPort, +} from "../../../shared/lab"; +import { renderWorkbench } from "../../../shared/workbench"; + +interface Env { + MIGRATION_WORKBENCH: DurableObjectNamespace; +} +interface OutboundProps { + stage: string; +} +interface ScheduledPayload { + createdAt: string; + delay: number; + stage: string; +} +type InspectorFactory = (options: { props: OutboundProps }) => Fetcher; +const STAGE = "direct" as const; +const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; + +export class OutboundInspector extends WorkerEntrypoint { + override fetch(request: Request) { + return new Response( + JSON.stringify({ + interceptedBy: "ctx.container.interceptOutboundHttp", + stage: this.ctx.props.stage, + url: request.url, + }), + { + headers: { + "content-type": "application/json", + "x-workbench-intercepted": "true", + }, + }, + ); + } +} + +export class MigrationWorkbench extends DurableObject { + private monitorAttached = false; + private outboundInstalled = false; + private runtime(): NonNullable { + if (!this.ctx.container) throw new Error("Container runtime unavailable"); + return this.ctx.container; + } + private record(message: string, detail?: unknown) { + return recordEvent(this.ctx.storage, STAGE, "direct", message, detail); + } + private async installOutbound() { + if (this.outboundInstalled) return; + const exports = this.ctx.exports as unknown as { + OutboundInspector: InspectorFactory; + }; + await this.runtime().interceptOutboundHttp( + "workbench.internal", + exports.OutboundInspector({ props: { stage: STAGE } }), + ); + this.outboundInstalled = true; + await this.record("interceptOutboundHttp installed"); + } + private attachMonitor() { + if (this.monitorAttached) return; + this.monitorAttached = true; + this.ctx.waitUntil( + this.runtime() + .monitor() + .then(() => this.record("monitor resolved: container exited")) + .catch((error: unknown) => + this.record("monitor rejected", { + message: error instanceof Error ? error.message : String(error), + }), + ) + .finally(() => { + this.monitorAttached = false; + this.outboundInstalled = false; + }), + ); + } + private async ensureContainer() { + const runtime = this.runtime(); + let startAttempts = 0; + if (!runtime.running) { + startAttempts = await startWithRetry(runtime, { + enableInternet: false, + env: { LAB_MODE: "direct", LAB_STAGE: STAGE }, + }); + await this.record("ctx.container.start", { startAttempts }); + } + const readinessAttempts = await Promise.all([ + waitForPort(runtime, 8080), + waitForPort(runtime, 9090), + ]); + await this.installOutbound(); + await runtime.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); + this.attachMonitor(); + return { startAttempts, readinessAttempts }; + } + + async alarm() { + const pending = await this.ctx.storage.get( + "lab:pending-cutover", + ); + if (!pending) { + await this.record("alarm fired without a workbench marker"); + return; + } + await this.ensureContainer(); + const response = await this.runtime() + .getTcpPort(8080) + .fetch( + requestForContainer("/scheduled", { + body: JSON.stringify(pending), + method: "POST", + }), + ); + await this.ctx.storage.delete("lab:pending-cutover"); + await this.record("Durable Object alarm handled scheduled work", { + payload: pending, + response: await response.json(), + }); + } + + async fetch(request: Request): Promise { + const action = actionFrom(request), + runtime = this.runtime(); + switch (action) { + case "start": + return json({ + running: runtime.running, + stage: STAGE, + ...(await this.ensureContainer()), + }); + case "status": + return json({ + alarm: await this.ctx.storage.getAlarm(), + pendingCutover: await this.ctx.storage.get("lab:pending-cutover"), + running: runtime.running, + stage: STAGE, + }); + case "echo": + await this.ensureContainer(); + return runtime.getTcpPort(8080).fetch( + requestForContainer("/echo?via=ctx.container", { + body: JSON.stringify({ hello: "from the direct Durable Object" }), + method: "POST", + }), + ); + case "alternate": + case "switch-port": + await this.ensureContainer(); + return runtime + .getTcpPort(9090) + .fetch(`http://container/echo?via=ctx.container-${action}`); + case "exec": { + await this.ensureContainer(); + const process = await runtime.exec(["node", "--version"]); + const result = await process.output(); + return json({ + exitCode: result.exitCode, + stdout: new TextDecoder().decode(result.stdout).trim(), + }); + } + case "outbound": + await this.ensureContainer(); + return runtime.getTcpPort(8080).fetch("http://container/outbound"); + case "renew": + await runtime.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); + await this.record("setInactivityTimeout renewed"); + return json({ renewed: true, timeoutMs: INACTIVITY_TIMEOUT_MS }); + case "schedule": + case "schedule-cutover": { + const rawDelay = new URL(request.url).searchParams.get("delay"); + const requested = rawDelay === null ? Number.NaN : Number(rawDelay); + const delay = Number.isFinite(requested) ? requested : 3; + const payload = { + createdAt: new Date().toISOString(), + delay, + stage: STAGE, + }; + await this.ctx.storage.put("lab:pending-cutover", payload); + await this.ctx.storage.setAlarm(Date.now() + delay * 1000); + await this.record("Durable Object alarm scheduled", payload); + return json({ payload, scheduled: true }, 202); + } + case "events": + return json(await readEvents(this.ctx.storage)); + case "stop": + runtime.signal(15); + await this.record("signal(15) called"); + return json({ stopping: true }); + case "destroy": + await runtime.destroy("Direct API workbench destroy"); + await this.record("destroy called"); + return json({ destroyed: true }); + default: + return json({ error: `Unknown action: ${action}` }, 404); + } + } +} + +export default { + async fetch(request: Request, env: Env) { + return routeWorkbench( + request, + env.MIGRATION_WORKBENCH, + { + availableModes: ["direct"], + description: + "The exported class and Durable Object namespace are unchanged. Runtime lifecycle, routing, monitoring, inactivity, outbound interception, and alarms now use platform APIs directly.", + stage: STAGE, + }, + renderWorkbench, + ); + }, +} satisfies ExportedHandler; diff --git a/examples/containers/container-api-migration/stages/3-durable-object-api/wrangler.jsonc b/examples/containers/container-api-migration/stages/3-durable-object-api/wrangler.jsonc new file mode 100644 index 00000000000..81a5d0d29bc --- /dev/null +++ b/examples/containers/container-api-migration/stages/3-durable-object-api/wrangler.jsonc @@ -0,0 +1,28 @@ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "container-api-migration-workbench", + "main": "./src/index.ts", + "compatibility_date": "2026-09-28", + "workers_dev": true, + "containers": [ + { + "class_name": "MigrationWorkbench", + "image": "../../container/Dockerfile", + "max_instances": 5, + }, + ], + "durable_objects": { + "bindings": [ + { + "name": "MIGRATION_WORKBENCH", + "class_name": "MigrationWorkbench", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["MigrationWorkbench"], + }, + ], +} diff --git a/examples/containers/container-api-migration/tsconfig.json b/examples/containers/container-api-migration/tsconfig.json new file mode 100644 index 00000000000..b02224e0f23 --- /dev/null +++ b/examples/containers/container-api-migration/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "allowJs": false, + "lib": ["ES2024"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2024", + "types": ["@cloudflare/workers-types"] + }, + "include": ["shared/**/*.ts", "stages/**/*.ts"] +} diff --git a/src/content/docs/containers/api/index.mdx b/src/content/docs/containers/api/index.mdx index e69721de3b4..57cd15d8790 100644 --- a/src/content/docs/containers/api/index.mdx +++ b/src/content/docs/containers/api/index.mdx @@ -9,7 +9,7 @@ products: - durable-objects --- -import { CardGrid, LinkTitleCard, TypeScriptExample } from "~/components"; +import { CardGrid, LinkTitleCard, TabItem, Tabs } from "~/components"; Containers provide two APIs for managing a container from a Durable Object. Both APIs address the same container runtime. @@ -38,15 +38,16 @@ For new applications, we recommend the Durable Object Container API. It lets you ## Choose an API -### Durable Object Container API - Inside a Durable Object, use `ctx.container` to control the container runtime directly. You can manage startup, shutdown, networking, and resource usage while using Durable Object storage and alarms for state and coordination. Add readiness checks, custom request routing, or lifecycle policies when needed. -This Durable Object starts its configured container when it receives a request. Starting a container does not mean its ports are ready. +The `Container` class extends `DurableObject` and wraps the container runtime API with convenience methods. Existing applications can use these methods for request proxying, readiness checks, lifecycle hooks, and scheduling. Durable Object storage and alarms remain available when needed. + +The following examples compare the starting structure for each API: - + + -```ts +```ts title="src/index.ts" import { DurableObject } from "cloudflare:workers"; interface Env {} @@ -66,17 +67,11 @@ export class MyContainer extends DurableObject { } ``` - - -For request routing, use [`getTcpPort()`](/containers/api/durable-object-container/#gettcpport) after checking port readiness. For all methods, refer to the [Durable Object Container API](/containers/api/durable-object-container/). + -### Container class + -The `Container` class extends `DurableObject` and wraps the container runtime API with convenience methods. Existing applications can use these methods for request proxying, readiness checks, lifecycle hooks, and scheduling. Durable Object storage and alarms remain available when needed. - - - -```ts +```ts title="src/index.ts" import { Container } from "@cloudflare/containers"; export class MyContainer extends Container { @@ -85,9 +80,11 @@ export class MyContainer extends Container { } ``` - + + + -For all properties and methods, refer to the [Container class API](/containers/api/container-class/). +Starting a container through the Durable Object Container API does not mean its ports are ready. For request routing, use [`getTcpPort()`](/containers/api/durable-object-container/#gettcpport) after checking port readiness. For all direct methods, refer to the [Durable Object Container API](/containers/api/durable-object-container/). For convenience methods, refer to the [Container class API](/containers/api/container-class/). The following table compares both options: diff --git a/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx b/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx index ffc89f37185..497317f1d54 100644 --- a/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx +++ b/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx @@ -13,7 +13,9 @@ import { Steps } from "~/components"; The Durable Object Container API lets your Durable Object coordinate container compute with persistent storage, alarms, and request handling. Access it as `this.ctx.container` inside a Durable Object with a container binding. When migrating from the `Container` class, replace its helpers with application code where needed. -Before changing code, identify the features your application uses. Keep the existing implementation if you depend on a helper that you cannot replace yet. For an API comparison, refer to [Choose an API](/containers/api/#choose-an-api). +Before changing code, identify which helpers your application uses. Some helpers require application code to preserve their existing behavior. For an API comparison, refer to [Choose an API](/containers/api/#choose-an-api). + +You can migrate the implementation without replacing the Durable Object. Keep the Worker name, exported class name, binding, container image, and existing migration tags unchanged. Changing the TypeScript base class does not require a new Durable Object migration. ## Replace Container class helpers @@ -21,14 +23,22 @@ The `Container` class extends `DurableObject`. Replace its inherited lifecycle a -1. In your Worker, change the class to extend `DurableObject` from `cloudflare:workers`. Keep the exported class name if you want to retain its existing container definition and Durable Object binding. Do not add a new Durable Object migration solely because you changed the base class. Refer to [Wrangler configuration](/containers/configuration/wrangler/). +1. In your Worker, change the class to extend `DurableObject` from `cloudflare:workers`. Keep the exported class name to retain its existing container definition and Durable Object binding. Do not add a new Durable Object migration solely because you changed the base class. Refer to [Wrangler configuration](/containers/configuration/wrangler/). 2. Replace `start()` calls with [`ctx.container.start()`](/containers/api/durable-object-container/#start). Replace `stop()` calls with [`signal()`](/containers/api/durable-object-container/#signal) or [`destroy()`](/containers/api/durable-object-container/#destroy), as appropriate. Do not assume `start()` waits for a port to become ready. 3. Replace `defaultPort`, `containerFetch()`, and automatic `fetch()` routing with [`getTcpPort(port).fetch()`](/containers/api/durable-object-container/#gettcpport) and your own request routing. Check port readiness before forwarding requests. 4. Replace `sleepAfter` with [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout). Replace lifecycle hooks and `schedule()` with application code, [`monitor()`](/containers/api/durable-object-container/#monitor), and [Durable Object alarms](/durable-objects/api/alarms/) where appropriate. -5. Test startup, concurrent requests, readiness, idle shutdown, and recovery after a container restart. Then remove `@cloudflare/containers` only if no other code imports it. +5. Test startup, concurrent requests, readiness, idle shutdown, alarm delivery, storage continuity, and recovery after a container restart. A container can be temporarily unavailable after `stop()` or `destroy()`. Retry allocation before checking port readiness. Then remove `@cloudflare/containers` only if no other code imports it. Keep the same image in the `containers` section of your Wrangler configuration unless you intend to change the container application itself. The direct API still runs the image associated with its Durable Object class. For process handling and output, refer to [Execute commands](/containers/guides/execute-commands/) and the [`exec()` reference](/containers/api/durable-object-container/#exec). + +:::note[Incremental migration] +You can use an incremental migration to test direct runtime calls before the final cutover. First, deploy and test the existing `Container` class implementation. + +Next, deploy a bridge that still extends `Container`. Add routes that use `this.ctx.container` directly, and migrate helpers incrementally. Both paths use the same Durable Object and container, which preserves storage without requiring a second container application. + +After the direct routes pass your tests, change the class to extend `DurableObject`. Migrate scheduling last because the `Container` class owns `alarm()`. Let scheduled work finish before cutover, or store an application-owned job marker for the new `alarm()` handler. +::: diff --git a/tsconfig.json b/tsconfig.json index 2251765572f..9eadb6219ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,10 @@ "jsxImportSource": "react" }, "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist", "worker", ".flue"] + "exclude": [ + "dist", + "worker", + ".flue", + "examples/containers/container-api-migration" + ] } From 41d14e7ec65c5f9383adac1ca10fa0b7c7ddd07c Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Mon, 28 Sep 2026 17:53:07 -0400 Subject: [PATCH 20/23] [Containers] Simplify API code examples --- .../api/durable-object-container.mdx | 60 ++++--------------- 1 file changed, 10 insertions(+), 50 deletions(-) diff --git a/src/content/docs/containers/api/durable-object-container.mdx b/src/content/docs/containers/api/durable-object-container.mdx index a1a024b4f39..5546b1c9a90 100644 --- a/src/content/docs/containers/api/durable-object-container.mdx +++ b/src/content/docs/containers/api/durable-object-container.mdx @@ -58,28 +58,20 @@ export class MyDurableObject extends DurableObject { `running` is `true` when the container is running. It does not confirm that the container is ready to accept requests. - - -```ts +```js this.ctx.container.running; ``` - - ## Methods ### `start` `start()` boots a container. It returns before the container is ready to accept requests. Confirm readiness before sending traffic. - - -```ts +```js this.ctx.container.start(); ``` - - #### Parameters - `options` (`object`, optional): Common container startup options: @@ -182,14 +174,10 @@ For task-oriented examples, refer to [Execute commands](/containers/guides/execu `destroy()` stops the container and can include an optional reason for the operation. - - -```ts +```js await this.ctx.container.destroy("Manually Destroyed"); ``` - - #### Parameters - `error` (`any`, optional): Optional reason associated with the destroy operation. A string is commonly used for logging or debugging. @@ -202,15 +190,11 @@ await this.ctx.container.destroy("Manually 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. - - -```ts +```js const SIGTERM = 15; this.ctx.container.signal(SIGTERM); ``` - - #### Parameters - `signal` (`number`): POSIX signal number to send to the container, such as `SIGTERM` (`15`) or `SIGKILL` (`9`). @@ -227,14 +211,10 @@ this.ctx.container.signal(SIGTERM); setInactivityTimeout(durationMs: number | bigint): Promise ``` - - -```ts +```js await this.ctx.container.setInactivityTimeout(10 * 60 * 1000); ``` - - #### Parameters - `durationMs` (`number | bigint`): Inactivity timeout in milliseconds. @@ -247,9 +227,7 @@ await this.ctx.container.setInactivityTimeout(10 * 60 * 1000); `getTcpPort()` returns a TCP port from the container. Use it to communicate with the container over TCP or HTTP. - - -```ts +```js const port = this.ctx.container.getTcpPort(8080); const res = await port.fetch("http://container/set-state", { body: initialState, @@ -257,11 +235,7 @@ const res = await port.fetch("http://container/set-state", { }); ``` - - - - -```ts +```js const conn = this.ctx.container.getTcpPort(8080).connect("10.0.0.1:8080"); await conn.opened; @@ -276,8 +250,6 @@ try { } ``` - - #### Parameters - `port` (`number`): TCP port number to use for communication with the container. @@ -325,9 +297,7 @@ class MyDurableObject extends DurableObject { `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. Open connections use the new handler without being dropped. - - -```ts +```js const worker = this.ctx.exports.MyWorker({ props: { message: "hello" } }); // Match a specific hostname @@ -343,8 +313,6 @@ await this.ctx.container.interceptOutboundHttp("15.0.0.1:80", worker); await this.ctx.container.interceptOutboundHttp("123.123.123.123/23", worker); ``` - - #### Parameters - `addr` (`string`): Hostname, hostname glob (for example, `*.example.com`), IP address, IP:port, or CIDR range to match. @@ -358,14 +326,10 @@ await this.ctx.container.interceptOutboundHttp("123.123.123.123/23", worker); `interceptAllOutboundHttp()` routes all outbound HTTP requests from the container through a `Fetcher`, regardless of destination. - - -```ts +```js await this.ctx.container.interceptAllOutboundHttp(worker); ``` - - #### Parameters - `binding` (`Fetcher`): Worker entrypoint or service binding that handles all outbound HTTP requests. @@ -380,9 +344,7 @@ await this.ctx.container.interceptAllOutboundHttp(worker); Hostname globs support `*` to match any sequence of characters. - - -```ts +```js const worker = this.ctx.exports.MyWorker({ props: {} }); // Match a specific hostname @@ -395,8 +357,6 @@ await this.ctx.container.interceptOutboundHttps("*.example.com", worker); await this.ctx.container.interceptOutboundHttps("*", worker); ``` - - #### Parameters - `addr` (`string`): Hostname or hostname glob pattern to match. Use `*` to intercept all HTTPS traffic. From 393ee91d55b9c279f37c09f307e2c766d4ee870f Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Mon, 28 Sep 2026 18:06:04 -0400 Subject: [PATCH 21/23] [Containers] Preserve outbound traffic configuration path --- public/__redirects | 3 ++- .../changelog/containers/2026-03-26-outbound-workers.mdx | 2 +- .../2026-04-13-sandbox-outbound-workers-tls-auth.mdx | 2 +- src/content/docs/containers/api/container-class.mdx | 4 ++-- .../containers/{guides => configuration}/outbound-traffic.mdx | 0 .../docs/containers/configuration/workers-connections.mdx | 4 ++-- src/content/docs/containers/faq.mdx | 2 +- .../diagrams/ai/enterprise-ai-vibe-coding-platform.mdx | 2 +- src/content/docs/sandbox/guides/outbound-traffic.mdx | 2 +- 9 files changed, 11 insertions(+), 10 deletions(-) rename src/content/docs/containers/{guides => configuration}/outbound-traffic.mdx (100%) diff --git a/public/__redirects b/public/__redirects index d099b461567..2a5f6efe42c 100644 --- a/public/__redirects +++ b/public/__redirects @@ -660,7 +660,7 @@ # Containers IA rework: platform-details/ dissolved into core sections /containers/platform-details/architecture/ /containers/concepts/architecture/ 301 /containers/platform-details/placement/ /containers/concepts/placement/ 301 -/containers/platform-details/outbound-traffic/ /containers/guides/outbound-traffic/ 301 +/containers/platform-details/outbound-traffic/ /containers/configuration/outbound-traffic/ 301 /containers/platform-details/workers-connections/ /containers/configuration/workers-connections/ 301 /containers/platform-details/environment-variables/ /containers/configuration/environment-variables/ 301 /containers/platform-details/rollouts/ /containers/configuration/rollouts/ 301 @@ -670,6 +670,7 @@ /containers/platform-details/durable-object-methods/ /containers/api/durable-object-container/ 301 /containers/platform-details/ /containers/concepts/architecture/ 301 # Containers IA rework: Configuration section + Local Development to Guides + Wrangler pages to Reference +/containers/guides/outbound-traffic/ /containers/configuration/outbound-traffic/ 301 /containers/reference/local-dev/ /containers/guides/local-dev/ 301 /containers/reference/environment-variables/ /containers/configuration/environment-variables/ 301 /containers/reference/scaling-and-routing/ /containers/configuration/scaling-and-routing/ 301 diff --git a/src/content/changelog/containers/2026-03-26-outbound-workers.mdx b/src/content/changelog/containers/2026-03-26-outbound-workers.mdx index 80efb1f9b20..4d09b7a470f 100644 --- a/src/content/changelog/containers/2026-03-26-outbound-workers.mdx +++ b/src/content/changelog/containers/2026-03-26-outbound-workers.mdx @@ -78,4 +78,4 @@ This provides an easy way to associate state with any container instance, and in Upgrade to `@cloudflare/containers` version 0.2.0 or later, or `@cloudflare/sandbox` version 0.8.0 or later to use outbound Workers. -Refer to [Containers outbound traffic](/containers/guides/outbound-traffic/) and [Sandboxes outbound traffic](/sandbox/guides/outbound-traffic/) for more details and examples. +Refer to [Containers outbound traffic](/containers/configuration/outbound-traffic/) and [Sandboxes outbound traffic](/sandbox/guides/outbound-traffic/) for more details and examples. diff --git a/src/content/changelog/containers/2026-04-13-sandbox-outbound-workers-tls-auth.mdx b/src/content/changelog/containers/2026-04-13-sandbox-outbound-workers-tls-auth.mdx index f5fe931784a..7b563931f02 100644 --- a/src/content/changelog/containers/2026-04-13-sandbox-outbound-workers-tls-auth.mdx +++ b/src/content/changelog/containers/2026-04-13-sandbox-outbound-workers-tls-auth.mdx @@ -109,4 +109,4 @@ Handlers accept `params`, so you can customize behavior per instance without def Upgrade to `@cloudflare/containers@0.3.0` or `@cloudflare/sandbox@0.8.9` to use these features. -For more details, refer to [Sandbox outbound traffic](/sandbox/guides/outbound-traffic/) and [Container outbound traffic](/containers/guides/outbound-traffic/). +For more details, refer to [Sandbox outbound traffic](/sandbox/guides/outbound-traffic/) and [Container outbound traffic](/containers/configuration/outbound-traffic/). diff --git a/src/content/docs/containers/api/container-class.mdx b/src/content/docs/containers/api/container-class.mdx index bdc689ec0bf..2d1cdfacf7c 100644 --- a/src/content/docs/containers/api/container-class.mdx +++ b/src/content/docs/containers/api/container-class.mdx @@ -104,7 +104,7 @@ Configure these as class fields on your subclass. They apply to every instance o `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](/containers/guides/outbound-traffic/). + traffic](/containers/configuration/outbound-traffic/). - **`pingEndpoint`** (`string`, default: `"ping"`) — the host and path the class uses to health-check the container @@ -721,7 +721,7 @@ export default { ```
-For more information, refer to [Handle outbound traffic](/containers/guides/outbound-traffic/). +For more information, refer to [Handle outbound traffic](/containers/configuration/outbound-traffic/). ## Utility functions diff --git a/src/content/docs/containers/guides/outbound-traffic.mdx b/src/content/docs/containers/configuration/outbound-traffic.mdx similarity index 100% rename from src/content/docs/containers/guides/outbound-traffic.mdx rename to src/content/docs/containers/configuration/outbound-traffic.mdx diff --git a/src/content/docs/containers/configuration/workers-connections.mdx b/src/content/docs/containers/configuration/workers-connections.mdx index 9e7ff7a4d7f..1aa35122871 100644 --- a/src/content/docs/containers/configuration/workers-connections.mdx +++ b/src/content/docs/containers/configuration/workers-connections.mdx @@ -8,7 +8,7 @@ products: - containers --- -Containers can access [Workers bindings](/workers/runtime-apis/bindings/) — KV, R2, D1, Durable Objects, and others — through [outbound handlers](/containers/guides/outbound-traffic/#define-outbound-handlers). An outbound handler intercepts HTTP requests from the container and runs inside the Workers runtime, where all of your configured bindings are available. +Containers can access [Workers bindings](/workers/runtime-apis/bindings/) — KV, R2, D1, Durable Objects, and others — through [outbound handlers](/containers/configuration/outbound-traffic/#define-outbound-handlers). An outbound handler intercepts HTTP requests from the container and runs inside the Workers runtime, where all of your configured bindings are available. The container makes a plain HTTP request to a virtual hostname (for example, `http://my.kv/some-key`), and the outbound handler resolves it using the bound resource. No SDK or client library is required inside the container. @@ -57,6 +57,6 @@ The `ctx` argument exposes `containerId`, which lets you interact with the conta ## Related resources -- [Handle outbound traffic](/containers/guides/outbound-traffic/) — Block, allow, and intercept all outbound HTTP from a container +- [Handle outbound traffic](/containers/configuration/outbound-traffic/) — Block, allow, and intercept all outbound HTTP from a container - [Environment variables and secrets](/containers/configuration/environment-variables/) — Configure secrets and environment variables - [Durable Object Container API](/containers/api/durable-object-container/) — Full `ctx.container` API reference diff --git a/src/content/docs/containers/faq.mdx b/src/content/docs/containers/faq.mdx index dfca242606f..94ddc8be3f1 100644 --- a/src/content/docs/containers/faq.mdx +++ b/src/content/docs/containers/faq.mdx @@ -177,4 +177,4 @@ For a complete working example, see the [Docker-in-Docker Containers example](ht ## How do I allow or disallow egress from my container? -Refer to [Handle outbound traffic](/containers/guides/outbound-traffic/) for how to control outbound traffic and internet access. +Refer to [Handle outbound traffic](/containers/configuration/outbound-traffic/) for how to control outbound traffic and internet access. diff --git a/src/content/docs/reference-architecture/diagrams/ai/enterprise-ai-vibe-coding-platform.mdx b/src/content/docs/reference-architecture/diagrams/ai/enterprise-ai-vibe-coding-platform.mdx index 52c4c03ea23..17b22409d26 100644 --- a/src/content/docs/reference-architecture/diagrams/ai/enterprise-ai-vibe-coding-platform.mdx +++ b/src/content/docs/reference-architecture/diagrams/ai/enterprise-ai-vibe-coding-platform.mdx @@ -54,7 +54,7 @@ With a local agent harness, developers use CLI-based tools like Cursor, Windsurf All LLM interactions are tracked and managed through [AI Gateway](/ai-gateway/), which provides provider routing, cost controls, prompt logging, and [DLP inspection](/cloudflare-one/data-loss-prevention/). [Cost tracking](/ai-gateway/observability/costs/) attributes usage to projects, teams, departments, and individual users. -All egress from the development environment is controlled at the platform level. For containers, an [outbound handler](/containers/guides/outbound-traffic/) intercepts HTTP traffic. For Dynamic Workers, [egress control](/dynamic-workers/usage/egress-control/) provides equivalent capabilities. Secrets required for downstream connectivity are stored in [Secrets Store](/secrets-store/) and injected by the outbound handler at the platform level. The sandboxed environment never has direct access to credentials. With this outbound handler, platform administrators can allow or deny specific origin destinations, reroute traffic, apply custom policies on outbound traffic, or connect to other Cloudflare resources through [bindings](/workers/runtime-apis/bindings/). For access to on-premises or internal systems, [Workers VPC](/workers-vpc/) establishes private connectivity without exposing those systems to the Internet. +All egress from the development environment is controlled at the platform level. For containers, an [outbound handler](/containers/configuration/outbound-traffic/) intercepts HTTP traffic. For Dynamic Workers, [egress control](/dynamic-workers/usage/egress-control/) provides equivalent capabilities. Secrets required for downstream connectivity are stored in [Secrets Store](/secrets-store/) and injected by the outbound handler at the platform level. The sandboxed environment never has direct access to credentials. With this outbound handler, platform administrators can allow or deny specific origin destinations, reroute traffic, apply custom policies on outbound traffic, or connect to other Cloudflare resources through [bindings](/workers/runtime-apis/bindings/). For access to on-premises or internal systems, [Workers VPC](/workers-vpc/) establishes private connectivity without exposing those systems to the Internet. Additional security controls can be layered into the development container through package version locking and organizational controls baked into the container image. If the harness uses MCP servers, [MCP portals](/cloudflare-one/access-controls/ai-controls/mcp-portals/) provide audit logging of tool invocations, permission management for tool access, and visibility into which tools agents use and what data they access. diff --git a/src/content/docs/sandbox/guides/outbound-traffic.mdx b/src/content/docs/sandbox/guides/outbound-traffic.mdx index e3da306c4a5..70b871df68a 100644 --- a/src/content/docs/sandbox/guides/outbound-traffic.mdx +++ b/src/content/docs/sandbox/guides/outbound-traffic.mdx @@ -289,6 +289,6 @@ Requests are evaluated in this order: ## Related resources - [Connect to Workers bindings](/sandbox/guides/workers-connections/) — Access KV, R2, Durable Objects, and other bindings from a sandbox -- [Handle outbound traffic (Containers)](/containers/guides/outbound-traffic/) — Container SDK API for outbound handlers +- [Handle outbound traffic (Containers)](/containers/configuration/outbound-traffic/) — Container SDK API for outbound handlers - [Sandbox options](/sandbox/configuration/sandbox-options/) — Configure sandbox behavior - [Environment variables](/sandbox/configuration/environment-variables/) — Configure secrets and environment variables From 5111fa229b8fded8a6b6fc7089d5eb11757f1e4d Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Mon, 28 Sep 2026 18:23:58 -0400 Subject: [PATCH 22/23] Remove incremental migration guidance --- .../guides/migrate-to-durable-object-container-api.mdx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx b/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx index 497317f1d54..ba0ae66d661 100644 --- a/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx +++ b/src/content/docs/containers/guides/migrate-to-durable-object-container-api.mdx @@ -34,11 +34,3 @@ The `Container` class extends `DurableObject`. Replace its inherited lifecycle a Keep the same image in the `containers` section of your Wrangler configuration unless you intend to change the container application itself. The direct API still runs the image associated with its Durable Object class. For process handling and output, refer to [Execute commands](/containers/guides/execute-commands/) and the [`exec()` reference](/containers/api/durable-object-container/#exec). - -:::note[Incremental migration] -You can use an incremental migration to test direct runtime calls before the final cutover. First, deploy and test the existing `Container` class implementation. - -Next, deploy a bridge that still extends `Container`. Add routes that use `this.ctx.container` directly, and migrate helpers incrementally. Both paths use the same Durable Object and container, which preserves storage without requiring a second container application. - -After the direct routes pass your tests, change the class to extend `DurableObject`. Migrate scheduling last because the `Container` class owns `alarm()`. Let scheduled work finish before cutover, or store an application-owned job marker for the new `alarm()` handler. -::: From 14e38b2c955873d6ae115a2c2283bac8756a750a Mon Sep 17 00:00:00 2001 From: Thomas Gauvin Date: Mon, 28 Sep 2026 18:24:37 -0400 Subject: [PATCH 23/23] Remove migration workbench from docs PR --- .../containers/container-api-migration/.npmrc | 2 - .../container-api-migration/README.md | 57 - .../container/Dockerfile | 11 - .../container/server.mjs | 96 -- .../container-api-migration/package.json | 23 - .../container-api-migration/pnpm-lock.yaml | 1186 ----------------- .../container-api-migration/scripts/e2e.mjs | 135 -- .../container-api-migration/shared/lab.ts | 137 -- .../shared/workbench.ts | 94 -- .../stages/1-container-class/src/index.ts | 176 --- .../stages/1-container-class/wrangler.jsonc | 28 - .../stages/2-bridge/src/index.ts | 345 ----- .../stages/2-bridge/wrangler.jsonc | 28 - .../stages/3-durable-object-api/src/index.ts | 223 ---- .../3-durable-object-api/wrangler.jsonc | 28 - .../container-api-migration/tsconfig.json | 14 - tsconfig.json | 7 +- 17 files changed, 1 insertion(+), 2589 deletions(-) delete mode 100644 examples/containers/container-api-migration/.npmrc delete mode 100644 examples/containers/container-api-migration/README.md delete mode 100644 examples/containers/container-api-migration/container/Dockerfile delete mode 100644 examples/containers/container-api-migration/container/server.mjs delete mode 100644 examples/containers/container-api-migration/package.json delete mode 100644 examples/containers/container-api-migration/pnpm-lock.yaml delete mode 100644 examples/containers/container-api-migration/scripts/e2e.mjs delete mode 100644 examples/containers/container-api-migration/shared/lab.ts delete mode 100644 examples/containers/container-api-migration/shared/workbench.ts delete mode 100644 examples/containers/container-api-migration/stages/1-container-class/src/index.ts delete mode 100644 examples/containers/container-api-migration/stages/1-container-class/wrangler.jsonc delete mode 100644 examples/containers/container-api-migration/stages/2-bridge/src/index.ts delete mode 100644 examples/containers/container-api-migration/stages/2-bridge/wrangler.jsonc delete mode 100644 examples/containers/container-api-migration/stages/3-durable-object-api/src/index.ts delete mode 100644 examples/containers/container-api-migration/stages/3-durable-object-api/wrangler.jsonc delete mode 100644 examples/containers/container-api-migration/tsconfig.json diff --git a/examples/containers/container-api-migration/.npmrc b/examples/containers/container-api-migration/.npmrc deleted file mode 100644 index 19c37820f0f..00000000000 --- a/examples/containers/container-api-migration/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -registry=https://registry.npmjs.org/ -@cloudflare:registry=https://registry.npmjs.org/ diff --git a/examples/containers/container-api-migration/README.md b/examples/containers/container-api-migration/README.md deleted file mode 100644 index 505261b97d1..00000000000 --- a/examples/containers/container-api-migration/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# Container API migration workbench - -This executable reference migrates one container-enabled Durable Object through three releases: - -1. **Container class** — uses `@cloudflare/containers` helpers. -2. **Bridge** — keeps extending `Container`, but exposes helper and direct `ctx.container` routes on the same Durable Object and container instance. -3. **Durable Object API** — changes the base class to `DurableObject` and replaces the remaining helpers. - -All stages keep the Worker name, exported `MigrationWorkbench` class, `MIGRATION_WORKBENCH` binding, `v1` migration tag, and container image unchanged. Changing the TypeScript base class does not require a new Durable Object migration. - -## Coverage - -The lab exercises two-port readiness, request proxying, `switchPort()`, `getTcpPort()`, environment variables, lifecycle hooks, `monitor()`, `exec()`, inactivity, signals, destruction, outbound interception, scheduling, alarms, and Durable Object storage retained across deployments. - -The E2E run found an important direct-API responsibility: a new instance may be temporarily unavailable immediately after `destroy()`. The direct implementations use a bounded startup retry before checking port readiness. - -## Install and run locally - -```sh -pnpm install --ignore-workspace --frozen-lockfile -pnpm dev:legacy -pnpm dev:bridge -pnpm dev:direct -``` - -Docker must be running. Stop each dev server before starting the next stage so all stages reuse `.wrangler/state`. - -## Deploy and verify - -Deploy each stage over the same Worker and use the URL printed by Wrangler: - -```sh -pnpm deploy:legacy -pnpm test:e2e https://container-api-migration-workbench..workers.dev legacy - -pnpm deploy:bridge -pnpm test:e2e https://container-api-migration-workbench..workers.dev bridge -``` - -Create an alarm through the bridge before the cutover: - -```sh -curl -X POST "https://container-api-migration-workbench..workers.dev/api/schedule-cutover?instance=reference-e2e&mode=helper&delay=120" -``` - -Then replace the base class without changing any Durable Object identifiers: - -```sh -pnpm deploy:direct -pnpm test:e2e https://container-api-migration-workbench..workers.dev direct -``` - -The final suite requires legacy and bridge events to remain in the same Durable Object storage. It also verifies that the direct `alarm()` handler processed the marker created before cutover. - -## Bridge limitation - -The bridge does not install a Durable Object `alarm()` handler. The `Container` class owns that handler until the final cutover. Direct scheduling therefore moves last; other `ctx.container` operations migrate incrementally first. diff --git a/examples/containers/container-api-migration/container/Dockerfile b/examples/containers/container-api-migration/container/Dockerfile deleted file mode 100644 index 5678d731f11..00000000000 --- a/examples/containers/container-api-migration/container/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM node:22-slim - -WORKDIR /workbench -COPY server.mjs ./server.mjs - -ENV PORT=8080 -ENV ALT_PORT=9090 - -EXPOSE 8080 9090 - -CMD ["node", "server.mjs"] diff --git a/examples/containers/container-api-migration/container/server.mjs b/examples/containers/container-api-migration/container/server.mjs deleted file mode 100644 index 7e7bd8c294b..00000000000 --- a/examples/containers/container-api-migration/container/server.mjs +++ /dev/null @@ -1,96 +0,0 @@ -import { createServer } from "node:http"; - -const startedAt = new Date().toISOString(); -let bootstrapCount = 0; -let requestCount = 0; - -async function readBody(request) { - const chunks = []; - for await (const chunk of request) chunks.push(chunk); - return Buffer.concat(chunks).toString("utf8"); -} - -function send(response, status, value) { - response.writeHead(status, { - "content-type": "application/json; charset=utf-8", - }); - response.end(`${JSON.stringify(value, null, 2)}\n`); -} - -function createHandler(port) { - return async (request, response) => { - requestCount += 1; - const url = new URL(request.url ?? "/", `http://container:${port}`); - if (url.pathname === "/ping" || url.pathname === "/health") { - send(response, 200, { ok: true, port, startedAt }); - return; - } - if (url.pathname === "/bootstrap" && request.method === "POST") { - bootstrapCount += 1; - send(response, 200, { bootstrapped: true, bootstrapCount, port }); - return; - } - if (url.pathname === "/echo") { - send(response, 200, { - body: await readBody(request), - bootstrapCount, - environment: { - LAB_MODE: process.env.LAB_MODE ?? null, - LAB_STAGE: process.env.LAB_STAGE ?? null, - }, - method: request.method, - pid: process.pid, - port, - requestCount, - startedAt, - via: url.searchParams.get("via"), - }); - return; - } - if (url.pathname === "/outbound") { - try { - const outbound = await fetch("http://workbench.internal/probe"); - send(response, outbound.status, { - body: await outbound.text(), - intercepted: - outbound.headers.get("x-workbench-intercepted") === "true", - status: outbound.status, - }); - } catch (error) { - send(response, 502, { - error: error instanceof Error ? error.message : String(error), - intercepted: false, - }); - } - return; - } - if (url.pathname === "/scheduled" && request.method === "POST") { - send(response, 200, { - body: await readBody(request), - handledAt: new Date().toISOString(), - port, - }); - return; - } - send(response, 404, { - error: "Unknown container route", - path: url.pathname, - }); - }; -} - -for (const port of [ - Number(process.env.PORT ?? 8080), - Number(process.env.ALT_PORT ?? 9090), -]) { - createServer(createHandler(port)).listen(port, "0.0.0.0", () => { - console.log(`Migration workbench listening on ${port}`); - }); -} - -for (const signal of ["SIGINT", "SIGTERM"]) { - process.on(signal, () => { - console.log(`Migration workbench received ${signal}`); - process.exit(0); - }); -} diff --git a/examples/containers/container-api-migration/package.json b/examples/containers/container-api-migration/package.json deleted file mode 100644 index e99775cdd2c..00000000000 --- a/examples/containers/container-api-migration/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "container-api-migration-workbench", - "private": true, - "type": "module", - "scripts": { - "check": "tsc --noEmit", - "dev:legacy": "wrangler dev --config stages/1-container-class/wrangler.jsonc --persist-to .wrangler/state", - "dev:bridge": "wrangler dev --config stages/2-bridge/wrangler.jsonc --persist-to .wrangler/state", - "dev:direct": "wrangler dev --config stages/3-durable-object-api/wrangler.jsonc --persist-to .wrangler/state", - "deploy:legacy": "wrangler deploy --config stages/1-container-class/wrangler.jsonc", - "deploy:bridge": "wrangler deploy --config stages/2-bridge/wrangler.jsonc", - "deploy:direct": "wrangler deploy --config stages/3-durable-object-api/wrangler.jsonc", - "test:e2e": "node scripts/e2e.mjs" - }, - "dependencies": { - "@cloudflare/containers": "0.3.7" - }, - "devDependencies": { - "@cloudflare/workers-types": "5.20260914.1", - "typescript": "5.9.3", - "wrangler": "4.131.2" - } -} diff --git a/examples/containers/container-api-migration/pnpm-lock.yaml b/examples/containers/container-api-migration/pnpm-lock.yaml deleted file mode 100644 index 22cba95f58c..00000000000 --- a/examples/containers/container-api-migration/pnpm-lock.yaml +++ /dev/null @@ -1,1186 +0,0 @@ -lockfileVersion: "9.0" - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - .: - dependencies: - "@cloudflare/containers": - specifier: 0.3.7 - version: 0.3.7 - devDependencies: - "@cloudflare/workers-types": - specifier: 5.20260914.1 - version: 5.20260914.1 - typescript: - specifier: 5.9.3 - version: 5.9.3 - wrangler: - specifier: 4.131.2 - version: 4.131.2(@cloudflare/workers-types@5.20260914.1) - -packages: - "@cloudflare/containers@0.3.7": - resolution: - { - integrity: sha512-DM9dm3FnIBSyiSJ1FLavKwl/lk3oAmTaynCzZQ9pZR0ncRPquSxkxd8Nu2MFILxmDDsPkxKsSNEh9mHHMty4Fw==, - } - - "@cloudflare/kv-asset-handler@0.5.0": - resolution: - { - integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==, - } - engines: { node: ">=22.0.0" } - - "@cloudflare/unenv-preset@2.16.1": - resolution: - { - integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==, - } - peerDependencies: - unenv: 2.0.0-rc.24 - workerd: ">1.20260305.0 <2.0.0-0" - peerDependenciesMeta: - workerd: - optional: true - - "@cloudflare/workerd-darwin-64@1.20260911.1": - resolution: - { - integrity: sha512-785eaY1bkR1cm4Z/PCUeteZYmTMe6lre2zz63/GdGGimsoMsKxgl4brFPRukim8iv28EyD1XoCB/VPYF20BERA==, - } - engines: { node: ">=16" } - cpu: [x64] - os: [darwin] - - "@cloudflare/workerd-darwin-arm64@1.20260911.1": - resolution: - { - integrity: sha512-WU4bFqEN0H7ndGWxoedegv95DmNVBtv0ncXcHG9nYFTUI78sxEb0qoT3U6Ga4hyBkzsJFBX/zvVBIGX3qKldGA==, - } - engines: { node: ">=16" } - cpu: [arm64] - os: [darwin] - - "@cloudflare/workerd-linux-64@1.20260911.1": - resolution: - { - integrity: sha512-0Y2gy62oxQxWa38qinSPE6zNL5+JmumJtDY9AWW1HB8KHuATxN71o5MGzmVFfB8PwZsiHfUd2Sv7O22krCOrhw==, - } - engines: { node: ">=16" } - cpu: [x64] - os: [linux] - - "@cloudflare/workerd-linux-arm64@1.20260911.1": - resolution: - { - integrity: sha512-kttNPnx1r2lCqFUoMH62z7CqGV+j4QBbw5fdtaz4pzOrzBv0AWkNATt7onFUe+SwP8zhcepMtbm2F4kKzTf6VA==, - } - engines: { node: ">=16" } - cpu: [arm64] - os: [linux] - - "@cloudflare/workerd-windows-64@1.20260911.1": - resolution: - { - integrity: sha512-5iO/YfoBDOgO3CrHdkiiVP8SL3O2jC+c6Ux3d378TSPKLhU5+CgHjtE/ZSodWQrzr4FzFRqdW8S7n5nbyD1MHQ==, - } - engines: { node: ">=16" } - cpu: [x64] - os: [win32] - - "@cloudflare/workers-types@5.20260914.1": - resolution: - { - integrity: sha512-9xGgkvmG1lw0QdKJmSR5YNXaA221DfefpDCfPLz5PlzdYU0BXzxONQoWakLXTsFIz8xtsNfeUg+mbspIA5P4Xg==, - } - - "@cspotcode/source-map-support@0.8.1": - resolution: - { - integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==, - } - engines: { node: ">=12" } - - "@emnapi/runtime@1.11.3": - resolution: - { - integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==, - } - - "@esbuild/aix-ppc64@0.28.1": - resolution: - { - integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==, - } - engines: { node: ">=18" } - cpu: [ppc64] - os: [aix] - - "@esbuild/android-arm64@0.28.1": - resolution: - { - integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [android] - - "@esbuild/android-arm@0.28.1": - resolution: - { - integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==, - } - engines: { node: ">=18" } - cpu: [arm] - os: [android] - - "@esbuild/android-x64@0.28.1": - resolution: - { - integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [android] - - "@esbuild/darwin-arm64@0.28.1": - resolution: - { - integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [darwin] - - "@esbuild/darwin-x64@0.28.1": - resolution: - { - integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [darwin] - - "@esbuild/freebsd-arm64@0.28.1": - resolution: - { - integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [freebsd] - - "@esbuild/freebsd-x64@0.28.1": - resolution: - { - integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [freebsd] - - "@esbuild/linux-arm64@0.28.1": - resolution: - { - integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [linux] - - "@esbuild/linux-arm@0.28.1": - resolution: - { - integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==, - } - engines: { node: ">=18" } - cpu: [arm] - os: [linux] - - "@esbuild/linux-ia32@0.28.1": - resolution: - { - integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==, - } - engines: { node: ">=18" } - cpu: [ia32] - os: [linux] - - "@esbuild/linux-loong64@0.28.1": - resolution: - { - integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==, - } - engines: { node: ">=18" } - cpu: [loong64] - os: [linux] - - "@esbuild/linux-mips64el@0.28.1": - resolution: - { - integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==, - } - engines: { node: ">=18" } - cpu: [mips64el] - os: [linux] - - "@esbuild/linux-ppc64@0.28.1": - resolution: - { - integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==, - } - engines: { node: ">=18" } - cpu: [ppc64] - os: [linux] - - "@esbuild/linux-riscv64@0.28.1": - resolution: - { - integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==, - } - engines: { node: ">=18" } - cpu: [riscv64] - os: [linux] - - "@esbuild/linux-s390x@0.28.1": - resolution: - { - integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==, - } - engines: { node: ">=18" } - cpu: [s390x] - os: [linux] - - "@esbuild/linux-x64@0.28.1": - resolution: - { - integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [linux] - - "@esbuild/netbsd-arm64@0.28.1": - resolution: - { - integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [netbsd] - - "@esbuild/netbsd-x64@0.28.1": - resolution: - { - integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [netbsd] - - "@esbuild/openbsd-arm64@0.28.1": - resolution: - { - integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [openbsd] - - "@esbuild/openbsd-x64@0.28.1": - resolution: - { - integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [openbsd] - - "@esbuild/openharmony-arm64@0.28.1": - resolution: - { - integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [openharmony] - - "@esbuild/sunos-x64@0.28.1": - resolution: - { - integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [sunos] - - "@esbuild/win32-arm64@0.28.1": - resolution: - { - integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [win32] - - "@esbuild/win32-ia32@0.28.1": - resolution: - { - integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==, - } - engines: { node: ">=18" } - cpu: [ia32] - os: [win32] - - "@esbuild/win32-x64@0.28.1": - resolution: - { - integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [win32] - - "@img/colour@1.1.0": - resolution: - { - integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==, - } - engines: { node: ">=18" } - - "@img/sharp-darwin-arm64@0.35.4": - resolution: - { - integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==, - } - engines: { node: ">=20.9.0" } - cpu: [arm64] - os: [darwin] - - "@img/sharp-darwin-x64@0.35.4": - resolution: - { - integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==, - } - engines: { node: ">=20.9.0" } - cpu: [x64] - os: [darwin] - - "@img/sharp-freebsd-wasm32@0.35.4": - resolution: - { - integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==, - } - engines: { node: ">=20.9.0" } - os: [freebsd] - - "@img/sharp-libvips-darwin-arm64@1.3.3": - resolution: - { - integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==, - } - cpu: [arm64] - os: [darwin] - - "@img/sharp-libvips-darwin-x64@1.3.3": - resolution: - { - integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==, - } - cpu: [x64] - os: [darwin] - - "@img/sharp-libvips-linux-arm64@1.3.3": - resolution: - { - integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==, - } - cpu: [arm64] - os: [linux] - - "@img/sharp-libvips-linux-arm@1.3.3": - resolution: - { - integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==, - } - cpu: [arm] - os: [linux] - - "@img/sharp-libvips-linux-ppc64@1.3.3": - resolution: - { - integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==, - } - cpu: [ppc64] - os: [linux] - - "@img/sharp-libvips-linux-riscv64@1.3.3": - resolution: - { - integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==, - } - cpu: [riscv64] - os: [linux] - - "@img/sharp-libvips-linux-s390x@1.3.3": - resolution: - { - integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==, - } - cpu: [s390x] - os: [linux] - - "@img/sharp-libvips-linux-x64@1.3.3": - resolution: - { - integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==, - } - cpu: [x64] - os: [linux] - - "@img/sharp-libvips-linuxmusl-arm64@1.3.3": - resolution: - { - integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==, - } - cpu: [arm64] - os: [linux] - - "@img/sharp-libvips-linuxmusl-x64@1.3.3": - resolution: - { - integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==, - } - cpu: [x64] - os: [linux] - - "@img/sharp-linux-arm64@0.35.4": - resolution: - { - integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==, - } - engines: { node: ">=20.9.0" } - cpu: [arm64] - os: [linux] - - "@img/sharp-linux-arm@0.35.4": - resolution: - { - integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==, - } - engines: { node: ">=20.9.0" } - cpu: [arm] - os: [linux] - - "@img/sharp-linux-ppc64@0.35.4": - resolution: - { - integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==, - } - engines: { node: ">=20.9.0" } - cpu: [ppc64] - os: [linux] - - "@img/sharp-linux-riscv64@0.35.4": - resolution: - { - integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==, - } - engines: { node: ">=20.9.0" } - cpu: [riscv64] - os: [linux] - - "@img/sharp-linux-s390x@0.35.4": - resolution: - { - integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==, - } - engines: { node: ">=20.9.0" } - cpu: [s390x] - os: [linux] - - "@img/sharp-linux-x64@0.35.4": - resolution: - { - integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==, - } - engines: { node: ">=20.9.0" } - cpu: [x64] - os: [linux] - - "@img/sharp-linuxmusl-arm64@0.35.4": - resolution: - { - integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==, - } - engines: { node: ">=20.9.0" } - cpu: [arm64] - os: [linux] - - "@img/sharp-linuxmusl-x64@0.35.4": - resolution: - { - integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==, - } - engines: { node: ">=20.9.0" } - cpu: [x64] - os: [linux] - - "@img/sharp-wasm32@0.35.4": - resolution: - { - integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==, - } - engines: { node: ">=20.9.0" } - - "@img/sharp-webcontainers-wasm32@0.35.4": - resolution: - { - integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==, - } - engines: { node: ">=20.9.0" } - cpu: [wasm32] - - "@img/sharp-win32-arm64@0.35.4": - resolution: - { - integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==, - } - engines: { node: ">=20.9.0" } - cpu: [arm64] - os: [win32] - - "@img/sharp-win32-ia32@0.35.4": - resolution: - { - integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==, - } - engines: { node: ^20.9.0 } - cpu: [ia32] - os: [win32] - - "@img/sharp-win32-x64@0.35.4": - resolution: - { - integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==, - } - engines: { node: ">=20.9.0" } - cpu: [x64] - os: [win32] - - "@jridgewell/resolve-uri@3.1.2": - resolution: - { - integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, - } - engines: { node: ">=6.0.0" } - - "@jridgewell/sourcemap-codec@1.6.0": - resolution: - { - integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==, - } - - "@jridgewell/trace-mapping@0.3.9": - resolution: - { - integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==, - } - - "@poppinss/colors@4.1.6": - resolution: - { - integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==, - } - - "@poppinss/dumper@0.6.5": - resolution: - { - integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==, - } - - "@poppinss/exception@1.2.3": - resolution: - { - integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==, - } - - "@sindresorhus/is@7.2.0": - resolution: - { - integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==, - } - engines: { node: ">=18" } - - "@speed-highlight/core@1.2.24": - resolution: - { - integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==, - } - - blake3-wasm@2.1.5: - resolution: - { - integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==, - } - - cookie@1.1.1: - resolution: - { - integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==, - } - engines: { node: ">=18" } - - detect-libc@2.1.2: - resolution: - { - integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, - } - engines: { node: ">=8" } - - error-stack-parser-es@1.0.5: - resolution: - { - integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==, - } - - esbuild@0.28.1: - resolution: - { - integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==, - } - engines: { node: ">=18" } - hasBin: true - - fsevents@2.3.3: - resolution: - { - integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, - } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } - os: [darwin] - - kleur@4.1.5: - resolution: - { - integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==, - } - engines: { node: ">=6" } - - miniflare@5.20260911.1-alpha: - resolution: - { - integrity: sha512-7IDj9monoYcCPrS8HfcTt90T3pDwKGvNEAR1Y061KbJhBd5JkStOwOpHMUDFBo9PEbjWVDxPICAnXGNNV2LNfQ==, - } - engines: { node: ">=22.0.0" } - - path-to-regexp@6.3.0: - resolution: - { - integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==, - } - - pathe@2.0.3: - resolution: - { - integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, - } - - semver@7.8.5: - resolution: - { - integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==, - } - engines: { node: ">=10" } - hasBin: true - - sharp@0.35.4: - resolution: - { - integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==, - } - engines: { node: ">=20.9.0" } - peerDependencies: - "@types/node": "*" - peerDependenciesMeta: - "@types/node": - optional: true - - supports-color@10.2.2: - resolution: - { - integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==, - } - engines: { node: ">=18" } - - tslib@2.8.1: - resolution: - { - integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, - } - - typescript@5.9.3: - resolution: - { - integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, - } - engines: { node: ">=14.17" } - hasBin: true - - undici@7.29.0: - resolution: - { - integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==, - } - engines: { node: ">=20.18.1" } - - unenv@2.0.0-rc.24: - resolution: - { - integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==, - } - - workerd@1.20260911.1: - resolution: - { - integrity: sha512-vRr8QdBxueQOZJO1hRCI73EZlix87IAyBAcSyI3rA1VB+6oxjw3oaqzYnIV8C4IOPtUgihbdMAgzkb5GM4V7DQ==, - } - engines: { node: ">=16" } - hasBin: true - - wrangler@4.131.2: - resolution: - { - integrity: sha512-jmkGE7monbPKyYQr1FPQN+SARVhddqw2fhXOmTKCw4lroqlFGSS6rit/RTvPi/qzNLKrXxkS8DhWXasJnStplg==, - } - engines: { node: ">=22.0.0" } - hasBin: true - peerDependencies: - "@cloudflare/workers-types": ^5.20260911.1 - peerDependenciesMeta: - "@cloudflare/workers-types": - optional: true - - ws@8.21.0: - resolution: - { - integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==, - } - engines: { node: ">=10.0.0" } - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - youch-core@0.3.3: - resolution: - { - integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==, - } - - youch@4.1.0-beta.10: - resolution: - { - integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==, - } - -snapshots: - "@cloudflare/containers@0.3.7": {} - - "@cloudflare/kv-asset-handler@0.5.0": {} - - "@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260911.1)": - dependencies: - unenv: 2.0.0-rc.24 - optionalDependencies: - workerd: 1.20260911.1 - - "@cloudflare/workerd-darwin-64@1.20260911.1": - optional: true - - "@cloudflare/workerd-darwin-arm64@1.20260911.1": - optional: true - - "@cloudflare/workerd-linux-64@1.20260911.1": - optional: true - - "@cloudflare/workerd-linux-arm64@1.20260911.1": - optional: true - - "@cloudflare/workerd-windows-64@1.20260911.1": - optional: true - - "@cloudflare/workers-types@5.20260914.1": {} - - "@cspotcode/source-map-support@0.8.1": - dependencies: - "@jridgewell/trace-mapping": 0.3.9 - - "@emnapi/runtime@1.11.3": - dependencies: - tslib: 2.8.1 - optional: true - - "@esbuild/aix-ppc64@0.28.1": - optional: true - - "@esbuild/android-arm64@0.28.1": - optional: true - - "@esbuild/android-arm@0.28.1": - optional: true - - "@esbuild/android-x64@0.28.1": - optional: true - - "@esbuild/darwin-arm64@0.28.1": - optional: true - - "@esbuild/darwin-x64@0.28.1": - optional: true - - "@esbuild/freebsd-arm64@0.28.1": - optional: true - - "@esbuild/freebsd-x64@0.28.1": - optional: true - - "@esbuild/linux-arm64@0.28.1": - optional: true - - "@esbuild/linux-arm@0.28.1": - optional: true - - "@esbuild/linux-ia32@0.28.1": - optional: true - - "@esbuild/linux-loong64@0.28.1": - optional: true - - "@esbuild/linux-mips64el@0.28.1": - optional: true - - "@esbuild/linux-ppc64@0.28.1": - optional: true - - "@esbuild/linux-riscv64@0.28.1": - optional: true - - "@esbuild/linux-s390x@0.28.1": - optional: true - - "@esbuild/linux-x64@0.28.1": - optional: true - - "@esbuild/netbsd-arm64@0.28.1": - optional: true - - "@esbuild/netbsd-x64@0.28.1": - optional: true - - "@esbuild/openbsd-arm64@0.28.1": - optional: true - - "@esbuild/openbsd-x64@0.28.1": - optional: true - - "@esbuild/openharmony-arm64@0.28.1": - optional: true - - "@esbuild/sunos-x64@0.28.1": - optional: true - - "@esbuild/win32-arm64@0.28.1": - optional: true - - "@esbuild/win32-ia32@0.28.1": - optional: true - - "@esbuild/win32-x64@0.28.1": - optional: true - - "@img/colour@1.1.0": {} - - "@img/sharp-darwin-arm64@0.35.4": - optionalDependencies: - "@img/sharp-libvips-darwin-arm64": 1.3.3 - optional: true - - "@img/sharp-darwin-x64@0.35.4": - optionalDependencies: - "@img/sharp-libvips-darwin-x64": 1.3.3 - optional: true - - "@img/sharp-freebsd-wasm32@0.35.4": - dependencies: - "@img/sharp-wasm32": 0.35.4 - optional: true - - "@img/sharp-libvips-darwin-arm64@1.3.3": - optional: true - - "@img/sharp-libvips-darwin-x64@1.3.3": - optional: true - - "@img/sharp-libvips-linux-arm64@1.3.3": - optional: true - - "@img/sharp-libvips-linux-arm@1.3.3": - optional: true - - "@img/sharp-libvips-linux-ppc64@1.3.3": - optional: true - - "@img/sharp-libvips-linux-riscv64@1.3.3": - optional: true - - "@img/sharp-libvips-linux-s390x@1.3.3": - optional: true - - "@img/sharp-libvips-linux-x64@1.3.3": - optional: true - - "@img/sharp-libvips-linuxmusl-arm64@1.3.3": - optional: true - - "@img/sharp-libvips-linuxmusl-x64@1.3.3": - optional: true - - "@img/sharp-linux-arm64@0.35.4": - optionalDependencies: - "@img/sharp-libvips-linux-arm64": 1.3.3 - optional: true - - "@img/sharp-linux-arm@0.35.4": - optionalDependencies: - "@img/sharp-libvips-linux-arm": 1.3.3 - optional: true - - "@img/sharp-linux-ppc64@0.35.4": - optionalDependencies: - "@img/sharp-libvips-linux-ppc64": 1.3.3 - optional: true - - "@img/sharp-linux-riscv64@0.35.4": - optionalDependencies: - "@img/sharp-libvips-linux-riscv64": 1.3.3 - optional: true - - "@img/sharp-linux-s390x@0.35.4": - optionalDependencies: - "@img/sharp-libvips-linux-s390x": 1.3.3 - optional: true - - "@img/sharp-linux-x64@0.35.4": - optionalDependencies: - "@img/sharp-libvips-linux-x64": 1.3.3 - optional: true - - "@img/sharp-linuxmusl-arm64@0.35.4": - optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64": 1.3.3 - optional: true - - "@img/sharp-linuxmusl-x64@0.35.4": - optionalDependencies: - "@img/sharp-libvips-linuxmusl-x64": 1.3.3 - optional: true - - "@img/sharp-wasm32@0.35.4": - dependencies: - "@emnapi/runtime": 1.11.3 - optional: true - - "@img/sharp-webcontainers-wasm32@0.35.4": - dependencies: - "@img/sharp-wasm32": 0.35.4 - optional: true - - "@img/sharp-win32-arm64@0.35.4": - optional: true - - "@img/sharp-win32-ia32@0.35.4": - optional: true - - "@img/sharp-win32-x64@0.35.4": - optional: true - - "@jridgewell/resolve-uri@3.1.2": {} - - "@jridgewell/sourcemap-codec@1.6.0": {} - - "@jridgewell/trace-mapping@0.3.9": - dependencies: - "@jridgewell/resolve-uri": 3.1.2 - "@jridgewell/sourcemap-codec": 1.6.0 - - "@poppinss/colors@4.1.6": - dependencies: - kleur: 4.1.5 - - "@poppinss/dumper@0.6.5": - dependencies: - "@poppinss/colors": 4.1.6 - "@sindresorhus/is": 7.2.0 - supports-color: 10.2.2 - - "@poppinss/exception@1.2.3": {} - - "@sindresorhus/is@7.2.0": {} - - "@speed-highlight/core@1.2.24": {} - - blake3-wasm@2.1.5: {} - - cookie@1.1.1: {} - - detect-libc@2.1.2: {} - - error-stack-parser-es@1.0.5: {} - - esbuild@0.28.1: - optionalDependencies: - "@esbuild/aix-ppc64": 0.28.1 - "@esbuild/android-arm": 0.28.1 - "@esbuild/android-arm64": 0.28.1 - "@esbuild/android-x64": 0.28.1 - "@esbuild/darwin-arm64": 0.28.1 - "@esbuild/darwin-x64": 0.28.1 - "@esbuild/freebsd-arm64": 0.28.1 - "@esbuild/freebsd-x64": 0.28.1 - "@esbuild/linux-arm": 0.28.1 - "@esbuild/linux-arm64": 0.28.1 - "@esbuild/linux-ia32": 0.28.1 - "@esbuild/linux-loong64": 0.28.1 - "@esbuild/linux-mips64el": 0.28.1 - "@esbuild/linux-ppc64": 0.28.1 - "@esbuild/linux-riscv64": 0.28.1 - "@esbuild/linux-s390x": 0.28.1 - "@esbuild/linux-x64": 0.28.1 - "@esbuild/netbsd-arm64": 0.28.1 - "@esbuild/netbsd-x64": 0.28.1 - "@esbuild/openbsd-arm64": 0.28.1 - "@esbuild/openbsd-x64": 0.28.1 - "@esbuild/openharmony-arm64": 0.28.1 - "@esbuild/sunos-x64": 0.28.1 - "@esbuild/win32-arm64": 0.28.1 - "@esbuild/win32-ia32": 0.28.1 - "@esbuild/win32-x64": 0.28.1 - - fsevents@2.3.3: - optional: true - - kleur@4.1.5: {} - - miniflare@5.20260911.1-alpha: - dependencies: - "@cspotcode/source-map-support": 0.8.1 - sharp: 0.35.4 - undici: 7.29.0 - workerd: 1.20260911.1 - ws: 8.21.0 - youch: 4.1.0-beta.10 - transitivePeerDependencies: - - "@types/node" - - bufferutil - - utf-8-validate - - path-to-regexp@6.3.0: {} - - pathe@2.0.3: {} - - semver@7.8.5: {} - - sharp@0.35.4: - dependencies: - "@img/colour": 1.1.0 - detect-libc: 2.1.2 - semver: 7.8.5 - optionalDependencies: - "@img/sharp-darwin-arm64": 0.35.4 - "@img/sharp-darwin-x64": 0.35.4 - "@img/sharp-freebsd-wasm32": 0.35.4 - "@img/sharp-libvips-darwin-arm64": 1.3.3 - "@img/sharp-libvips-darwin-x64": 1.3.3 - "@img/sharp-libvips-linux-arm": 1.3.3 - "@img/sharp-libvips-linux-arm64": 1.3.3 - "@img/sharp-libvips-linux-ppc64": 1.3.3 - "@img/sharp-libvips-linux-riscv64": 1.3.3 - "@img/sharp-libvips-linux-s390x": 1.3.3 - "@img/sharp-libvips-linux-x64": 1.3.3 - "@img/sharp-libvips-linuxmusl-arm64": 1.3.3 - "@img/sharp-libvips-linuxmusl-x64": 1.3.3 - "@img/sharp-linux-arm": 0.35.4 - "@img/sharp-linux-arm64": 0.35.4 - "@img/sharp-linux-ppc64": 0.35.4 - "@img/sharp-linux-riscv64": 0.35.4 - "@img/sharp-linux-s390x": 0.35.4 - "@img/sharp-linux-x64": 0.35.4 - "@img/sharp-linuxmusl-arm64": 0.35.4 - "@img/sharp-linuxmusl-x64": 0.35.4 - "@img/sharp-webcontainers-wasm32": 0.35.4 - "@img/sharp-win32-arm64": 0.35.4 - "@img/sharp-win32-ia32": 0.35.4 - "@img/sharp-win32-x64": 0.35.4 - - supports-color@10.2.2: {} - - tslib@2.8.1: - optional: true - - typescript@5.9.3: {} - - undici@7.29.0: {} - - unenv@2.0.0-rc.24: - dependencies: - pathe: 2.0.3 - - workerd@1.20260911.1: - optionalDependencies: - "@cloudflare/workerd-darwin-64": 1.20260911.1 - "@cloudflare/workerd-darwin-arm64": 1.20260911.1 - "@cloudflare/workerd-linux-64": 1.20260911.1 - "@cloudflare/workerd-linux-arm64": 1.20260911.1 - "@cloudflare/workerd-windows-64": 1.20260911.1 - - wrangler@4.131.2(@cloudflare/workers-types@5.20260914.1): - dependencies: - "@cloudflare/kv-asset-handler": 0.5.0 - "@cloudflare/unenv-preset": 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260911.1) - blake3-wasm: 2.1.5 - esbuild: 0.28.1 - miniflare: 5.20260911.1-alpha - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.24 - workerd: 1.20260911.1 - optionalDependencies: - "@cloudflare/workers-types": 5.20260914.1 - fsevents: 2.3.3 - transitivePeerDependencies: - - "@types/node" - - bufferutil - - utf-8-validate - - ws@8.21.0: {} - - youch-core@0.3.3: - dependencies: - "@poppinss/exception": 1.2.3 - error-stack-parser-es: 1.0.5 - - youch@4.1.0-beta.10: - dependencies: - "@poppinss/colors": 4.1.6 - "@poppinss/dumper": 0.6.5 - "@speed-highlight/core": 1.2.24 - cookie: 1.1.1 - youch-core: 0.3.3 diff --git a/examples/containers/container-api-migration/scripts/e2e.mjs b/examples/containers/container-api-migration/scripts/e2e.mjs deleted file mode 100644 index 9e880e614b3..00000000000 --- a/examples/containers/container-api-migration/scripts/e2e.mjs +++ /dev/null @@ -1,135 +0,0 @@ -import assert from "node:assert/strict"; - -const [baseUrl, stage, instance = "reference-e2e"] = process.argv.slice(2); -if (!baseUrl || !["legacy", "bridge", "direct"].includes(stage)) { - console.error( - "Usage: node scripts/e2e.mjs [instance]", - ); - process.exit(2); -} -const results = []; -async function call(action, mode, options = {}) { - const url = new URL(`/api/${action}`, baseUrl); - url.searchParams.set("instance", instance); - url.searchParams.set("mode", mode); - if (options.delay) url.searchParams.set("delay", String(options.delay)); - const started = Date.now(); - const response = await fetch(url, { - method: action === "status" || action === "events" ? "GET" : "POST", - signal: AbortSignal.timeout(120_000), - }); - const text = await response.text(); - let body; - try { - body = JSON.parse(text); - } catch { - body = text; - } - results.push({ - action, - durationMs: Date.now() - started, - mode, - status: response.status, - }); - return { body, response }; -} -async function startEventually(mode) { - let result; - for (let attempt = 1; attempt <= 8; attempt += 1) { - result = await call("start", mode); - if (result.response.ok) return result; - if (attempt < 8) await new Promise((resolve) => setTimeout(resolve, 5_000)); - } - return result; -} -async function exercise(mode) { - console.log(`\n[${stage}/${mode}] start and readiness`); - let result = await startEventually(mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - result = await call("status", mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - result = await call("echo", mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - assert.equal(result.body.port, 8080); - result = await call("alternate", mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - assert.equal(result.body.port, 9090); - result = await call("switch-port", mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - assert.equal(result.body.port, 9090); - result = await call("exec", mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - assert.equal(result.body.exitCode, 0); - assert.match(result.body.stdout, /^v\d+/); - result = await call("outbound", mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - assert.equal(result.body.intercepted, true, JSON.stringify(result.body)); - result = await call("renew", mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - const scheduleStartedAt = Date.now(); - result = await call("schedule", mode); - if (stage === "bridge" && mode === "direct") - assert.equal(result.response.status, 409, JSON.stringify(result.body)); - else { - assert.equal(result.response.status, 202, JSON.stringify(result.body)); - await new Promise((resolve) => setTimeout(resolve, 4_500)); - } - result = await call("events", mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - assert.ok(Array.isArray(result.body)); - assert.ok(result.body.length > 0); - if (!(stage === "bridge" && mode === "direct")) { - const expectedMessage = - stage === "direct" - ? "Durable Object alarm handled scheduled work" - : "Container.schedule callback"; - assert.ok( - result.body.some( - (event) => - event.stage === stage && - event.message === expectedMessage && - Date.parse(event.at) >= scheduleStartedAt - 1000, - ), - `${expectedMessage} did not run for the current test`, - ); - } - result = await call("stop", mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - await new Promise((resolve) => setTimeout(resolve, 2_000)); - result = await startEventually(mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - result = await call("echo", mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - assert.equal(result.body.port, 8080); - result = await call("outbound", mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - assert.equal(result.body.intercepted, true, JSON.stringify(result.body)); - result = await call("destroy", mode); - assert.equal(result.response.ok, true, JSON.stringify(result.body)); - await new Promise((resolve) => setTimeout(resolve, 4_000)); -} -const modes = - stage === "bridge" - ? ["helper", "direct"] - : [stage === "legacy" ? "helper" : "direct"]; -for (const mode of modes) await exercise(mode); -const ledger = await call("events", modes.at(-1)); -assert.equal(ledger.response.ok, true, JSON.stringify(ledger.body)); -const seenStages = new Set(ledger.body.map((event) => event.stage)); -assert.ok(seenStages.has(stage)); -if (stage === "direct") { - assert.ok(seenStages.has("legacy"), "Legacy events did not survive"); - assert.ok(seenStages.has("bridge"), "Bridge events did not survive"); - assert.ok( - ledger.body.some( - (event) => - event.message === "Durable Object alarm handled scheduled work" && - event.detail?.payload?.stage === "bridge", - ), - "The direct alarm handler did not process the bridge cutover job", - ); -} -console.table(results); -console.log( - `PASS: ${stage} end-to-end suite completed for instance ${instance}`, -); diff --git a/examples/containers/container-api-migration/shared/lab.ts b/examples/containers/container-api-migration/shared/lab.ts deleted file mode 100644 index 742b946eae6..00000000000 --- a/examples/containers/container-api-migration/shared/lab.ts +++ /dev/null @@ -1,137 +0,0 @@ -export type LabStage = "legacy" | "bridge" | "direct"; -export type LabMode = "helper" | "direct"; - -export interface LabEvent { - at: string; - detail?: unknown; - message: string; - mode: LabMode | "system"; - stage: LabStage; -} - -export interface WorkbenchOptions { - availableModes: LabMode[]; - description: string; - stage: LabStage; -} - -const EVENT_KEY = "lab:events"; - -export async function recordEvent( - storage: DurableObjectStorage, - stage: LabStage, - mode: LabEvent["mode"], - message: string, - detail?: unknown, -): Promise { - const event: LabEvent = { - at: new Date().toISOString(), - detail, - message, - mode, - stage, - }; - const events = (await storage.get(EVENT_KEY)) ?? []; - events.push(event); - await storage.put(EVENT_KEY, events.slice(-100)); - return event; -} - -export async function readEvents( - storage: DurableObjectStorage, -): Promise { - return (await storage.get(EVENT_KEY)) ?? []; -} - -export function json(value: unknown, status = 200): Response { - return Response.json(value, { - status, - headers: { "cache-control": "no-store" }, - }); -} - -export function actionFrom(request: Request): string { - return ( - new URL(request.url).pathname.split("/").filter(Boolean).at(-1) ?? "status" - ); -} - -export function modeFrom(request: Request, fallback: LabMode): LabMode { - return new URL(request.url).searchParams.get("mode") === "direct" - ? "direct" - : fallback; -} - -export function requestForContainer(path: string, init?: RequestInit): Request { - return new Request(`http://container${path}`, init); -} - -export async function startWithRetry( - container: NonNullable, - options: Parameters["start"]>[0], - attempts = 20, -): Promise { - let lastError: unknown; - for (let attempt = 1; attempt <= attempts; attempt += 1) { - if (container.running) return attempt; - try { - container.start(options); - return attempt; - } catch (error) { - lastError = error; - if (attempt < attempts) await scheduler.wait(1000); - } - } - throw new Error( - `Container could not be allocated after ${attempts} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`, - ); -} - -export async function waitForPort( - container: NonNullable, - port: number, - attempts = 40, -): Promise { - let lastError: unknown; - for (let attempt = 1; attempt <= attempts; attempt += 1) { - try { - const response = await container - .getTcpPort(port) - .fetch("http://container/ping"); - if (response.ok) return attempt; - } catch (error) { - lastError = error; - } - await scheduler.wait(250); - } - throw new Error( - `Port ${port} did not become ready: ${lastError instanceof Error ? lastError.message : String(lastError)}`, - ); -} - -export async function routeWorkbench< - T extends Rpc.DurableObjectBranded | undefined, ->( - request: Request, - namespace: DurableObjectNamespace, - options: WorkbenchOptions, - render: (options: WorkbenchOptions) => string, -): Promise { - const url = new URL(request.url); - if (!url.pathname.startsWith("/api/")) { - return new Response(render(options), { - headers: { - "cache-control": "no-store", - "content-type": "text/html; charset=utf-8", - }, - }); - } - const instance = url.searchParams.get("instance")?.trim() || "reference"; - if (!/^[a-zA-Z0-9_-]{1,64}$/.test(instance)) { - return json( - { error: "Instance names may contain letters, numbers, _ and -." }, - 400, - ); - } - return namespace.getByName(instance).fetch(request); -} diff --git a/examples/containers/container-api-migration/shared/workbench.ts b/examples/containers/container-api-migration/shared/workbench.ts deleted file mode 100644 index e311568a69e..00000000000 --- a/examples/containers/container-api-migration/shared/workbench.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { WorkbenchOptions } from "./lab"; - -export function renderWorkbench({ - availableModes, - description, - stage, -}: WorkbenchOptions): string { - const stages = [ - ["legacy", "01", "Container class"], - ["bridge", "02", "Bridge release"], - ["direct", "03", "Direct API"], - ] as const; - return ` - - - - - Container API migration workbench - - -
-
Executable migration reference / laboratory 03

Container API
migration workbench

Stage: ${stage}
- -
Current experiment

${description}

Identity held constant
class MigrationWorkbenchbinding MIGRATION_WORKBENCHimage container/Dockerfile
-
-
-
Control path
${availableModes.map((mode, index) => ``).join("")}
-
${[ - ["start", "Start + readiness"], - ["status", "Read state"], - ["echo", "Proxy request"], - ["alternate", "Alternate port"], - ["switch-port", "Switch port"], - ["exec", "Execute process"], - ["outbound", "Intercept outbound"], - ["renew", "Renew activity"], - ["schedule", "Schedule +3 s"], - ["events", "Read event ledger"], - ["stop", "Graceful stop"], - ["destroy", "Destroy"], - ] - .map( - ([action, label]) => - ``, - ) - .join("")}
-
Runtime responseready
Select an operation to begin.
Event ledger loads here.
-
One image · one class name · one namespaceState ledger persists across stage deployments
-
`; -} diff --git a/examples/containers/container-api-migration/stages/1-container-class/src/index.ts b/examples/containers/container-api-migration/stages/1-container-class/src/index.ts deleted file mode 100644 index 8c8e6e4b344..00000000000 --- a/examples/containers/container-api-migration/stages/1-container-class/src/index.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { - Container, - ContainerProxy, - getContainer, - switchPort, - type StopParams, -} from "@cloudflare/containers"; -import { - actionFrom, - json, - readEvents, - recordEvent, - requestForContainer, - routeWorkbench, -} from "../../../shared/lab"; -import { renderWorkbench } from "../../../shared/workbench"; - -interface Env { - MIGRATION_WORKBENCH: DurableObjectNamespace; -} -const STAGE = "legacy" as const; - -export class MigrationWorkbench extends Container { - defaultPort = 8080; - requiredPorts = [8080, 9090]; - sleepAfter = "10m"; - envVars = { LAB_MODE: "helper", LAB_STAGE: STAGE }; - enableInternet = false; - allowedHosts = ["workbench.internal"]; - pingEndpoint = "ping"; - - private record(message: string, detail?: unknown) { - return recordEvent(this.ctx.storage, STAGE, "helper", message, detail); - } - override async onStart() { - await this.record("onStart hook"); - await this.containerFetch("http://container/bootstrap", { method: "POST" }); - } - override async onStop(params: StopParams) { - await this.record("onStop hook", params); - } - override onError(error: unknown): never { - void this.record("onError hook", { - message: error instanceof Error ? error.message : String(error), - }); - throw error; - } - override async onActivityExpired() { - await this.record("onActivityExpired hook"); - await this.stop(); - } - - async scheduledProbe(payload: unknown) { - const response = await this.containerFetch( - requestForContainer("/scheduled", { - body: JSON.stringify(payload), - method: "POST", - }), - ); - await this.record("Container.schedule callback", { - payload, - response: await response.json(), - }); - } - - override async fetch(request: Request): Promise { - const action = actionFrom(request); - this.renewActivityTimeout(); - switch (action) { - case "start": - await this.startAndWaitForPorts({ - ports: this.requiredPorts, - startOptions: { envVars: this.envVars, enableInternet: false }, - }); - await this.record("startAndWaitForPorts", { - ports: this.requiredPorts, - }); - return json({ stage: STAGE, state: await this.getState() }); - case "status": - return json({ stage: STAGE, state: await this.getState() }); - case "echo": - return this.containerFetch( - requestForContainer("/echo?via=containerFetch", { - body: JSON.stringify({ hello: "from the Container class" }), - method: "POST", - }), - ); - case "alternate": - return this.containerFetch( - "http://container/echo?via=containerFetch-port-argument", - {}, - 9090, - ); - case "switch-port": - return super.fetch( - switchPort(requestForContainer("/echo?via=switchPort"), 9090), - ); - case "exec": { - await this.startAndWaitForPorts({ ports: 8080 }); - const process = await this.ctx.container?.exec(["node", "--version"]); - if (!process) - return json({ error: "Container runtime unavailable" }, 503); - const result = await process.output(); - return json({ - exitCode: result.exitCode, - stdout: new TextDecoder().decode(result.stdout).trim(), - }); - } - case "outbound": - return this.containerFetch("http://container/outbound"); - case "renew": - this.renewActivityTimeout(); - await this.record("renewActivityTimeout"); - return json({ renewed: true, sleepAfter: this.sleepAfter }); - case "schedule": { - const schedule = await this.schedule(3, "scheduledProbe", { - createdAt: new Date().toISOString(), - stage: STAGE, - }); - await this.record("Container.schedule created", schedule); - return json(schedule, 202); - } - case "events": - return json(await readEvents(this.ctx.storage)); - case "stop": - await this.stop("SIGTERM"); - await this.record("stop helper called"); - return json({ stopping: true }); - case "destroy": - await this.destroy(); - await this.record("destroy helper called"); - return json({ destroyed: true }); - default: - return json({ error: `Unknown action: ${action}` }, 404); - } - } -} - -MigrationWorkbench.outboundByHost = { - "workbench.internal": (request, _env, context) => - new Response( - JSON.stringify({ - containerId: context.containerId, - interceptedBy: "Container.outboundByHost", - url: request.url, - }), - { - headers: { - "content-type": "application/json", - "x-workbench-intercepted": "true", - }, - }, - ), -}; -export { ContainerProxy }; - -export default { - async fetch(request: Request, env: Env) { - if (new URL(request.url).pathname.startsWith("/api/")) { - const instance = - new URL(request.url).searchParams.get("instance") || "reference"; - return getContainer(env.MIGRATION_WORKBENCH, instance).fetch(request); - } - return routeWorkbench( - request, - env.MIGRATION_WORKBENCH, - { - availableModes: ["helper"], - description: - "The baseline uses Container class routing, readiness, lifecycle hooks, scheduling, state, inactivity, port switching, and outbound interception.", - stage: STAGE, - }, - renderWorkbench, - ); - }, -} satisfies ExportedHandler; diff --git a/examples/containers/container-api-migration/stages/1-container-class/wrangler.jsonc b/examples/containers/container-api-migration/stages/1-container-class/wrangler.jsonc deleted file mode 100644 index 81a5d0d29bc..00000000000 --- a/examples/containers/container-api-migration/stages/1-container-class/wrangler.jsonc +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "../../node_modules/wrangler/config-schema.json", - "name": "container-api-migration-workbench", - "main": "./src/index.ts", - "compatibility_date": "2026-09-28", - "workers_dev": true, - "containers": [ - { - "class_name": "MigrationWorkbench", - "image": "../../container/Dockerfile", - "max_instances": 5, - }, - ], - "durable_objects": { - "bindings": [ - { - "name": "MIGRATION_WORKBENCH", - "class_name": "MigrationWorkbench", - }, - ], - }, - "migrations": [ - { - "tag": "v1", - "new_sqlite_classes": ["MigrationWorkbench"], - }, - ], -} diff --git a/examples/containers/container-api-migration/stages/2-bridge/src/index.ts b/examples/containers/container-api-migration/stages/2-bridge/src/index.ts deleted file mode 100644 index 8e96d151bd0..00000000000 --- a/examples/containers/container-api-migration/stages/2-bridge/src/index.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { - Container, - ContainerProxy, - getContainer, - switchPort, - type StopParams, -} from "@cloudflare/containers"; -import { WorkerEntrypoint } from "cloudflare:workers"; -import { - actionFrom, - json, - modeFrom, - readEvents, - recordEvent, - requestForContainer, - routeWorkbench, - startWithRetry, - waitForPort, - type LabMode, -} from "../../../shared/lab"; -import { renderWorkbench } from "../../../shared/workbench"; - -interface Env { - MIGRATION_WORKBENCH: DurableObjectNamespace; -} -interface OutboundProps { - stage: string; -} -type InspectorFactory = (options: { props: OutboundProps }) => Fetcher; -const STAGE = "bridge" as const; - -export class OutboundInspector extends WorkerEntrypoint { - override fetch(request: Request) { - return new Response( - JSON.stringify({ - interceptedBy: "ctx.container.interceptOutboundHttp", - stage: this.ctx.props.stage, - url: request.url, - }), - { - headers: { - "content-type": "application/json", - "x-workbench-intercepted": "true", - }, - }, - ); - } -} - -export class MigrationWorkbench extends Container { - defaultPort = 8080; - requiredPorts = [8080, 9090]; - sleepAfter = "10m"; - envVars = { LAB_MODE: "helper", LAB_STAGE: STAGE }; - enableInternet = false; - allowedHosts = ["workbench.internal"]; - pingEndpoint = "ping"; - private directMonitorAttached = false; - private directOutboundInstalled = false; - - private runtime(): NonNullable { - if (!this.ctx.container) throw new Error("Container runtime unavailable"); - return this.ctx.container; - } - private record(mode: LabMode | "system", message: string, detail?: unknown) { - return recordEvent(this.ctx.storage, STAGE, mode, message, detail); - } - private async installDirectOutbound() { - if (this.directOutboundInstalled) return; - const exports = this.ctx.exports as unknown as { - OutboundInspector: InspectorFactory; - }; - await this.runtime().interceptOutboundHttp( - "workbench.internal", - exports.OutboundInspector({ props: { stage: STAGE } }), - ); - this.directOutboundInstalled = true; - await this.record("direct", "interceptOutboundHttp installed"); - } - private attachDirectMonitor() { - if (this.directMonitorAttached) return; - this.directMonitorAttached = true; - this.ctx.waitUntil( - this.runtime() - .monitor() - .then(() => this.record("direct", "monitor resolved: container exited")) - .catch((error: unknown) => - this.record("direct", "monitor rejected", { - message: error instanceof Error ? error.message : String(error), - }), - ) - .finally(() => { - this.directMonitorAttached = false; - this.directOutboundInstalled = false; - }), - ); - } - private async ensureDirect() { - const runtime = this.runtime(); - let startAttempts = 0; - if (!runtime.running) { - startAttempts = await startWithRetry(runtime, { - enableInternet: false, - env: { LAB_MODE: "direct", LAB_STAGE: STAGE }, - }); - await this.record("direct", "ctx.container.start", { startAttempts }); - } - const readinessAttempts = await Promise.all([ - waitForPort(runtime, 8080), - waitForPort(runtime, 9090), - ]); - await this.installDirectOutbound(); - await runtime.setInactivityTimeout(10 * 60 * 1000); - this.attachDirectMonitor(); - return { startAttempts, readinessAttempts }; - } - - override async onStart() { - await this.record("helper", "onStart hook"); - await this.containerFetch("http://container/bootstrap", { method: "POST" }); - } - override async onStop(params: StopParams) { - await this.record("helper", "onStop hook", params); - } - override onError(error: unknown): never { - void this.record("helper", "onError hook", { - message: error instanceof Error ? error.message : String(error), - }); - throw error; - } - override async onActivityExpired() { - await this.record("helper", "onActivityExpired hook"); - await this.stop(); - } - async scheduledProbe(payload: unknown) { - const response = await this.containerFetch( - requestForContainer("/scheduled", { - body: JSON.stringify(payload), - method: "POST", - }), - ); - await this.ctx.storage.delete("lab:pending-cutover"); - await this.record("helper", "Container.schedule callback", { - payload, - response: await response.json(), - }); - } - - private async helperAction( - action: string, - request: Request, - ): Promise { - this.renewActivityTimeout(); - switch (action) { - case "start": - await this.startAndWaitForPorts({ - ports: this.requiredPorts, - startOptions: { envVars: this.envVars, enableInternet: false }, - }); - await this.record("helper", "startAndWaitForPorts", { - ports: this.requiredPorts, - }); - return json({ mode: "helper", state: await this.getState() }); - case "status": - return json({ - classState: await this.getState(), - mode: "helper", - runtimeRunning: this.runtime().running, - }); - case "echo": - return this.containerFetch( - requestForContainer("/echo?via=bridge-containerFetch", { - body: JSON.stringify({ hello: "from the bridge helper path" }), - method: "POST", - }), - ); - case "alternate": - return this.containerFetch( - "http://container/echo?via=bridge-containerFetch-port", - {}, - 9090, - ); - case "switch-port": - return super.fetch( - switchPort(requestForContainer("/echo?via=bridge-switchPort"), 9090), - ); - case "exec": { - await this.startAndWaitForPorts({ ports: 8080 }); - const process = await this.runtime().exec(["node", "--version"]); - const result = await process.output(); - return json({ - exitCode: result.exitCode, - stdout: new TextDecoder().decode(result.stdout).trim(), - }); - } - case "outbound": - return this.containerFetch("http://container/outbound"); - case "renew": - this.renewActivityTimeout(); - await this.record("helper", "renewActivityTimeout"); - return json({ mode: "helper", renewed: true }); - case "schedule": - case "schedule-cutover": { - const rawDelay = new URL(request.url).searchParams.get("delay"); - const requested = rawDelay === null ? Number.NaN : Number(rawDelay); - const delay = Number.isFinite(requested) - ? requested - : action === "schedule-cutover" - ? 120 - : 3; - const payload = { - createdAt: new Date().toISOString(), - delay, - stage: STAGE, - }; - await this.ctx.storage.put("lab:pending-cutover", payload); - const schedule = await this.schedule(delay, "scheduledProbe", payload); - await this.record("helper", "Container.schedule created", schedule); - return json({ ...schedule, cutoverMarker: true }, 202); - } - case "stop": - await this.stop("SIGTERM"); - await this.record("helper", "stop helper called"); - return json({ mode: "helper", stopping: true }); - case "destroy": - await this.destroy(); - await this.record("helper", "destroy helper called"); - return json({ destroyed: true, mode: "helper" }); - default: - return json({ error: `Unknown helper action: ${action}` }, 404); - } - } - - private async directAction(action: string): Promise { - const runtime = this.runtime(); - switch (action) { - case "start": - return json({ - mode: "direct", - running: runtime.running, - ...(await this.ensureDirect()), - }); - case "status": - return json({ - classState: await this.getState(), - mode: "direct", - runtimeRunning: runtime.running, - }); - case "echo": - await this.ensureDirect(); - return runtime.getTcpPort(8080).fetch( - requestForContainer("/echo?via=ctx.container", { - body: JSON.stringify({ hello: "from the bridge direct path" }), - method: "POST", - }), - ); - case "alternate": - case "switch-port": - await this.ensureDirect(); - return runtime - .getTcpPort(9090) - .fetch(`http://container/echo?via=ctx.container-${action}`); - case "exec": { - await this.ensureDirect(); - const process = await runtime.exec(["node", "--version"]); - const result = await process.output(); - return json({ - exitCode: result.exitCode, - stdout: new TextDecoder().decode(result.stdout).trim(), - }); - } - case "outbound": - await this.ensureDirect(); - return runtime.getTcpPort(8080).fetch("http://container/outbound"); - case "renew": - await runtime.setInactivityTimeout(10 * 60 * 1000); - await this.record("direct", "setInactivityTimeout renewed"); - return json({ mode: "direct", renewed: true }); - case "schedule": - return json( - { - error: - "The Container class owns alarm() during the bridge stage. Migrate scheduling during the final cutover.", - }, - 409, - ); - case "stop": - runtime.signal(15); - await this.record("direct", "signal(15) called"); - return json({ mode: "direct", stopping: true }); - case "destroy": - await runtime.destroy("Bridge direct path destroy"); - await this.record("direct", "destroy called"); - return json({ destroyed: true, mode: "direct" }); - default: - return json({ error: `Unknown direct action: ${action}` }, 404); - } - } - - override async fetch(request: Request) { - const action = actionFrom(request); - if (action === "events") return json(await readEvents(this.ctx.storage)); - return modeFrom(request, "helper") === "direct" - ? this.directAction(action) - : this.helperAction(action, request); - } -} - -MigrationWorkbench.outboundByHost = { - "workbench.internal": (request, _env, context) => - new Response( - JSON.stringify({ - containerId: context.containerId, - interceptedBy: "Container.outboundByHost", - url: request.url, - }), - { - headers: { - "content-type": "application/json", - "x-workbench-intercepted": "true", - }, - }, - ), -}; -export { ContainerProxy }; -export default { - async fetch(request: Request, env: Env) { - if (new URL(request.url).pathname.startsWith("/api/")) { - const instance = - new URL(request.url).searchParams.get("instance") || "reference"; - return getContainer(env.MIGRATION_WORKBENCH, instance).fetch(request); - } - return routeWorkbench( - request, - env.MIGRATION_WORKBENCH, - { - availableModes: ["helper", "direct"], - description: - "Both routes control the same Durable Object and container instance. Runtime calls migrate first; alarm ownership remains with the Container class until cutover.", - stage: STAGE, - }, - renderWorkbench, - ); - }, -} satisfies ExportedHandler; diff --git a/examples/containers/container-api-migration/stages/2-bridge/wrangler.jsonc b/examples/containers/container-api-migration/stages/2-bridge/wrangler.jsonc deleted file mode 100644 index 81a5d0d29bc..00000000000 --- a/examples/containers/container-api-migration/stages/2-bridge/wrangler.jsonc +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "../../node_modules/wrangler/config-schema.json", - "name": "container-api-migration-workbench", - "main": "./src/index.ts", - "compatibility_date": "2026-09-28", - "workers_dev": true, - "containers": [ - { - "class_name": "MigrationWorkbench", - "image": "../../container/Dockerfile", - "max_instances": 5, - }, - ], - "durable_objects": { - "bindings": [ - { - "name": "MIGRATION_WORKBENCH", - "class_name": "MigrationWorkbench", - }, - ], - }, - "migrations": [ - { - "tag": "v1", - "new_sqlite_classes": ["MigrationWorkbench"], - }, - ], -} diff --git a/examples/containers/container-api-migration/stages/3-durable-object-api/src/index.ts b/examples/containers/container-api-migration/stages/3-durable-object-api/src/index.ts deleted file mode 100644 index ef4597aef8b..00000000000 --- a/examples/containers/container-api-migration/stages/3-durable-object-api/src/index.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { DurableObject, WorkerEntrypoint } from "cloudflare:workers"; -import { - actionFrom, - json, - readEvents, - recordEvent, - requestForContainer, - routeWorkbench, - startWithRetry, - waitForPort, -} from "../../../shared/lab"; -import { renderWorkbench } from "../../../shared/workbench"; - -interface Env { - MIGRATION_WORKBENCH: DurableObjectNamespace; -} -interface OutboundProps { - stage: string; -} -interface ScheduledPayload { - createdAt: string; - delay: number; - stage: string; -} -type InspectorFactory = (options: { props: OutboundProps }) => Fetcher; -const STAGE = "direct" as const; -const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; - -export class OutboundInspector extends WorkerEntrypoint { - override fetch(request: Request) { - return new Response( - JSON.stringify({ - interceptedBy: "ctx.container.interceptOutboundHttp", - stage: this.ctx.props.stage, - url: request.url, - }), - { - headers: { - "content-type": "application/json", - "x-workbench-intercepted": "true", - }, - }, - ); - } -} - -export class MigrationWorkbench extends DurableObject { - private monitorAttached = false; - private outboundInstalled = false; - private runtime(): NonNullable { - if (!this.ctx.container) throw new Error("Container runtime unavailable"); - return this.ctx.container; - } - private record(message: string, detail?: unknown) { - return recordEvent(this.ctx.storage, STAGE, "direct", message, detail); - } - private async installOutbound() { - if (this.outboundInstalled) return; - const exports = this.ctx.exports as unknown as { - OutboundInspector: InspectorFactory; - }; - await this.runtime().interceptOutboundHttp( - "workbench.internal", - exports.OutboundInspector({ props: { stage: STAGE } }), - ); - this.outboundInstalled = true; - await this.record("interceptOutboundHttp installed"); - } - private attachMonitor() { - if (this.monitorAttached) return; - this.monitorAttached = true; - this.ctx.waitUntil( - this.runtime() - .monitor() - .then(() => this.record("monitor resolved: container exited")) - .catch((error: unknown) => - this.record("monitor rejected", { - message: error instanceof Error ? error.message : String(error), - }), - ) - .finally(() => { - this.monitorAttached = false; - this.outboundInstalled = false; - }), - ); - } - private async ensureContainer() { - const runtime = this.runtime(); - let startAttempts = 0; - if (!runtime.running) { - startAttempts = await startWithRetry(runtime, { - enableInternet: false, - env: { LAB_MODE: "direct", LAB_STAGE: STAGE }, - }); - await this.record("ctx.container.start", { startAttempts }); - } - const readinessAttempts = await Promise.all([ - waitForPort(runtime, 8080), - waitForPort(runtime, 9090), - ]); - await this.installOutbound(); - await runtime.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); - this.attachMonitor(); - return { startAttempts, readinessAttempts }; - } - - async alarm() { - const pending = await this.ctx.storage.get( - "lab:pending-cutover", - ); - if (!pending) { - await this.record("alarm fired without a workbench marker"); - return; - } - await this.ensureContainer(); - const response = await this.runtime() - .getTcpPort(8080) - .fetch( - requestForContainer("/scheduled", { - body: JSON.stringify(pending), - method: "POST", - }), - ); - await this.ctx.storage.delete("lab:pending-cutover"); - await this.record("Durable Object alarm handled scheduled work", { - payload: pending, - response: await response.json(), - }); - } - - async fetch(request: Request): Promise { - const action = actionFrom(request), - runtime = this.runtime(); - switch (action) { - case "start": - return json({ - running: runtime.running, - stage: STAGE, - ...(await this.ensureContainer()), - }); - case "status": - return json({ - alarm: await this.ctx.storage.getAlarm(), - pendingCutover: await this.ctx.storage.get("lab:pending-cutover"), - running: runtime.running, - stage: STAGE, - }); - case "echo": - await this.ensureContainer(); - return runtime.getTcpPort(8080).fetch( - requestForContainer("/echo?via=ctx.container", { - body: JSON.stringify({ hello: "from the direct Durable Object" }), - method: "POST", - }), - ); - case "alternate": - case "switch-port": - await this.ensureContainer(); - return runtime - .getTcpPort(9090) - .fetch(`http://container/echo?via=ctx.container-${action}`); - case "exec": { - await this.ensureContainer(); - const process = await runtime.exec(["node", "--version"]); - const result = await process.output(); - return json({ - exitCode: result.exitCode, - stdout: new TextDecoder().decode(result.stdout).trim(), - }); - } - case "outbound": - await this.ensureContainer(); - return runtime.getTcpPort(8080).fetch("http://container/outbound"); - case "renew": - await runtime.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); - await this.record("setInactivityTimeout renewed"); - return json({ renewed: true, timeoutMs: INACTIVITY_TIMEOUT_MS }); - case "schedule": - case "schedule-cutover": { - const rawDelay = new URL(request.url).searchParams.get("delay"); - const requested = rawDelay === null ? Number.NaN : Number(rawDelay); - const delay = Number.isFinite(requested) ? requested : 3; - const payload = { - createdAt: new Date().toISOString(), - delay, - stage: STAGE, - }; - await this.ctx.storage.put("lab:pending-cutover", payload); - await this.ctx.storage.setAlarm(Date.now() + delay * 1000); - await this.record("Durable Object alarm scheduled", payload); - return json({ payload, scheduled: true }, 202); - } - case "events": - return json(await readEvents(this.ctx.storage)); - case "stop": - runtime.signal(15); - await this.record("signal(15) called"); - return json({ stopping: true }); - case "destroy": - await runtime.destroy("Direct API workbench destroy"); - await this.record("destroy called"); - return json({ destroyed: true }); - default: - return json({ error: `Unknown action: ${action}` }, 404); - } - } -} - -export default { - async fetch(request: Request, env: Env) { - return routeWorkbench( - request, - env.MIGRATION_WORKBENCH, - { - availableModes: ["direct"], - description: - "The exported class and Durable Object namespace are unchanged. Runtime lifecycle, routing, monitoring, inactivity, outbound interception, and alarms now use platform APIs directly.", - stage: STAGE, - }, - renderWorkbench, - ); - }, -} satisfies ExportedHandler; diff --git a/examples/containers/container-api-migration/stages/3-durable-object-api/wrangler.jsonc b/examples/containers/container-api-migration/stages/3-durable-object-api/wrangler.jsonc deleted file mode 100644 index 81a5d0d29bc..00000000000 --- a/examples/containers/container-api-migration/stages/3-durable-object-api/wrangler.jsonc +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "../../node_modules/wrangler/config-schema.json", - "name": "container-api-migration-workbench", - "main": "./src/index.ts", - "compatibility_date": "2026-09-28", - "workers_dev": true, - "containers": [ - { - "class_name": "MigrationWorkbench", - "image": "../../container/Dockerfile", - "max_instances": 5, - }, - ], - "durable_objects": { - "bindings": [ - { - "name": "MIGRATION_WORKBENCH", - "class_name": "MigrationWorkbench", - }, - ], - }, - "migrations": [ - { - "tag": "v1", - "new_sqlite_classes": ["MigrationWorkbench"], - }, - ], -} diff --git a/examples/containers/container-api-migration/tsconfig.json b/examples/containers/container-api-migration/tsconfig.json deleted file mode 100644 index b02224e0f23..00000000000 --- a/examples/containers/container-api-migration/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "allowJs": false, - "lib": ["ES2024"], - "module": "ESNext", - "moduleResolution": "Bundler", - "noEmit": true, - "skipLibCheck": true, - "strict": true, - "target": "ES2024", - "types": ["@cloudflare/workers-types"] - }, - "include": ["shared/**/*.ts", "stages/**/*.ts"] -} diff --git a/tsconfig.json b/tsconfig.json index 9eadb6219ae..2251765572f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,10 +11,5 @@ "jsxImportSource": "react" }, "include": [".astro/types.d.ts", "**/*"], - "exclude": [ - "dist", - "worker", - ".flue", - "examples/containers/container-api-migration" - ] + "exclude": ["dist", "worker", ".flue"] }