Postmortem #001 · Cuando una actualización automática dejó nuestras webs fuera de servicio
El 30 de septiembre de 2026 varias webs de Skilly dejaron de estar disponibles durante casi seis horas.
Los servicios que servían el contenido seguían funcionando. Los contenedores estaban levantados. El servidor también.
Sin embargo, desde Internet no se podía acceder a las webs.
El problema terminó estando en otro sitio: el reverse proxy que recibe las conexiones había quedado detenido después de una actualización automática del sistema.
Pero decir simplemente "Nginx se cayó" no explica realmente el incidente.
Lo interesante es entender:
- por qué se detuvo;
- por qué no pudo volver a arrancar;
- qué papel jugó DNS;
- por qué el resto de aplicaciones podía estar funcionando y aun así las webs no eran accesibles;
- y qué podemos hacer para evitar que una situación parecida vuelva a provocar una caída.
Nota de seguridad
Algunos nombres, dominios, rutas y detalles de infraestructura de este artículo han sido anonimizados o simplificados.
La secuencia técnica y la causa del incidente se mantienen fieles al caso real.
El primer síntoma: ERR_CONNECTION_REFUSED
Nos dimos cuenta del problema al intentar acceder a una de las webs.
El navegador mostraba:
ERR_CONNECTION_REFUSED
Y el mismo error aparecía en varios servicios.
Esta información ya nos daba una primera pista.
No era un:
404 Not Found
ni un:
502 Bad Gateway
ni siquiera un:
503 Service Unavailable
La conexión directamente estaba siendo rechazada.
¿Qué diferencia hay?
Cuando recibimos un error HTTP, normalmente significa que algún servidor web ha conseguido atender nuestra conexión.
Por ejemplo:
| Error | Significado simplificado |
|---|---|
404 | El servidor responde, pero no encuentra el recurso |
502 | El proxy responde, pero tiene problemas hablando con el backend |
503 | El servidor responde, pero el servicio no está disponible |
ERR_CONNECTION_REFUSED | No conseguimos establecer la conexión con el servicio |
Así que una de las primeras preguntas fue:
¿Está realmente funcionando el servidor web?
Primera comprobación: Nginx
En esta infraestructura utilizamos Nginx como reverse proxy.
Comprobamos su estado:
sudo systemctl status nginx
Y encontramos algo parecido a esto:
nginx.service
Active: failed
Además aparecía un error especialmente interesante:
host not found in upstream "backend.example"
nginx: configuration file test failed
Ahí apareció la primera pista importante.
Nginx había intentado arrancar, pero durante la validación de su configuración no había conseguido resolver el nombre de uno de los servicios a los que debía enviar tráfico.
Antes de continuar: ¿qué es un reverse proxy?
Para entender el incidente necesitamos conocer cómo funciona normalmente una arquitectura de este tipo.
Un usuario no tiene por qué conectarse directamente con cada aplicación.
Podemos tener algo parecido a esto:
Internet
│
▼
Nginx
reverse proxy
┌──────┼──────┐
│ │ │
▼ ▼ ▼
Web Cursos Panel
│
▼
Backend
Nginx actúa como puerta de entrada.
Recibe una petición como:
https://ejemplo.com
y decide qué aplicación debe atenderla.
Por ejemplo:
/ → frontend
/api/ → backend
/cursos/ → otra aplicación
Esta arquitectura es muy habitual porque permite centralizar:
- HTTPS;
- certificados;
- cabeceras de seguridad;
- redirecciones;
- dominios;
- balanceo;
- acceso a diferentes aplicaciones.
Pero también introduce una consecuencia importante:
Si el reverse proxy deja de funcionar, aplicaciones que continúan perfectamente sanas pueden dejar de ser accesibles desde Internet.
Y eso fue exactamente lo que ocurrió.
Los servicios seguían funcionando
Una de las siguientes comprobaciones fue revisar los contenedores.
Los servicios principales seguían levantados y saludables.
Simplificando:
Aplicación web healthy
Plataforma cursos healthy
Esto puede parecer contradictorio:
¿Cómo puede estar una aplicación healthy pero no funcionar la web?
Porque son dos cosas diferentes.
Imaginemos un restaurante.
La cocina puede estar funcionando perfectamente:
cocina → OK
pero si la puerta principal está cerrada:
entrada → cerrada
los clientes no pueden entrar.
En nuestro caso:
Aplicaciones ✅
Contenedores ✅
Servidor ✅
Nginx ❌
Nginx era esa puerta de entrada.
¿Se había reiniciado el servidor?
Otra posible explicación era un reinicio completo de la máquina.
Pero comprobamos el tiempo de actividad y descartamos esa posibilidad.
El servidor llevaba meses encendido.
Por tanto, no estábamos ante:
reboot
↓
algo no arranca
Había ocurrido algo mientras el servidor seguía funcionando.
Había que reconstruir la secuencia.
Reconstruyendo el incidente con journalctl
Una herramienta tremendamente útil para investigar problemas en Linux es:
journalctl
systemd-journald mantiene registros de muchos eventos del sistema:
- servicios que arrancan;
- servicios que paran;
- errores;
- procesos;
- red;
- tareas automáticas;
- etc.
En lugar de revisar todo el histórico, buscamos únicamente los minutos alrededor del incidente.
Por ejemplo:
sudo journalctl \
--since "06:10" \
--until "06:15"
Y ahí apareció la secuencia completa.
Simplificada, era esta:
06:11
Comienza una actualización automática
│
▼
06:12
El sistema actualiza varios paquetes
│
▼
Se solicita el reinicio de varios servicios
│
├───────────┐
▼ ▼
Nginx DNS
se para se reinicia
│
▼
Nginx intenta arrancar
│
▼
Necesita resolver el nombre
de uno de sus backends
│
▼
DNS todavía no está disponible
│
▼
La resolución falla
│
▼
nginx -t falla
│
▼
Nginx queda en estado FAILED
Ahí estaba la causa.
¿Qué había provocado todos esos reinicios?
El servidor tiene habilitadas actualizaciones automáticas de seguridad.
Durante la madrugada se ejecutó:
unattended-upgrades
que es el mecanismo de Ubuntu para instalar automáticamente determinadas actualizaciones.
Esto es algo positivo.
Desactivar las actualizaciones de seguridad no sería una buena conclusión de este incidente.
El problema no era que el servidor estuviese actualizado.
El problema apareció por la interacción entre varios componentes durante ese proceso.
Después de actualizar determinadas bibliotecas del sistema entró en juego otro componente:
needrestart
¿Qué es needrestart?
Cuando Linux actualiza una biblioteca, los programas que ya estaban ejecutándose pueden seguir utilizando la versión antigua que tenían cargada en memoria.
needrestart comprueba precisamente eso.
Su trabajo es detectar servicios que necesitan reiniciarse para empezar a utilizar las versiones actualizadas de las bibliotecas.
Conceptualmente:
Actualizamos una biblioteca
│
▼
¿Hay servicios usando la versión antigua?
│
sí
│
▼
needrestart
│
▼
reiniciar servicios afectados
Durante nuestro incidente se decidió reiniciar varios servicios.
Entre ellos estaban:
reverse proxy
servicio de red
servicio de resolución DNS
otros servicios del sistema
Y ahí apareció una condición de carrera.
Una condición de carrera en infraestructura
Normalmente asociamos el concepto de race condition o condición de carrera con programación concurrente.
Pero también puede aparecer en infraestructura.
Dos cosas estaban ocurriendo prácticamente al mismo tiempo:
DNS intentando volver a estar disponible
y:
Nginx intentando arrancar
El problema era que Nginx necesitaba DNS durante su arranque.
Por tanto el resultado dependía del orden:
Escenario A
DNS arranca
↓
Nginx arranca
↓
resuelve backend
↓
TODO OK
Escenario B
Nginx arranca
↓
DNS todavía no está listo
↓
no resuelve backend
↓
nginx -t falla
Nos tocó el escenario B.
¿Por qué Nginx necesitaba DNS para arrancar?
Uno de los virtual hosts enviaba determinadas peticiones hacia otro servicio utilizando un nombre DNS.
Un ejemplo simplificado sería:
location /api/ {
proxy_pass https://backend.example;
}
Antes de arrancar, Nginx valida su configuración.
El servicio de systemd ejecuta algo equivalente a:
nginx -t
Esto permite detectar configuraciones incorrectas antes de lanzar el proceso.
Normalmente es una protección excelente.
Pero durante esa validación Nginx necesitaba resolver:
backend.example
y en ese preciso instante la resolución DNS no estaba disponible.
Resultado:
host not found in upstream
y después:
configuration file test failed
Como la validación había fallado, Nginx decidió correctamente:
no arrancar con una configuración que en ese momento consideraba inválida.
Entonces, ¿el problema era DNS?
Sí... pero solo durante unos segundos.
Esto es importante.
No había desaparecido el dominio.
No se había borrado ningún registro DNS.
No había un error permanente de configuración.
Poco después, el sistema DNS volvió a funcionar normalmente.
El problema real fue esta pequeña ventana temporal:
Nginx necesita DNS
+
DNS está reiniciándose
=
Nginx no puede arrancar
Si DNS volvió, ¿por qué la web siguió caída?
Esta fue una de las partes más interesantes de la investigación.
Podríamos esperar algo así:
06:12:17 DNS no funciona
06:12:18 Nginx falla
06:12:20 DNS vuelve
06:12:21 Nginx vuelve a intentarlo
Pero eso último no ocurrió.
La unidad de systemd encargada de Nginx no tenía configurada una política de reinicio automático después de un fallo de arranque.
Por tanto ocurrió esto:
Nginx intenta arrancar
│
▼
falla
│
▼
estado = failed
│
▼
DNS vuelve
│
▼
...
│
▼
...
Nada más.
El sistema no tenía ningún motivo para volver a probar.
Y por eso un problema que duró apenas unos instantes provocó una indisponibilidad de varias horas.
Una diferencia importante: causa inmediata y causa raíz
En un postmortem no basta con encontrar el primer error.
Podríamos decir:
La web se cayó porque Nginx estaba parado.
Pero eso no explica nada.
También podríamos decir:
Nginx no arrancó porque DNS falló.
Eso se acerca más, pero sigue sin contar toda la historia.
Podemos separar varios niveles.
Síntoma
Las webs no eran accesibles.
Fallo inmediato
Nginx estaba detenido.
Motivo del fallo
La validación de Nginx no consiguió resolver un backend.
Desencadenante
Una actualización automática reinició simultáneamente
el proxy y servicios relacionados con la resolución DNS.
Factor que amplificó el incidente
Nginx no tenía configurado un reintento automático después
de un fallo de arranque.
Esta última parte es especialmente importante.
El problema temporal ocurrió durante segundos.
La caída duró horas.
¿Cómo recuperamos el servicio?
Una vez entendimos que el DNS ya funcionaba correctamente, comprobamos nuevamente Nginx.
La misma configuración que había fallado varias horas antes ahora era válida.
Arrancamos el servicio manualmente:
sudo systemctl start nginx
y verificamos su estado:
sudo systemctl status nginx
El resultado:
Active: active (running)
Las webs volvieron a estar accesibles inmediatamente.
Recuperar no significa solucionar
Este punto merece su propia sección.
Ejecutar:
systemctl start nginx
recuperó el servicio.
Pero no eliminó la causa del incidente.
Si mañana vuelve a darse exactamente la misma combinación:
actualización
+
reinicio de DNS
+
reinicio de Nginx
+
Nginx necesita DNS
podría ocurrir otra vez.
En operaciones existen dos conceptos diferentes:
Recovery
Conseguir que el servicio vuelva a funcionar.
Remediation
Modificar el sistema para reducir la probabilidad de que el mismo fallo vuelva a producirse.
Nosotros ya habíamos completado el primero.
Ahora tocaba trabajar en el segundo.
¿Cómo evitar que vuelva a pasar?
Después de recuperar el servicio identificamos varias líneas de mejora.
Todavía debemos estudiar y probar cada cambio antes de llevarlo a producción.
1. Evitar depender de DNS durante el arranque
La primera medida consiste en revisar cómo Nginx resuelve los backends.
Actualmente un backend definido directamente por nombre puede convertirse en una dependencia durante el arranque.
Queremos conseguir algo conceptualmente parecido a:
DNS falla temporalmente
│
▼
ese backend puede fallar temporalmente
│
▼
pero Nginx sigue funcionando
│
▼
el resto de webs continúa disponible
La idea fundamental es aislar los fallos.
Un problema en una aplicación no debería inutilizar aplicaciones independientes.
2. Añadir mecanismos de recuperación automática
Otra posibilidad es establecer una política de reintentos.
Conceptualmente:
Nginx falla
│
▼
esperamos unos segundos
│
▼
reintentamos
Si el problema era simplemente una ventana temporal mientras DNS volvía a estar disponible, un segundo intento habría recuperado probablemente el servicio automáticamente.
Esto no sustituye a solucionar la dependencia.
Pero añade una segunda capa de defensa.
3. Monitorización externa
Hay otro problema evidente en este incidente:
el servicio estuvo caído durante horas porque nos enteramos al intentar acceder a él.
Queremos añadir monitorización desde fuera de nuestra propia infraestructura.
Por ejemplo:
Monitor externo
│
├── comprueba Web A
├── comprueba Web B
└── comprueba Web C
│
▼
¿alguna no responde?
│
sí
│
▼
alerta
La monitorización debe realizarse externamente.
Si el mismo servidor que queremos vigilar es también el encargado de avisarnos, una caída completa podría impedir incluso la alerta.
4. Alertar cuando un servicio entra en failed
También podemos detectar directamente situaciones como:
nginx.service → failed
Esto permitiría distinguir entre:
la aplicación responde lentamente
y:
nuestro punto de entrada está directamente apagado
5. Revisar qué servicios se reinician automáticamente
Las actualizaciones automáticas siguen siendo importantes.
La solución no debería ser simplemente:
desactivar actualizaciones
sino entender mejor:
- qué servicios necesitan reiniciarse;
- cuándo se reinician;
- qué dependencias tienen;
- qué ocurre si alguno no vuelve correctamente.
Automatizar mantenimiento es útil.
Automatizarlo sin observar sus consecuencias puede introducir nuevos modos de fallo.
6. Probar el escenario antes de darlo por solucionado
Una medida preventiva no debería darse por buena simplemente porque "parece correcta".
Después de aplicar las mejoras tendremos que intentar reproducir de forma controlada situaciones como:
DNS temporalmente no disponible
y comprobar:
¿arranca Nginx?
¿siguen disponibles las webs independientes?
¿se recupera automáticamente?
¿recibimos una alerta?
Solo entonces podremos considerar cerrada completamente la remediación.
Qué aprendimos
Este incidente nos dejó varias lecciones interesantes.
Un servicio healthy puede no estar disponible para el usuario
Nuestros servicios internos seguían funcionando.
Eso no significa que el sistema completo funcionase.
La salud debe comprobarse desde la perspectiva del usuario.
DNS es una dependencia
Cuando pensamos en dependencias solemos imaginar:
base de datos
API
Redis
Docker
backend
Pero DNS también lo es.
Si nuestro software necesita convertir:
backend.example
en:
dirección IP
estamos dependiendo de que exista un resolver funcionando.
After= no significa "estará sano para siempre"
En systemd podemos declarar relaciones de orden entre servicios.
Pero ordenar:
A después de B
no equivale necesariamente a:
cuando A se ejecute, B estará completamente operativo
y todas sus funciones responderán correctamente.
Una dependencia de orden no sustituye a un health check.
Los fallos pequeños pueden amplificarse
La indisponibilidad inicial de DNS fue temporal.
Pero produjo un estado persistente:
nginx = failed
Es un ejemplo de cómo:
fallo de segundos
puede transformarse en:
incidente de horas
si no existe un mecanismo de recuperación.
Los puntos únicos de entrada necesitan especial atención
Un reverse proxy simplifica muchísimo una infraestructura.
Pero también puede convertirse en un componente crítico.
Si muchas aplicaciones comparten la misma puerta de entrada, debemos evitar que un fallo aislado en una de ellas impida arrancar todo el proxy.
Los logs permiten reconstruir una historia
Al principio solo sabíamos esto:
la web no funciona
Después de revisar:
systemctl
journalctl
logs de actualizaciones
estado de contenedores
estado de DNS
pudimos reconstruir una secuencia de apenas unos segundos ocurrida varias horas antes.
Esa es una de las partes más interesantes de administrar sistemas:
los logs no son simplemente mensajes de error. Son evidencias que permiten reconstruir lo que ocurrió.
Cronología final
La secuencia simplificada del incidente quedó así:
06:11
│
├─ comienza el proceso automático de actualización
│
06:12
│
├─ se actualizan paquetes del sistema
│
├─ se determina que varios servicios necesitan reiniciarse
│
├─ se reinicia el reverse proxy
│
├─ se reinician servicios de red y DNS
│
├─ Nginx ejecuta su validación previa al arranque
│
├─ no puede resolver temporalmente un backend
│
├─ la validación falla
│
└─ Nginx queda en estado failed
│
│
│ El resto de aplicaciones continúa funcionando,
│ pero deja de ser accesible desde Internet.
│
▼
Más tarde detectamos el incidente
│
├─ comprobamos Nginx
├─ comprobamos contenedores
├─ descartamos un reinicio del servidor
├─ analizamos journalctl
├─ reconstruimos la actualización automática
└─ identificamos la condición de carrera
│
▼
Arrancamos Nginx manualmente
│
▼
Servicio recuperado
Conclusión
La causa de este incidente no fue un gran fallo espectacular.
Fue la combinación de varias cosas perfectamente normales:
- una actualización automática;
- varios servicios que necesitaban reiniciarse;
- una dependencia DNS;
- dos servicios arrancando prácticamente al mismo tiempo;
- una validación de configuración;
- y la ausencia de un reintento posterior.
Cada pieza por separado tenía sentido.
La interacción entre ellas produjo la caída.
Y probablemente esa sea la lección más importante:
En sistemas, muchos incidentes no aparecen porque un componente esté mal diseñado de forma aislada, sino por cómo interactúan varios componentes cuando ocurre algo fuera del flujo habitual.
Recuperar el servicio fue sencillo:
systemctl start nginx
Entender por qué había ocurrido llevó bastante más trabajo.
Pero esa segunda parte es precisamente la que permite convertir una caída en una mejora de la infraestructura.
Y también en una oportunidad para aprender.
