Cloudflare Containers a fondo: contenedores Docker controlados desde un Worker
Qué es Cloudflare Containers y cómo funciona: cada contenedor es un Durable Object, se arranca bajo demanda, se duerme solo y se factura cada 10 ms. Tipos de instancia, precios, disco efímero, SIGTERM y un Worker con su imagen Docker, construida con Wrangler y probada en local.
Contenido
Los Workers son rapidísimos, pero tienen un modelo concreto: JavaScript (o WebAssembly) en un isolate, sin sistema de ficheros, sin procesos y con límites de memoria y CPU. Hay cosas que no caben ahí: un binario de FFmpeg, una librería nativa de Python, un compilador, una base de código que ya vive en una imagen Docker o un entorno aislado para ejecutar código que no es tuyo.
Cloudflare Containers resuelve ese hueco: ejecutas contenedores Docker normales en la red de Cloudflare y los controlas desde un Worker. Cada contenedor va unido a un Durable Object, que decide cuándo arrancarlo, le reenvía las peticiones y lo duerme cuando deja de usarse. Pagas solo mientras está en marcha.
En este artículo vemos cómo funciona por dentro, los tipos de instancia y precios, las diferencias con un servidor de verdad y un ejemplo completo: un Worker que reparte peticiones entre contenedores con nombre, con su Dockerfile.
wrangler dev construye la imagen a partir del Dockerfile y la imagen funciona en Docker (responde, recibe variables de entorno y se cierra limpia con SIGTERM). Lo que no he podido probar: la petición completa Worker → contenedor en local, porque el sidecar de red que arranca Wrangler necesita un módulo de iptables que no tiene la máquina de pruebas (lo cuento abajo), ni nada desplegado en Cloudflare. Precios y límites comprobados en la documentación oficial ese mismo día.Cómo funciona

