DEV Community

Juan Torchia
Juan Torchia

Posted on • Originally published at juanchi.dev

Docker pull falla en España por Cloudflare y un partido de fútbol — nadie habla del patrón real

Pasé tres horas convencido de que el problema era mío.

Era un martes a la tarde, tenía una llamada con mi cliente en Madrid en dos horas, y el pipeline de deploy estaba roto. docker pull timeout. Registro sin respuesta. Yo checkeando mi configuración de red, mis DNS, mis credenciales — como si hubiera tocado algo sin darme cuenta. La sensación clásica de "esto funcionaba ayer, ¿qué rompí?".

Nada. No rompí nada. Un partido de fútbol en España hizo que los ISPs bloquearan rangos de IP de Cloudflare para cumplir con una orden judicial antipiratería, y Docker Hub — que corre sobre infraestructura de Cloudflare — quedó en el camino como daño colateral.

Lo cuento porque cometí el error de asumir que la tubería es neutra. Que los CDNs son como el agua — fluyen igual para todos, siempre. Y ese supuesto silencioso está embebido en cada decisión de arquitectura que tomé en los últimos años.

Cloudflare DNS bloqueo infraestructura: lo que realmente pasó

La historia tiene un contexto legal: en España existe un mecanismo por el cual los titulares de derechos pueden pedir a los ISPs que bloqueen IPs asociadas a sitios de piratería durante eventos deportivos en vivo. El objetivo son streamings ilegales de partidos de LaLiga, Champions, ese tipo de cosas.

El problema es que ejecutar ese bloqueo en 2025 es quirúrgicamente imposible. Cloudflare usa rangos de IP compartidos. Miles de servicios viven en la misma IP. Cuando un ISP bloquea 104.21.x.x para cortar un stream pirata, está bloqueando potencialmente cualquier otro servicio que comparta ese rango.

En este caso, Docker Hub quedó inaccesible desde varios ISPs españoles durante el partido. No por minutos — por horas. Y el problema es que desde afuera del país, el servicio respondía perfecto. Desde Argentina, yo veía Docker Hub sin problemas. Desde Madrid, mi cliente no podía hacer docker pull de nada.

# Lo que veía mi cliente en Madrid
$ docker pull node:20-alpine
Error response from daemon: Get "https://registry-1.docker.io/v2/": 
net/http: request canceled while waiting for connection 
(Client.Timeout exceeded while awaiting headers)

# Lo que veía yo en Buenos Aires, al mismo tiempo
$ docker pull node:20-alpine
20-alpine: Pulling from library/node
# ... todo perfecto, obvio
Enter fullscreen mode Exit fullscreen mode

El daño colateral clásico de infraestructura compartida. Y la parte que más me inquieta no es el bloqueo en sí — es que tardamos 45 minutos en entender que el problema no era nuestro.

El supuesto que nadie escribe en la documentación

Toda nuestra arquitectura tiene supuestos implícitos. Los escribimos en los ADRs cuando somos prolijos, pero la mayoría viven en la cabeza de quien tomó las decisiones hace dos años.

Uno de esos supuestos, que yo tenía sin haberlo articulado nunca, es este:

Los servicios de infraestructura base — registros de contenedores, CDNs, resolución DNS — son carriers neutros. No tienen geografía relevante. No tienen política. Están siempre disponibles de la misma manera para todos.

Este incidente prueba que ese supuesto es falso en al menos tres dimensiones:

1. Geografía importa, incluso para infraestructura.

Docker Hub no es un servicio con SLA diferencial por país. Pero su disponibilidad efectiva depende de cómo los ISPs locales resuelven conflictos legales que nada tienen que ver con vos. Un partido de fútbol en España puede romper tu pipeline de deploy si tu cliente está en Madrid. Eso no aparece en ningún status page.

2. Los CDNs no son neutrales, son agregadores de riesgo.

