Skip to content

Cloudflare Containers Deployment Guide

Cloudflare Containers Deployment Guide

This guide explains how to deploy the Bloqr Compiler to Cloudflare Containers.

Overview

Cloudflare Containers allows you to deploy Docker containers globally alongside your Workers. The container configuration is set up in wrangler.toml and the container image is defined in Dockerfile.container.


What is a Cloudflare Container? (And how is it different from Docker?)

This is one of the most commonly misunderstood aspects of the platform. Cloudflare Containers use a Docker/OCI image format, but the lifecycle and management model is completely different from a traditional Docker container.

The mental model

Cloudflare ContainerTraditional Docker Container
Image formatStandard OCI/Docker image ✅Standard OCI/Docker image ✅
You manage the hostNo — Cloudflare handles itYes — your server, Kubernetes, ECS, etc.
Startup triggerOn-demand, by a Durable ObjectManual, orchestrator, or scheduled
Always on?No — sleeps after idle timeoutCan be long-running, always-on
ScalingCloudflare handles globallyYou configure replicas/HPA/etc.
StateEphemeral by defaultCan be stateful with volumes
Your entire app stack on the image?No — only the computation layerYes, typically

Key point: Unlike a traditional “self-contained” Docker app where your entire stack (web server, app, dependencies) lives on the image, Cloudflare Containers are one layer of a larger Workers platform architecture. The Worker handles HTTP routing, auth, rate limiting, and CORS. The Container handles only the heavy computation that exceeds Worker CPU limits.

This app’s architecture

HTTP Request
→ Cloudflare Worker (stateless, handles auth/routing/rate-limiting)
→ BloqrCompiler Durable Object (stateful, owns container lifecycle)
→ Container (Linux process — runs container-server.ts)
→ WorkerCompiler (AGTree AST parsing, filter compilation)

The Durable Object is the “brain” — it has a 1:1 relationship with the Container instance and manages when it starts and stops. The Angular frontend and the SSR Worker (bloqr-frontend) never interact with the container directly.


When does the Container activate?

The container is not always running. It follows an on-demand lifecycle:

  1. A request hits POST /compile/container on the backend Worker
  2. The Worker resolves the BLOQR_COMPILER_DO Durable Object via env.BLOQR_COMPILER_DO.get(id)
  3. The Durable Object (which extends Container) automatically starts the container if it isn’t already running
  4. The container cold-starts from the Docker image (typically a few hundred milliseconds to ~2 seconds)
  5. The Durable Object proxies the request to container-server.ts on port 8787
  6. container-server.ts handles the compilation using WorkerCompiler and returns the result
  7. After 10 minutes of inactivity (sleepAfter = '10m'), the container is automatically suspended — no charges while idle

Why use a Container instead of a Worker directly?

Cloudflare Workers have a 10ms–30ms CPU time limit per request (soft/hard). For large blocklist compilations — full AGTree AST parsing across hundreds of sources — this limit can be hit. The Container has no such CPU time limit and runs as a normal Linux process.


UI Container Status Widget

The Angular frontend includes a real-time container status widget that keeps users informed during cold starts and warm-up delays.

How It Works

  • Backend: GET /api/container/status pings the container’s /health endpoint via the BLOQR_COMPILER_DO Durable Object binding and returns the current lifecycle state (running, starting, sleeping, error, or unavailable) along with the round-trip latency and a timestamp.
  • Frontend service: ContainerStatusService polls the endpoint at a configurable interval (default 5 s) using RxJS interval + switchMap. Polling cadence can be tuned per context — e.g. 2 s during active compilation, 30 s for background monitoring.
  • UI component: ContainerStatusWidgetComponent is a standalone, zoneless-compatible Angular component that renders a pulsing status dot, a label, optional latency, and an animated “waking up” message when the container is starting or sleeping.

Where It Appears

