Docker to Cloudflare Migration

Replace your entire Docker Compose stack with Cloudflare services. No containers, no orchestration, no infrastructure to manage — just Python code deployed globally in 30 seconds.

Your Typical Docker Compose Stack

Here's what a typical Python application looks like in Docker — and why each service is no longer needed:

# docker-compose.yml
version: '3.8'
services:
  nginx:
    image: nginx:alpine          # ❌ Not needed - Workers serve static assets
    volumes:
      - ./static:/usr/share/nginx/html
      - ./nginx.conf:/etc/nginx/nginx.conf

  app:
    build: .
    image: myapp:latest          # ❌ Not needed - Deploy Python directly
    command: gunicorn app:app --workers 4  # ❌ No WSGI server needed
    environment:
      - DATABASE_URL=postgresql://...

  redis:
    image: redis:alpine          # ❌ Use Workers KV instead

  postgres:
    image: postgres:14           # ❌ Use D1 or Hyperdrive
    volumes:
      - pgdata:/var/lib/postgresql/data

  celery:
    build: .
    command: celery -A tasks worker  # ❌ Use Queues instead

  celery-beat:
    build: .
    command: celery -A tasks beat    # ❌ Use Cron Triggers

  prometheus:
    image: prom/prometheus       # ❌ Use Analytics Engine

  grafana:
    image: grafana/grafana       # ❌ Built-in analytics

The Cloudflare Equivalent

# main.py - Your entire "stack" in one file
from js import Response, env
from fastapi import FastAPI
import asyncio

app = FastAPI()

# Serve static assets directly
@app.get("/static/{path:path}")
async def static(path: str):
    asset = await env.ASSETS.get(path)
    return Response.new(asset.body, headers={
        "Content-Type": asset.httpMetadata.contentType
    })

# Your app logic
@app.post("/api/process")
async def process(data: dict):
    # Cache in KV (replaces Redis)
    await env.KV.put(f"cache:{data['id']}", data)

    # Queue background work (replaces Celery)
    await env.QUEUE.send(data)

    # Store in D1 (replaces Postgres)
    await env.DB.prepare(
        "INSERT INTO items (data) VALUES (?)"
    ).bind(data).run()

    return {"status": "ok"}

Service-by-Service Mapping

Docker Service Purpose Cloudflare Replacement What Changes
nginx Reverse proxy, static files, SSL Workers built-in No config files, automatic SSL, global CDN
gunicorn/uwsgi WSGI server Workers runtime No process management, auto-scaling
redis Caching, sessions, queues Workers KV Globally distributed, no memory limits
postgres/mysql Primary database D1 or Hyperdrive Serverless or accelerated existing DB
celery workers Background tasks Queues No broker needed, automatic retries
celery beat Scheduled tasks Cron Triggers Simple cron syntax, guaranteed execution
elasticsearch Full-text search R2 + Workers AI Use embeddings for semantic search
rabbitmq Message broker Queues Direct producer-consumer, no broker
memcached In-memory cache Workers KV Persistent, globally distributed
haproxy Load balancer Cloudflare LB Automatic, no configuration
prometheus Metrics collection Analytics Engine No scraping, unlimited cardinality
grafana Visualization Cloudflare Dashboard Built-in analytics, API access

Volume Mounts Become Bindings

Docker Volumes

volumes:
  - ./static:/app/static       # Static files
  - ./uploads:/app/uploads     # User uploads
  - pgdata:/var/lib/postgresql/data  # Database
  - ./logs:/app/logs           # Log files

Cloudflare Bindings

# wrangler.toml
[[kv_namespaces]]
binding = "STATIC"     # Replaces static file volume

[[r2_buckets]]
binding = "UPLOADS"    # Replaces uploads volume

[[d1_databases]]
binding = "DB"         # Replaces database volume

# Logs automatically available via wrangler tail

Environment Variables & Secrets

Docker Approach

# docker-compose.yml
environment:
  - DATABASE_URL=${DATABASE_URL}
  - REDIS_URL=${REDIS_URL}
  - SECRET_KEY=${SECRET_KEY}
  - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}

Cloudflare Approach

# Set secrets (encrypted at rest)
wrangler secret put DATABASE_URL
wrangler secret put API_KEY

# Access in code
async def on_fetch(request):
    db_url = env.DATABASE_URL  # Securely injected

Development Workflow Comparison

Docker Workflow

# Local development
docker-compose up -d
docker-compose logs -f app
docker-compose exec app pytest
docker-compose down

# Build and push
docker build -t myapp:latest .
docker push registry.com/myapp:latest

# Deploy (still need k8s, ECS, etc.)
kubectl apply -f k8s/

Cloudflare Workflow

# Install wrangler (once)
bun install -g wrangler

# Local development
wrangler dev main.py  # Hot reload included

# Run tests
python -m pytest  # Just regular Python

# Deploy to production
wrangler deploy  # 30 seconds to global deployment

Migration Example: FastAPI + Gunicorn + Nginx

Before

# app.py
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/api/users/{user_id}")
async def get_user(user_id: int):
    # Database query
    return {"user_id": user_id}

# + Dockerfile, nginx.conf, gunicorn config, k8s manifests...

After

# main.py
from fastapi import FastAPI
from js import Response

app = FastAPI()

@app.get("/api/users/{user_id}")
async def get_user(user_id: int):
    result = await env.DB.prepare(
        "SELECT * FROM users WHERE id = ?"
    ).bind(user_id).first()
    return {"user": result}

# That's it. Deploy with: wrangler deploy