Hay tres piezas:
- El Worker recibe la petición, como siempre. Decide a qué contenedor mandarla.
- Un Durable Object por contenedor. Tu clase extiende
Container(del paquete@cloudflare/containers), que a su vez extiendeDurableObject. Cada instancia del objeto controla una instancia de contenedor: la arranca cuando llega la primera petición, espera a que escuche en su puerto, le reenvía el tráfico y la para cuando lleva un rato sin actividad. Como es un Durable Object, también tiene su almacenamiento SQLite y sus alarmas. - El contenedor, una imagen Docker para
linux/amd64. Wrangler la construye desde tuDockerfileal desplegar y la sube al registro de Cloudflare; también puedes apuntar a una imagen ya publicada.
De ahí salen dos formas de repartir el trabajo:
- Instancias con nombre (
env.MI_CONTENEDOR.getByName("ana")): el mismo nombre llega siempre al mismo contenedor. Sirve para sesiones, un entorno por usuario o un sandbox por tarea. - Instancias intercambiables (
getRandom(env.MI_CONTENEDOR, 3)): eliges al azar entre un número fijo de ellas. Es el reparto de carga sencillo que ofrece hoy la librería.
Arranque, sueño y parada
- Arranque en frío. La primera petición a una instancia parada arranca el contenedor. La documentación habla de 1 a 3 segundos normalmente, según el tamaño de la imagen y lo que tarde tu proceso en arrancar.
- Sueño.
sleepAfterfija cuánto tiempo sin peticiones aguanta el contenedor antes de pararse. Por defecto, 10 minutos. - Disco efímero. Cuando el contenedor se duerme, pierde su disco: el siguiente arranque empieza con la imagen limpia. Si necesitas guardar algo, usa el almacenamiento del Durable Object, R2 o una base de datos.
- Parada por la plataforma. Cloudflare no garantiza que un contenedor dure un tiempo concreto: un servidor puede reiniciarse. Antes de pararlo envía
SIGTERMal proceso principal, espera hasta 15 minutos y, si sigue vivo, mandaSIGKILL. Las actualizaciones de imagen en un despliegue siguen la misma secuencia, con un despliegue gradual. - Ubicación. Al desplegar, Cloudflare prepara instancias en varias regiones. Al arrancar una, se elige la libre más cercana; no siempre está en el mismo sitio que la petición.
SIGTERM) y puede arrancar en otra región. Diseña tu imagen como si fuera desechable: estado fuera, arranque rápido y cierre limpio.Dos políticas de planificación
La configuración por defecto (scheduling_policy: "default") define en Wrangler una imagen y un tamaño para toda la aplicación, y los cambios se aplican con despliegues graduales. Existe una segunda política, durable_object, en beta, en la que el propio código del Durable Object elige la imagen cada vez que arranca una instancia, pensada para sandboxes y entornos de agentes. La política no se puede cambiar después: para pasar de una a otra hay que crear otra aplicación. En este artículo uso la de por defecto.
Tipos de instancia, límites y precios
Containers solo está disponible en el plan Workers Paid (5 $/mes). Eliges un tipo de instancia:
| Tipo | vCPU | Memoria | Disco |
|---|---|---|---|
lite | 1/16 | 256 MiB | 2 GB |
basic | 1/4 | 1 GiB | 4 GB |
standard-1 | 1/2 | 4 GiB | 8 GB |
standard-2 | 1 | 6 GiB | 12 GB |
standard-3 | 2 | 8 GiB | 16 GB |
standard-4 | 4 | 12 GiB | 20 GB |
También puedes definir un tipo a medida, de 1 a 4 vCPU, hasta 12 GiB de memoria y 20 GB de disco, con al menos 3 GiB por vCPU. Los nombres antiguos dev y standard siguen funcionando como alias de lite y standard-1.
Se factura cada 10 ms que el contenedor está en marcha, desde que llega la primera petición (o lo arrancas a mano) hasta que se duerme:
| Recurso | Incluido al mes | Después |
|---|---|---|
| Memoria | 25 GiB-hora | 0,0000025 $ por GiB-segundo |
| CPU | 375 vCPU-minutos | 0,000020 $ por vCPU-segundo |
| Disco | 200 GB-hora | 0,00000007 $ por GB-segundo |
La memoria y el disco se cobran por lo aprovisionado (el tamaño de la instancia, lo uses o no); la CPU, solo por uso real. La salida de datos tiene su propio precio: en Norteamérica y Europa, 1 TB al mes incluido y luego 0,025 $/GB. Además pagas las peticiones al Worker y el Durable Object de cada contenedor, con sus precios habituales.
Un cálculo rápido: una instancia lite (256 MiB) encendida 8 horas al día durante 30 días suma 60 GiB-hora de memoria. Restando las 25 incluidas, quedan 35 GiB-hora = 126.000 GiB-segundo × 0,0000025 $ ≈ 0,32 $, más la CPU que use de verdad. Lo que cuesta dinero es dejar contenedores grandes despiertos: por eso importa sleepAfter.
Por cuenta, puedes tener a la vez hasta 6 TiB de memoria, 1.500 vCPU y 30 TB de disco en contenedores, y 50 GB de imágenes almacenadas. Cada imagen puede ocupar como mucho el disco de su tipo de instancia.
El código
La imagen
Un servidor HTTP mínimo en Node.js con dos rutas: /info (quién soy) y /primos?n= (un trabajo de CPU con la criba de Eratóstenes, para tener algo que medir). Lo importante está al final: atiende SIGTERM y cierra el servidor antes de salir.
import http from "node:http";
import os from "node:os";
const arranque = Date.now();
function primos(limite) {
// Criba de Eratóstenes: trabajo de CPU "de verdad" para el ejemplo
const criba = new Uint8Array(limite + 1);
let total = 0;
for (let i = 2; i <= limite; i++) {
if (criba[i]) continue;
total++;
for (let j = i * i; j <= limite; j += i) criba[j] = 1;
}
return total;
}
const servidor = http.createServer((req, res) => {
const url = new URL(req.url, "http://contenedor");
if (url.pathname.endsWith("/primos")) {
const n = Math.min(Number(url.searchParams.get("n") ?? 1e6), 5e7);
const t0 = Date.now();
const total = primos(n);
res.writeHead(200, { "content-type": "application/json" });
return res.end(JSON.stringify({ n, total, ms: Date.now() - t0 }));
}
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({
mensaje: process.env.MENSAJE ?? null,
ruta: url.pathname,
hostname: os.hostname(),
pid: process.pid,
vivoDesdeMs: Date.now() - arranque,
}));
});
servidor.listen(8080, () => console.log("escuchando en :8080"));
// Cloudflare manda SIGTERM antes de parar la instancia: cerramos limpio
process.on("SIGTERM", () => {
console.log("SIGTERM recibido, cerrando…");
servidor.close(() => process.exit(0));
});FROM node:22-alpine
WORKDIR /app
COPY server.mjs .
EXPOSE 8080
CMD ["node", "server.mjs"]La configuración
Tres bloques: containers (qué imagen, qué tamaño, cuántas instancias como mucho), el binding del Durable Object y la declaración de la clase con SQLite. Uso exports, la forma nueva de declarar clases de Durable Objects; los proyectos con el bloque migrations de siempre siguen funcionando, pero no se pueden mezclar los dos.
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "containers-lab",
"main": "src/index.ts",
"compatibility_date": "2026-10-01",
"containers": [
{
"class_name": "Calculadora",
"image": "./contenedor/Dockerfile",
"instance_type": "lite",
"max_instances": 5
}
],
"durable_objects": {
"bindings": [{ "name": "CALCULADORA", "class_name": "Calculadora" }]
},
"exports": {
"Calculadora": { "type": "durable-object", "storage": "sqlite" }
}
}El Worker
import { Container, getRandom } from "@cloudflare/containers";
export class Calculadora extends Container<Env> {
defaultPort = 8080; // puerto en el que escucha el contenedor
sleepAfter = "2m"; // se duerme tras 2 minutos sin peticiones
envVars = { MENSAJE: "hola desde la clase Calculadora" };
override onStart() {
console.log("contenedor arrancado");
}
override onStop() {
console.log("contenedor parado");
}
override onError(error: unknown) {
console.log("error en el contenedor:", error);
throw error;
}
}
export default {
async fetch(request, env): Promise<Response> {
const url = new URL(request.url);
// /c/<nombre>/... → una instancia con nombre (estado y dirección propios)
const m = url.pathname.match(/^\/c\/([\w-]+)(\/.*)?$/);
if (m) {
const instancia = env.CALCULADORA.getByName(m[1]);
return instancia.fetch(request);
}
// /lb/... → reparte entre 3 instancias intercambiables
if (url.pathname.startsWith("/lb")) {
const instancia = await getRandom(env.CALCULADORA, 3);
return instancia.fetch(request);
}
return new Response("Prueba /c/<nombre>/info, /c/<nombre>/primos?n=1000000 o /lb/info\n");
},
} satisfies ExportedHandler<Env>;defaultPortes el puerto al que se reenvían las peticiones. Además, bloquea la petición hasta que el contenedor escucha en ese puerto, así que no tienes que esperar tú.sleepAfter = "2m"duerme el contenedor a los 2 minutos sin tráfico. Para un servicio con peticiones esporádicas, cuanto más bajo, más barato (a cambio de más arranques en frío).envVarsllega al contenedor como variables de entorno.getByNameygetRandomson las dos estrategias de reparto de antes.
Lo que pasó al probarlo
wrangler dev necesita un Docker local (Docker Desktop, Colima o similar) y, en mi caso, también buildx: sin él, el build falla con unknown flag: --load. Con buildx instalado, Wrangler construyó la imagen y detectó el binding:
env.CALCULADORA (Calculadora) Durable Object local
The following containers are available:
- containers-lab-calculadora (/home/yo/containers-lab/contenedor/Dockerfile)
⎔ Preparing container image(s)...
⎔ Container image(s) readyPero cada petición devolvió Failed to start container (500). La causa no está en el código: Wrangler arranca junto a cada contenedor un sidecar de red (cloudflare/proxy-everything) que configura iptables, y el kernel de mi máquina de pruebas no tiene la extensión que necesita:
iptables v1.8.11 (nf_tables): RULE_APPEND failed (No such file or directory): rule in chain PREROUTING
Warning: Extension socket revision 0 not supported, missing kernel module?En un Docker Desktop o un Linux normal no deberías encontrarte esto, pero no lo he podido comprobar. Lo que sí pude probar es la imagen que construyó Wrangler, con docker run:
$ docker run -d -e MENSAJE=hola -p 18081:8080 <imagen>
$ curl localhost:18081/info
{"mensaje":"hola","ruta":"/info","hostname":"b9ea4ad3b41e","pid":1,"vivoDesdeMs":1951}
$ curl "localhost:18081/primos?n=10000000"
{"n":10000000,"total":664579,"ms":79}
$ docker stop prueba3 && docker logs prueba3
escuchando en :8080
SIGTERM recibido, cerrando…
código de salida: 0El contenedor responde, recibe la variable de entorno, calcula los 664.579 primos menores de 10 millones en unos 80 ms y, al recibir SIGTERM (docker stop), cierra el servidor y sale con código 0, que es justo lo que hará Cloudflare antes de pararlo.
r en la terminal de wrangler dev para reconstruir la imagen y reiniciar las instancias. max_instances no se aplica en local, y Wrangler no borra las imágenes viejas: docker image prune de vez en cuando.Errores comunes (y cómo evitarlos)
- Guardar datos en el disco del contenedor. Se pierde al dormirse. Guárdalos en el Durable Object, R2 o una base de datos.
- Ignorar
SIGTERM. Si tu proceso no lo atiende, cualquier reinicio o despliegue lo mata a los 15 minutos conSIGKILL, a medias. Atiéndelo y cierra limpio. - Imagen para ARM. Si construyes en un Mac con Apple Silicon una imagen solo
arm64, no funcionará: tiene que serlinux/amd64. - Instancias grandes despiertas. Memoria y disco se cobran por lo aprovisionado. Empieza con
liteobasicy ajustasleepAfter. - Esperar estado compartido entre instancias de
getRandom. Son contenedores distintos; cada uno tiene su memoria y su disco. - Servidores que se quejan del nombre de host. En Cloudflare el hostname del contenedor tiene 64 caracteres, uno más de lo que permite una etiqueta DNS. La documentación avisa de que
python3 -m http.servery otros servidores basados enhttp.serverde Python fallan al arrancar por eso. En local, con Docker, no lo verás. - Usarlo para lo que un Worker ya hace. Un contenedor arranca en segundos; un Worker, en milisegundos. Si tu código cabe en un Worker, déjalo ahí.
Cuándo usar Containers (y cuándo no)
Úsalo cuando necesites algo que un Worker no puede ejecutar: binarios nativos (FFmpeg, ImageMagick, LibreOffice), lenguajes o librerías que no compilan a WebAssembly, cargas con mucha memoria, una aplicación que ya tienes empaquetada en Docker o sandboxes para ejecutar código de terceros, cada uno con su nombre.
No lo uses para una API web corriente (un Worker es más rápido y barato), para tareas largas en varios pasos que se pueden partir (Workflows) ni como servidor siempre encendido con estado en disco: para eso sigue siendo mejor un VPS o tu homelab detrás de un túnel.
Sigue leyendo
- Cloudflare a fondo: la visión general de la plataforma.
- Cloudflare Durable Objects a fondo: la pieza que controla cada contenedor.
- Cloudflare Workers a fondo: el Worker que va delante.
- Cloudflare R2 a fondo: dónde guardar lo que el contenedor no puede.
- Planes de Cloudflare Workers: Free frente a Paid: Containers exige el plan Paid.