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 Container | Traditional Docker Container | |
|---|---|---|
| Image format | Standard OCI/Docker image ✅ | Standard OCI/Docker image ✅ |
| You manage the host | No — Cloudflare handles it | Yes — your server, Kubernetes, ECS, etc. |
| Startup trigger | On-demand, by a Durable Object | Manual, orchestrator, or scheduled |
| Always on? | No — sleeps after idle timeout | Can be long-running, always-on |
| Scaling | Cloudflare handles globally | You configure replicas/HPA/etc. |
| State | Ephemeral by default | Can be stateful with volumes |
| Your entire app stack on the image? | No — only the computation layer | Yes, 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:
- A request hits
POST /compile/containeron the backend Worker - The Worker resolves the
BLOQR_COMPILER_DODurable Object viaenv.BLOQR_COMPILER_DO.get(id) - The Durable Object (which extends
Container) automatically starts the container if it isn’t already running - The container cold-starts from the Docker image (typically a few hundred milliseconds to ~2 seconds)
- The Durable Object proxies the request to
container-server.tson port 8787 container-server.tshandles the compilation usingWorkerCompilerand returns the result- 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/statuspings the container’s/healthendpoint via theBLOQR_COMPILER_DODurable Object binding and returns the current lifecycle state (running,starting,sleeping,error, orunavailable) along with the round-trip latency and a timestamp. - Frontend service:
ContainerStatusServicepolls the endpoint at a configurable interval (default 5 s) using RxJSinterval+switchMap. Polling cadence can be tuned per context — e.g. 2 s during active compilation, 30 s for background monitoring. - UI component:
ContainerStatusWidgetComponentis 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
| Location | Polling interval | Notes |
|---|---|---|
| Compiler page | 2 s (fast) during compilation, 30 s after | Shown when container mode is selected or status is noteworthy |
| Admin Dashboard | 30 s (background) | Dedicated “Container Runtime” card below System Health |
| Performance page | 15 s | Inline row inside the System Health card below the status chips |
Status Values
| Status | Meaning |
|---|---|
running | Container is warm and responded to /health |
starting | Container is cold-starting (AbortError after timeout) |
sleeping | Container is idle / stopped |
error | Container responded with a non-OK HTTP status |
unavailable | BLOQR_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 / WSL —
enable_containers = true(the default in this repo) runs containers in localwrangler dev. Docker Desktop on Mac with Apple Silicon uses Rosetta 2 to run thelinux/amd64image transparently. - Native Windows — set
enable_containers = falseand 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-secretFor production, add it as a Worker Secret:
npx wrangler secret put CONTAINER_SECRETCurrent 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 = trueworker/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
- 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: ```bashdeno task wrangler login- Container support in your Cloudflare plan — Containers are available on the Workers Paid plan.
Deployment Steps
1. Deploy to Cloudflare
deno task wrangler:deployThis 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
npx wrangler containers listThis 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:
-
Use WSL (Windows Subsystem for Linux)
Terminal window wslcd /mnt/d/source/bloqr-backenddeno task wrangler:dev -
Disable containers for local dev (current configuration) The
wrangler.tomlhasenable_containers = falsein 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:
deno task wrangler:devVisit 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:
# Check local container (defaults to http://localhost:8787)deno task container:health
# Check a deployed container with the compile smoke-testdeno task container:health -- --url https://bloqr-backend.jk-com.workers.dev --secret my-secret
# Override the request timeoutdeno task container:health -- --url http://localhost:8787 --timeout 30The script:
- Hits
GET /healthand validates the response shape ({ status: "ok", version: string }) with Zod. - Optionally sends a minimal
POST /compilesmoke-test when--secretis provided. - Prints a pass/fail summary and exits with code
0(all pass) or1(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
- A request reaches the Cloudflare Worker (
worker/worker.ts) - The Worker passes the request to a
BloqrCompilerDurable Object instance - The
BloqrCompiler(which extendsContainer) starts a container instance if one isn’t already running - The container (
Dockerfile.container) runsworker/container-server.ts— a Deno HTTP server - The server handles the compilation request using
WorkerCompilerand returns the result - The container sleeps after 10 minutes of inactivity (
sleepAfter = '10m')
Container Server Endpoints
worker/container-server.ts exposes:
| Method | Path | Description |
|---|---|---|
| GET | /health | Liveness probe — returns { status: 'ok' } |
| POST | /compile | Compile 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
| Variable | Description |
|---|---|
BLOQR_COMPILER_DO | Durable Object binding to the container (configured in wrangler.toml) |
CONTAINER_SECRET | Shared secret forwarded as X-Container-Secret header to authenticate Worker → Container requests |
Set CONTAINER_SECRET locally in .dev.vars:
CONTAINER_SECRET=dev-local-secretFor production:
wrangler secret put CONTAINER_SECRETRequest
Same body as POST /compile — a JSON object matching CompileRequestSchema / ConfigurationSchema.
Response
200 OK—text/plaincompiled filter list output400 Bad Request— Invalid request body (Zod validation error from the container)401 Unauthorized—X-Container-Secretheader mismatch503 Service Unavailable—BLOQR_COMPILER_DObinding orCONTAINER_SECRETnot configured, or container server has missingCONTAINER_SECRET
Middleware
The route applies the same middleware stack as other compile routes:
bodySizeMiddleware()— enforcesMAX_REQUEST_BODY_MBlimitrateLimitMiddleware()— tier-based rate limitingturnstileMiddleware()— Cloudflare Turnstile human verification
Production Deployment Workflow
-
Build and test locally (without containers)
Terminal window deno task wrangler:dev -
Test Docker image (optional)
Terminal window docker build -f Dockerfile.container -t bloqr-backend-container:test .docker run -p 8787:8787 bloqr-backend-container:testcurl http://localhost:8787/health -
Deploy to Cloudflare
Terminal window deno task wrangler:deploy -
Check deployment status
Terminal window npx wrangler containers list -
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 instancesSleep Timeout
Configured in worker/worker.ts on the BloqrCompiler class:
sleepAfter = '10m'; // Stop the container after 10 minutes of inactivityBindings Available
The container/worker has access to:
env.COMPILATION_CACHE— KV Namespace for caching compiled resultsenv.RATE_LIMIT— KV Namespace for rate limitingenv.METRICS— KV Namespace for metrics storageenv.FILTER_STORAGE— R2 Bucket for filter list storageenv.ASSETS— Static assets (HTML, CSS, JS)env.COMPILER_VERSION— Version stringenv.BLOQR_COMPILER_DO— Durable Object binding to containerenv.CONTAINER_SECRET— Shared secret for Worker → Container authentication (X-Container-Secretheader)
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 runningSolution: Start Docker Desktop and run docker info to verify.
Container won’t provision
Error: Container failed to startSolution:
- Check
npx wrangler containers listfor status - Check container logs with
deno task wrangler:tail - Verify
Dockerfile.containerbuilds 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 Unavailable —
CONTAINER_SECRETis not set in the container environment. Add it to.dev.varsfor local dev or runnpx wrangler secret put CONTAINER_SECRETfor production. - 401 Unauthorized — The
X-Container-Secretheader sent by the Worker doesn’t matchCONTAINER_SECRET. Ensure both sides use the same value. - 400 Bad Request with JSON details — The request body failed Zod schema validation. Inspect the
detailsfield 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
-
Deploy to production:
Terminal window deno task wrangler:deploy -
Set up custom domain (optional)
Terminal window npx wrangler deployments domains add <your-domain> -
Monitor performance
Terminal window deno task wrangler:tail -
Update container configuration as needed in
wrangler.tomlandworker/worker.ts -
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
- Cloudflare Containers Documentation
- @cloudflare/containers package
- Wrangler CLI Documentation
- Container Examples
- Containers Limits
Support
For issues or questions:
- GitHub Issues: https://github.com/jaypatrick/bloqr-compiler/issues
- Cloudflare Discord: https://discord.gg/cloudflaredev