Tienes una pequeña pila “de producción” en Docker Compose. Ha ido bien durante meses. Entonces una actualización rutinaria se convierte en una interrupción de 2 minutos,
tu chat se llena de “¿está caído?” y te quedas mirando docker compose up -d como si te hubiera traicionado personalmente.
Compose puede ejecutar cargas reales sin problema. Pero “actualizaciones sin tiempo de inactividad en Compose” no es una característica que activas: es un conjunto de decisiones operativas
que implementas, pruebas y ocasionalmente lamentas. Separemos el mito del marketing de la realidad ingenieril, y luego construyamos patrones
que realmente aguanten cuando los clientes están conectados, hay solicitudes en vuelo y tu base de datos está sintiendo cosas.
Mito vs realidad: qué puede y qué no puede hacer Compose
The myth: “Compose does rolling updates”
Docker Compose, por sí solo, no es un orquestador. No realiza de forma nativa actualizaciones rolling con drenado consciente del tráfico, descubrimiento de servicios
entre hosts, o reemplazo automático frente a fallos. Cuando ejecutas docker compose up -d, Compose reconcilia el estado deseado en un host. Puede recrear contenedores.
Puede detenerlos y arrancarlos. No coordinará “mantener el viejo, arrancar el nuevo, desviar tráfico, y luego retirarlo” a menos que construyas esos mecanismos alrededor.
The reality: “Compose can do zero-downtime-ish with a reverse proxy and discipline”
Puedes acercarte mucho al cero tiempo de inactividad para muchas cargas web. El truco es dejar de tratar “el contenedor de la app” como el dueño del socket.
Tu punto de entrada estable debe ser un proxy reverso (o proxy L4) que permanezca en pie mientras intercambias backends detrás. Luego añades:
- Healthchecks que reflejen readiness, no solo liveness.
- Apagado gradual (señales de stop y timeouts) para que las solicitudes en vuelo terminen.
- Un método de despliegue que arranque instancias nuevas antes de parar las viejas.
- Disciplina en cambios de base de datos (expandir/contraer, compatible hacia atrás, evitar migraciones “stop the world”).
Si tu pila es un único contenedor escuchando en el puerto 443 en el host, estás intentando hacer “cero inactividad” mientras arrancas y arrancas el suelo bajo tus pies.
Es posible. También es un hobby extraño.
Una cita para tener en mente: La esperanza no es una estrategia
— a menudo atribuida a círculos de fiabilidad; trátala como una idea parafraseada,
no como una prueba de sala judicial.
Hechos interesantes y contexto histórico (por qué esto confunde)
- Compose empezó como “Fig” (2014). Fue diseñado para flujos de trabajo de desarrollador, no para despliegues de producción con SLOs.
-
Docker introdujo “Swarm mode” más tarde con actualizaciones rolling, reprogramación basada en salud y abstracciones de servicio—características que Compose
nunca llegó a tener. -
depends_onnunca significó “esperar hasta que esté listo”. Es ordenamiento, no control de readiness. Esta mala interpretación ha causado
más salidas “funciona en mi portátil” de las que la gente admite. -
Los healthchecks llegaron después (era Docker 1.12). Antes de eso, la gente usaba “sleep 10” como estrategia de readiness. Sigue siendo popular,
por razones mayormente psicológicas. -
Las políticas de reinicio no son orquestación.
restart: alwayses un cinturón de seguridad, no un piloto automático. - Nginx ha soportado recarga gradual por años—una razón importante por la que se convirtió en la “puerta de entrada” por defecto para despliegues DIY sin downtime.
-
Linux añadió
SO_REUSEPORT(uso general desde 2013 aprox.) permitiendo que varios procesos enlacen el mismo puerto en algunos diseños,
pero no arregla mágicamente la coordinación de despliegues para contenedores. - El despliegue blue/green precede a los contenedores. Los equipos de operaciones lo hacían con pares de VM y balanceadores de carga mucho antes de que Docker lo pusiera de moda.
- Las migraciones de base de datos son la verdadera fábrica de downtime. Los contenedores de app son fáciles; los cambios de esquema con locks exclusivos son donde los sueños van a morir.
Define “tiempo de inactividad cero” como un adulto
“Tiempo de inactividad cero” es una frase que significa cosas muy distintas según quién esté sudando. Define claramente antes de cambiar nada.
Aquí están las versiones comunes:
- Sin interrupción de aceptación TCP: el puerto nunca deja de aceptar conexiones. Los clientes aún pueden ver errores si tu backend no está listo.
- Sin picos de 5xx: las solicitudes siguen teniendo éxito. Puede aceptarse algún aumento de latencia.
- Sin interrupción visible para el usuario: las sesiones persisten, los websockets sobreviven, las long polls continúan. Esto es más difícil de lo que parece.
- Sin consumo del presupuesto de errores por despliegue: el cambio aún puede causar problemas, pero el proceso de despliegue no debería ser la causa.
Para pilas basadas en Compose, el objetivo realista suele ser: sin picos de 5xx y sin caída en el endpoint público, con un aumento de latencia acotado
durante el intercambio. Si sirves websockets, redefine el éxito: puedes tener “cero inactividad” y aun así desconectar clientes a menos que implementes
drenado de conexiones y enrutamiento sticky explícito.
Broma #1: Si alguien te dice que tiene “verdadero cero inactividad” en Compose con un solo contenedor, pregunta qué usa para viajar en el tiempo.
Los modos de fallo que realmente golpean en producción
1) El enlace de puerto es un punto único de dolor
Si tu contenedor de app enlaza 0.0.0.0:443 en el host, no puedes arrancar la nueva versión hasta que la antigua libere el puerto.
Eso significa una brecha. Aunque sean 200 ms, sigue siendo una brecha. Bajo carga, los clientes retroceden mal, las reintentos se encadenan y de repente “milisegundos” se convierte en
“por qué falló nuestro checkout”.
2) “Contenedor arrancado” ≠ “servicio listo”
Muchas apps arrancan su proceso rápidamente y luego pasan 5–30 segundos ejecutando migraciones, calentando cachés o esperando dependencias. Si tu proxy enruta
tráfico temprano, servirás errores. Si bloqueas el enrutamiento hasta que esté listo, estarás bien—siempre que la readiness esté medida correctamente.
3) Manejo de SIGTERM no es opcional
Docker enviará SIGTERM (por defecto) y luego SIGKILL tras el timeout de parada. Si tu app ignora SIGTERM, perderás las solicitudes en vuelo.
Si el stop timeout es demasiado corto, también perderás solicitudes en vuelo. Si terminas el proxy primero, perderás todo.
4) Migraciones de base de datos que bloquean tablas
La típica salida con Compose no es culpa de Docker. Es una migración que toma un lock exclusivo, o una reescritura de columna, o una creación de índice sin concurrencia.
La app deja de responder, el healthcheck falla, Compose la reinicia, y ahora tienes un DoS auto-infligido.
5) Servicios stateful detrás de patrones “stateless”
Puedes blue/green tu capa web. No puedes blue/green casualmente una base de datos de instancia única arrancando “otro contenedor” a menos que tu historia de almacenamiento,
replicación y failover ya sea madura. Compose puede ejecutar Postgres. Compose no es una solución HA para Postgres.
6) El asesino silencioso: pools de conexión y DNS obsoleto
Si rotas backends intercambiando IPs de contenedores y esperas que los clientes “simplemente se reconecten”, descubrirás que los pools de conexión y el caché DNS
tienen opiniones. Algunos drivers cachean IPs resueltas más tiempo del que esperas. Algunas apps nunca se reconectan a menos que las reinicies. Por eso importan
los nombres de servicio estables (frontales proxy).
Patrones viables para cerca de cero inactividad en Compose
Patrón A: Proxy reverso estable + servicios de app versionados (estilo blue/green)
Este es el patrón que recomiendo con más frecuencia porque coincide con las fortalezas de Compose: simple, local, determinista. Mantienes un contenedor proxy estable
enlazado a los puertos del host (80/443). Tu app corre detrás en una red definida por el usuario. Durante el despliegue, arrancas un nuevo servicio de app (green) junto
al viejo (blue), verificas readiness y luego cambias el upstream del proxy y retiras el viejo de forma gradual.
Características clave:
- El proxy es lo único enlazando puertos del host.
- Ambas versiones de la app pueden correr simultáneamente en la red interna.
- El cambio de tráfico es una recarga de configuración, no la recreación de un contenedor.
- El rollback es volver a cambiar el proxy y matar la versión mala.
Puedes implementar el proxy con Nginx, HAProxy, Traefik, Caddy. Escoge uno que puedas operar. “Operar” significa: puedes recargarlo de forma segura, leer sus logs y explicar qué pasa cuando un backend falla.
Patrón B: Escalado con --scale + drenado + recreación (rolling limitado)
Compose soporta escalar un servicio a múltiples réplicas en un host. Eso por sí solo no te da actualizaciones rolling, pero te da margen de maniobra:
levanta réplicas adicionales de la nueva versión, dirige tráfico hacia ellas y luego elimina las réplicas viejas. La limitación es que Compose no hace
“orden de actualización” de forma nativa como lo hace un orquestador. Estás escribiendo el playbook.
Esto funciona mejor cuando:
- Tu app es stateless o el estado de sesión está externalizado.
- Tu proxy/balanceador puede detectar salud de backends y dejar de enrutar.
- Te conformas con una secuencia manual o un pequeño script de despliegue.
Patrón C: Activación de socket / propiedad de puerto a nivel host (avanzado, con bordes filosos)
Si estás decidido a evitar un contenedor proxy, puedes dejar que systemd posea el socket y se lo entregue a la instancia de app actual (socket activation). Esto puede funcionar.
También puede convertirse en un generador de outages artesanales si tu app no lo soporta limpiamente o no pruebas la recarga bajo carga.
Para la mayoría de equipos que ejecutan Compose, el patrón del proxy estable es el punto óptimo. Menos creativo. Más fiable.
Patrón D: “Migraciones aburridas” + despliegue de app (el requisito oculto)
Incluso un intercambio perfecto de contenedores no ayuda si tu migración bloquea la base de datos por 30 segundos. El patrón de despliegue debe incluir disciplina en la base de datos:
- Cambios de esquema expandir/contraer (añadir columnas/tablas primero, desplegar código y luego eliminar lo antiguo).
- Lecturas y escrituras compatibles hacia atrás durante la transición.
- Construcción de índices en línea cuando sea posible.
- Feature flags cuando un cambio no puede ser instantáneo.
Compose no te impide hacer esto. Tampoco te lo recuerda. Tú eres quien recuerda.
Broma #2: Una migración con lock exclusivo es lo único que puede tumbar tu app más rápido que tu CEO intentando “ayudar con el despliegue”.
Tareas prácticas: comandos, salidas y decisiones (12+)
Estas son las comprobaciones que realmente ejecuto en un host Compose cuando intento hacer real el “cero inactividad”. Cada tarea incluye: el comando, cómo suele verse la salida,
qué significa y qué decisión tomas a continuación.
Tarea 1: Confirma lo que Compose cree que está corriendo
cr0x@server:~$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
prod-proxy-1 nginx:1.25-alpine "/docker-entrypoint.…" proxy 2 weeks ago Up 2 weeks 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
prod-app-blue-1 registry/app:1.9.3 "/app/start" app-blue 2 weeks ago Up 2 weeks (healthy)
prod-app-green-1 registry/app:1.9.4 "/app/start" app-green 3 minutes ago Up 3 minutes (healthy)
Qué significa: Tienes dos versiones de la app en vivo y un proxy estable.
Decisión: Si green está saludable, estás listo para desviar tráfico cambiando el upstream del proxy, no parando blue.
Tarea 2: Inspeccionar el estado y temporización del healthcheck
cr0x@server:~$ docker inspect --format '{{json .State.Health}}' prod-app-green-1
{"Status":"healthy","FailingStreak":0,"Log":[{"Start":"2026-01-03T10:12:01.123Z","End":"2026-01-03T10:12:01.187Z","ExitCode":0,"Output":"ok\n"}]}
Qué significa: El contenedor reporta readiness (asumiendo que tu healthcheck es significativo).
Decisión: Si el estado es starting o unhealthy, no desvíes tráfico. Arregla la readiness o el arranque de la app primero.
Tarea 3: Verificar que el proxy vea ambos backends y cuál está activo
cr0x@server:~$ docker exec -it prod-proxy-1 nginx -T | sed -n '1,120p'
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# configuration content:
upstream app_upstream {
server prod-app-blue-1:8080 max_fails=2 fail_timeout=5s;
server prod-app-green-1:8080 max_fails=2 fail_timeout=5s;
}
server {
listen 80;
location / {
proxy_pass http://app_upstream;
}
}
Qué significa: El proxy está configurado para balancear carga entre ambos.
Decisión: Si quieres un corte estricto (no balanceo), configura pesos o cambia a un upstream con un único objetivo para el corte.
Tarea 4: Validar que la recarga de Nginx sea gradual (sin workers caídos)
cr0x@server:~$ docker exec -it prod-proxy-1 nginx -s reload && docker exec -it prod-proxy-1 tail -n 5 /var/log/nginx/error.log
2026/01/03 10:15:22 [notice] 1#1: signal process started
2026/01/03 10:15:22 [notice] 1#1: reconfiguring
2026/01/03 10:15:22 [notice] 1#1: using the "epoll" event method
Qué significa: La recarga tuvo éxito; Nginx no reinició desde cero.
Decisión: Si la recarga falla, no despliegues. Arregla la generación de config y ejecuta nginx -t antes de recargar.
Tarea 5: Confirmar stop timeout y comportamiento de señales (apagado gradual)
cr0x@server:~$ docker inspect --format 'StopSignal={{.Config.StopSignal}} StopTimeout={{.Config.StopTimeout}}' prod-app-blue-1
StopSignal=SIGTERM StopTimeout=30
Qué significa: Docker enviará SIGTERM y esperará 30s antes de SIGKILL.
Decisión: Si tu app necesita 60s para drenar, configura stop_grace_period: 60s. Si ignora SIGTERM, arregla la app. Ningún YAML te salvará.
Tarea 6: Vigilar conexiones en vuelo durante una ventana de drenado
cr0x@server:~$ docker exec -it prod-app-blue-1 ss -Hnt state established '( sport = :8080 )' | wc -l
47
Qué significa: 47 conexiones TCP establecidas a la app.
Decisión: Si el número no baja después de quitar el backend del proxy, puedes tener conexiones de larga duración (websockets) y necesitar un plan de drenado más largo.
Tarea 7: Confirmar qué contenedor está recibiendo realmente tráfico
cr0x@server:~$ docker logs --since=2m prod-app-green-1 | tail -n 5
10.0.2.5 - - [03/Jan/2026:10:16:01 +0000] "GET /healthz HTTP/1.1" 200 2 "-" "kube-probe/1.0"
10.0.2.5 - - [03/Jan/2026:10:16:04 +0000] "GET /api/orders HTTP/1.1" 200 431 "-" "Mozilla/5.0"
Qué significa: Green está recibiendo solicitudes reales.
Decisión: Si solo las health checks golpean a green, no has desviado tráfico de producción; cambia el enrutamiento o los pesos deliberadamente.
Tarea 8: Detectar si estás recreando el proxy accidentalmente (generador de outages)
cr0x@server:~$ docker compose up -d --no-deps proxy
[+] Running 1/0
✔ Container prod-proxy-1 Running
Qué significa: Compose no recreó el contenedor proxy.
Decisión: Si esto imprime “Recreated”, estás rebotando la puerta de entrada. Para. Fija los cambios de configuración del proxy y recarga dentro del contenedor en su lugar.
Tarea 9: Comparar digests de imágenes en ejecución (evitar sorpresas con “latest”)
cr0x@server:~$ docker images --digests registry/app | head -n 5
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
registry/app 1.9.4 sha256:8b9a2f6d3c1e8f... 4a1f2c3d4e5f 2 days ago 156MB
registry/app 1.9.3 sha256:1c2d3e4f5a6b7c... 7b6a5c4d3e2f 2 weeks ago 155MB
Qué significa: Puedes identificar exactamente qué está desplegado.
Decisión: Si usas :latest sin digests, deja de hacerlo. Etiqueta tus releases y guarda una imagen de rollback conocida.
Tarea 10: Comprobar eventos de Docker durante el despliegue (encuentra quién reinició qué)
cr0x@server:~$ docker events --since 10m --until 0m | tail -n 12
2026-01-03T10:12:10.000000000Z container start 2f1a... (name=prod-app-green-1, image=registry/app:1.9.4)
2026-01-03T10:14:02.000000000Z container health_status: healthy 2f1a... (name=prod-app-green-1)
2026-01-03T10:15:22.000000000Z container exec_start: nginx -s reload 9aa2... (name=prod-proxy-1)
Qué significa: Línea de tiempo de lo que realmente pasó, no de lo que recuerdas haber hecho.
Decisión: Si ves eventos de reinicio/recreación del proxy durante el despliegue, tu historia de “cero inactividad” tiene un agujero.
Tarea 11: Verificar presión de recursos a nivel kernel (throttling CPU e IO wait)
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 51200 21000 320000 0 0 120 80 600 1200 25 10 55 10 0
3 1 0 48000 20500 318000 0 0 2200 300 900 1700 30 12 38 20 0
Qué significa: La segunda muestra muestra mayor IO wait (wa) y procesos bloqueados (b).
Decisión: Si IO wait se dispara durante el despliegue (pull de imagen, migraciones), programa el despliegue fuera de pico, mueve imágenes a un registro/local cache, o arregla el rendimiento del almacenamiento.
Tarea 12: Identificar qué proceso posee el puerto del host (atrapar binds accidentales)
cr0x@server:~$ sudo ss -Htlpn '( sport = :443 )'
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("docker-proxy",pid=2143,fd=4))
Qué significa: Docker proxy posee el puerto publicado. Si el contenedor se recrea, el bind hará flap.
Decisión: Mantén un contenedor proxy estable. No publiques puertos de app directamente si quieres swaps sin interrupciones.
Tarea 13: Confirmar attachments de red (¿el proxy está en la red correcta?)
cr0x@server:~$ docker inspect --format '{{range .NetworkSettings.Networks}}{{.NetworkID}} {{end}}' prod-proxy-1
a8c1f0d3e2b1c4d5e6f7a8b9c0d1e2f3
Qué significa: El proxy está en al menos una red definida por el usuario.
Decisión: Si el proxy y la app no están en la misma red, la resolución de nombres como prod-app-green-1 no funcionará. Arregla las redes antes de culpar a Docker.
Tarea 14: Comprobar locks en la base de datos durante migración (el arma humeante del downtime)
cr0x@server:~$ docker exec -it prod-db-1 psql -U postgres -d app -c "select pid, wait_event_type, wait_event, state, query from pg_stat_activity where state <> 'idle' order by pid;"
pid | wait_event_type | wait_event | state | query
------+-----------------+---------------+--------+----------------------------------------
2412 | Lock | relation | active | ALTER TABLE orders ADD COLUMN foo text;
2550 | Lock | transactionid | active | UPDATE orders SET foo = 'x' WHERE ...
Qué significa: Sesiones activas esperando locks. Tu “caída por deploy” puede ser un lock de esquema.
Decisión: Para la migración si no es segura, o rediseñala (enfoque online, batching, índices concurrentes). No sigas reiniciando contenedores de app esperando que se resuelva.
Tarea 15: Confirmar que Compose no cambió contenedores silenciosamente por drift de config
cr0x@server:~$ docker compose config | sed -n '1,120p'
name: prod
services:
app-blue:
image: registry/app:1.9.3
healthcheck:
test:
- CMD
- /bin/sh
- -c
- curl -fsS http://localhost:8080/ready || exit 1
interval: 5s
timeout: 2s
retries: 12
stop_grace_period: 60s
proxy:
image: nginx:1.25-alpine
ports:
- mode: ingress
target: 80
published: "80"
protocol: tcp
- mode: ingress
target: 443
published: "443"
protocol: tcp
Qué significa: Esta es la configuración completamente renderizada que Compose está aplicando.
Decisión: Si la salida de config difiere de lo que crees que desplegaste, arregla la gestión de variables de entorno y fija valores. “Config sorpresa” es la mejor amiga del downtime.
Tres mini-historias corporativas desde las trincheras
Mini-historia 1: El incidente causado por una suposición equivocada (“depends_on significa ready”)
Un equipo SaaS de tamaño medio ejecutaba su pila de producción en una sola VM con Docker Compose. Fue una elección deliberada: querían menos piezas móviles
y su tráfico cabía holgadamente en un host. Sensato. El sistema tenía una API, un worker, Redis y Postgres.
Hicieron una actualización rutinaria: nueva imagen de la API, migración menor, reinicio. El playbook de despliegue era una línea: docker compose up -d.
Asumieron que sus ajustes de depends_on implicaban que la API esperaría por Postgres. No lo hacía.
Postgres tardó más de lo habitual en arrancar porque la VM también estaba tirando imágenes y realizando escrituras en el sistema de archivos. La API arrancó, falló al conectar,
salió, reinició, falló de nuevo. Su política de reinicio convirtió un arranque lento en un loop cerrado. Mientras tanto el proxy reverso enrutó tráfico a un backend inestable.
Los usuarios vieron 502 intermitentes durante varios minutos—lo suficiente para tickets de soporte y bingo interno de culpas.
La solución no fue exótica. Añadieron un endpoint real de readiness a la API, conectaron un healthcheck a él y configuraron el proxy para enrutar solo a backends saludables.
También añadieron un periodo de inicio para que fallos transitorios de arranque no marcaran instantáneamente el servicio como no saludable.
La lección fue incómoda porque era aburrida: Compose ordenaba arranques de contenedores; no garantizaba que las dependencias estuvieran listas. Dejaran de asumir “arrancado” significa “listo”, y sus despliegues dejaron de ser dramáticos.
Mini-historia 2: La optimización que salió mal (despliegues rápidos, discos lentos)
Otra organización ejecutaba Compose en un host potente con NVMe—hasta que comprasar sustituyó por hardware “similar” con mucho CPU y RAM pero almacenamiento mediocre. El equipo optimizó despliegues tirando siempre imágenes nuevas justo antes de la actualización. Lanzamientos más rápidos, menos “funciona en CI” distintos. En teoría.
Bajo carga, el despliegue descargó una gran capa de imagen saturando IO. El checkpointing de Postgres se ralentizó. Las latencias de la API subieron. Los healthchecks
empezaron a caducar. El proxy marcó backends como no saludables. De repente “desplegar más rápido” se convirtió en “el despliegue causa un brownout en cascada”.
Respondieron endureciendo los umbrales de healthcheck para “ser más sensibles”, que fue exactamente la reacción equivocada. Eso hizo que el proxy expulsara backends antes,
amplificando la caída. Su tasa de consumo del SLO se disparó, y todos aprendieron la diferencia entre “detectar fallo” y “causar fallo”.
La solución final: pre-pull de imágenes durante periodos tranquilos, limitar impacto de IO (usando programación a nivel host y a veces solo disciplina humana), y hacer los healthchecks tolerantes a picos cortos.
También movieron el directorio de datos de Docker a almacenamiento más rápido y separaron el IO de la base de datos de la extracción de capas de imagen tanto como fue práctico.
Lección de optimización: la velocidad no es gratis. Si haces despliegues más rápidos moviendo trabajo a la ventana crítica, tus usuarios pagarán el interés.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día (dos versiones + rollback fácil)
Un equipo empresarial regulado—procesos pesados, mucho papeleo—ejecutaba un portal de clientes en Compose. No eran modernos. Tampoco estaban casi nunca caídos.
Su método de despliegue parecía de la vieja escuela: proxy estable, dos servicios de app (“blue” y “green”), puertas de salud explícitas y un cambio manual de tráfico.
Un viernes, la nueva versión pasó los tests pero tenía una fuga de memoria sutil activada por un patrón de petición raro. Veinte minutos después del corte, el RSS empezó a aumentar.
La latencia subió. El on-call lo vio desarrollarse con la calma de alguien que lo ha ensayado.
Cambiaron el tráfico de vuelta a blue recargando la config del proxy y luego pararon green. El rollback tomó menos tiempo del que llevó explicar por qué hacían rollback. Los clientes apenas lo notaron: un pequeño
pico de latencia, sin banner de caída, sin pánico.
Más tarde, en el postmortem, nadie elogió el YAML. Elogiaron la disciplina aburrida: siempre mantener la versión previa corriendo hasta que la nueva se pruebe, y hacer del rollback una acción reversible única, no una danza heroica de varios pasos.
La lección: la fiabilidad es mayormente repetición poco glamorosa. Si tu despliegue requiere valentía, ya has perdido.
Guía de diagnóstico rápido: encuentra el cuello de botella pronto
Cuando un “despliegue sin inactividad” causa un pico, no tienes tiempo para debates filosóficos. Necesitas una secuencia de triage rápida que reduzca el culpable a: proxy/cambio de tráfico, readiness de la app, base de datos o recursos del host.
Primero: ¿La puerta de entrada está estable?
- Comprueba si el contenedor proxy fue recreado o reiniciado durante el despliegue (
docker events, uptime endocker ps). - Comprueba continuidad del bind de puertos del host (
ss -ltnppara 80/443). - Revisa logs del proxy por fallos en upstream y errores de recarga.
Si el proxy hizo flap, ese es tu outage. Arregla el proceso para que el proxy permanezca y solo recargue configuración.
Segundo: ¿Los backends están saludables y realmente listos?
- Inspecciona el estado de salud de los contenedores nuevos.
- Golpea el endpoint de readiness desde dentro de la red del proxy.
- Confirma que el proxy está enroutando al conjunto de backends previsto.
Si los healthchecks dicen “healthy” pero la app falla bajo tráfico, tu healthcheck está mintiendo. Hazlo más representativo.
Tercero: ¿La base de datos está bloqueando el mundo?
- Revisa queries activas y locks durante ventanas de migración.
- Busca errores/timeouts de conexión en los logs de la app.
- Revisa IO wait y comportamiento de checkpoints de la BD (las métricas del host ayudan).
Si los locks se acumulan, deja de redeployar. Arregla las migraciones. Los locks de esquema no responden al optimismo.
Cuarto: ¿El host está bajo presión de recursos?
- Steal/throttle de CPU, IO wait, presión de memoria.
- Pulls de imágenes Docker y extracción de capas durante el despliegue.
- Saturación de disco en el directorio de datos de Docker y el volumen de la base de datos.
Si el host se está ahogando, el mejor patrón de despliegue del mundo no te salvará. La fiabilidad empieza por capacidad aburrida.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: breve caída en cada despliegue (unos segundos de 502)
Causa raíz: El contenedor de la app posee el puerto del host; reiniciarlo/recrearlo desune el puerto.
Solución: Pon un proxy estable en los puertos del host; enruta a la app en una red interna. Despliega añadiendo primero el nuevo backend.
2) Síntoma: la nueva versión arranca, recibe tráfico de inmediato y devuelve 500 durante 10–30 segundos
Causa raíz: No hay gating de readiness; el healthcheck solo prueba “el proceso existe” o falta.
Solución: Implementa un endpoint real /ready que compruebe dependencias; usa Docker healthcheck y gating del proxy basado en él.
3) Síntoma: el despliegue desencadena un loop de reinicios; logs muestran fallos de conexión a BD
Causa raíz: BD no lista, migraciones en curso o contención de locks; la política de reinicio amplifica el problema.
Solución: Añade periodos de inicio; evita loops de reinicio durante migraciones; separa la tarea de migración del arranque de la app; haz migraciones online e incrementales.
4) Síntoma: las conexiones se cortan durante el despliegue aunque el proxy siga arriba
Causa raíz: La app no maneja SIGTERM; stop timeout demasiado corto; conexiones de larga duración no drenadas.
Solución: Implementa shutdown gradual; incrementa stop_grace_period; configura el proxy para dejar de enrutar antes de parar contenedores.
5) Síntoma: el rollback tarda más que el despliegue y es arriesgado
Causa raíz: No hay ejecución en paralelo; el despliegue reemplaza in-place; cambios en BD no compatibles hacia atrás.
Solución: Servicios backend blue/green; mantener la versión previa ejecutable y enrutada hasta que la nueva se pruebe; usar expand/contract en migraciones y feature flags.
6) Síntoma: “Funcionó en staging” pero el despliegue en producción causa picos de latencia
Causa raíz: Contención de recursos en producción (IO, CPU) durante pulls de imagen/migraciones; healthchecks demasiado agresivos.
Solución: Pre-pull de imágenes; programa operaciones pesadas; ajusta timeouts/retries de healthcheck; separa caminos de IO para BD y Docker cuando sea posible.
7) Síntoma: el proxy enruta a backends muertos después del despliegue
Causa raíz: Config de upstream del proxy usa IPs estáticas o nombres de contenedor obsoletos; mismatch en attachments de red.
Solución: Usa descubrimiento de servicio vía DNS de Docker en una red definida por el usuario; referencia nombres de servicio; asegura que el proxy esté en la misma red.
8) Síntoma: Compose “up -d” recrea más de lo esperado
Causa raíz: Drift de config (cambios en env vars, volúmenes, tags de imagen) dispara recreación; el proxy no está fijado.
Solución: Bloquea envs; usa docker compose config para inspeccionar la configuración final; evita cambiar el contenedor proxy a menos que sea necesario; recarga config en su interior en su lugar.
Listas de verificación / plan paso a paso
Checklist 1: Pila mínima viable “casi cero inactividad” con Compose
- Contenedor proxy estable publica puertos host 80/443.
- Backends de app no publicados a puertos del host; solo expuestos en la red interna.
- Healthchecks de readiness (no “el proceso existe”).
- Apagado gradual: manejo de SIGTERM + periodo de gracia suficiente.
- Migraciones de base de datos separadas del arranque de la app y diseñadas para cambios en línea.
- Plan de rollback: mantener la versión anterior ejecutable y enrutable hasta que la nueva se pruebe.
Checklist 2: Despliegue paso a paso (blue/green con switch de proxy)
-
Pre-pull de imagen para evitar picos de IO durante la ventana crítica.
cr0x@server:~$ docker pull registry/app:1.9.4 1.9.4: Pulling from app Digest: sha256:8b9a2f6d3c1e8f... Status: Downloaded newer image for registry/app:1.9.4Decisión: Si el pull tarda demasiado o pica IO, hazlo antes o arregla el almacenamiento.
-
Arrancar el nuevo backend junto al viejo.
cr0x@server:~$ docker compose up -d app-green [+] Running 1/1 ✔ Container prod-app-green-1 StartedDecisión: Si recrea blue o el proxy, tu modelo Compose es incorrecto. Para y aísla servicios.
-
Esperar la readiness de green.
cr0x@server:~$ docker inspect --format '{{.State.Health.Status}}' prod-app-green-1 healthyDecisión: Si está unhealthy, haz rollback parando green e investiga logs.
-
Cambiar enrutamiento del proxy (pesos o upstream de objetivo único), y recargar.
cr0x@server:~$ docker exec -it prod-proxy-1 nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successfulcr0x@server:~$ docker exec -it prod-proxy-1 nginx -s reload 2026/01/03 10:20:12 [notice] 1#1: signal process startedDecisión: Si
nginx -tfalla, no recargues. Arregla la generación de config primero. -
Observar tráfico y errores durante un periodo de soak.
cr0x@server:~$ docker exec -it prod-proxy-1 tail -n 10 /var/log/nginx/access.log 10.0.1.10 - - [03/Jan/2026:10:20:15 +0000] "GET /api/orders HTTP/1.1" 200 431 "-" "Mozilla/5.0" 10.0.1.11 - - [03/Jan/2026:10:20:16 +0000] "POST /api/pay HTTP/1.1" 200 1024 "-" "Mozilla/5.0"Decisión: Si ves 502/504 upstream, revisa readiness de backend, locks en BD y timeouts del proxy.
-
Drenar y parar blue tras tener confianza.
cr0x@server:~$ docker compose stop app-blue [+] Running 1/1 ✔ Container prod-app-blue-1 StoppedDecisión: Si aún necesitas rollback instantáneo, no elimines blue todavía—mantenla parada pero disponible, o mantenla corriendo pero sin enrutar.
Checklist 3: Disciplina en cambios de BD para despliegues con Compose
- Nunca combines migraciones riesgosas con un despliegue que no puedas revertir.
- Prefiere cambios aditivos primero (columna nullable nueva, tabla nueva, índice concurrente si está soportado).
- Backfill en lotes usando un job/worker con limitación de tasa.
- Cambia lecturas/escrituras al nuevo esquema via feature flag o ruta de código versionada.
- Sólo entonces elimina columnas/tablas antiguas en un despliegue posterior.
Preguntas frecuentes
1) ¿Puede Docker Compose hacer despliegues con verdadero cero inactividad?
No como una característica de orquestador integrada. Puedes lograr efectivamente cero inactividad visible para muchos apps web manteniendo un proxy estable y
cambiando backends saludables detrás, más apagado gradual y migraciones sensatas.
2) ¿Por qué no usar simplemente docker compose up -d y confiar en él?
Porque Compose reconcilia el estado deseado recreando contenedores cuando detecta cambios. Si el contenedor posee el puerto del host, recrearlo equivale a
un flap de puerto. No es malicia; es diseño.
3) ¿depends_on asegura que mi app espere por la base de datos?
No. Hace cumplir el orden de inicio, no la readiness. Usa healthchecks, lógica explícita de espera en tu app o un proceso de despliegue que verifique la readiness de las dependencias.
4) ¿Cuál es el patrón más simple y viable?
Proxy reverso estable en puertos del host + dos servicios de app (blue/green) en una red definida por el usuario. Arranca green, verifica salud, recarga proxy para enrutar a
green, luego drena y para blue.
5) ¿Debería usar Traefik para esto?
Traefik está bien si ya sabes operarlo. Brilla con configuración dinámica vía labels de contenedores. Pero “dinámico” no significa “seguro”;
aún necesitas healthchecks, comportamiento de drenado y planificación de rollback.
6) ¿Y los websockets y conexiones de larga duración?
Planea drenado de conexiones. Muchos clientes websocket se desconectarán al reiniciar el backend. Puedes reducir el dolor aumentando periodos de gracia, dejando de enrutar antes de parar y diseñando clientes para reconectarse limpiamente. “Cero inactividad” puede seguir significando “algunas reconexiones”.
7) ¿Puedo hacer rolling updates con --scale?
Puedes hacer un playbook de rolling update manual: escala la nueva versión, enruta tráfico y luego reduce la antigua. Compose no lo coordinará por ti, así que debes escribir y probar los pasos.
8) ¿Cuál es la causa oculta más grande de downtime durante despliegues con Compose?
Migraciones de base de datos que bloquean tablas o saturan IO. El swap de contenedores suele ser lo fácil. El cambio de esquema es la pelea final.
9) ¿Es más seguro ejecutar migraciones al inicio del contenedor?
Usualmente no. Acopla el éxito del despliegue al éxito de la migración, fomenta “reinicia hasta que funcione” y puede causar hordas si múltiples réplicas arrancan. Prefiere un paso de migración controlado y explícito.
10) ¿Cuándo debo dejar de usar Compose en producción?
Cuando necesites programación multi-host, auto-reparación entre máquinas, actualizaciones rolling automáticas con gestión de tráfico, o distribución robusta de secretos/config. En ese punto, quieres un orquestador, no un script bash muy disciplinado.
Conclusión: siguientes pasos prácticos
Compose no te da cero inactividad. Te da una definición limpia y legible de lo que debe correr en un host. El resto—cambio de tráfico, readiness,
drenado y disciplina de esquema—depende de ti. Eso no es una queja. Es el contrato.
Si quieres actualizaciones casi sin inactividad viables en Compose, haz esto a continuación:
- Pon un proxy reverso estable delante de todo y deja de publicar puertos de app directamente.
- Añade un endpoint real de readiness y conéctalo a los healthchecks de Docker y al comportamiento de enrutamiento del proxy.
- Implementa apagado gradual: manejo de SIGTERM, periodo de gracia adecuado y drenado antes de parar.
- Separa migraciones del arranque de la app y adopta cambios de esquema expand/contract.
- Escribe un playbook de despliegue que puedas ejecutar a las 3 a.m.—y ensaya el rollback hasta que sea aburrido.
Luego prueba bajo carga. No en teoría. No en staging con tres solicitudes por minuto. En algo que se parezca a producción, donde el sistema ya está ocupado haciendo su trabajo mientras intentas reemplazar partes de él.