Skip to content

Cloudflare.Containers reference

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.

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!"),
),
});
}),
);

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/containersContainer. 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

src/worker.ts
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

alchemy.run.ts
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

src/worker.ts
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);
},
};

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 (+ optional dockerfile) — 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:buildcache
Deployed 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))),
) {}

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 },
}

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) => ... });
}),
);

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!),
},
}) {}

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 },
};
}),
) {}

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.

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.

alchemy.run.ts
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)),
);

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 }),
),
),
) {}

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.

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.

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.