Cloudflare.Containers reference
Container
Section titled “Container”Source:
src/Cloudflare/Containers/Container.ts
A Cloudflare Container that runs a long-lived process alongside a Durable Object.
Containers always use the Container Layer pattern — the class
and .make() must live in separate files. A Container must be
bound to a Durable Object, and the DO imports the class to get a
typed handle. If the class and .make() lived in the same file,
the DO’s bundle would pull in all of the container’s runtime
dependencies (process spawners, Node APIs, SDKs, etc.), which
would bloat the bundle and likely break the Cloudflare Workers
runtime. Keeping them separate ensures the bundler only includes
the tiny class in the DO’s output.
See the Runtime concept page for how this fits into the async / effect / layer progression.
Container: Container Layer
Section titled “Container: Container Layer”Define the class and .make() in separate files. The class
declares the container’s identity, configuration, and typed
shape. .make() provides the runtime implementation as a
default export. Use Container.of to construct the typed
shape — it ensures your implementation matches the methods
declared on the class.
Container class
// src/Sandbox.ts — the tag carries only the name + typed shape;// configuration lives on `.make()`.export class Sandbox extends Cloudflare.Container< Sandbox, { exec: (cmd: string) => Effect.Effect<{ exitCode: number; stdout: string; stderr: string; }>; }>()("Sandbox") {}Container .make()
// src/Sandbox.runtime.ts — props are the first argument to `.make()`export default Sandbox.make( { main: import.meta.url }, Effect.gen(function* () { const cp = yield* ChildProcessSpawner;
return Sandbox.of({ exec: (command) => cp.spawn(ChildProcess.make(command, { shell: true })).pipe( Effect.flatMap(({ exitCode, stdout, stderr }) => Effect.all({ exitCode, stdout: stdout.pipe(Stream.decodeText, Stream.mkString), stderr: stderr.pipe(Stream.decodeText, Stream.mkString), }), ), Effect.scoped, ), fetch: Effect.succeed( HttpServerResponse.text("Hello from container!"), ), }); }),);Container: Async Workers
Section titled “Container: Async Workers”An async Worker can host a container-backed Durable Object class that
ships as plain JavaScript — @cloudflare/sandbox‘s Sandbox, or your
own class extending @cloudflare/containers’ Container. The class
lives in the worker script; Container (the npm one) handles the
lifecycle and forwards fetch to the port inside the container.
The worker script exports the container-backed class
import { Container } from "@cloudflare/containers";
export class Sandbox extends Container { defaultPort = 8080;}Declare it in the stack by binding a Cloudflare.Container in the
Worker’s env — the Container is the Durable Object binding and its
ContainerApplication together. Alchemy emits the
durable_object_namespace binding, marks the class as container-backed
in the script metadata, provisions the ContainerApplication, and attaches
it to the class’s namespace. The Durable Object class name defaults to
the binding name (the env key); set className when the exported class
is named differently.
Binding the container-backed class in the stack
import type { Sandbox } from "./src/worker.ts";
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { Sandbox: Cloudflare.Container<Sandbox>("Sandbox", { image: "docker.io/cloudflare/sandbox:0.1.3", }), },});The type parameter (Container<Sandbox>) is the class from worker.ts —
it types env.Sandbox as DurableObjectNamespace<Sandbox> via
Cloudflare.InferEnv, so the handler reaches the container with full
types.
Reaching the container from the async handler
import { getContainer } from "@cloudflare/containers";import type * as Cloudflare from "alchemy/Cloudflare";import type { Worker } from "../alchemy.run.ts";
export default { async fetch(request: Request, env: Cloudflare.InferEnv<typeof Worker>) { return getContainer(env.Sandbox, "default").fetch(request); },};Container: Image Sources
Section titled “Container: Image Sources”A container’s image comes from one of three sources, picked by which prop you set:
main— bundle your Effect program into a generated image.context(+ optionaldockerfile) — build your own Dockerfile.image— pull a pre-built remote image and re-push it.
Only the main source bundles and injects an Effect runtime — so it
has a typed shape and a .make(props, impl) runtime. The other two
ship an arbitrary image as-is: they have no runtime to provide, so
you declare the class with its props inline and register it purely
via Cloudflare.Containers.layer from the hosting Durable Object.
Effect-native image (main)
// Alchemy bundles this file's Effect program and bakes it into a// generated image as the entrypoint.export class Sandbox extends Cloudflare.Container< Sandbox, { ping: () => Effect.Effect<string> }>()("Sandbox") {}
export default Sandbox.make( { main: import.meta.url }, Effect.gen(function* () { return Sandbox.of({ ping: () => Effect.succeed("pong"), fetch: Effect.succeed(HttpServerResponse.text("hello")), }); }),);Build your own Dockerfile (context / dockerfile)
// Alchemy builds the Dockerfile against the context directory — no// Effect bundling, no `.make()`. `dockerfile` defaults to// `<context>/Dockerfile`. The props are declared inline on the tag.export class Web extends Cloudflare.Container<Web>()("Web", { context: `${import.meta.dirname}/context`,}) {}Builds are cached by default at
registry.cloudflare.com/<account-id>/<application-physical-name>.
The generated physical name includes the stage and resource instance, so
subsequent updates in that stage can reuse matching images. Replacement or
destroy/recreate can change the name and start a new cache. This does not
automatically share one repository across every container in the stage.
The same defaults apply to inline Dockerfiles and Effect-native main builds.
Reuse builds across stages
export class Web extends Cloudflare.Container<Web>()("Web", { context: `${import.meta.dirname}/context`, publish: { repository: "web" },}) {}repository: "web" names the destination repository, not a source image or
the container application. With the default registry host, Alchemy lowercases
the name and expands it to registry.cloudflare.com/<account-id>/web.
Supply only the repository name, without a registry host, account ID, tag,
or digest. A build produces these references:
Published build: registry.cloudflare.com/<account-id>/web:<build-hash>Build cache: registry.cloudflare.com/<account-id>/web:buildcacheDeployed image: registry.cloudflare.com/<account-id>/web@sha256:<manifest-digest>The build hash identifies the inputs; the manifest digest identifies the
published artifact. Applications and stages in the same account can use
publish: { repository: "web" } to reuse matching published builds. Changed
inputs produce another hash tag in the same repository. Builds targeting
that repository import reusable layers from its shared :buildcache tag,
including when full input hashes differ. This mutable tag points to the
latest exported inline cache, not a combined cache of every historical
image. Reusing a finished image does not move the layer-cache tag.
Pin base images and downloaded dependencies: changes outside the build context cannot invalidate the input hash. Each stage still has its own Container application, runtime settings, and instances; only images and build layers are shared.
Publish an existing image into a named repository
export class Proxy extends Cloudflare.Container<Proxy>()("Proxy", { image: "nginx:alpine", publish: { repository: "web-proxy" },}) {}// Re-publishes nginx into registry.cloudflare.com/<account-id>/web-proxy// and deploys registry.cloudflare.com/<account-id>/web-proxy@sha256:<manifest-digest>.Remote images are re-published without building them. An image already in
the target registry keeps its existing repository; publish.repository
does not copy it into another one.
Remote image (image)
// Alchemy pulls the public image and re-pushes it to Cloudflare's// registry — no build, no bundling, no `.make()`.export class Echo extends Cloudflare.Container<Echo>()("Echo", { image: "mendhak/http-https-echo:latest",}) {}Reaching an arbitrary image’s port from a Durable Object
// `external` and `remote` images expose no RPC methods, so the DO// talks to them purely over their TCP port via `getTcpPort`.export class WebObject extends Cloudflare.DurableObject<WebObject>()( "WebObject", Effect.gen(function* () { const web = yield* Web; return Effect.gen(function* () { return { hello: () => Effect.gen(function* () { const { fetch } = yield* web.getTcpPort(8080); const res = yield* fetch(HttpClientRequest.get("http://container/")); return yield* res.text; }), }; }); }).pipe(Effect.provide(Cloudflare.Containers.layer(Web))),) {}Container: Bundling & Tree-shaking
Section titled “Container: Bundling & Tree-shaking”main is bundled with rolldown at deploy time. Unused code is
tree-shaken. effect, alchemy, and @distilled.cloud are marked
pure so unused parts prune more aggressively. Your app is not
marked pure.
Mark additional packages as pure
Only list packages with no top-level side effects.
{ main: import.meta.url, build: { pure: { packages: ["my-lib", "@my-scope/*"] }, },}Turn it off
{ main: import.meta.url, build: { pure: false },}Container: Configuration
Section titled “Container: Configuration”The props object — the first argument to .make() — accepts main
(entrypoint file), instanceType (compute size), runtime
("bun" or "node"), and observability settings. Use
Stack.useSync to read the surrounding stack and pick a beefier
instanceType in prod while keeping the cheap dev instance for
preview environments.
export const SandboxLive = Sandbox.make( Stack.useSync((stack) => ({ main: import.meta.url, instanceType: stack.stage === "prod" ? "standard-1" : "dev", observability: { logs: { enabled: true } }, })), Effect.gen(function* () { return Sandbox.of({ exec: (cmd) => ... }); }),);Container: Environment Variables
Section titled “Container: Environment Variables”A container is a process, not a Worker: it has no bindings, so every
piece of configuration reaches it through env. Each entry lands on
the deployment and shows up in process.env inside the image —
generated (main), built (context), or pre-built (image) alike.
Wrap a secret in Redacted to keep it encrypted in state and out of
plan output; the container still reads a plain string.
export class Api extends Cloudflare.Container<Api>()("Api", { context: `${import.meta.dirname}/api`, ports: [{ name: "http", port: 8080 }], env: { PORT: "8080", SESSION_KEY: Redacted.make(process.env.SESSION_KEY!), },}) {}Container: Props from Other Resources
Section titled “Container: Props from Other Resources”A class body is module scope, so there is nowhere to yield* the
database, queue, or bucket whose output you need — and a bare
declaration is an Effect, not a resolved handle, so
Uploads.bucketName reads as undefined. Pass the props as an
Effect.gen instead: inside it sibling resources resolve normally,
and the reference orders the deploy.
export const Uploads = Cloudflare.R2.Bucket("Uploads");
export class Api extends Cloudflare.Container<Api>()( "Api", Effect.gen(function* () { const uploads = yield* Uploads; return { context: `${import.meta.dirname}/api`, env: { BUCKET_NAME: uploads.bucketName }, }; }),) {}Container: Database Connections
Section titled “Container: Database Connections”An effectful (main) container runs your Effect program, so it
resolves a database capability the same way a Worker does — you never
name DATABASE_URL. The container has no bindings (it is a process,
not a Worker), so Prisma.Connect writes the connection’s outputs
onto the deployment as environment variables and reads them back at
runtime; the capability owns both ends.
Binding a Prisma connection inside the container runtime
export default Api.make( { main: import.meta.url }, Effect.gen(function* () { const db = yield* Prisma.Connect(Connection); const sql = yield* SQL.Postgres({ url: db.databaseUrl });
return Api.of({ fetch: Effect.gen(function* () { const users = yield* sql`SELECT * FROM users`; return yield* HttpServerResponse.json(users); }), }); }).pipe(Effect.provide(Prisma.ConnectBinding)),);An image you brought yourself knows nothing about alchemy, so there is no capability to bind — name the variable and hand it the provider’s pooled connection string.
Passing a pooled database URL to an arbitrary image
export class Web extends Cloudflare.Container<Web>()( "Web", Effect.gen(function* () { const connection = yield* Connection; return { context: `${import.meta.dirname}/web`, env: { DATABASE_URL: connection.databaseUrl }, }; }),) {}Either way, start it with
Cloudflare.Containers.layer(Api, { enableInternet: true }) — without
outbound networking the container never reaches the database.
Cloudflare.Hyperdrive.Connect is the one that cannot work here: it
is a workerd binding, so no container process can resolve it.
Container: Stack-level wiring
Section titled “Container: Stack-level wiring”The .make() export default is the side-effect that registers
the container’s runtime. It must be reachable from your
alchemy.run.ts so the bundler emits the runtime entrypoint.
Provide it on the Stack’s generator with Effect.provide.
import SandboxLive from "./src/Sandbox.runtime.ts";
export default Alchemy.Stack( "MyApp", { providers: Cloudflare.providers(), state: Cloudflare.state() }, Effect.gen(function* () { const worker = yield* Worker; return { url: worker.url }; }).pipe(Effect.provide(SandboxLive)),);Container: Calling from a Durable Object
Section titled “Container: Calling from a Durable Object”yield* Sandbox resolves a running container instance — every
method declared on the container’s shape plus a getTcpPort
helper. Provide Cloudflare.Containers.layer(Sandbox, …) on the
DO’s init to configure how the container runs; that layer binds,
starts, and monitors it and satisfies the Sandbox tag. Because
only the class is imported, the runtime implementation in
Sandbox.runtime.ts is tree-shaken out of the DO’s bundle.
export default class Agent extends Cloudflare.DurableObject<Agent>()( "Agents", Effect.gen(function* () { const sandbox = yield* Sandbox;
return Effect.gen(function* () { return { exec: (cmd: string) => sandbox.exec(cmd), }; }); }).pipe( Effect.provide( Cloudflare.Containers.layer(Sandbox, { enableInternet: true }), ), ),) {}Container: HTTP Requests to Container Ports
Section titled “Container: HTTP Requests to Container Ports”Use getTcpPort on the running container instance to get a fetch
handle for a specific port. This lets you make HTTP requests to
servers running inside the container process.
export default class Agent extends Cloudflare.DurableObject<Agent>()( "Agents", Effect.gen(function* () { const sandbox = yield* Sandbox;
return Effect.gen(function* () { const { fetch } = yield* sandbox.getTcpPort(3000);
return { health: () => Effect.gen(function* () { const response = yield* fetch( HttpClientRequest.get("http://container/health"), ); return yield* response.text; }), }; }); }).pipe( Effect.provide( Cloudflare.Containers.layer(Sandbox, { enableInternet: true }), ), ),) {}ContainerApplication
Section titled “ContainerApplication”Source:
src/Cloudflare/Containers/ContainerApplication.ts
A Cloudflare Container Application — the deployed, scalable unit that runs a
containerized program on Cloudflare’s compute platform. Alchemy bundles the
main entrypoint, builds a Docker image, pushes it to the Cloudflare
registry, and reconciles the application’s scaling and runtime configuration.
This is the lower-level resource backing the Container platform
binding; in application code you typically extend Cloudflare.Container to
define and bind a container to a Durable Object rather than referencing this
resource directly. The same props shape (main, instanceType, instances,
etc.) is accepted by the Cloudflare.Container(...) class form shown below.
ContainerApplication: Defining a Container Application
Section titled “ContainerApplication: Defining a Container Application”Point main at the container’s entrypoint file; Alchemy bundles it and uses
it as the image’s entrypoint. The application name is derived deterministically
from the stack, stage, and logical ID unless you set an explicit name, and
handler selects which export to run when it isn’t the default.
Minimal container
import * as Cloudflare from "alchemy/Cloudflare";
export class Sandbox extends Cloudflare.Container<Sandbox>()("Sandbox", { main: import.meta.url,}) {}The single main prop is enough to ship a container: Alchemy bundles the
entrypoint, builds and pushes the image, and provisions an application with
one instance. Reach for the other props only when you need to scale, expose
ports, or customize the build.
Named container with a non-default handler export
export class Worker extends Cloudflare.Container<Worker>()("Worker", { main: import.meta.url, handler: "runWorker", name: "background-worker",}) {}name pins a stable application name (instead of the generated one), which is
useful for adopting an existing application, while handler runs the named
runWorker export rather than the module’s default.
ContainerApplication: Image Sources
Section titled “ContainerApplication: Image Sources”The image is resolved from exactly one of three props, checked in order:
main (bundle an Effect program into a generated image), then image
(pull and re-push a remote image), then context / dockerfile (build
your own Dockerfile). Only main injects an Effect runtime; the other two
ship an arbitrary image unchanged.
Build your own Dockerfile (context / dockerfile)
export class Web extends Cloudflare.Container<Web>()("Web", { context: `${import.meta.dirname}/context`, dockerfile: "Dockerfile",}) {}Alchemy builds dockerfile against the context directory with no main
bundling. dockerfile is resolved relative to context and defaults to
<context>/Dockerfile.
Remote image (image)
export class Echo extends Cloudflare.Container<Echo>()("Echo", { image: "mendhak/http-https-echo:latest",}) {}Alchemy pulls the pre-built public image and re-pushes it to Cloudflare’s managed registry instead of building anything.
ContainerApplication: Bundling & Dependencies
Section titled “ContainerApplication: Bundling & Dependencies”By default the entrypoint is bundled for the bun runtime. Use runtime to
switch to Node, external to keep native/precompiled packages out of the
bundle (auto-installed in the image unless autoInstallExternals is false),
image (or an inline dockerfile) to pick the environment the generated
Dockerfile starts FROM, and registryId to override the registry host.
Node runtime with external native deps
export class ImageApi extends Cloudflare.Container<ImageApi>()("ImageApi", { main: import.meta.url, runtime: "node", external: ["sharp"], autoInstallExternals: true,}) {}Marking sharp as external stops Rolldown from bundling the native module;
because autoInstallExternals is true, Alchemy runs npm install sharp
inside the image so the dependency is present at runtime.
Custom environment image and registry
export class Custom extends Cloudflare.Container<Custom>()("Custom", { main: import.meta.url, image: "oven/bun:1", autoInstallExternals: false, registryId: "registry.cloudflare.com",}) {}Alchemy generates the Dockerfile — FROM your image, then the
program-copy and entrypoint steps — so you control the starting image;
autoInstallExternals: false skips the redundant install step when the
environment already ships your external packages.
Inline environment Dockerfile (extra build steps)
import * as Dockerfile from "alchemy/Docker/Dockerfile";
export class Transcoder extends Cloudflare.Container<Transcoder>()( "Transcoder", { main: import.meta.url, dockerfile: Dockerfile.inline` FROM oven/bun:1 RUN apt-get update && apt-get install -y ffmpeg `, },) {}Inline dockerfile content replaces the generated FROM line, so the
environment can run extra build steps (system packages, config) while the
bundled program is still layered on top.
ContainerApplication: Bundling & Tree-shaking
Section titled “ContainerApplication: Bundling & Tree-shaking”main is bundled with rolldown at deploy time. Unused code is
tree-shaken. effect, alchemy, and @distilled.cloud are marked
pure so unused parts prune more aggressively. Your app is not
marked pure.
Mark additional packages as pure
Only list packages with no top-level side effects.
{ main: import.meta.url, build: { pure: { packages: ["my-lib", "@my-scope/*"] }, },}Turn it off
{ main: import.meta.url, build: { pure: false },}ContainerApplication: Scaling & Instance Types
Section titled “ContainerApplication: Scaling & Instance Types”Control the desired and maximum instance counts with instances/maxInstances
and pick a compute size with instanceType. For finer control, override
vcpu, memory, and disk directly.
Autoscaling with a larger instance type
export class Sandbox extends Cloudflare.Container<Sandbox>()("Sandbox", { main: import.meta.url, instanceType: "standard-1", instances: 1, maxInstances: 5,}) {}The application keeps one instance running and may scale out to five under
load, each on the standard-1 size. Use a larger instanceType (or the
explicit overrides below) when the default dev size is too small.
Explicit CPU, memory, and disk overrides
export class Heavy extends Cloudflare.Container<Heavy>()("Heavy", { main: import.meta.url, vcpu: 2, memory: "4GB", disk: { size: "10GB" },}) {}These props override the per-instance resource allocation independently of
instanceType, which is handy when a workload needs, say, extra disk for
scratch space without bumping every other dimension.
ContainerApplication: Runtime Configuration
Section titled “ContainerApplication: Runtime Configuration”Inject configuration with environmentVariables (plain values) and secrets
(references to stored secrets), and override the image’s command or
entrypoint. labels attach metadata to the deployment.
Environment variables, secrets, and a command override
export class Api extends Cloudflare.Container<Api>()("Api", { main: import.meta.url, environmentVariables: [{ name: "LOG_LEVEL", value: "info" }], secrets: [{ name: "API_KEY", type: "env", secret: "my-stored-secret" }], command: ["bun", "run", "start"], labels: [{ name: "team", value: "payments" }],}) {}environmentVariables are visible plain values, while secrets map a stored
secret into the runtime as an env var without exposing it in config; command
overrides the container’s startup command and labels tag the deployment for
organization.
Passing env and selecting runtime exports
export class Job extends Cloudflare.Container<Job>()("Job", { main: import.meta.url, env: { REGION: "wnam", FEATURE_FLAG: "on" }, exports: ["default"],}) {}env injects values into the bundled program’s runtime context (as opposed to
the deployment-level environmentVariables), and exports declares which
symbols from the entrypoint module the runtime should wire up.
ContainerApplication: Networking & Health Checks
Section titled “ContainerApplication: Networking & Health Checks”Configure outbound/inbound networking with network and dns, expose
ports, and gate readiness with checks.
export class Web extends Cloudflare.Container<Web>()("Web", { main: import.meta.url, ports: [{ name: "http", port: 8080 }], network: { assignIpv4: "predefined", mode: "public" }, dns: { servers: ["1.1.1.1"], searches: ["internal"] }, checks: [{ name: "ready", type: "http", port: "8080", tls: false }],}) {}ports publishes the named port the program listens on, network controls IP
assignment and public/private reachability, dns overrides resolver settings,
and checks tells Cloudflare how to probe the container before routing
traffic to it.
ContainerApplication: Observability & Access
Section titled “ContainerApplication: Observability & Access”Turn on log shipping with observability and install sshPublicKeyIds for
interactive access to running instances.
export class Api extends Cloudflare.Container<Api>()("Api", { main: import.meta.url, observability: { logs: { enabled: true } }, sshPublicKeyIds: ["ssh-key-id-123"],}) {}observability.logs.enabled streams the container’s logs into Cloudflare’s
telemetry pipeline (queryable via the resource’s logs/tail operations),
and sshPublicKeyIds authorizes the listed keys to connect to instances for
debugging.
ContainerApplication: Scheduling & Placement
Section titled “ContainerApplication: Scheduling & Placement”Influence where and how Cloudflare schedules instances with schedulingPolicy,
constraints, and affinities.
export class Edge extends Cloudflare.Container<Edge>()("Edge", { main: import.meta.url, schedulingPolicy: "regional", constraints: { tier: 1 }, affinities: { colocation: "datacenter" },}) {}schedulingPolicy selects the control-plane placement strategy,
constraints.tier restricts which capacity tier instances may land on, and
affinities.colocation keeps related instances in the same datacenter to
reduce inter-instance latency.
ContainerApplication: Rollouts
Section titled “ContainerApplication: Rollouts”When an update changes the configuration, rollout controls how the new
version is rolled out across instances.
export class Api extends Cloudflare.Container<Api>()("Api", { main: import.meta.url, instances: 4, maxInstances: 4, rollout: { strategy: "rolling", stepPercentage: 25 },}) {}A rolling strategy with stepPercentage: 25 replaces instances in 25%
increments so the application stays available during the update; the default
immediate strategy swaps everything at once. Steps advance automatically
as new instances become healthy; each replaced instance receives SIGTERM
and has 15 minutes to shut down cleanly before SIGKILL.
Rollouts replace instances — they do not split requests between two image
versions (request-level traffic splitting exists one layer up, on the
Worker, via version.traffic). The fronting Worker and Durable Object cut
over immediately while instances roll, so keep the Worker-to-container
protocol compatible across both image versions until a rollout completes.