Skip to content

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.

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

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

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

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

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

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