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

Noqta Team
Par Noqta Team ·

Chargement du lecteur de synthèse vocale...

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.x

Connectez-vous à votre compte Cloudflare :

wrangler login

Un onglet de navigateur s'ouvre pour autoriser le CLI. Une fois autorisé, vérifiez l'accès :

wrangler whoami

Vous 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 -y

Installez les dépendances d'exécution de l'application conteneur :

npm install express sharp
npm install -D typescript @types/node @types/express tsx

Cré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 .dockerignore

Ajoutez 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-api

Dans 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 containers indique à Cloudflare quel Dockerfile construire et la taille de chaque instance. Les types d'instance sont dev, basic, standard et enhanced — 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_instances limite 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_CACHE

Copiez 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/containers

Trois comportements méritent d'être soulignés :

  1. 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.
  2. sleepAfter dit à 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.
  3. 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 dev

Wrangler va :

  1. Construire l'image Docker localement
  2. Lancer une instance conteneurisée
  3. Démarrer le Worker sur http://localhost:8787
  4. 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.webp

Vous 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 deploy

Wrangler 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.webp

La 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 regions restreint 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 tail

Vous 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ée

Dans 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 MLtransformers.js ou un modèle ONNX léger tient confortablement dans une instance standard
  • 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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Construire une Progressive Web App (PWA) avec Next.js App Router.

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.

28 min read·