El deploy terminó correctamente, pero el sitio está caído
Tu dashboard indica que está en ejecución, pero tus usuarios ven un error. Descubre por qué el éxito del deploy y el estado de salud de la aplicación son señales diferentes, y cómo hacer que un deploy en verde signifique que la aplicación realmente responde.
Hay un tipo muy concreto de mala mañana que empieza con una marca de verificación verde. El deploy terminó correctamente, pero el sitio está caído, el dashboard indica running y alguien te está enviando una captura de un error 502.
No es un caso extremo poco frecuente. Es el resultado predecible de que una plataforma informe de una cosa y mida otra, y conviene entenderlo con precisión, porque la solución no es «comprobar más», sino cambiar lo que se permite que signifique la palabra running.
Tres preguntas diferentes, una sola luz de estado
Cuando una plataforma indica que un servicio está en ejecución, podría estar respondiendo a cualquiera de estas preguntas:
- ¿Se inició el contenedor? El proceso existe y no se ha detenido.
- ¿Está abierto el puerto? Hay algo escuchando donde la plataforma espera.
- ¿La aplicación responde correctamente? Una solicitud recibe una respuesta que significa que la aplicación está lista para trabajar.
Son garantías completamente distintas, y la mayoría de los incidentes de este tipo se producen porque el dashboard responde a la pregunta 1 mientras tú dabas por hecho que respondía a la pregunta 3.
Un proceso de Node que arranca, no consigue conectarse a su base de datos y queda atrapado en un bucle de reintentos cumple la pregunta 1 indefinidamente. No se ha bloqueado. Nunca atenderá una solicitud. El contenedor está «en ejecución» en todos los sentidos que le importan al orquestador.
El hueco donde vive la caída
La ventana peligrosa se encuentra entre «la nueva versión se ha iniciado» y «la nueva versión puede funcionar». Durante esa ventana, una plataforma ingenua ya ha dirigido el tráfico, porque iniciar era lo único que medía.
Esto es peor que un simple bloqueo por la historia de rollback. Un bucle de bloqueos es ruidoso: el contenedor se detiene, se reinicia, vuelve a detenerse y la plataforma acaba dándose cuenta. Un arranque seguido de un bloqueo es silencioso. Nada se reinicia, no salta ninguna alerta y la versión anterior que funcionaba normalmente ya se ha eliminado.
Esa última parte es el daño real. La versión anterior estaba bien. Se eliminó porque una nueva había arrancado, y se confundió iniciar con funcionar.
Cómo funciona un health gate real
La solución es estructural, no procedimental. El tráfico no debería cambiar de destino hasta que la nueva versión haya respondido a una solicitud.
En Dockup, un release funciona así: la nueva versión se compila de forma aislada, se inicia junto a la versión que está atendiendo las solicitudes y, entonces, se le hace una pregunta. El dominio solo apunta a ella cuando responde. Si nunca responde, el release se detiene ahí y la versión anterior continúa atendiendo las solicitudes; nadie fuera de tu dashboard llega a saber que se intentó hacer un deploy.
Por eso un deploy fallido en Dockup no es una caída. El contenedor anterior nunca se eliminó suponiendo que el nuevo funcionaría correctamente.
# The health gate is per-service configuration, not a platform default you inherit
dockup info my-project/my-api --json
El bloque healthCheck de esa salida es todo el contrato: qué ruta se solicita, cuánto tiempo se espera una respuesta, cuántas veces se intenta y cuánto tiempo transcurre entre intentos.
Configura la comprobación para responder a la pregunta 3
Un endpoint de health que devuelve 200 incondicionalmente es peor que no tener ninguno, porque convierte un gate real en un simple trámite. El objetivo de la comprobación es que falle cuando la aplicación no puede hacer su trabajo.
Un endpoint de readiness útil verifica aquello sin lo que la aplicación no puede funcionar:
// Not this — it proves only that the process is alive
app.get('/healthz', (req, res) => res.send('ok'))
// This — it proves the app can actually serve a request
app.get('/healthz', async (req, res) => {
try {
await db.query('select 1') // the dependency that is usually the problem
if (!cacheReady) throw new Error('cache warming')
res.status(200).json({ ok: true })
} catch (err) {
res.status(503).json({ ok: false, reason: err.message })
}
})
Dos reglas hacen que esto funcione en la práctica:
Comprueba las dependencias sin las que no puedes atender solicitudes, y nada más. Si tu aplicación puede degradarse correctamente cuando el índice de búsqueda no está disponible, no hagas que el readiness falle por el índice de búsqueda: bloquearás los deploys por algo que no es una caída.
Mantenlo barato. El endpoint se llama repetidamente durante cada release. Una comprobación de readiness que ejecuta una consulta costosa crea un problema de carga autoinfligido.
Dale el tiempo suficiente, pero no un tiempo ilimitado
Dos ajustes determinan si el gate ayuda o perjudica:
- El timeout de cada intento debe superar la cold start legítima más lenta. Una aplicación que se conecta a una base de datos y calienta una caché en ocho segundos fallará siempre una comprobación de tres segundos, y acabarás «solucionándolo» desactivando el gate; así volverás al punto de partida.
- Los reintentos deben cubrir el tiempo total de arranque, no un solo intento. Intervalo × reintentos es el presupuesto real.
En Dockup, esos ajustes son healthCheckInterval, healthCheckTimeout y healthCheckRetries, y se configuran por servicio porque un monolito de Rails y un sidecar de Go no se inician siguiendo el mismo ritmo.
Cuando ya está caído
Si estás leyendo esto en mitad de un incidente, este orden es el que lo resuelve más rápido:
- Comprueba si la aplicación responde directamente, evitando el dominio. Si responde en su puerto pero no a través del dominio, se trata de un problema de routing, no de la aplicación, y deberías dejar de depurar tu código.
- Lee los logs de runtime, no los logs de build. El build terminó correctamente: esa es la premisa. Lo que quieres saber es qué hizo el proceso después de iniciarse.
- Haz rollback antes de diagnosticar. Diagnosticar es más barato cuando nadie está mirando.
dockup logs my-project/my-api --follow # what the running process is saying
dockup deployments my-project/my-api # what was live before this
dockup rollback <deployment-id> my-project/my-api # put that back
En Dockup, un rollback es un cambio de versión y no una nueva compilación, porque la versión anterior sigue en el disco. Esto importa a las 3 de la mañana: la recuperación más rápida es la que no tiene que compilar nada.
La pregunta que debes hacerle a una plataforma
Cuando estés eligiendo dónde ejecutar producción, esta es una buena prueba que conviene hacer deliberadamente: despliega una aplicación que se inicie correctamente y después no consiga conectarse a su base de datos. Observa qué indica el dashboard.
Si dice running, ahora sabes exactamente cuánto valdrá esa palabra durante tu próximo incidente.
Preguntas frecuentes
¿Por qué mi dashboard indica que está en ejecución cuando el sitio está caído? Porque «en ejecución» normalmente significa que existe el proceso del contenedor, no que la aplicación pueda atender una solicitud. Un proceso bloqueado intentando conectarse a una base de datos cumple indefinidamente esa definición.
¿Una comprobación de health debería acceder a la base de datos? Sí, si tu aplicación no puede atender solicitudes sin ella. Comprueba las dependencias que realmente necesitas y omite aquellas sin las que puedas degradarte correctamente.
¿Cuál es la diferencia entre liveness y readiness? Liveness pregunta si el proceso debería reiniciarse. Readiness pregunta si debería recibir tráfico. El gate que evita este fallo es readiness, y debe ejecutarse antes de cambiar el tráfico.
¿Cómo puedo evitar por completo que un deploy defectuoso haga caer el sitio? Cambia el tráfico solo después de que la nueva versión responda a una solicitud real y conserva la versión anterior hasta confirmar el cambio. Así, un release fallido será un release que nunca llegó a producirse, en lugar de una caída.