LocationPolling intervalNotes
Compiler page2 s (fast) during compilation, 30 s afterShown when container mode is selected or status is noteworthy
Admin Dashboard30 s (background)Dedicated “Container Runtime” card below System Health
Performance page15 sInline row inside the System Health card below the status chips

Status Values

StatusMeaning
runningContainer is warm and responded to /health
startingContainer is cold-starting (AbortError after timeout)
sleepingContainer is idle / stopped
errorContainer responded with a non-OK HTTP status
unavailableBLOQR_COMPILER_DO binding not present in this deployment

Known Gotchas

These are the most common configuration mistakes that cause silent or hard-to-diagnose failures.

1. Missing --platform=linux/amd64 in Dockerfile.container

Cloudflare Containers only runs linux/amd64 images. If you build on Apple Silicon (M1/M2/M3) or an ARM-based CI runner without pinning the platform, Docker will produce an arm64 image that will silently fail to start on Cloudflare.

The FROM line in Dockerfile.container must read:

FROM --platform=linux/amd64 denoland/deno:${DENO_VERSION}

2. enable_containers in wrangler.toml [dev]

The [dev] section of wrangler.toml contains an enable_containers flag:

[dev]
# Set to true on Linux/macOS or WSL. Must be false on native Windows
# because Cloudflare Containers requires a Linux Docker daemon.
enable_containers = true
  • Linux / macOS / WSLenable_containers = true (the default in this repo) runs containers in local wrangler dev. Docker Desktop on Mac with Apple Silicon uses Rosetta 2 to run the linux/amd64 image transparently.
  • Native Windows — set enable_containers = false and use WSL instead (see Windows Limitation).

3. CONTAINER_SECRET environment variable

The container server (worker/container-server.ts) requires the CONTAINER_SECRET environment variable to be set. Requests to POST /compile are rejected with 503 Service Unavailable if the variable is missing or with 401 Unauthorized if the header value doesn’t match.

Set it locally by adding a line to .dev.vars:

CONTAINER_SECRET=your-local-secret

For production, add it as a Worker Secret:

Terminal window
npx wrangler secret put CONTAINER_SECRET

Current Configuration

wrangler.toml

[[containers]]
class_name = "BloqrCompiler"
image = "./Dockerfile.container"
max_instances = 5
[[durable_objects.bindings]]
class_name = "BloqrCompiler"
name = "BLOQR_COMPILER_DO"
[[migrations]]
new_sqlite_classes = ["BloqrCompiler"]
tag = "v1"
[dev]
enable_containers = true

worker/worker.ts

The BloqrCompiler class extends the Container class from @cloudflare/containers:

import { Container } from '@cloudflare/containers';
export class BloqrCompiler extends Container {
override defaultPort = 8787;
override sleepAfter = '10m';
override onStart(): void {
console.log('[BloqrCompiler] Container started');
}
override onStop(_: { exitCode: number; reason: string }): void {
console.log('[BloqrCompiler] Container stopped');
}
override onError(error: unknown): void {
console.error('[BloqrCompiler] Container error:', error);
}
}

Dockerfile.container

A minimal Deno image that runs worker/container-server.ts — a lightweight HTTP server that handles compilation requests forwarded by the Worker.

Prerequisites

  1. Docker must be running — Wrangler uses Docker to build and push images
    Terminal window

docker info

