> ## Content Index
> Fetch the complete content index at: https://blog.devutilites.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Cloudflare Containers a fondo: contenedores Docker controlados desde un Worker
- URL: https://blog.devutilites.com/cloudflare-containers-a-fondo/
- Published: 2026-10-03T03:13:59.000Z
- Updated: 2026-10-03T03:13:59.000Z
- Description: 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.
- Author: Luis Collantes
- Tags: Cloudflare, Backend

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

🧪

**Lo que se ha probado** el 3 de octubre de 2026, con **Wrangler 4.147.0**, **@cloudflare/containers 0.3.7**, **Docker 26.1.5** y **Node.js 24.21.0**: el Worker compila con TypeScript, `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

![Esquema de Cloudflare Containers: una petición llega al Worker, que obtiene un Durable Object por nombre o al azar; el Durable Object, de la clase Container, arranca el contenedor si hace falta y le reenvía la petición al puerto configurado; el contenedor es una imagen Docker linux/amd64 con disco efímero](https://blog.devutilites.com/content/images/2026/10/cf-containers-esquema.png)

El Worker elige la instancia; el Durable Object la arranca, le pasa la petición y la duerme cuando se queda quieta. Diagrama: devutilites.

Hay tres piezas:

1. **El Worker** recibe la petición, como siempre. Decide a qué contenedor mandarla.
2. **Un Durable Object por contenedor.** Tu clase extiende `Container` (del paquete `@cloudflare/containers`), que a su vez extiende `DurableObject`. 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.
3. **El contenedor**, una imagen Docker para `**linux/amd64**`. Wrangler la construye desde tu `Dockerfile` al 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.** `sleepAfter` fija 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](https://blog.devutilites.com/cloudflare-r2-a-fondo/) 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 **`SIGTERM`** al proceso principal, espera **hasta 15 minutos** y, si sigue vivo, manda **`SIGKILL`**. 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.

⚠️

**Un contenedor no es un servidor que siempre está ahí.** Pierde el disco al dormirse, puede pararse en cualquier momento (con aviso por `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](https://blog.devutilites.com/cloudflare-workers-free-vs-paid/).

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.

```javascript
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));
});
```

```dockerfile
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**.

```json
{
  "$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

```typescript
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>;
```

- `defaultPort` es 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).
- `envVars` llega al contenedor como variables de entorno.
- `getByName` y `getRandom` son 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:

```text
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) ready
```

Pero 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:

```text
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`:

```text
$ 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: 0
```

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

💡

**Desarrollo en local:** el código del Worker se recarga al guardar, pero el del contenedor no. Pulsa `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 con `SIGKILL`, 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 ser `linux/amd64`.
- **Instancias grandes despiertas.** Memoria y disco se cobran por lo aprovisionado. Empieza con `lite` o `basic` y ajusta `sleepAfter`.
- **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.server` y otros servidores basados en `http.server` de 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](https://blog.devutilites.com/cloudflare-workers-a-fondo/) es más rápido y barato), para tareas largas en varios pasos que se pueden partir ([Workflows](https://blog.devutilites.com/cloudflare-workflows-a-fondo/)) 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](https://blog.devutilites.com/cloudflare-tunnel-a-fondo/).

## Sigue leyendo

- [Cloudflare a fondo](https://blog.devutilites.com/cloudflare-a-fondo/): la visión general de la plataforma.
- [Cloudflare Durable Objects a fondo](https://blog.devutilites.com/cloudflare-durable-objects-a-fondo/): la pieza que controla cada contenedor.
- [Cloudflare Workers a fondo](https://blog.devutilites.com/cloudflare-workers-a-fondo/): el Worker que va delante.
- [Cloudflare R2 a fondo](https://blog.devutilites.com/cloudflare-r2-a-fondo/): dónde guardar lo que el contenedor no puede.
- [Planes de Cloudflare Workers: Free frente a Paid](https://blog.devutilites.com/cloudflare-workers-free-vs-paid/): Containers exige el plan Paid.