Skip to content

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.

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.

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.

main is bundled with rolldown at deploy time. Top-level calls in the effect, @effect/*, alchemy, @alchemy.run/*, and @distilled.cloud/* packages receive #__PURE__ annotations by default, so anything the container program doesn’t use from those packages is tree-shaken out of the bundle. Any other package — including your own app — is left untouched unless you list it explicitly.

Treat additional packages as pure

Pass package names (or picomatch globs) via build.pure.packages to annotate them in addition to the defaults.

{
main: import.meta.url,
build: {
pure: { packages: ["my-lib", "@my-scope/*"] },
},
}

Listing a package annotates calls whose result is bound (variable initializers, exports) — safe anywhere. If a listed package also declares "sideEffects": false (or []) in its package.json, that combination opts it into full annotation: top-level calls whose result is discarded (e.g. router.on("/path", handler) registrations) are also marked pure and deleted under minification when unused. Only list a sideEffects: false package if its modules really are free of meaningful top-level side effects. The effect, alchemy, and @distilled.cloud defaults declare exactly that, on purpose — their modules are designed to be fully tree-shakeable.

Disable pure annotations

{
main: import.meta.url,
build: { pure: false },
}

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.

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.

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.

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.

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.