If this fails, start Docker Desktop or your Docker daemon.
2. **Wrangler authentication** — Authenticate with your Cloudflare account:
```bash
deno task wrangler login
  1. Container support in your Cloudflare plan — Containers are available on the Workers Paid plan.

Deployment Steps

1. Deploy to Cloudflare

Terminal window
deno task wrangler:deploy

This command will:

  • Build the Docker container image from Dockerfile.container
  • Push the image to Cloudflare’s Container Registry (backed by R2)
  • Deploy your Worker with the container binding
  • Configure Cloudflare’s network to spawn container instances on-demand

2. Wait for Provisioning

After the first deployment, wait 2–3 minutes before making requests. Unlike Workers, containers take time to be provisioned across the edge network.

3. Check Deployment Status

Terminal window
npx wrangler containers list

This shows all containers in your account and their deployment status.

Local Development

Windows Limitation

Containers are not supported for local development on Windows. You have two options:

  1. Use WSL (Windows Subsystem for Linux)

    Terminal window
    wsl
    cd /mnt/d/source/bloqr-backend
    deno task wrangler:dev
  2. Disable containers for local dev (current configuration) The wrangler.toml has enable_containers = false in the [dev] section, which allows you to develop the Worker functionality locally without containers.

Local Development Without Containers

You can still test the Worker API locally:

Terminal window
deno task wrangler:dev

Visit http://localhost:8787 to access:

  • /api — API documentation
  • /compile — JSON compilation endpoint
  • /compile/stream — Streaming compilation with SSE
  • /compile/container — Container-proxied compilation endpoint
  • /metrics — Request metrics

Note: The BLOQR_COMPILER_DO Durable Object binding is available in local dev. With enable_containers = true (the default), wrangler dev will start the Docker container automatically. On native Windows without WSL, set enable_containers = false in the [dev] section of wrangler.toml.

Health Check

Use the container:health script to quickly verify that a running container server is healthy:

Terminal window
# Check local container (defaults to http://localhost:8787)
deno task container:health
# Check a deployed container with the compile smoke-test
deno task container:health -- --url https://bloqr-backend.jk-com.workers.dev --secret my-secret
# Override the request timeout
deno task container:health -- --url http://localhost:8787 --timeout 30

The script:

  1. Hits GET /health and validates the response shape ({ status: "ok", version: string }) with Zod.
  2. Optionally sends a minimal POST /compile smoke-test when --secret is provided.
  3. Prints a pass/fail summary and exits with code 0 (all pass) or 1 (any failure).

Container Architecture

The BloqrCompiler class in worker/worker.ts extends the Container base class from @cloudflare/containers, which handles container lifecycle, request proxying, and automatic restart:

import { Container } from '@cloudflare/containers';
export class BloqrCompiler extends Container {
defaultPort = 8787;
sleepAfter = '10m';
}

How It Works

  1. A request reaches the Cloudflare Worker (worker/worker.ts)
  2. The Worker passes the request to a BloqrCompiler Durable Object instance
  3. The BloqrCompiler (which extends Container) starts a container instance if one isn’t already running
  4. The container (Dockerfile.container) runs worker/container-server.ts — a Deno HTTP server
  5. The server handles the compilation request using WorkerCompiler and returns the result
  6. The container sleeps after 10 minutes of inactivity (sleepAfter = '10m')

Container Server Endpoints

worker/container-server.ts exposes:

MethodPathDescription
GET/healthLiveness probe — returns { status: 'ok' }
POST/compileCompile a filter list, returns plain text

Container API Route

The Worker exposes POST /compile/container which proxies requests to the BloqrCompiler Durable Object container via the BLOQR_COMPILER_DO binding.

Endpoint

POST /compile/container

Required Environment Variables

VariableDescription
BLOQR_COMPILER_DODurable Object binding to the container (configured in wrangler.toml)
CONTAINER_SECRETShared secret forwarded as X-Container-Secret header to authenticate Worker → Container requests

Set CONTAINER_SECRET locally in .dev.vars:

CONTAINER_SECRET=dev-local-secret

For production:

Terminal window
wrangler secret put CONTAINER_SECRET

Request

Same body as POST /compile — a JSON object matching CompileRequestSchema / ConfigurationSchema.

Response

  • 200 OKtext/plain compiled filter list output
  • 400 Bad Request — Invalid request body (Zod validation error from the container)
  • 401 UnauthorizedX-Container-Secret header mismatch
  • 503 Service UnavailableBLOQR_COMPILER_DO binding or CONTAINER_SECRET not configured, or container server has missing CONTAINER_SECRET

Middleware

The route applies the same middleware stack as other compile routes:

  1. bodySizeMiddleware() — enforces MAX_REQUEST_BODY_MB limit
  2. rateLimitMiddleware() — tier-based rate limiting
  3. turnstileMiddleware() — Cloudflare Turnstile human verification

Production Deployment Workflow

  1. Build and test locally (without containers)

    Terminal window
    deno task wrangler:dev
  2. Test Docker image (optional)

    Terminal window
    docker build -f Dockerfile.container -t bloqr-backend-container:test .
    docker run -p 8787:8787 bloqr-backend-container:test
    curl http://localhost:8787/health
  3. Deploy to Cloudflare

    Terminal window
    deno task wrangler:deploy
  4. Check deployment status

    Terminal window
    npx wrangler containers list
  5. Monitor logs

    Terminal window
    deno task wrangler:tail

Container Configuration Options

Scaling

[[containers]]
class_name = "BloqrCompiler"
image = "./Dockerfile.container"
max_instances = 5 # Maximum concurrent container instances

Sleep Timeout

Configured in worker/worker.ts on the BloqrCompiler class:

sleepAfter = '10m'; // Stop the container after 10 minutes of inactivity

Bindings Available

The container/worker has access to:

  • env.COMPILATION_CACHE — KV Namespace for caching compiled results
  • env.RATE_LIMIT — KV Namespace for rate limiting
  • env.METRICS — KV Namespace for metrics storage
  • env.FILTER_STORAGE — R2 Bucket for filter list storage
  • env.ASSETS — Static assets (HTML, CSS, JS)
  • env.COMPILER_VERSION — Version string
  • env.BLOQR_COMPILER_DO — Durable Object binding to container
  • env.CONTAINER_SECRET — Shared secret for Worker → Container authentication (X-Container-Secret header)

Cost Considerations

  • Containers are billed per millisecond of runtime (10ms granularity)
  • Automatically scale to zero when not in use (sleepAfter = '10m')
  • No charges when idle
  • Container registry storage is free (backed by R2)

Troubleshooting

Docker not running

Error: Docker is not running

Solution: Start Docker Desktop and run docker info to verify.

Container won’t provision

Error: Container failed to start

Solution:

  1. Check npx wrangler containers list for status
  2. Check container logs with deno task wrangler:tail
  3. Verify Dockerfile.container builds locally: docker build -f Dockerfile.container -t test .

Image architecture mismatch

If the container starts but immediately crashes (or Cloudflare reports an image error), the image was likely built for the wrong CPU architecture.

Cause: The FROM line in Dockerfile.container is missing the --platform=linux/amd64 flag. Builds on Apple Silicon or ARM CI runners default to arm64, which Cloudflare Containers cannot run.

Solution: Ensure Dockerfile.container uses:

FROM --platform=linux/amd64 denoland/deno:${DENO_VERSION}

Then rebuild and redeploy.

Container request body rejected (400 / 503)

  • 503 Service UnavailableCONTAINER_SECRET is not set in the container environment. Add it to .dev.vars for local dev or run npx wrangler secret put CONTAINER_SECRET for production.
  • 401 Unauthorized — The X-Container-Secret header sent by the Worker doesn’t match CONTAINER_SECRET. Ensure both sides use the same value.
  • 400 Bad Request with JSON details — The request body failed Zod schema validation. Inspect the details field in the JSON response for field-level error messages.

Module not found errors

If you see Cannot find module '@cloudflare/containers':

Solution: Run pnpm install to install the @cloudflare/containers package.

Next Steps

  1. Deploy to production:

    Terminal window
    deno task wrangler:deploy
  2. Set up custom domain (optional)

    Terminal window
    npx wrangler deployments domains add <your-domain>
  3. Monitor performance

    Terminal window
    deno task wrangler:tail
  4. Update container configuration as needed in wrangler.toml and worker/worker.ts

  5. Monitor container lifecycle — use the built-in container status widget on the Compiler page, Admin Dashboard, or Performance page. See the UI Container Status Widget section above for details.

Resources

Support

For issues or questions: