Skip to content
GitHubXDiscordRSS

Container

Deploy Docker containers on Cloudflare's global network

A Container is a running Docker image running in Cloudflare’s global network, managed by a Cloudflare Durable Object.

You’ll need:

  1. a Dockerfile for your Container
  2. an alchemy.run.ts to deploy to Cloudflare
  3. a MyContainer class to own a running Container Instance
  4. a worker.ts that exports fetch and routes requests to Container Instances

A Container’s lifecycle is managed by a Durable Object class that you define.

We recommend using the Container class from @cloudflare/containers since it takes care of the basic container lifecycle for you:

import { Container } from "@cloudflare/containers";
import type { worker } from "../alchemy.run.ts";
export class MyContainer extends Container {
declare env: typeof worker.Env;
defaultPort = 8080; // The default port for the container to listen on
sleepAfter = "3m"; // Sleep the container if no requests are made in this timeframe
envVars = {
MESSAGE: "I was passed in via the container class!",
};
override onStart() {
console.log("Container successfully started");
}
override onStop() {
console.log("Container successfully shut down");
}
override onError(error: unknown) {
console.log("Container error:", error);
}
}

Now, create a Container Resource in your alchemy.run.ts file and connect it to your MyContainer class:

import { Container, Worker } from "alchemy/cloudflare";
import { Image } from "alchemy/docker";
// import the type of your Container's implementation
import type { MyContainer } from "./src/container.ts";
const container = await Container<MyContainer>("my-container", {
className: "MyContainer", // <- and ^
});

This will build your Dockerfile and prepare it for publishing to Cloudflare’s Image Registry.

You can use a pre-built image by passing an image reference to the image property:

const container = await Container<MyContainer>("my-container", {
className: "MyContainer",
image: "alpine:latest",
});
const image = await Image("my-image", {
name: "my-image",
tag: "latest",
build: {
context: path.join(import.meta.dirname, "container"),
},
});
const container = await Container<MyContainer>("my-container", {
className: "MyContainer",
image,
});
const remoteImage = await RemoteImage("alpine", {
image: "alpine:latest"
});
const container = await Container<MyContainer>("my-container", {
className: "MyContainer",
image: remoteImage,
});

By default, if a container application with the same name already exists, Alchemy will throw an error. However, you can use the adopt property to take over management of an existing container application:

const container = await Container<MyContainer>("my-container", {
className: "MyContainer",
adopt: true, // Will adopt existing container instead of failing
});

Container resources have special behavior in development mode to support local testing:

By default, when running in dev mode (using --dev flag), Container images are not pushed to Cloudflare’s registry. Instead, they use a local cloudflare-dev/ prefix that Miniflare can read from your local Docker daemon:

const container = await Container<MyContainer>("my-container", {
className: "MyContainer",
// In dev mode: uses local Docker image with cloudflare-dev/ prefix
// In production: pushes to Cloudflare's registry
});

If you need to use a Container with a remote Worker during development, set dev: { remote: true } to push the image to Cloudflare:

const container = await Container<MyContainer>("my-container", {
className: "MyContainer",
dev: {
remote: true // Forces push to Cloudflare registry even in dev mode
}
});

[!WARNING] Limitations with Remote Containers

  • If you set dev: { remote: true } on a Container, it cannot be used as a local binding in development
  • Remote container bindings are not supported for locally emulated workers

When calling container.fetch() while the container is still starting up, you may see this warning in the console:

Error checking if container is ready: connect(): Connection refused: container port not found. Make sure you exposed the port in your container definition.

This warning can be safely ignored - the binding still works correctly and this is expected behavior during container startup.

You can configure rollout strategies for container updates using the rollout property. This controls how updates are deployed across instances.

For immediate updates that apply to all instances at once:

const container = await Container<MyContainer>("my-container", {
className: "MyContainer",
rollout: {
strategy: "immediate",
},
});

For gradual updates that reduce risk by updating instances incrementally:

const container = await Container<MyContainer>("my-container", {
className: "MyContainer",
rollout: {
strategy: "rolling",
stepPercentage: 25, // Update 25% of instances at a time
},
});
PropertyTypeDefaultDescription
strategy"rolling" | "immediate""rolling"The rollout strategy - “immediate” updates all at once, “rolling” updates gradually
kind"full_auto""full_auto"Automation level - proceeds without manual intervention
stepPercentagenumber25Percentage of instances to update in each step (1-100). Ignored when strategy is “immediate”

The target configuration (image, instance type, observability) is automatically derived from the container’s own configuration - you don’t need to specify it manually.

To deploy the Container to Cloudflare, you need to bind it to a Worker:

export const worker = await Worker("my-worker", {
name: "my-worker",
entrypoint: "./src/worker.ts",
bindings: {
MY_CONTAINER: container,
},
});

[!NOTE] Binding a Container to a Worker will also bind a Durable Object Namespace to the Worker.

To route requests, have your Worker’s fetch handler resolve a Durable Object instance and proxy the request to it:

import { getContainer } from "@cloudflare/containers";
import type { worker } from "../alchemy.run.ts";
// the class must be exported for Cloudflare
export { MyContainer } from "./container.ts";
export default {
async fetch(request: Request, env: typeof worker.Env): Promise<Response> {
const container = getContainer(env.CONTAINER, "container");
return container.fetch(request);
},
};

[!TIP] Notice how the type of our Worker environment is inferred with typeof worker.Env, see the Type-safe Bindings documentation for more information.

Cloudflare’s unique design allows you to implement your own routing strategies in pure JavaScript.

For example, you can round-robin requests across a fixed pool by simply generating a random instance ID between 0 and the number of instances:

export async function loadBalance<T extends Container>(
binding: DurableObjectNamespace<T>,
instances = 3
): Promise<DurableObjectStub<T>> {
const containerId = binding.idFromName(`instance-${rand(0, instances)}`);
const container = binding.get(containerId);
return container.fetch(request);
}