Cuando Cloudflare tiene un problema — y los ha tenido — no afecta a un servicio. Afecta a miles simultáneamente. La concentración de infraestructura en pocos proveedores crea single points of failure que no existen en el diagrama de arquitectura de ninguno de esos servicios individuales.

Hablé de esto de manera tangencial en el post sobre TigerFS y la obsesión con meter todo adentro de PostgreSQL — hay un patrón de consolidación que reduce complejidad operacional pero amplifica el radio de blast cuando algo falla.

3. Los status pages mienten por omisión.

Cuando Docker Hub está bloqueado para usuarios en España, el status page muestra verde. Técnicamente es correcto — el servicio está up. Pero para un porcentaje de usuarios, es efectivamente down. La métrica de disponibilidad agregada oculta la experiencia real de subconjuntos geográficos.

// El supuesto implícito en casi todo retry logic que escribí
async function pullDockerImage(image: string): Promise<void> {
  const maxRetries = 3;

  for (let i = 0; i < maxRetries; i++) {
    try {
      await execCommand(`docker pull ${image}`);
      return; // Éxito — seguimos
    } catch (error) {
      // Supuesto silencioso: si falla, es transitorio
      // Nunca consideré: ¿y si es geográfico?
      // ¿Y si hacer retry 3 veces no cambia nada?
      if (i === maxRetries - 1) throw error;
      await sleep(2000 * (i + 1));
    }
  }
}

// La versión que debería haber escrito
async function pullDockerImage(
  image: string,
  options: {
    fallbackRegistry?: string;  // registry.empresa.com/mirror
    timeout?: number;
  } = {}
): Promise<void> {
  const registries = [
    'registry-1.docker.io',           // Docker Hub (default)
    options.fallbackRegistry,          // Mirror propio
    'mirror.gcr.io',                   // Google Container Registry mirror
  ].filter(Boolean);

  for (const registry of registries) {
    try {
      const imageWithRegistry = registry !== 'registry-1.docker.io'
        ? `${registry}/${image}`
        : image;

      await execCommand(`docker pull ${imageWithRegistry}`);
      return;
    } catch (error) {
      // Loguear qué registry falló, no solo que falló
      console.warn(`Registry ${registry} no disponible:`, error.message);
      continue;
    }
  }

  throw new Error(`No se pudo hacer pull de ${image} desde ningún registry`);
}
Enter fullscreen mode Exit fullscreen mode

La diferencia no es técnicamente compleja. Es conceptual. Implica aceptar que el registry puede no estar disponible de manera que el retry no resuelve.

El patrón que vine viendo esta semana

No es la primera vez que escribo sobre dependencias que asumimos estables y no lo son.

Cuando revisé los PRs con keys hardcodeadas, el problema de fondo era el mismo: asumimos que la API de Anthropic, de OpenAI, del servicio X, va a estar disponible de la misma manera para todos los que corren el código. La key hardcodeada es el síntoma. El supuesto de disponibilidad universal es la enfermedad.

Cuando leí el debate sobre contribuir al kernel de Linux con IA, lo que más me resonó no fue la IA — fue que el kernel tiene décadas de decisiones tomadas asumiendo que la red es best-effort, no garantizada. Los protocolos de bajo nivel tienen fallbacks porque sus autores vivieron en un mundo donde nada era confiable. Nosotros vivimos en un mundo donde todo parece confiable, y eso nos hace peores arquitectos.

Y cuando analicé cómo rompieron los benchmarks de agentes de IA, el patrón era: single point of failure disfrazado de solución elegante. Un agente que depende de un solo modelo, un solo API endpoint, un solo registro de contenedores — es frágil de una manera que el benchmark no mide.

El incidente de Docker en España es el mismo patrón con distinto disfraz.

Cómo lo mitigamos (y qué me falta todavía)

Lo primero que hice después del incidente fue hablar con mi cliente en Madrid sobre mirrors. Docker soporta registry mirrors de manera nativa en el daemon:

// /etc/docker/daemon.json
{
  "registry-mirrors": [
    "https://mirror.empresa-madrid.com",
    "https://mirror.gcr.io"
  ],
  "max-concurrent-downloads": 3,
  "max-concurrent-uploads": 5
}
Enter fullscreen mode Exit fullscreen mode

Con esto, si Docker Hub no responde, el daemon intenta los mirrors automáticamente. El mirror propio requiere infraestructura (podés levantar un Harbor o un simple registry de Docker), pero es la solución real para equipos con deploy frecuente.

Lo segundo fue revisar nuestro pipeline de CI/CD en Railway para identificar qué otros pasos tienen dependencias externas asumidas como estables:

# railway.toml — lo que teníamos
[build]
dockerfilePath = "./Dockerfile"

# Lo que queremos agregar
[build]
dockerfilePath = "./Dockerfile"
# Variables que Railway va a resolver en build time
[build.env]
DOCKER_BUILDKIT = "1"
# Si usamos base images, que vengan del mirror interno
BASE_REGISTRY = "mirror.empresa-madrid.com"
Enter fullscreen mode Exit fullscreen mode

Y en el Dockerfile:

# Antes: dependencia directa de Docker Hub
FROM node:20-alpine

# Después: parametrizable, con fallback documentado
ARG BASE_REGISTRY=""
ARG NODE_VERSION="20-alpine"

# Si BASE_REGISTRY está definido, usarlo; si no, Docker Hub
FROM ${BASE_REGISTRY:+${BASE_REGISTRY}/}node:${NODE_VERSION}

# El resto del Dockerfile sin cambios
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

Lo que todavía me falta: un sistema de health check que distinga entre "el servicio está down" y "el servicio está inaccesible desde esta geografía". Son problemas diferentes con soluciones diferentes, y hoy los trato igual.

Los errores que veo repetir (y que yo mismo repetí)

Confundir "siempre funcionó" con "siempre va a funcionar".

Docker Hub lleva años siendo confiable. Cloudflare lleva años siendo confiable. Esa historia no es garantía de disponibilidad futura, especialmente cuando la causa de fallo puede ser completamente ajena al proveedor (como una orden judicial de un partido de fútbol).

Diseñar el happy path y llamarlo arquitectura.

Si tu diagrama de arquitectura no tiene flechas rojas mostrando qué pasa cuando cada dependencia externa falla, no es un diagrama de arquitectura completo. Es un diagrama de cómo querés que funcione.

Asumir que el status page es la realidad.

Los status pages reportan disponibilidad global agregada. Tu usuario en Madrid, en Irán, en China, puede tener una experiencia completamente diferente mientras el status page muestra verde. Necesitás synthetic monitoring desde las geografías de tus usuarios reales.

No tener mirror para imágenes base.

Si deployás más de una vez por semana con Docker, un registry mirror interno no es gold plating — es ingeniería básica de resiliencia. El costo de levantarlo es horas. El costo de no tenerlo lo pagás cuando menos podés pagarlo.

FAQ: Cloudflare, DNS, bloqueos y resiliencia de infraestructura

¿Por qué el bloqueo de Cloudflare afecta a servicios que no tienen nada que ver con piratería?

Cloudflare usa IPs compartidas entre miles de clientes. Cuando un ISP bloquea una IP de Cloudflare para cortar el acceso a un sitio específico, está bloqueando todos los servicios que comparten esa IP. Docker Hub, servicios de monitoreo, APIs de terceros — todo puede quedar en el camino. Es el costo del modelo de infraestructura compartida.

¿Docker Hub tiene algún mecanismo de redundancia geográfica que evite esto?

Docker Hub tiene múltiples puntos de presencia y usa Cloudflare como CDN, pero eso no resuelve el problema del bloqueo de IP — al contrario, lo centraliza. La redundancia geográfica de Docker Hub no te ayuda si los ISPs de tu región están bloqueando los rangos de IP del CDN que Docker Hub usa. La solución está del lado del cliente: mirrors propios o registries alternativos.

¿Qué es un registry mirror y cómo lo levanto?

Un registry mirror es un proxy/caché local de imágenes de Docker. Cuando pedís docker pull node:20, el daemon consulta primero el mirror. Si tiene la imagen cacheada, la sirve localmente. Si no, la baja de Docker Hub y la cachea para la próxima vez. Podés levantar uno con Harbor (enterprise, más features) o con el registry oficial de Docker (registry:2 image) que es más simple. La configuración va en /etc/docker/daemon.json con la clave registry-mirrors.

¿Esto es exclusivo de España o puede pasar en otros países?

Puede pasar en cualquier país donde los ISPs ejecuten órdenes de bloqueo basadas en IP en lugar de en dominio. España es un caso documentado por las órdenes antipiratería de LaLiga, pero el mismo patrón existe en UK (bloqueos de copyright), en varios países de Medio Oriente (bloqueos políticos), y potencialmente en cualquier jurisdicción donde los mecanismos de bloqueo no sean lo suficientemente quirúrgicos. Si tenés clientes o usuarios distribuidos globalmente, es un riesgo real.

¿Por qué Cloudflare no resuelve esto desde su lado?

Cloudflare puede hacer algunas cosas — como rotar IPs o usar rangos más granulares — pero el problema estructural es que el modelo de negocio de CDN está basado en compartir infraestructura para reducir costos. No hay una solución técnica perfecta mientras los mecanismos de bloqueo sean por IP en lugar de por SNI o por contenido. Cloudflare tiene incentivos para resolver esto, pero los ISPs que ejecutan las órdenes judiciales tienen incentivos para no invertir en soluciones más quirúrgicas.

¿Cómo detecto si un fallo de infraestructura es geográfico antes de perder horas debuggeando?

Tres pasos rápidos: 1) Revisá downdetector.es o equivalente para el servicio afectado, filtrando por región. 2) Usá host-tracker.com o similar para hacer un check del servicio desde múltiples geografías simultáneamente — si responde desde US pero no desde ES, es geográfico. 3) Preguntale a alguien en otra red o país que pueda confirmar. Esos 5 minutos de diagnóstico me habrían ahorrado 45 minutos de buscar el problema en mi configuración.

La conclusión que no quiero suavizar

Hay algo incómodo en este incidente que va más allá de la solución técnica.

Construimos sistemas que asumen que la infraestructura base es estable, neutral y universal. Eso nunca fue completamente cierto, pero el nivel de centralización actual — Cloudflare manejando una porción enorme del tráfico web, Docker Hub siendo el registry default de facto, AWS/GCP/Azure concentrando la mayor parte del compute — hace que ese supuesto sea más peligroso que nunca.

No es que Cloudflare sea malo. No es que Docker Hub sea irresponsable. Es que el modelo de "todo en el mismo CDN, todo en el mismo registro, todo en el mismo proveedor de compute" crea interdependencias que ninguno de los actores individuales controla o documenta completamente.

Yo lo viví con la interfaz cerebro-computadora de la bailarina con ALS — un sistema médico que dependía de latencia de red estable. Y lo veo en cada discusión sobre Surelock y deadlocks en Rust — la resiliencia real requiere pensar en los casos de fallo desde el diseño, no agregarlos como afterthought.

La pregunta que me quedó después del incidente con mi cliente en Madrid no es "¿cómo evito que Docker Hub falle?". Es "¿cuántos otros supuestos silenciosos de disponibilidad universal tengo en mi arquitectura, esperando que un partido de fútbol los quiebre?".

No sé la respuesta completa. Pero ahora al menos sé que tengo que buscarla.


Este artículo fue publicado originalmente en juanchi.dev

Top comments (0)