| 1 | --- |
| 2 | title: Containers |
| 3 | description: Cloudflare Containers run long-lived processes beside a Durable Object — declare a typed container class, implement its runtime in a separate file, and alchemy builds the image, pushes it, and wires the DO pairing. |
| 4 | --- |
| 5 | |
| 6 | A Cloudflare Container is a long-lived process running next to a |
| 7 | [Durable Object](/cloudflare/compute/durable-objects): the DO owns the |
| 8 | container's lifecycle, and callers reach the container through it. |
| 9 | In alchemy a container is a class with a typed RPC surface — the |
| 10 | same tagged-shape ceremony as a Durable Object — plus a runtime |
| 11 | implementation that alchemy bundles into a Docker image and pushes |
| 12 | to Cloudflare's registry for you. |
| 13 | |
| 14 | Reach for a container when a [Worker](/cloudflare/compute/workers) isn't |
| 15 | enough: you need a real OS process — a sandboxed shell, a binary you |
| 16 | can't compile to wasm, an HTTP server listening on a port, a |
| 17 | runtime Workers doesn't offer. If you only need per-entity state |
| 18 | and coordination, a plain Durable Object is lighter; if you only |
| 19 | need request/response compute, a Worker is. |
| 20 | |
| 21 | ## Declare the container class |
| 22 | |
| 23 | The class declares the container's name and its public RPC surface; |
| 24 | configuration lives on `.make()`, not here: |
| 25 | |
| 26 | ```typescript |
| 27 | // src/Sandbox.ts |
| 28 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 29 | import type * as Effect from "effect/Effect"; |
| 30 | |
| 31 | export class Sandbox extends Cloudflare.Container< |
| 32 | Sandbox, |
| 33 | { ping: () => Effect.Effect<string> } |
| 34 | >()("Sandbox") {} |
| 35 | ``` |
| 36 | |
| 37 | Anything that binds `Sandbox` sees `ping()` as a typed RPC method |
| 38 | returning `Effect<string>` — no schemas, no serialization |
| 39 | boilerplate, exactly like DO methods. |
| 40 | |
| 41 | ## Implement the runtime in a separate file |
| 42 | |
| 43 | The runtime always lives in its own file — the DO imports the class, |
| 44 | and if the implementation lived alongside it, the DO bundle would |
| 45 | pull in process spawners, Node APIs, and SDKs that the Workers |
| 46 | runtime rejects. `.make()` takes the deploy-time props first and the |
| 47 | implementation second: |
| 48 | |
| 49 | ```typescript |
| 50 | // src/Sandbox.runtime.ts |
| 51 | import * as Effect from "effect/Effect"; |
| 52 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 53 | import { Sandbox } from "./Sandbox.ts"; |
| 54 | |
| 55 | export default Sandbox.make( |
| 56 | { main: import.meta.url }, |
| 57 | Effect.gen(function* () { |
| 58 | return Sandbox.of({ |
| 59 | ping: () => Effect.succeed("pong"), |
| 60 | fetch: Effect.succeed( |
| 61 | HttpServerResponse.text("Hello from the container!"), |
| 62 | ), |
| 63 | }); |
| 64 | }), |
| 65 | ); |
| 66 | ``` |
| 67 | |
| 68 | `main` tells alchemy to bundle this file's Effect program and bake |
| 69 | it into a generated image as the entrypoint. RPC methods and `fetch` |
| 70 | are independent: `fetch` serves HTTP on port `3000` inside the |
| 71 | container by default, while methods like `ping` are invoked directly |
| 72 | through the typed handle. |
| 73 | |
| 74 | ## Run it from a Durable Object |
| 75 | |
| 76 | Yield the class inside a DO's Construction phase and provide |
| 77 | `Cloudflare.Containers.layer` — that layer binds, starts, and |
| 78 | monitors the container, then hands you a running instance: |
| 79 | |
| 80 | ```typescript |
| 81 | // src/Agent.ts |
| 82 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 83 | import * as Effect from "effect/Effect"; |
| 84 | import { Sandbox } from "./Sandbox.ts"; |
| 85 | |
| 86 | export default class Agent extends Cloudflare.DurableObject<Agent>()( |
| 87 | "Agents", |
| 88 | Effect.gen(function* () { |
| 89 | const sandbox = yield* Sandbox; |
| 90 | |
| 91 | return Effect.gen(function* () { |
| 92 | return { |
| 93 | ping: () => sandbox.ping(), |
| 94 | }; |
| 95 | }); |
| 96 | }).pipe( |
| 97 | Effect.provide( |
| 98 | Cloudflare.Containers.layer(Sandbox, { enableInternet: true }), |
| 99 | ), |
| 100 | ), |
| 101 | ) {} |
| 102 | ``` |
| 103 | |
| 104 | Importing `Sandbox` (the class) does not pull in the runtime file — |
| 105 | it gets tree-shaken out of the DO's bundle. A Worker then binds |
| 106 | `Agent` and calls `agents.getByName(name).ping()` like any other DO |
| 107 | method. |
| 108 | |
| 109 | ## Wire the runtime into the Stack |
| 110 | |
| 111 | The `.make()` default export is the side-effect that registers the |
| 112 | container's runtime, so it must be reachable from `alchemy.run.ts`: |
| 113 | |
| 114 | ```typescript |
| 115 | // alchemy.run.ts |
| 116 | import SandboxLive from "./src/Sandbox.runtime.ts"; |
| 117 | |
| 118 | export default Alchemy.Stack( |
| 119 | "MyApp", |
| 120 | { providers: Cloudflare.providers(), state: Cloudflare.state() }, |
| 121 | Effect.gen(function* () { |
| 122 | const worker = yield* Worker; |
| 123 | return { url: worker.url }; |
| 124 | }).pipe(Effect.provide(SandboxLive)), |
| 125 | ); |
| 126 | ``` |
| 127 | |
| 128 | On deploy, alchemy bundles the entrypoint, builds and pushes the Docker |
| 129 | image as needed, and reconciles the application's scaling and runtime |
| 130 | configuration. The first deploy takes a minute or two longer while the |
| 131 | registry is provisioned. Builds are [cached by default](#cached-builds-by-default): |
| 132 | a matching image can be reused from the registry without invoking Docker. |
| 133 | |
| 134 | ## Proxy HTTP to a container port |
| 135 | |
| 136 | `getTcpPort(port)` returns a `fetch` handle for any port inside the |
| 137 | container, so an HTTP server you'd normally run in Docker just |
| 138 | works: |
| 139 | |
| 140 | ```typescript |
| 141 | // inside the DO's inner Effect |
| 142 | const { fetch } = yield* sandbox.getTcpPort(3000); |
| 143 | |
| 144 | return { |
| 145 | hello: () => |
| 146 | Effect.gen(function* () { |
| 147 | const response = yield* fetch( |
| 148 | HttpClientRequest.get("http://container/"), |
| 149 | ); |
| 150 | return yield* response.text; |
| 151 | }), |
| 152 | }; |
| 153 | ``` |
| 154 | |
| 155 | The returned `fetch` retries with backoff while the container is |
| 156 | still booting, so you don't coordinate readiness yourself. |
| 157 | |
| 158 | ## Process scope vs request scope |
| 159 | |
| 160 | Unlike Workers, a Container is a **real process** — and that changes |
| 161 | what the instance scope means. The bundled program runs under a root |
| 162 | scope that closes when the process shuts down gracefully, so |
| 163 | resources acquired in the constructor (a connection kept warm across requests, |
| 164 | a background consumer) are genuinely released on exit. Serverless |
| 165 | runtimes only approximate this: workerd never closes its instance |
| 166 | scope at all, and Lambda gets a best-effort 500 ms SIGTERM window at |
| 167 | sandbox shutdown. A hard kill still skips finalizers, as in any |
| 168 | process, so treat instance-level cleanup as best-effort. |
| 169 | |
| 170 | Each incoming request to the container's HTTP server still gets its |
| 171 | own request `Scope`, released when the response settles — the same |
| 172 | per-event contract as every other runtime. See |
| 173 | [Instance scope vs request scope](/infrastructure-as-effects/runtime#instance-scope-vs-request-scope) |
| 174 | for the model across all runtimes. |
| 175 | |
| 176 | ## Bring your own image |
| 177 | |
| 178 | `main` is one of three image sources. Point `context` at a directory |
| 179 | with a Dockerfile to build your own image, or `image` at a pre-built |
| 180 | remote image that alchemy pulls and re-pushes — no Effect bundling, |
| 181 | no `.make()`, props declared inline on the class: |
| 182 | |
| 183 | ```typescript |
| 184 | export class Web extends Cloudflare.Container<Web>()("Web", { |
| 185 | context: `${import.meta.dirname}/context`, |
| 186 | }) {} |
| 187 | |
| 188 | export class Echo extends Cloudflare.Container<Echo>()("Echo", { |
| 189 | image: "mendhak/http-https-echo:latest", |
| 190 | }) {} |
| 191 | ``` |
| 192 | |
| 193 | Arbitrary images expose no RPC methods — the DO talks to them purely |
| 194 | over their TCP port via `getTcpPort`. To build, tag, and push that |
| 195 | image as part of the same Stack, see |
| 196 | [Build & push images](/docker/build-and-push) in the Docker hub. |
| 197 | |
| 198 | When `image` already references Cloudflare's managed registry — |
| 199 | for example a digest reference pushed by CI, like |
| 200 | `registry.cloudflare.com/<accountId>/app@sha256:...` — alchemy |
| 201 | deploys the reference as-is and skips the docker pull and push |
| 202 | entirely. |
| 203 | |
| 204 | ## Cached builds by default |
| 205 | |
| 206 | Dockerfile builds, inline Dockerfiles, and Effect-native `main` builds use |
| 207 | registry caching without extra configuration: |
| 208 | |
| 209 | ```typescript |
| 210 | export class Web extends Cloudflare.Container<Web>()("Web", { |
| 211 | context: `${import.meta.dirname}/context`, |
| 212 | }) {} |
| 213 | ``` |
| 214 | |
| 215 | The default repository is |
| 216 | `registry.cloudflare.com/<account-id>/<application-physical-name>`. |
| 217 | The generated physical name includes the stack, stage, and resource-instance |
| 218 | identity. Updates to that application within the stage can reuse published |
| 219 | images, including when reverting to previously built inputs. Replacement or |
| 220 | destroy/recreate can change the physical name and start a new cache. |
| 221 | |
| 222 | This is resource-specific caching within a stage, not one shared repository |
| 223 | for every container. To reuse images across stages or applications, choose |
| 224 | a common `publish.repository` as shown below. |
| 225 | |
| 226 | :::caution[Pin external build inputs] |
| 227 | Pin base images and downloaded dependencies. A changed upstream image tag or |
| 228 | package download is outside the build context and cannot invalidate its |
| 229 | content hash, so a matching cached image may still be reused. |
| 230 | ::: |
| 231 | |
| 232 | ## Share published images across stages |
| 233 | |
| 234 | Set `publish.repository` to choose a destination repository shared by |
| 235 | applications and stages in the same Cloudflare account: |
| 236 | |
| 237 | ```typescript |
| 238 | export class Web extends Cloudflare.Container<Web>()("Web", { |
| 239 | context: `${import.meta.dirname}/context`, |
| 240 | publish: { repository: "web" }, |
| 241 | }) {} |
| 242 | ``` |
| 243 | |
| 244 | `"web"` is the repository name, not a source image, application name, |
| 245 | registry host, or complete image reference. Alchemy lowercases it and adds |
| 246 | the configured account ID. With the default registry host, a build uses: |
| 247 | |
| 248 | ```text |
| 249 | Published build: registry.cloudflare.com/<account-id>/web:<build-hash> |
| 250 | Build cache: registry.cloudflare.com/<account-id>/web:buildcache |
| 251 | Deployed image: registry.cloudflare.com/<account-id>/web@sha256:<manifest-digest> |
| 252 | ``` |
| 253 | |
| 254 | The build hash identifies the inputs; the manifest digest identifies the |
| 255 | published artifact. Stages using the same repository and matching build |
| 256 | inputs reuse the published image without invoking Docker. Changed inputs |
| 257 | produce a new hash tag in that repository and can reuse its shared inline |
| 258 | build-layer cache. Each stage still has its own Container application, |
| 259 | runtime settings, and running instances; only images and build layers are shared. |
| 260 | |
| 261 | All builds targeting a repository import reusable layers from its |
| 262 | `:buildcache` tag, even when their complete input hashes differ. That tag |
| 263 | points to the latest exported inline cache, not an aggregate of every image |
| 264 | ever published to the repository. A finished-image cache hit leaves the |
| 265 | layer-cache tag unchanged. The Container deploys the immutable digest, not |
| 266 | the mutable `buildcache` tag. |
| 267 | |
| 268 | For an Effect-native Container, put `publish` in the props passed to |
| 269 | `.make()`, alongside `main`. Omitting `publish` keeps the default cached, |
| 270 | application-specific repository. |
| 271 | |
| 272 | For a pre-built external `image`, `publish.repository` chooses where it is |
| 273 | re-published, but does not enable finished-image cache reuse. Images already |
| 274 | in the target registry keep their existing repository instead of being |
| 275 | copied into the requested one. |
| 276 | |
| 277 | ## Configure the container with `env` |
| 278 | |
| 279 | A container is a process, not a Worker: it has no bindings, so every |
| 280 | piece of configuration reaches it as an environment variable. `env` |
| 281 | takes a plain map and lands each entry on the deployment, where the |
| 282 | program reads it from `process.env` — the same for a generated |
| 283 | (`main`) image, a `context` build, and a pre-built `image`: |
| 284 | |
| 285 | ```typescript |
| 286 | export class Api extends Cloudflare.Container<Api>()("Api", { |
| 287 | context: `${import.meta.dirname}/api`, |
| 288 | ports: [{ name: "http", port: 8080 }], |
| 289 | env: { |
| 290 | PORT: "8080", |
| 291 | LOG_LEVEL: "info", |
| 292 | }, |
| 293 | }) {} |
| 294 | ``` |
| 295 | |
| 296 | Values are strings. Wrap a secret in `Redacted` and it stays |
| 297 | encrypted in alchemy's state and out of plan output, while the |
| 298 | container still reads a plain string: |
| 299 | |
| 300 | ```typescript |
| 301 | env: { |
| 302 | SESSION_KEY: Redacted.make(process.env.SESSION_KEY!), |
| 303 | } |
| 304 | ``` |
| 305 | |
| 306 | ## Read another resource's outputs |
| 307 | |
| 308 | A class body is module scope, so there is nowhere to `yield*` the |
| 309 | bucket, queue, or database whose output you need — and a bare |
| 310 | declaration is an Effect, not a resolved handle, so |
| 311 | `Uploads.bucketName` is `undefined` until something yields it. Pass |
| 312 | the props as an `Effect.gen` instead; inside it, sibling resources |
| 313 | resolve like anywhere else: |
| 314 | |
| 315 | ```typescript |
| 316 | import * as Effect from "effect/Effect"; |
| 317 | |
| 318 | export const Uploads = Cloudflare.R2.Bucket("Uploads"); |
| 319 | |
| 320 | export class Api extends Cloudflare.Container<Api>()( |
| 321 | "Api", |
| 322 | Effect.gen(function* () { |
| 323 | const uploads = yield* Uploads; |
| 324 | return { |
| 325 | context: `${import.meta.dirname}/api`, |
| 326 | env: { BUCKET_NAME: uploads.bucketName }, |
| 327 | }; |
| 328 | }), |
| 329 | ) {} |
| 330 | ``` |
| 331 | |
| 332 | The reference is what orders the deploy: the bucket is created |
| 333 | before the container application that reads its name. |
| 334 | |
| 335 | ## Connect to a SQL database |
| 336 | |
| 337 | An **effectful** (`main`) container runs your Effect program, so it |
| 338 | resolves a database capability the same way a Worker does — declare |
| 339 | the connection once, then bind it: |
| 340 | |
| 341 | ```typescript |
| 342 | // src/Db.ts |
| 343 | import * as Prisma from "alchemy/Prisma"; |
| 344 | import * as Effect from "effect/Effect"; |
| 345 | |
| 346 | export const Connection = Effect.gen(function* () { |
| 347 | const project = yield* Prisma.Project("app", { |
| 348 | createDatabase: false, |
| 349 | region: "us-east-1", |
| 350 | }); |
| 351 | const postgres = yield* Prisma.Postgres("db", { |
| 352 | project, |
| 353 | region: "us-east-1", |
| 354 | }); |
| 355 | return yield* Prisma.Connection("api", { database: postgres }); |
| 356 | }); |
| 357 | ``` |
| 358 | |
| 359 | ```typescript |
| 360 | // src/Api.runtime.ts |
| 361 | import * as Prisma from "alchemy/Prisma"; |
| 362 | import * as SQL from "alchemy/SQL/Postgres"; |
| 363 | import * as Effect from "effect/Effect"; |
| 364 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 365 | import { Api } from "./Api.ts"; |
| 366 | import { Connection } from "./Db.ts"; |
| 367 | |
| 368 | export default Api.make( |
| 369 | { main: import.meta.url }, |
| 370 | Effect.gen(function* () { |
| 371 | const db = yield* Prisma.Connect(Connection); |
| 372 | const sql = yield* SQL.Postgres({ url: db.databaseUrl }); |
| 373 | |
| 374 | return Api.of({ |
| 375 | fetch: Effect.gen(function* () { |
| 376 | const users = yield* sql`SELECT * FROM users`; |
| 377 | return yield* HttpServerResponse.json(users); |
| 378 | }), |
| 379 | }); |
| 380 | }).pipe(Effect.provide(Prisma.ConnectBinding)), |
| 381 | ); |
| 382 | ``` |
| 383 | |
| 384 | You never name `DATABASE_URL`. The container has no bindings — it is |
| 385 | a process, not a Worker — so `Prisma.Connect` writes the connection's |
| 386 | outputs onto the deployment as environment variables and reads them |
| 387 | back at runtime. The capability owns both ends. |
| 388 | |
| 389 | Start it with outbound networking, or the container never reaches the |
| 390 | database at all: |
| 391 | |
| 392 | ```typescript |
| 393 | Effect.provide(Cloudflare.Containers.layer(Api, { enableInternet: true })) |
| 394 | ``` |
| 395 | |
| 396 | ## Connect an arbitrary image to a database |
| 397 | |
| 398 | An image you brought yourself knows nothing about alchemy, so there |
| 399 | is no capability to bind — you name the variable and hand it the |
| 400 | connection string. Give it the provider's **pooled** endpoint |
| 401 | (Prisma's `databaseUrl` already prefers it; on Neon it is |
| 402 | `branch.pooledConnectionUri`, on PlanetScale `role.connectionUrlPooled`), |
| 403 | since nothing else is pooling those connections on the container's |
| 404 | behalf: |
| 405 | |
| 406 | ```typescript |
| 407 | // src/Web.ts |
| 408 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 409 | import * as Effect from "effect/Effect"; |
| 410 | import { Connection } from "./Db.ts"; |
| 411 | |
| 412 | export class Web extends Cloudflare.Container<Web>()( |
| 413 | "Web", |
| 414 | Effect.gen(function* () { |
| 415 | const connection = yield* Connection; |
| 416 | return { |
| 417 | context: `${import.meta.dirname}/web`, |
| 418 | ports: [{ name: "http", port: 8080 }], |
| 419 | env: { |
| 420 | DATABASE_URL: connection.databaseUrl, |
| 421 | PORT: "8080", |
| 422 | }, |
| 423 | }; |
| 424 | }), |
| 425 | ) {} |
| 426 | ``` |
| 427 | |
| 428 | Your image reads `DATABASE_URL` with whatever client it already |
| 429 | uses — `pg`, Prisma, Rails, Django. Nothing about the container is |
| 430 | alchemy-specific; only the value's provenance is. |
| 431 | |
| 432 | A Worker fronting the same database still binds Hyperdrive — see |
| 433 | [Hyperdrive](/cloudflare/data/hyperdrive). The two coexist: point |
| 434 | Hyperdrive at the *direct* origin (it pools for the Worker) and the |
| 435 | container at the pooled one. |
| 436 | |
| 437 | Under `alchemy dev` the same wiring targets your local emulators. A |
| 438 | local dev database hands out `localhost` URLs, but inside the Docker |
| 439 | container `localhost` is the container itself — so the dev runtime |
| 440 | rewrites loopback hosts in the container's env to |
| 441 | `host.docker.localhost`. The alias deliberately keeps `localhost` in |
| 442 | the hostname so clients that gate plain-HTTP on a local-looking host |
| 443 | (Prisma's `prisma+postgres://` protocol, for one) keep working against |
| 444 | the local dev server. |
| 445 | |
| 446 | On Docker Desktop that alias is Docker's `host-gateway`, which |
| 447 | forwards to processes bound to `127.0.0.1` on your machine. |
| 448 | |
| 449 | On native Linux, `host-gateway` is the docker bridge IP |
| 450 | (`172.17.0.1`). A TCP SYN to that address is host INPUT — UFW, |
| 451 | firewalld, and nftables on CachyOS/Arch will drop it. Binding the |
| 452 | same port on the bridge or on your LAN NIC does not skip INPUT. |
| 453 | Alchemy does not go that way: |
| 454 | |
| 455 | 1. `@prisma/dev` (and any host server you start) stay on `127.0.0.1`. |
| 456 | 2. A unix socket on the host forwards to that port. |
| 457 | 3. The workerd `<name>-proxy` sidecar bind-mounts the socket and maps |
| 458 | `host.docker.localhost` to `127.0.0.1` in the shared netns. |
| 459 | 4. A listener in that netns accepts `127.0.0.1:<port>` and forwards |
| 460 | through the socket. |
| 461 | |
| 462 | The container talks to itself. The socket crosses the boundary. UFW |
| 463 | never sees a SYN to `172.17.0.1`. |
| 464 | |
| 465 | If the unix-socket path cannot attach (missing `nsenter`, EPERM), |
| 466 | Alchemy logs this UFW hole — do not set `DEFAULT_INPUT_POLICY=ACCEPT`: |
| 467 | |
| 468 | ``` |
| 469 | sudo ufw allow from 172.16.0.0/12 to any port <prisma-or-dev-ports> proto tcp |
| 470 | ``` |
| 471 | |
| 472 | Neon and PlanetScale have no local emulator: even under `alchemy |
| 473 | dev` the URL is a cloud host (`*.neon.tech`, `*.psdb.cloud`) and the |
| 474 | rewrite is a no-op. The container still needs |
| 475 | `enableInternet: true` to reach them. |
| 476 | |
| 477 | ## Which capabilities reach a container |
| 478 | |
| 479 | The split follows the transport. A capability whose config travels as |
| 480 | environment variables works in a container: `Prisma.Connect`, and the |
| 481 | token-scoped HTTP clients for |
| 482 | [R2](/cloudflare/data/r2), [KV](/cloudflare/data/kv), and |
| 483 | [Queues](/cloudflare/messaging/queues) — |
| 484 | `Cloudflare.R2.ReadWriteBucketHttp` and friends, which mint a scoped |
| 485 | API token instead of using a native binding. |
| 486 | |
| 487 | A capability that *is* a workerd binding cannot: |
| 488 | `Cloudflare.Hyperdrive.Connect`, `Cloudflare.D1.QueryDatabase`, and |
| 489 | every `*Binding` layer resolve against the Workers runtime, which no |
| 490 | container process has. Use the `*Http` layer where one exists, and |
| 491 | environment variables otherwise. |
| 492 | |
| 493 | ## Bind on an async Worker |
| 494 | |
| 495 | An async Worker can host a container-backed Durable Object class |
| 496 | that ships as plain JavaScript — `@cloudflare/sandbox`'s `Sandbox`, |
| 497 | or your own class extending `@cloudflare/containers`' `Container`. |
| 498 | The class lives in the worker script: |
| 499 | |
| 500 | ```typescript |
| 501 | // src/worker.ts |
| 502 | import { Container } from "@cloudflare/containers"; |
| 503 | |
| 504 | export class Sandbox extends Container { |
| 505 | defaultPort = 8080; |
| 506 | } |
| 507 | ``` |
| 508 | |
| 509 | `Sandbox` is the Durable Object class Cloudflare instantiates for |
| 510 | each container instance; `Container` (from `@cloudflare/containers`) |
| 511 | handles the lifecycle and forwards `fetch` to the port inside it. |
| 512 | |
| 513 | Declare it in the stack by binding a `Cloudflare.Container` in the |
| 514 | Worker's `env` — the Container is the Durable Object binding and its |
| 515 | ContainerApplication together. Alchemy emits the |
| 516 | `durable_object_namespace` binding, marks the class as |
| 517 | container-backed in the script metadata, provisions the application, |
| 518 | and attaches it to the class's namespace: |
| 519 | |
| 520 | ```typescript |
| 521 | // alchemy.run.ts |
| 522 | import type { Sandbox } from "./src/worker.ts"; |
| 523 | |
| 524 | export const Worker = Cloudflare.Worker("Worker", { |
| 525 | main: "./src/worker.ts", |
| 526 | env: { |
| 527 | Sandbox: Cloudflare.Container<Sandbox>("Sandbox", { |
| 528 | image: "docker.io/cloudflare/sandbox:0.1.3", |
| 529 | }), |
| 530 | }, |
| 531 | }); |
| 532 | ``` |
| 533 | |
| 534 | The Durable Object class name defaults to the binding name (the |
| 535 | `env` key). Set `className` when the exported class is named |
| 536 | differently. The type parameter (`Container<Sandbox>`) is the class |
| 537 | from `worker.ts` above — it types `env.Sandbox` as |
| 538 | `DurableObjectNamespace<Sandbox>` through |
| 539 | [`InferEnv`](/cloudflare/compute/workers#async-workers), |
| 540 | so the handler reaches the container with full types: |
| 541 | |
| 542 | ```typescript |
| 543 | // src/worker.ts |
| 544 | import { getContainer } from "@cloudflare/containers"; |
| 545 | import type * as Cloudflare from "alchemy/Cloudflare"; |
| 546 | import type { Worker } from "../alchemy.run.ts"; |
| 547 | |
| 548 | export default { |
| 549 | async fetch(request: Request, env: Cloudflare.InferEnv<typeof Worker>) { |
| 550 | return getContainer(env.Sandbox, "default").fetch(request); |
| 551 | }, |
| 552 | }; |
| 553 | ``` |
| 554 | |
| 555 | Only image-backed containers (`image`, or `context`/`dockerfile`) |
| 556 | can be bound this way. An effectful (`main`) container's runtime is |
| 557 | provided by its `.make()` Layer, which needs an Effect-native |
| 558 | Durable Object host. |
| 559 | |
| 560 | ## ContainerApplication |
| 561 | |
| 562 | Under the hood every container is backed by a |
| 563 | [ContainerApplication](/providers/cloudflare/containers/containerapplication) |
| 564 | — the deployed, scalable unit that carries the image, instance |
| 565 | type, instance counts, and observability settings. You typically |
| 566 | extend `Cloudflare.Container` rather than using it directly, but |
| 567 | the same props shape (`instanceType`, `observability`, `runtime`, |
| 568 | …) flows through `.make()`. |
| 569 | |
| 570 | ## Rollouts |
| 571 | |
| 572 | When a deploy changes the image or configuration, running instances |
| 573 | are replaced with the new version. By default the replacement is |
| 574 | immediate — every instance at once. Set `rollout` to replace them in |
| 575 | steps instead, so the application stays available through the update: |
| 576 | |
| 577 | ```typescript |
| 578 | export class Api extends Cloudflare.Container<Api>()("Api", { |
| 579 | main: import.meta.url, |
| 580 | instances: 4, |
| 581 | maxInstances: 4, |
| 582 | rollout: { strategy: "rolling", stepPercentage: 25 }, |
| 583 | }) {} |
| 584 | ``` |
| 585 | |
| 586 | A `rolling` strategy replaces `stepPercentage` of the instances per |
| 587 | step and advances automatically as the new instances come up. Each |
| 588 | replaced instance receives `SIGTERM` and has 15 minutes to shut down |
| 589 | cleanly before `SIGKILL`. |
| 590 | |
| 591 | Rollouts replace instances; they do not split requests between two |
| 592 | image versions. The Worker and Durable Object fronting the container |
| 593 | cut over immediately while instances roll, so keep the |
| 594 | Worker-to-container protocol compatible across both image versions |
| 595 | until the rollout completes. Request-level traffic splitting exists |
| 596 | one layer up, on the Worker — see |
| 597 | [Gradual deployments](/cloudflare/compute/gradual-deployments). |
| 598 | |
| 599 | ## Where next |
| 600 | |
| 601 | The full walkthrough: |
| 602 | |
| 603 | - [Run a Container](/cloudflare/compute/run-a-container) — build the |
| 604 | sandbox above out to a shell-executing container with RPC, HTTP |
| 605 | proxying, stage-dependent config, and tests. |
| 606 | |
| 607 | Related: |
| 608 | |
| 609 | - [Durable Objects](/cloudflare/compute/durable-objects) — every container |
| 610 | is fronted by one. |
| 611 | - [Workers](/cloudflare/compute/workers) — the entrypoint that reaches the |
| 612 | DO (and through it, the container). |
| 613 | |
| 614 | Reference: |
| 615 | |
| 616 | - [Container API reference](/providers/cloudflare/containers/container) |
| 617 | - [ContainerApplication API reference](/providers/cloudflare/containers/containerapplication) |