Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 145 additions & 21 deletions src/content/docs/containers/examples/container-backend.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,31 @@ 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.

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

<WranglerConfig>
```json
{
"name": "cron-container",
"name": "static-frontend-container-backend",
"main": "src/index.ts",
"compatibility_date": "$today",
"observability": {
"enabled": true
},
"assets": {
"directory": "./dist",
"binding": "ASSETS"
Expand All @@ -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"
}
]
}
}
```
</WranglerConfig>
Expand Down Expand Up @@ -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.

<Tabs>
<TabItem label="Durable Object Container API">

<TypeScriptExample filename="src/index.ts" omitTabs={true}>
```ts
import { DurableObject } from "cloudflare:workers";

const INSTANCE_COUNT = 3;

interface Env {
ASSETS: Fetcher;
BACKEND: DurableObjectNamespace<Backend>;
}

export class Backend extends DurableObject<Env> {
private ready: Promise<void> | undefined;

async fetch(request: Request): Promise<Response> {
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<void> {
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<Response> {
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);
},
};
```
</TypeScriptExample>

</TabItem>
<TabItem label="Container class">

<TypeScriptExample filename="src/index.ts" omitTabs={true}>
```ts
import { Container, getRandom } from "@cloudflare/containers";

const INSTANCE_COUNT = 3;

class Backend extends Container {
interface Env {
ASSETS: Fetcher;
BACKEND: DurableObjectNamespace<Backend>;
}

export class Backend extends Container<Env> {
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<Response> {
const url = new URL(request.url);
if (url.pathname.startsWith("/api")) {
const containerInstance = await getRandom(env.BACKEND, INSTANCE_COUNT);
Expand All @@ -160,15 +261,17 @@ export default {
},
};
```
</TypeScriptExample>

:::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).

</TabItem>
</Tabs>

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
Expand All @@ -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"},
Expand All @@ -202,10 +305,31 @@ 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))
}

```

</Details>

The health endpoint lets the Worker wait for the backend before forwarding requests. Build the backend image with this Dockerfile in the project root:

<Details header="Dockerfile">

```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"]
```

</Details>
106 changes: 83 additions & 23 deletions src/content/docs/containers/examples/cron.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

<WranglerConfig>

```json
```jsonc
{
"name": "cron-container",
"main": "src/index.ts",
"compatibility_date": "$today",
"observability": {
"enabled": true
},
"triggers": {
"crons": [
"*/2 * * * *" // Run every 2 minutes
Expand All @@ -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"
}
]
}
}
```

</WranglerConfig>

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.

<Tabs>
<TabItem label="Durable Object Container API">

<TypeScriptExample filename="src/index.ts">
```ts
import { DurableObject } from "cloudflare:workers";

interface Env {
CRON_CONTAINER: DurableObjectNamespace<CronContainer>;
}

export class CronContainer extends DurableObject<Env> {
private currentRun: Promise<void> | undefined;

run(startTime: string): Promise<void> {
this.currentRun ??= this.runOnce(startTime).finally(() => {
this.currentRun = undefined;
});
return this.currentRun;
}

private async runOnce(startTime: string): Promise<void> {
Comment thread
thomasgauvin marked this conversation as resolved.
const container = this.ctx.container!;
if (!container.running) {
console.log("Starting scheduled task:", startTime);
container.start();
}
await container.monitor();
}
}

export default {
async fetch(): Promise<Response> {
return new Response("This Worker runs a scheduled Container task.");
},

async scheduled(controller: ScheduledController, env: Env): Promise<void> {
await env.CRON_CONTAINER
.getByName("cron")
.run(new Date(controller.scheduledTime).toISOString());
},
};
```
</TypeScriptExample>

</TabItem>
<TabItem label="Container class">

<TypeScriptExample filename="src/index.ts">
```ts
import { Container, getContainer } from '@cloudflare/containers';
import { Container, getContainer } from "@cloudflare/containers";

export class CronContainer extends Container {
sleepAfter = '10s';
Expand All @@ -72,17 +123,26 @@ export class CronContainer extends Container {
}

export default {
async fetch(): Promise<Response> {
return new Response("This Worker runs a cron job to execute a container on a schedule.");
},

async scheduled(_controller: any, env: { CRON_CONTAINER: DurableObjectNamespace<CronContainer> }) {
let container = getContainer(env.CRON_CONTAINER);
await container.start({
envVars: {
MESSAGE: "Start Time: " + new Date().toISOString(),
}
})
},
async fetch(): Promise<Response> {
return new Response("This Worker runs a cron job to execute a container on a schedule.");
},

async scheduled(
controller: ScheduledController,
env: { CRON_CONTAINER: DurableObjectNamespace<CronContainer> },
): Promise<void> {
const container = getContainer(env.CRON_CONTAINER);
await container.start({
envVars: {
MESSAGE: `Start Time: ${new Date(controller.scheduledTime).toISOString()}`,
},
});
},
};
```
</TypeScriptExample>

For a full Container class example, see the [Cron Container Template](https://github.com/mikenomitch/cron-container/tree/main).

</TabItem>
</Tabs>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading