Una hora 40 caída por una IP que dejamos hardcodeada: post-mortem honesto
Nuestro propio sitio se cayó por casi dos horas. La causa: una dirección IP que un día funcionó y un deploy de rutina volvió obsoleta. Aquí está el post-mortem completo, sin disfrazarlo, y la lección que ahora aplicamos en cada servidor que administramos.
A las 22:49 UTC un deploy automático corrió en uno de nuestros servidores. Era un cambio menor en este mismo sitio. El pipeline pasó verde, los health checks dijeron "todo bien" y nos fuimos a dormir. Una hora cuarenta minutos después el sitio seguía caído con un cordial 502 Bad Gateway sirviéndose a cada visitante.
Este es el post-mortem completo. Lo escribimos porque cada vez que nos cuentan que "los servidores serios no se caen" sabemos que hablamos con alguien que nunca ha estado al frente de uno.
El síntoma
https://serverstack.solutionsdevolvía 502 Bad Gateway.- Los demás sitios del mismo servidor funcionaban perfectamente.
- El contenedor del sitio aparecía como
healthy(los health checks decían que todo estaba bien). - Si dentro del servidor probabas la aplicación con
curl localhost, respondía correctamente con un 200 OK.
Ese conjunto de síntomas es desconcertante: la aplicación funciona, el contenedor está sano, pero el visitante recibe un error. El problema no estaba en la aplicación. Estaba en quien le entregaba el tráfico.
La cadena del tráfico (en simple)
Cuando alguien escribe serverstack.solutions en el navegador, el tráfico hace este recorrido:
- Cloudflare recibe la petición y la cifra.
- Cloudflare Tunnel (un agente que corre en nuestro servidor) recibe la petición ya descifrada.
- Nginx Proxy Manager la enruta al contenedor correcto según el dominio.
- El contenedor sirve la página.
El error estaba entre el paso 2 y el 3.
La causa raíz
Cloudflare Tunnel tenía hardcodeada la dirección IP interna de Nginx Proxy Manager: 172.20.0.9. Esa IP, en la red interna de Docker, era de Nginx Proxy Manager. Hasta el deploy.
Durante el deploy, Nginx Proxy Manager fue reiniciado como parte del proceso. Docker, al levantarlo de nuevo, le asignó una IP distinta: 172.20.0.6. La IP 172.20.0.9 quedó libre y minutos después la heredó otro contenedor (Grafana, en este caso).
Resultado: Cloudflare Tunnel siguió mandando todo el tráfico web a la IP que ahora era de Grafana. Grafana respondió "yo no atiendo este dominio" y el visitante vio el 502.
| Momento | IP de Nginx Proxy Manager |
|---|---|
| Antes del deploy | 172.20.0.9 |
| Después del reinicio | 172.20.0.6 |
Cloudflared apuntaba a .9. Listo, sitio caído.
La línea de configuración que nos costó hora y media
Esto es lo que estaba en la configuración de Cloudflare Tunnel:
# ANTES (frágil)
service: http://172.20.0.9:80
Y este es el cambio que hizo que el problema no pueda volver a ocurrir:
# DESPUÉS (estable)
service: http://localhost:80
¿La diferencia? localhost siempre apunta al servidor mismo, no a un contenedor específico. La IP de un contenedor Docker es dinámica por diseño. Confiar en que va a ser estable es una bomba con temporizador.
Por qué pasó (el "por qué" detrás del "qué")
Tres factores se alinearon:
- La configuración asumía que el orden de arranque siempre sería el mismo. Era cierto el día que se montó. Dejó de serlo el día que reiniciamos un servicio en orden distinto.
- El monitoreo no detectó el problema desde fuera. Los health checks internos decían "el contenedor está vivo" y técnicamente lo estaban — pero los visitantes reales no podían entrar. Faltaba un chequeo desde fuera del servidor que simulara un visitante de verdad.
- El deploy fue tarde, fuera de horario laboral. No había nadie viendo activamente. La hora cuarenta del incidente se fue completa antes de que alguien revisara.
Las correcciones que hicimos
Después de levantar el sitio, dedicamos la mañana siguiente a que esto en particular no pueda repetirse:
- Cambiamos todas las IPs hardcodeadas por nombres estables. Donde antes había
172.20.0.9, ahora haylocalhosto el nombre DNS del contenedor. - Añadimos un chequeo externo (UptimeRobot). Cada minuto, un servicio fuera de nuestro servidor le pega al sitio público y verifica que devuelve 200. Si falla, alerta de inmediato.
- Limpiamos cron jobs huérfanos que también encontramos durante la revisión (un script de mantenimiento apuntaba a un archivo que ya no existía).
- Cerramos un puerto que un servicio dejaba abierto sin necesidad — descubierto al revisar el estado del servidor.
Las lecciones aplicables a cualquiera que tenga un servidor
- Nunca hardcodees una IP que se asigna automáticamente. Si la red la genera, la red la puede cambiar. Usa nombres estables: DNS interno,
localhost, hostnames de Docker. - El monitoreo debe simular un cliente de verdad. Un check que solo dice "el proceso está vivo" no te avisa cuando el sitio está caído para los visitantes. Necesitas un chequeo externo que toque el dominio público y mida la respuesta real.
- Los deploys fuera de horario son una decisión, no un descuido. Si los haces así, asegúrate de que el monitoreo despierta a alguien si algo falla — no que te enteras al día siguiente por un cliente molesto.
- Después de cada incidente, escríbelo. El post-mortem honesto es la única forma de que un equipo aprenda. Sin escribirlo, en seis meses se repite.
Por qué publicamos esto
Porque la única diferencia entre un equipo que aprende y uno que no es escribir lo que falló y compartirlo. Los que dicen "nunca hemos tenido un incidente" no es que no los hayan tenido. Es que no los cuentan.
En cada servidor que administramos a partir de este caso, esta configuración ya viene corregida desde el día uno. Y el monitoreo externo no es opcional — es parte del setup.
En ServerStack Solutions
Si tienes un servidor en producción y no estás 100% seguro de que el monitoreo te despertaría si se cae a las 3 de la mañana, esa es exactamente la conversación que vale la pena tener.
Hablemos de tu monitoreo — sin compromiso, sin venderte alarmas innecesarias.
Relacionado: Auditamos nuestro propio servidor · Cuando 'healthy' miente: el container sano que no servía · Nginx reverse proxy + firewall.
Preguntas frecuentes
¿Por qué Docker asigna IPs dinámicas en lugar de fijas?
Porque las IPs internas de Docker están pensadas para comunicación entre contenedores que pueden levantarse y bajarse en cualquier orden. Docker provee un sistema de nombres (DNS interno) precisamente para que no tengas que conocer la IP — escribes el nombre del servicio y Docker resuelve. Cuando una configuración usa la IP directa, está saltándose ese sistema y por eso es frágil.
¿Qué es un health check y por qué no detectó el problema?
Un health check es un comando o llamada que verifica que un servicio esté funcionando. En este caso, el health check del contenedor preguntaba "¿el proceso responde dentro del contenedor?" — y la respuesta era sí. El problema era que el tráfico desde fuera llegaba a otro lugar. La lección es que los health checks internos cubren una parte; necesitas también monitoreo externo que simule a un cliente real.
¿Cuánto cuesta un servicio de monitoreo externo tipo UptimeRobot?
La mayoría tiene un plan gratuito que cubre lo esencial para 50 sitios con chequeo cada 5 minutos. Para PyMEs, eso suele ser más que suficiente. Si necesitas chequeos cada minuto desde varias regiones del mundo, el costo es típicamente entre 5 y 15 USD al mes. Es de las inversiones más baratas que existen comparada con el costo de no enterarse a tiempo de una caída.