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
97 changes: 69 additions & 28 deletions containers-template/README.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,100 @@
# Containers Starter

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/templates/tree/main/containers-template)

# Containers Starter

![Containers Template Preview](https://imagedelivery.net/_yJ02hpOMj_EnGvsU2aygw/5aba1fb7-b937-46fd-fa67-138221082200/public)

<!-- dash-content-start -->

This is a [Container](https://developers.cloudflare.com/containers/) starter template.
Run a Go HTTP server in [Cloudflare Containers](https://developers.cloudflare.com/containers/), with a Worker routing requests to named instances, a single shared instance, or a pool of three instances.

It demonstrates basic Container configuration, launching and routing to individual container, load balancing over multiple container, running basic hooks on container status changes.
The template uses the `durable_object` scheduling policy. Each Durable Object selects its container image and size, passes environment variables at startup, waits for HTTP readiness, and monitors the container's exit. An intentional-failure route demonstrates error handling.

<!-- dash-content-end -->

Outside of this repo, you can start a new project with this template using [C3](https://developers.cloudflare.com/pages/get-started/c3/) (the `create-cloudflare` CLI):
## Get started

Install Node.js and start a Docker-compatible engine before running this template locally. The template pins Wrangler 4.136.1; local development with the `durable_object` policy requires Wrangler 4.136.0 or later.

Create a project with [C3](https://developers.cloudflare.com/workers/get-started/guide/):

```bash
npm create cloudflare@latest -- --template=cloudflare/templates/containers-template
```

## Getting Started

First, run:
From your new project directory, install dependencies and start the development server:

```bash
npm install
# or
yarn install
# or
pnpm install
# or
bun install
npm run dev
```

Then run the development server (using the package manager of your choice):
Open [http://localhost:8787](http://localhost:8787) to see the available endpoints. The first container request builds and starts the Go server, so it can take longer than subsequent requests.

## Try the routes

| Route | Behavior |
| ---------------- | --------------------------------------------------------------------------------------------------- |
| `/` | List the available endpoints without starting a container. |
| `/container/one` | Start or reuse the named instance `one`. Change the final path segment to use a different instance. |
| `/singleton` | Route every request to the same shared instance. |
| `/lb` | Randomly select one of three named instances. This is a fixed pool. |
| `/error` | Stop a dedicated test instance with exit code 1. The Worker returns HTTP 502 and logs the failure. |

```bash
npm run dev
curl http://localhost:8787/container/one
curl http://localhost:8787/container/two
curl http://localhost:8787/singleton
curl http://localhost:8787/lb
curl -i http://localhost:8787/error
```

Open [http://localhost:8787](http://localhost:8787) with your browser to see the result.
Successful container responses include the startup message and Durable Object ID. The Worker passes its ID through `INSTANCE_ID` so the output identifies each instance in both local development and deployed applications. Repeated requests for the same name reach the same Durable Object. A stopped container starts again on the next request; its local filesystem is not persistent application storage.

The `/lb` pool has three names, but `/container/<ID>` can start additional instances. Add authentication and application-specific limits before exposing arbitrary instance creation to users. Running containers count toward your [account limits](https://developers.cloudflare.com/containers/platform/limits/#account-limits).

## How it works

`wrangler.jsonc` associates the `MyContainer` Durable Object with the `durable_object` policy. The named image `base` is built from `Dockerfile` and exposed as `ctx.container.images.base`. The `exports` entry declares the class with SQLite storage.

You can start editing your Worker by modifying `src/index.ts` and you can start
editing your Container by editing the content of `container_src`.
In `src/index.ts`, `MyContainer` extends `DurableObject` from `cloudflare:workers`. Its `fetch()` method:

## Deploying To Production
1. Starts the configured image with the `lite` instance size, outbound Internet access turned off, and `MESSAGE` and `INSTANCE_ID` environment variables.
2. Waits for a successful response from `/health` on port 8080. Concurrent requests share this readiness check.
3. Forwards the incoming request to `ctx.container.getTcpPort(8080)`. Application requests are not automatically retried.

| Command | Action |
| :--------------- | :------------------------------------ |
| `npm run deploy` | Deploy your application to Cloudflare |
`monitor()` logs successful exits and failures. The Durable Object restores monitoring and its two-minute inactivity timeout when it restarts with an existing container. A pending monitor can keep the Durable Object active for up to 15 minutes, so the inactivity timeout is **not** a two-minute deadline from the last HTTP request. See the [Container API lifecycle methods](https://developers.cloudflare.com/containers/api/durable-object-container/#monitor).

## Learn More
Edit `container_src/main.go` to change the Go server and `src/index.ts` to change routing or startup options. Set runtime environment variables in `start({ env: { ... } })`. Keep secrets out of source code; use [Worker secrets](https://developers.cloudflare.com/workers/configuration/secrets/) when needed.

## Check your changes

```bash
npm run cf-typegen
npm run check
npm test
```

The unit tests run the Worker code with mocked container boundaries, without Docker. To test the actual image, use `npm run dev` and the routes above. If Go is installed locally, run its handler and process-exit tests too:

```bash
cd container_src
go test ./...
```

## Deploy

With Docker running, deploy the Worker and its named image:

```bash
npm run deploy
```

To learn more about Containers, take a look at the following resources:
Use the deployed Worker URL to try the same routes. Container instances start when they receive requests. Updating the named image does not restart existing instances; running instances keep their startup image until they stop. See [image updates](https://developers.cloudflare.com/containers/guides/image-management/#roll-out-a-named-image-update).

- [Container Documentation](https://developers.cloudflare.com/containers/) - learn about Containers
- [Container Class](https://github.com/cloudflare/containers) - learn about the Container helper class
## Learn more

Your feedback and contributions are welcome!
- [Containers examples](https://developers.cloudflare.com/containers/examples/)
- [Durable Object Container API](https://developers.cloudflare.com/containers/api/durable-object-container/)
- [Scheduling policy](https://developers.cloudflare.com/containers/configuration/scheduling-policy/#use-the-durable-object-scheduling-policy)
- [Local development](https://developers.cloudflare.com/containers/guides/local-dev/)
26 changes: 18 additions & 8 deletions containers-template/container_src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,38 @@ import (

func handler(w http.ResponseWriter, r *http.Request) {
message := os.Getenv("MESSAGE")
instanceId := os.Getenv("CLOUDFLARE_DURABLE_OBJECT_ID")
instanceId := os.Getenv("INSTANCE_ID")
fmt.Fprintf(w, "Hi, I'm a container and this is my message: \"%s\", my instance ID is: %s", message, instanceId)

}

func errorHandler(w http.ResponseWriter, r *http.Request) {
panic("This is a panic")
// net/http recovers handler panics. Exit explicitly to demonstrate a
// container failure that the Durable Object observes with monitor().
log.Println("Exiting with code 1 for the /error demonstration")
os.Exit(1)
}

func main() {
// Listen for SIGINT and SIGTERM
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

func newRouter() http.Handler {
router := http.NewServeMux()
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
router.HandleFunc("/", handler)
router.HandleFunc("/container", handler)
router.HandleFunc("/error", errorHandler)

return router
}

func main() {
// Listen for SIGINT and SIGTERM
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

server := &http.Server{
Addr: ":8080",
Handler: router,
Handler: newRouter(),
}

go func() {
Expand Down
44 changes: 44 additions & 0 deletions containers-template/container_src/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"net/http"
"net/http/httptest"
"os"
"os/exec"
"strings"
"testing"
)

func TestHealth(t *testing.T) {
w := httptest.NewRecorder()
newRouter().ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/health", nil))
if w.Code != http.StatusOK || w.Body.Len() != 0 {
t.Fatalf("health: status=%d body=%q", w.Code, w.Body.String())
}
}

func TestMessageAndInstance(t *testing.T) {
t.Setenv("MESSAGE", "hello from startup")
t.Setenv("INSTANCE_ID", "test-instance")
for _, path := range []string{"/container/one", "/lb", "/singleton"} {
w := httptest.NewRecorder()
newRouter().ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "hello from startup") || !strings.Contains(w.Body.String(), "test-instance") {
t.Fatalf("%s: status=%d body=%q", path, w.Code, w.Body.String())
}
}
}

func TestErrorExitsProcess(t *testing.T) {
if os.Getenv("TEST_CONTAINER_ERROR_EXIT") == "1" {
newRouter().ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/error", nil))
return
}
command := exec.Command(os.Args[0], "-test.run=^TestErrorExitsProcess$")
command.Env = append(os.Environ(), "TEST_CONTAINER_ERROR_EXIT=1")
output, err := command.CombinedOutput()
exit, ok := err.(*exec.ExitError)
if !ok || exit.ExitCode() != 1 || !strings.Contains(string(output), "Exiting with code 1") {
t.Fatalf("expected exit 1 from error route; err=%v output=%s", err, output)
}
}
Loading
Loading