diff --git a/src/content/docs/containers/examples/container-backend.mdx b/src/content/docs/containers/examples/container-backend.mdx
index fc458f7461a..3062758c36c 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 } 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.
@@ -18,15 +24,17 @@ then pass backend requests to a containerized backend application.
In this example, we'll show an example using a simple `index.html` file served as a static asset,
but you can select from one of many frontend frameworks. See our [Workers framework examples](/workers/framework-guides/web-apps/) for more information.
-For a full example, see the [Static Frontend + Container Backend Template](https://github.com/mikenomitch/static-frontend-container-backend).
-
## Configure Static Assets and a Container
```json
{
- "name": "cron-container",
+ "name": "static-frontend-container-backend",
"main": "src/index.ts",
+ "compatibility_date": "$today",
+ "observability": {
+ "enabled": true
+ },
"assets": {
"directory": "./dist",
"binding": "ASSETS"
@@ -46,14 +54,12 @@ For a full example, see the [Static Frontend + Container Backend Template](https
}
]
},
- "migrations": [
- {
- "new_sqlite_classes": [
- "Backend"
- ],
- "tag": "v1"
+ "exports": {
+ "Backend": {
+ "type": "durable-object",
+ "storage": "sqlite"
}
- ]
+ }
}
```
@@ -138,18 +144,113 @@ 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.
-```javascript
+Because `container.start()` does not wait for readiness, this example polls `GET /health` before forwarding requests.
+
+
+
+
+
+```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;
+
+ 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!;
+ await container.setInactivityTimeout(2 * 60 * 60 * 1000);
+ if (!container.running) {
+ container.start();
+ }
+ this.ctx.waitUntil(
+ container
+ .monitor()
+ .then(() => console.log("Backend container exited"))
+ .catch((error: unknown) =>
+ console.error("Backend container errored:", error),
+ ),
+ );
+
+ 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
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);
@@ -160,15 +261,17 @@ export default {
},
};
```
+
-:::note
-This example uses `getRandom`, which randomly selects one of a fixed number of Container
-instances for each request.
+For a full Container class example, see the [Static Frontend + Container Backend Template](https://github.com/mikenomitch/static-frontend-container-backend).
+
+
+
-In the future, we will provide improved latency-aware load balancing and autoscaling.
+:::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 will make scaling stateless instances simple and routing more efficient. See the
-[autoscaling documentation](/containers/configuration/scaling-and-routing) for more details.
+See the [autoscaling documentation](/containers/configuration/scaling-and-routing) for more details about scaling stateless instances and routing.
:::
## Define a backend container
@@ -188,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"},
@@ -202,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))
}
@@ -209,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 beb5c71d036..61567becda1 100644
--- a/src/content/docs/containers/examples/cron.mdx
+++ b/src/content/docs/containers/examples/cron.mdx
@@ -10,20 +10,22 @@ 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/).
-For a full example, see the [Cron Container Template](https://github.com/mikenomitch/cron-container/tree/main).
-
Use a cron expression in your Wrangler config to specify the schedule:
-```json
+```jsonc
{
"name": "cron-container",
"main": "src/index.ts",
+ "compatibility_date": "$today",
+ "observability": {
+ "enabled": true
+ },
"triggers": {
"crons": [
"*/2 * * * *" // Run every 2 minutes
@@ -43,21 +45,70 @@ Use a cron expression in your Wrangler config to specify the schedule:
}
]
},
- "migrations": [
- {
- "new_sqlite_classes": ["CronContainer"],
- "tag": "v1"
+ "exports": {
+ "CronContainer": {
+ "type": "durable-object",
+ "storage": "sqlite"
}
- ]
+ }
}
```
-Then in your Worker, call your Container from the "scheduled" handler:
+Call the Container from the Worker's `scheduled()` handler. The container image runs the task on startup and exits when finished. Await `monitor()` to observe completion or failure.
+
+
+
+
+
+```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!;
+ if (!container.running) {
+ console.log("Starting scheduled task:", startTime);
+ container.start();
+ }
+ await container.monitor();
+ }
+}
+
+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(controller.scheduledTime).toISOString());
+ },
+};
+```
+
+
+
+
+
```ts
-import { Container, getContainer } from '@cloudflare/containers';
+import { Container, getContainer } from "@cloudflare/containers";
export class CronContainer extends Container {
sleepAfter = '10s';
@@ -72,17 +123,26 @@ 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: {
- MESSAGE: "Start Time: " + new Date().toISOString(),
- }
- })
- },
+ 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 },
+ ): Promise {
+ const container = getContainer(env.CRON_CONTAINER);
+ await container.start({
+ envVars: {
+ MESSAGE: `Start Time: ${new Date(controller.scheduledTime).toISOString()}`,
+ },
+ });
+ },
};
```
+
+
+For a full Container class example, see the [Cron Container Template](https://github.com/mikenomitch/cron-container/tree/main).
+
+
+
diff --git a/src/content/docs/containers/examples/durable-object-interface.mdx b/src/content/docs/containers/examples/durable-object-interface.mdx
index f957451fa12..2b3c2663a36 100644
--- a/src/content/docs/containers/examples/durable-object-interface.mdx
+++ b/src/content/docs/containers/examples/durable-object-interface.mdx
@@ -2,7 +2,7 @@
summary: Various examples calling Containers directly from Durable Objects
pcx_content_type: example
-title: Using Durable Objects Directly
+title: More examples
external_link: https://github.com/cloudflare/containers-demos
sidebar:
order: 10
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 4dfa0e739bf..56e96741ab1 100644
--- a/src/content/docs/containers/examples/env-vars-and-secrets.mdx
+++ b/src/content/docs/containers/examples/env-vars-and-secrets.mdx
@@ -81,6 +81,9 @@ in Wrangler configuration.
```json
{
"name": "my-container-worker",
+ "observability": {
+ "enabled": true
+ },
"vars": {
"ENV_VAR": "my-env-var"
},
diff --git a/src/content/docs/containers/examples/stateless.mdx b/src/content/docs/containers/examples/stateless.mdx
index 8cf6275a0df..285fc1815ca 100644
--- a/src/content/docs/containers/examples/stateless.mdx
+++ b/src/content/docs/containers/examples/stateless.mdx
@@ -10,14 +10,97 @@ 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.
+
+`container.start()` returns before the container is ready. The Durable Object Container API example polls `GET /health` on port `8080` before forwarding requests. The container must return a successful response only when it is ready to receive traffic.
+
+
+
+
+
+```ts
+import { DurableObject } from "cloudflare:workers";
+
+const INSTANCE_COUNT = 3;
+
+interface Env {
+ BACKEND: DurableObjectNamespace;
+}
+
+export class Backend extends DurableObject {
+ private ready: Promise | undefined;
+
+ 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!;
+ 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 < 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;
-class Backend extends Container {
+interface Env {
+ BACKEND: DurableObjectNamespace;
+}
+
+export class Backend extends Container {
defaultPort = 8080;
sleepAfter = "2h";
}
@@ -29,13 +112,13 @@ export default {
},
};
```
+
-:::note
-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.
+:::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 will make scaling stateless instances simple and routing more efficient. See the
-[autoscaling documentation](/containers/configuration/scaling-and-routing) for more details.
+See the [autoscaling documentation](/containers/configuration/scaling-and-routing/) for more details about scaling stateless instances and routing.
:::
diff --git a/src/content/docs/containers/examples/status-hooks.mdx b/src/content/docs/containers/examples/status-hooks.mdx
index 3d38fcd9e2b..27179779ded 100644
--- a/src/content/docs/containers/examples/status-hooks.mdx
+++ b/src/content/docs/containers/examples/status-hooks.mdx
@@ -11,11 +11,86 @@ 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/api/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.
+
+The direct API example requires the Container to expose `GET /health` on port `4000`. The endpoint must return a successful response when the application is ready. Because `container.start()` does not wait for readiness, the Worker polls this endpoint before forwarding requests.
+
+
+
+
+
+```ts
+import { DurableObject } from "cloudflare:workers";
+
+interface Env {}
+
+export class MyContainer extends DurableObject {
+ private ready: Promise | undefined;
+
+ 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!;
+ await container.setInactivityTimeout(5 * 60 * 1000);
+ 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 } from "@cloudflare/containers";
+import { Container, type StopParams } from "@cloudflare/containers";
export class MyContainer extends Container {
defaultPort = 4000;
@@ -25,7 +100,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 {
@@ -40,8 +115,14 @@ export class MyContainer extends Container {
await this.stop();
}
- override onError(error: string) {
+ override onError(error: unknown) {
console.log("Container error:", error);
}
}
```
+
+
+
+
+
+The `monitor()` promise resolves without a value when the container exits successfully. For nonzero exits, it rejects with an error containing the exit code. The `setInactivityTimeout()` method does not invoke a callback when the timeout expires. Use the [`Container` class lifecycle hooks](/containers/api/container-class/#lifecycle-hooks) for structured stop parameters and an inactivity callback.
diff --git a/src/content/docs/containers/examples/websocket.mdx b/src/content/docs/containers/examples/websocket.mdx
index 66a5968d448..71d3619472b 100644
--- a/src/content/docs/containers/examples/websocket.mdx
+++ b/src/content/docs/containers/examples/websocket.mdx
@@ -10,24 +10,108 @@ 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";
-```js
+Forward an incoming WebSocket upgrade request through the Durable Object to the listening port on the Container.
+
+Because `container.start()` does not wait for readiness, this example polls `GET /health` before forwarding requests. 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;
+
+ 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!;
+ await container.setInactivityTimeout(2 * 60 * 1000);
+ 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
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);
},
};
```
+
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 */}