Déployer des conteneurs Docker sur le réseau edge de Cloudflare avec Cloudflare Containers

De vrais conteneurs Docker, sur le edge. Cloudflare Containers exécute de vraies images OCI dans plus de 300 emplacements mondiaux de Cloudflare, orchestrés par les Workers. Pas de Kubernetes, pas de gestion de cluster, pas de démarrage à froid. Vous payez par requête et par seconde de calcul — uniquement quand le trafic atteint votre conteneur.
Ce que vous allez construire
Dans ce tutoriel, vous allez déployer une API de traitement d'images Node.js + Express conteneurisée sur Cloudflare Containers. Vous routerez les requêtes via un Worker, ferez monter en charge les conteneurs automatiquement selon le trafic, et connecterez l'ensemble à KV pour le cache. À la fin, vous aurez un service edge prêt pour la production capable d'exécuter des charges lourdes (manipulation d'images, génération PDF, inférence ML) au plus près des utilisateurs partout dans le monde.
Caractéristiques de la stack finale :
- Une vraie image Docker exécutant une API Express (Sharp pour le redimensionnement d'images)
- Un Worker servant de porte d'entrée, de routeur, de cache et de limiteur de débit
- Mise à l'échelle automatique par région selon le volume de requêtes
- Cache des réponses adossé à KV pour réduire les invocations de conteneur
- Sondes de santé, journaux structurés, observabilité via les tableaux de bord Cloudflare
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (télécharger)
- Docker Desktop lancé en local (installer Docker)
- Un compte Cloudflare sur le plan Workers Paid (Containers le requiert — environ cinq dollars par mois)
- Wrangler CLI v4+ — l'outil développeur de Cloudflare
- Une certaine familiarité avec les bases de Docker et TypeScript
- Un éditeur de code (VS Code recommandé)
Cloudflare Containers est disponible en disponibilité générale depuis début 2026. Chaque instance de conteneur dispose jusqu'à 4 vCPU et 8 Go de RAM, et vous ne payez que les secondes durant lesquelles votre conteneur sert activement des requêtes, plus une petite redevance par requête.
Étape 1 : Installer Wrangler et s'authentifier
Ouvrez votre terminal et installez la dernière version de Wrangler globalement :
npm install -g wrangler@latest
wrangler --version
# wrangler 4.x.xConnectez-vous à votre compte Cloudflare :
wrangler loginUn onglet de navigateur s'ouvre pour autoriser le CLI. Une fois autorisé, vérifiez l'accès :
wrangler whoamiVous verrez l'e-mail du compte et son identifiant. Copiez l'identifiant du compte — vous en aurez besoin plus tard.
Étape 2 : Créer la structure du projet
Créez un nouveau dossier et initialisez le projet :
mkdir image-edge-api
cd image-edge-api
npm init -yInstallez les dépendances d'exécution de l'application conteneur :
npm install express sharp
npm install -D typescript @types/node @types/express tsxCréez la structure de fichiers :
mkdir -p container/src worker/src
touch container/src/index.ts container/Dockerfile
touch worker/src/index.ts wrangler.jsonc
touch tsconfig.json .dockerignoreAjoutez un tsconfig.json simple :
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["container/src", "worker/src"]
}Et un .dockerignore pour ne pas alourdir l'image :
node_modules
dist
.git
.env
*.md
worker
Étape 3 : Construire l'API conteneurisée
Ouvrez container/src/index.ts et écrivez une petite application Express qui redimensionne des images :
import express, { Request, Response } from "express";
import sharp from "sharp";
const app = express();
const PORT = Number(process.env.PORT) || 8080;
app.use(express.raw({ type: "image/*", limit: "10mb" }));
app.get("/health", (_req: Request, res: Response) => {
res.json({ status: "ok", region: process.env.CF_REGION ?? "unknown" });
});
app.post("/resize", async (req: Request, res: Response) => {
const width = Number(req.query.w) || 800;
const format = (req.query.fmt as string) || "webp";
if (!Buffer.isBuffer(req.body) || req.body.length === 0) {
return res.status(400).json({ error: "no image body" });
}
try {
const output = await sharp(req.body)
.resize({ width, withoutEnlargement: true })
.toFormat(format as keyof sharp.FormatEnum)
.toBuffer();
res.set("Content-Type", `image/${format}`);
res.set("X-Container-Region", process.env.CF_REGION ?? "unknown");
res.send(output);
} catch (err) {
console.error("resize_failed", err);
res.status(500).json({ error: "resize failed" });
}
});
app.listen(PORT, () => {
console.log(`image-edge-api listening on :${PORT}`);
});Notez que nous lisons CF_REGION depuis l'environnement — Cloudflare injecte automatiquement cette variable dans les conteneurs en cours d'exécution, ce qui aide à déboguer le routage régional.
Étape 4 : Écrire le Dockerfile
Cloudflare Containers accepte n'importe quelle image OCI standard. Utilisez une image de base Node légère et un build multi-étapes pour garder l'image finale fine.
# container/Dockerfile
FROM node:20-bookworm-slim AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY container/src ./container/src
RUN npx tsc -p tsconfig.json
FROM node:20-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
EXPOSE 8080
CMD ["node", "dist/container/src/index.js"]Construisez-le localement pour vérifier que tout compile :
docker build -t image-edge-api -f container/Dockerfile .
docker run --rm -p 8080:8080 image-edge-apiDans un autre terminal, frappez le point de santé :
curl http://localhost:8080/health
# {"status":"ok","region":"unknown"}Pressez Ctrl+C pour arrêter le conteneur local.
Cloudflare Containers exige des images linux/amd64. Si vous êtes sur Apple Silicon, construisez avec le drapeau de plateforme : docker build --platform=linux/amd64 ...
Étape 5 : Configurer wrangler.jsonc pour Containers
Voici la nouvelle pièce importante. Cloudflare expose les conteneurs comme un binding à l'intérieur des Workers — votre Worker est l'orchestrateur qui décide quelle instance traite chaque requête.
Ouvrez wrangler.jsonc :
{
"name": "image-edge-api",
"main": "worker/src/index.ts",
"compatibility_date": "2026-04-15",
"containers": [
{
"class_name": "ImageContainer",
"image": "./container/Dockerfile",
"instance_type": "standard",
"max_instances": 25
}
],
"durable_objects": {
"bindings": [
{
"name": "IMAGE_CONTAINER",
"class_name": "ImageContainer"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["ImageContainer"]
}
],
"kv_namespaces": [
{
"binding": "IMAGE_CACHE",
"id": "REPLACE_WITH_KV_ID"
}
],
"observability": { "enabled": true }
}Quelques points importants à comprendre :
- Le bloc
containersindique à Cloudflare quel Dockerfile construire et la taille de chaque instance. Les types d'instance sontdev,basic,standardetenhanced— choisissez selon vos besoins en mémoire et CPU. - Les conteneurs sont exposés via les Durable Objects. Chaque conteneur est encapsulé dans une classe Durable Object, ce qui apporte gratuitement isolation, ancrage régional et hooks de cycle de vie.
max_instanceslimite le nombre de conteneurs concurrents que Cloudflare peut lancer mondialement.
Créez l'espace de noms KV et remplacez la valeur fictive :
wrangler kv namespace create IMAGE_CACHECopiez l'identifiant retourné dans wrangler.jsonc.
Étape 6 : Écrire l'orchestrateur Worker
Le Worker est le point d'entrée public. Il reçoit chaque requête, applique la mise en cache et la validation, puis transfère le trafic au conteneur.
Ouvrez worker/src/index.ts :
import { Container, getContainer } from "@cloudflare/containers";
export interface Env {
IMAGE_CONTAINER: DurableObjectNamespace<ImageContainer>;
IMAGE_CACHE: KVNamespace;
}
export class ImageContainer extends Container {
defaultPort = 8080;
sleepAfter = "5m";
override onStart() {
console.log("container_started", { id: this.ctx.id.toString() });
}
override onError(err: unknown) {
console.error("container_error", err);
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/health") {
const container = getContainer(env.IMAGE_CONTAINER, "primary");
return container.fetch(request);
}
if (url.pathname === "/resize" && request.method === "POST") {
const cacheKey = await buildCacheKey(request);
const cached = await env.IMAGE_CACHE.get(cacheKey, "stream");
if (cached) {
return new Response(cached, {
headers: { "Content-Type": "image/webp", "X-Cache": "HIT" },
});
}
const container = getContainer(env.IMAGE_CONTAINER, "primary");
const upstream = await container.fetch(request);
if (upstream.ok) {
const buf = await upstream.arrayBuffer();
await env.IMAGE_CACHE.put(cacheKey, buf, { expirationTtl: 86400 });
return new Response(buf, {
headers: {
"Content-Type": upstream.headers.get("Content-Type") ?? "image/webp",
"X-Cache": "MISS",
},
});
}
return upstream;
}
return new Response("Not Found", { status: 404 });
},
};
async function buildCacheKey(request: Request): Promise<string> {
const url = new URL(request.url);
const body = await request.clone().arrayBuffer();
const hash = await crypto.subtle.digest("SHA-256", body);
const hex = [...new Uint8Array(hash)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return `${url.search}:${hex}`;
}Installez le paquet d'aide Cloudflare Containers :
npm install @cloudflare/containersTrois comportements méritent d'être soulignés :
getContainer(namespace, name)retourne un stub lié à une instance de conteneur spécifique. Passez le même nom pour un routage collant, ou aléatoirisez-le pour répartir la charge.sleepAfterdit à Cloudflare d'arrêter le conteneur après cinq minutes d'inactivité. Vous arrêtez de payer quand personne ne l'utilise, et les redémarrages à chaud durent généralement moins de 300 millisecondes.- Le Worker gère le cache en amont du conteneur. Une lecture KV coûte une fraction du calcul d'un conteneur, donc mettre en cache les transformations courantes est un puissant levier d'économies.
Étape 7 : Développement local avec Wrangler
L'environnement de développement local de Cloudflare lance désormais de vrais conteneurs via Docker — pas besoin de mocker.
wrangler devWrangler va :
- Construire l'image Docker localement
- Lancer une instance conteneurisée
- Démarrer le Worker sur http://localhost:8787
- Faire transiter vos requêtes HTTP par l'orchestrateur
Testez le flux complet :
curl http://localhost:8787/health
# {"status":"ok","region":"local"}
curl -X POST "http://localhost:8787/resize?w=400&fmt=webp" \
--data-binary "@./sample.jpg" \
-H "Content-Type: image/jpeg" \
-o resized.webpVous avez désormais une stack pleinement fonctionnelle — Worker plus conteneur — qui tourne sur votre machine et se comporte exactement comme en production.
Étape 8 : Déployer sur le edge de Cloudflare
Une fois satisfait du comportement local, poussez tout vers la production :
wrangler deployWrangler construit l'image Docker, la pousse vers le registre de conteneurs Cloudflare, enregistre la classe Durable Object, et déploie le Worker. Le premier déploiement peut prendre trois à cinq minutes parce que le push d'image n'est pas encore mis en cache. Les déploiements suivants prennent généralement moins d'une minute.
À la fin du déploiement, vous verrez quelque chose comme :
Deployed image-edge-api to:
https://image-edge-api.<your-subdomain>.workers.dev
Container instance class: ImageContainer (standard, max 25 instances)
Frappez votre endpoint en production :
curl -X POST "https://image-edge-api.<subdomain>.workers.dev/resize?w=800" \
--data-binary "@./sample.jpg" \
-H "Content-Type: image/jpeg" \
-o output.webpLa première requête déclenche un démarrage à froid du conteneur, qui se termine généralement en environ 600 millisecondes. Les requêtes suivantes réutilisent l'instance chaude et se terminent en quelques dizaines de millisecondes.
Étape 9 : Configurer l'auto-scaling et le routage régional
Par défaut, Cloudflare maintient une seule instance « primary » par région. Pour une charge réelle, vous voudrez des règles de mise à l'échelle explicites.
Mettez à jour le bloc containers dans wrangler.jsonc :
"containers": [
{
"class_name": "ImageContainer",
"image": "./container/Dockerfile",
"instance_type": "standard",
"max_instances": 50,
"scale_rules": {
"concurrent_requests": 30,
"cool_down_seconds": 60
},
"regions": ["wnam", "enam", "weu", "eeu", "apac", "mena"]
}
]Ce que fait cette configuration :
- Cloudflare lance un conteneur supplémentaire dans toute région dont les instances dépassent 30 requêtes concurrentes.
- Après 60 secondes de charge sous le seuil, les instances inactives sont arrêtées.
- Le tableau
regionsrestreint les conteneurs aux régions Cloudflare listées — utile pour la résidence des données ou pour éviter de servir le trafic depuis des régions où la réduction de latence ne se ferait pas sentir.
Pour un routage collant (charges liées à la session par exemple), passez un nom stable à getContainer. Pour des tâches embarrassingly parallèles, aléatoirisez-le :
const id = crypto.randomUUID();
const container = getContainer(env.IMAGE_CONTAINER, id);Redéployez pour appliquer les changements :
wrangler deployÉtape 10 : Sondes de santé, journaux et observabilité
Cloudflare redémarre automatiquement les conteneurs malsains, mais vous décidez ce que « sain » signifie. Étendez ImageContainer dans worker/src/index.ts :
export class ImageContainer extends Container {
defaultPort = 8080;
sleepAfter = "5m";
healthCheck = {
path: "/health",
intervalSeconds: 30,
timeoutSeconds: 5,
failureThreshold: 3,
};
override onHealthCheckFailed(err: unknown) {
console.error("health_check_failed", err);
}
}Côté logs, le binding d'observabilité du Worker ("observability": { "enabled": true } dans wrangler.jsonc) diffuse automatiquement chaque console.log du Worker et du conteneur vers le tableau de bord Cloudflare. Suivez les logs en temps réel :
wrangler tailVous verrez des événements structurés à mesure que le trafic circule :
container_started { id: "abc123..." }
resize ok width=800 region="weu" duration_ms=42
Pour des analyses plus poussées, expédiez les logs vers un puits externe (Datadog, Axiom, S3) avec Logpush — configurez-le une fois dans le tableau de bord et Cloudflare gère le batching à votre place.
Étape 11 : Sécurité et secrets
Ne stockez jamais d'identifiants dans l'image Docker. Utilisez les secrets chiffrés de Wrangler, qui apparaissent comme variables d'environnement à l'intérieur du conteneur :
wrangler secret put IMGBB_API_KEY
# collez la valeur, pressez EntréeDans container/src/index.ts :
const apiKey = process.env.IMGBB_API_KEY;
if (!apiKey) {
console.error("missing_api_key");
process.exit(1);
}Pour l'authentification entrante, validez côté Worker avant d'invoquer le conteneur :
const token = request.headers.get("Authorization");
if (token !== `Bearer ${env.SHARED_TOKEN}`) {
return new Response("Unauthorized", { status: 401 });
}Ce schéma économise de l'argent — les requêtes non autorisées n'atteignent jamais le conteneur, donc vous n'êtes pas facturé pour du calcul sur du trafic rejeté.
Tester votre implémentation
Lancez un petit test de charge contre votre endpoint en production avec hey :
hey -n 1000 -c 50 -m POST \
-H "Content-Type: image/jpeg" \
-D ./sample.jpg \
"https://image-edge-api.<subdomain>.workers.dev/resize?w=400"Dans le tableau de bord Cloudflare, vous devriez observer :
- Plusieurs instances de conteneurs apparaissant à travers les régions à mesure que la concurrence monte
- Le taux de hits cache qui grimpe à mesure que des requêtes répétées sont résolues depuis KV
- Une latence p50 sous 80 millisecondes pour les requêtes chaudes, p99 sous 500 millisecondes
Si vous voyez des échecs de mise à l'échelle ou des pics 5xx, vérifiez wrangler tail et l'onglet Containers du tableau de bord pour les compteurs de redémarrages et la pression sur les ressources.
Dépannage
Le conteneur échoue avec le code 137. Mémoire insuffisante. Passez à un type d'instance plus grand (enhanced) ou profilez votre image avec docker stats.
Démarrages à froid de plus de deux secondes. Votre image est trop grosse. Les builds multi-étapes, les images de base slim et l'élagage des dépendances de dev réduisent généralement la taille de 60 à 80 pour cent.
Le Worker renvoie « container not bound ». La migration Durable Object n'a pas été appliquée. Vérifiez que migrations dans wrangler.jsonc inclut la classe et redéployez.
wrangler dev reste bloqué sur « starting container ». Docker Desktop n'est pas lancé ou votre image cible la mauvaise architecture. Construisez explicitement avec --platform=linux/amd64.
Le cache KV renvoie des résultats périmés. Augmentez expirationTtl, ou ajoutez un hash de contenu à la clé de cache afin que des entrées différentes n'entrent jamais en collision.
Étapes suivantes
Vous avez maintenant une stack Cloudflare Containers fonctionnelle. Voici quelques pistes pour aller plus loin :
- Ajoutez une file avec Cloudflare Queues pour le traitement par lots asynchrone
- Branchez une base de données — voir notre tutoriel Cloudflare Workers, Hono et D1 pour un pattern voisin
- Exécutez un modèle ML —
transformers.jsou un modèle ONNX léger tient confortablement dans une instancestandard - Enveloppez-le dans une passerelle API avec Hono pour un routage plus riche
- Livrez une application fullstack — combinez Containers avec Next.js sur Workers
Conclusion
Cloudflare Containers comble l'inconfortable terrain intermédiaire entre les Workers (parfaits pour des fonctions sans état et rapides) et le Kubernetes traditionnel (excessif pour la plupart des équipes). Avec Containers, vous obtenez de vraies images Docker, un vrai temps CPU, et de vrais processus persistants — tous servis depuis le edge de Cloudflare, avec la couche Worker qui s'occupe du routage et du cache.
Dans ce tutoriel, vous avez monté une stack edge complète : une image Docker avec Express et Sharp, un orchestrateur Worker avec cache KV, l'auto-scaling, l'ancrage régional, des sondes de santé et de l'observabilité. Le même squelette fonctionne pour la génération de PDF, l'inférence ML, les tâches de navigateur headless, et tout ce qui est trop lourd pour un Worker mais trop bon marché pour justifier un cluster dédié.
Cloudflare Containers transforme le edge mondial en hôte Docker — et une fois que vous aurez bâti un service de cette manière, vous y reviendrez chaque fois que vous aurez besoin de calcul réel au plus près des utilisateurs.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Docker Compose pour développeurs Full-Stack : Next.js, PostgreSQL et Redis
Apprenez à conteneuriser une application Next.js full-stack avec PostgreSQL et Redis en utilisant Docker Compose. Ce tutoriel pratique couvre l'orchestration multi-services, les workflows de développement, le rechargement à chaud, les health checks et les configurations prêtes pour la production.

Construire des API REST prêtes pour la production avec FastAPI, PostgreSQL et Docker
Apprenez à créer, tester et déployer une API REST de qualité production en utilisant le framework FastAPI de Python avec PostgreSQL, SQLAlchemy, les migrations Alembic et Docker Compose — de zéro au déploiement.

Créer et déployer une API serverless avec Cloudflare Workers, Hono et D1
Apprenez à construire une API REST prête pour la production avec Cloudflare Workers, le framework Hono et la base de données D1 — de la configuration initiale au déploiement mondial.