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 {@link https://alchemy.run/concepts/platform | Platform concept} page for how this fits into the async / effect / layer progression.
Container Layer
Section titled “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!"), ), }); }),);Image Sources
Section titled “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`,}) {}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))),) {}Configuration
Section titled “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) => ... }); }),);Stack-level wiring
Section titled “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)),);Calling from a Durable Object
Section titled “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 }), ), ),) {}HTTP Requests to Container Ports
Section titled “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 }), ), ),) {}