Docker: azul/verde en un único host — el enfoque más simple que funciona

¿Te fue útil?

El problema: tienes una máquina Linux, una IP pública y una aplicación que no puede caerse. “Solo redeploy” se convierte en un apagón de cinco minutos, una reversión que no lo es, y un hilo de Slack con ejecutivos que parece un informe de escena del crimen.

El azul/verde en un único host no es glamuroso. No es Kubernetes. No es un service mesh. Es un conjunto pequeño y disciplinado de pasos que te dan dos versiones ejecutándose en paralelo y una conmutación que puedes revertir sin rezos.

Qué significa azul/verde en un host (y qué no significa)

El despliegue azul/verde consiste en ejecutar dos versiones completas de tu servicio en paralelo: la que actualmente sirve (“azul”) y la candidata (“verde”). Validás la verde mientras la azul sigue atendiendo tráfico. Luego cambias el tráfico a la verde. Si algo huele mal, vuelves a la azul.

En un único host, las limitaciones son contundentes:

  • Tienes un kernel, una NIC y un subsistema de disco. Tu “redundancia” es mayormente procedimental.
  • No puedes asumir que un despliegue malo no pueda afectar el host (logs desbocados, fugas de memoria, llenado de disco).
  • Tu conmutación debe ocurrir en L7 (proxy inverso) o mediante reasignación local de puertos. DNS es demasiado lento y “simplemente cambia el puerto en el cliente” es comedia.

Por tanto, el objetivo no es “alta disponibilidad” en el sentido clásico. El objetivo es cambio seguro: ventanas de downtime más cortas, rollback predecible y menos postmortems tipo “no sabemos qué pasó”.

La opinión aquí es: si estás en un host, haz azul/verde con un contenedor proxy inverso (Nginx o HAProxy), mantén tus contenedores de aplicación simples y haz explícitos los cambios de estado. Si intentás ser ingenioso con magia de iptables y scripts ad-hoc, vas a tener éxito hasta que no.

Un chiste, porque lo merecemos: desplegar en un único host es como usar dos cinturones de seguridad en un auto. Ayuda, pero no convierte el auto en un avión.

Hechos interesantes y breve historia (para que dejes de repetir errores antiguos)

  1. El azul/verde precede a los contenedores. El patrón viene de la ingeniería de releases mucho antes de Docker: dos entornos idénticos y un cambio de router era la historia más simple de “sin tiempo de inactividad”.
  2. “Despliegue atómico” solía significar cambios de symlink. Muchos sistemas pre-contenedores desplegaban en directorios versionados y cambiaban un symlink para rollback instantáneo. El cambio de upstream en un reverse-proxy es el sucesor espiritual.
  3. Las redes tempranas de Docker eran más complicadas de lo que tu memoria admite. El bridge por defecto evolucionó; prácticas como “solo publica un puerto y listo” vienen de épocas en que menos gente hacía enrutamiento serio de múltiples servicios en un host.
  4. Las comprobaciones de salud no siempre fueron prioridad. Docker Compose normalizó gradualmente las health checks explícitas; las pilas antiguas usaban scripts frágiles de “sleep 10” y esperaban que la app estuviera lista.
  5. Nginx ha sido una herramienta de releases por dos décadas. Sus semánticas de reload (recarga de configuración sin cortar conexiones) lo convirtieron en un interruptor natural incluso antes de que “cloud-native” fuera una frase.
  6. HAProxy popularizó la salud explícita de backends y el comportamiento de circuitos. Muchos equipos SRE aprendieron que “el upstream escucha” no es lo mismo que “el servicio está sano”.
  7. Los despliegues de un solo host siguen siendo normales. Muchas aplicaciones internas rentables y servicios edge se ejecutan en máquinas únicas porque el negocio valora la simplicidad sobre la resiliencia teórica.
  8. La metáfora “mascotas vs ganado” nunca aplicó a tu base financiera. Incluso en mundo de contenedores, los servicios stateful siguen siendo especiales. Tu estrategia de despliegue debe reconocerlo.

El diseño más simple que realmente funciona

Construimos cuatro piezas móviles:

  • proxy: un contenedor proxy inverso que posee los puertos públicos (80/443). Enruta a upstream “azul” o “verde”.
  • app-blue: contenedor de la app en producción actual.
  • app-green: contenedor candidato.
  • sidecars opcionales: runner de migraciones, tests de humo one-shot, o un pequeño endpoint “whoami” para validar el enrutamiento.

Las reglas que mantienen esto sensato:

  1. Sólo el proxy liga puertos públicos. Azul y verde están en una red Docker interna sin puertos publicados. Eso evita exposición accidental y conflictos de puertos.
  2. Mantén la configuración del proxy intercambiable y recargable. Un archivo único con la “upstream activa” es más fácil que templatear cincuenta líneas en medio de un incidente.
  3. Condiciona la conmutación a una comprobación de salud real. Si tu app no tiene un endpoint /health, añádelo. Podés enviar funciones después; no podés desplegar sin saber si está viva.
  4. Haz del rollback un comando de primera clase. Si el rollback requiere “recordar qué cambiamos”, no tenés rollback. Tenés teatro improvisado.
  5. Los cambios de estado son lo difícil. Azul/verde funciona genial para código sin estado. Para cambios de esquema en BD, necesitás una estrategia de compatibilidad o un paso de mantenimiento deliberado.

Una idea parafraseada de John Allspaw: “En operaciones, el fallo es normal; la resiliencia viene de prepararse para ello, no de fingir que no va a pasar”.

Distribución en el host: puertos, redes, volúmenes y lo único que no debes compartir

Puertos

Públicos:

  • proxy:80 y proxy:443 están publicados en el host.

Internos:

  • app-blue escucha en 8080 dentro del contenedor.
  • app-green escucha en 8080 dentro del contenedor.

Nginx enruta a app-blue:8080 o app-green:8080 en una red interna compartida.

Redes

Crea una red dedicada, por ejemplo bg-net. No reutilices la red por defecto si quieres agradecerle a tu yo futuro.

Volúmenes

Aquí está el truco de un host: el almacenamiento compartido escribible entre azul y verde puede arruinarte el día.

  • Está bien compartir: assets de solo lectura, configuración de confianza, certificados TLS y quizá una caché si perderla es aceptable.
  • Compartir con precaución: directorios de uploads. Dos versiones de la app podrían escribir en formatos, permisos o rutas diferentes.
  • No compartas a ciegas: archivos SQLite, bases de datos embebidas o cualquier cosa donde dos procesos escriban concurrentemente sin coordinación.

Si tu app escribe en un directorio local, preferí una de estas opciones:

  1. Externalizar el estado (object storage, base de datos, etc.).
  2. Directorios de estado versionados por color, luego un paso de migración controlado y un cambio controlado.
  3. Un plan de esquema “compartido pero compatible” donde ambas versiones puedan correr contra la misma BD y toleren tráfico mezclado durante la conmutación.

Logging

En un único host, el crecimiento de logs es un asesino de despliegues. Usá las opciones de rotación de logs de Docker. Si no, tu “despliegue sin downtime” se convierte en “sin disco, sin servicio”.

Un blueprint funcional de Docker Compose

Esto es deliberadamente aburrido. Boring es bueno. Podés ajustarlo después una vez que sea fiable.

cr0x@server:~$ cat docker-compose.yml
version: "3.9"

services:
  proxy:
    image: nginx:1.25-alpine
    container_name: bg-proxy
    ports:
      - "80:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/snippets:/etc/nginx/snippets:ro
    depends_on:
      app-blue:
        condition: service_healthy
      app-green:
        condition: service_healthy
    networks:
      - bg-net

  app-blue:
    image: myapp:blue
    container_name: app-blue
    environment:
      - APP_COLOR=blue
    expose:
      - "8080"
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health | grep -q ok"]
      interval: 5s
      timeout: 2s
      retries: 10
      start_period: 10s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "5"
    networks:
      - bg-net

  app-green:
    image: myapp:green
    container_name: app-green
    environment:
      - APP_COLOR=green
    expose:
      - "8080"
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health | grep -q ok"]
      interval: 5s
      timeout: 2s
      retries: 10
      start_period: 10s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "5"
    networks:
      - bg-net

networks:
  bg-net:
    name: bg-net

La configuración del proxy decide qué color recibe tráfico. Mantén esa decisión en un archivo pequeño.

cr0x@server:~$ mkdir -p nginx/conf.d nginx/snippets
...output...
cr0x@server:~$ cat nginx/snippets/upstream-active.conf
set $upstream app-blue;
...output...
cr0x@server:~$ cat nginx/conf.d/default.conf
server {
  listen 80;

  location /healthz {
    return 200 "proxy ok\n";
  }

  location / {
    include /etc/nginx/snippets/upstream-active.conf;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Request-Id $request_id;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_pass http://$upstream:8080;
  }
}
...output...

Este patrón usa una variable de upstream objetivo. Es fácil de cambiar, fácil de comparar, y recarga limpiamente.

Mecánica de la conmutación: atómica, observable, reversible

Desplegar la verde sin tocar el tráfico

Build/pull de la nueva imagen como myapp:green. Iniciála junto a la azul. Validá salud y comportamiento a través del proxy (o directamente en la red interna vía un curl con exec).

Cambiar tráfico modificando un archivo y recargando Nginx

La conmutación debe ser:

  • Lo suficientemente atómica: un cambio, una recarga.
  • Observable: podés ver qué upstream está sirviendo.
  • Reversible: mismo mecanismo a la inversa.

La recarga de Nginx es graciosa: carga nueva config y deja que los workers viejos terminen conexiones activas. Eso no previene todos los casos límite (streams de larga duración), pero es la opción menos mala en un host único.

Revertir cambiando el archivo y recargando

El procedimiento de rollback debe ser mecánicamente idéntico a la conmutación. Si el rollback es “diferente”, fallará a las 2 a.m. cuando tu cerebro es mayormente cafeína.

Segundo y último chiste: el sistema de monitorización más fiable sigue siendo “un cliente”, pero tiene terrible ruteo de alertas y sin soporte de ventana de mantenimiento.

Tareas prácticas (comandos, salidas, decisiones)

Estos son los movimientos que realmente harás. Cada tarea incluye: comando, salida de ejemplo, qué significa y qué decisión tomás a partir de ello.

Task 1: Confirmar versiones de Docker y Compose (chequeo de capacidad)

cr0x@server:~$ docker version --format '{{.Server.Version}}'
26.1.3

Qué significa: estás en un motor Docker moderno. Eso importa para la estabilidad de redes y el comportamiento de health checks.

Decisión: si esto es antiguo (piensa en la era 19.x), presupone tiempo para una actualización antes de culpar a la app por rarezas.

cr0x@server:~$ docker compose version
Docker Compose version v2.29.1

Qué significa: Compose v2 está disponible; podés confiar en el comportamiento actual del CLI.

Decisión: estandarizá en docker compose (plugin) en lugar del viejo binario docker-compose para reducir variaciones de “funciona en mi laptop”.

Task 2: Crear la red una vez (reducir sorpresas)

cr0x@server:~$ docker network create bg-net
8d4b7b7f0d3e5e2e0f6f5f5a8d1c0d3a9c2b1f2e3a4b5c6d7e8f9a0b1c2d3e4

Qué significa: la red existe; los contenedores pueden resolverse por nombre de servicio.

Decisión: si tu org tiene múltiples stacks en un host, nombrar redes explícitamente evita colisiones y esos momentos de “¿por qué este contenedor ve esa base de datos?”.

Task 3: Levantar azul y verde (pero no conmutar aún)

cr0x@server:~$ docker compose up -d
[+] Running 3/3
 ✔ Container app-blue   Started
 ✔ Container app-green  Started
 ✔ Container bg-proxy   Started

Qué significa: todo arrancó. Esto no es lo mismo que “todo está healthy”.

Decisión: verificá inmediatamente el estado de salud; no celebres por un “Started”.

Task 4: Comprobar salud de contenedores (puerta para conmutación)

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}'
NAMES      STATUS
bg-proxy   Up 25 seconds
app-blue   Up 25 seconds (healthy)
app-green  Up 25 seconds (healthy)

Qué significa: ambas versiones pasan sus health checks. El proxy está en ejecución.

Decisión: si green aparece (unhealthy), detenete. Inspeccioná logs y arreglá antes de cambiar un solo paquete.

Task 5: Verificar qué color está atendiendo a través del proxy (verdad de terreno)

cr0x@server:~$ curl -sS localhost/ | head -n 1
hello from blue

Qué significa: tu snippet de Nginx actual apunta a app-blue.

Decisión: si no es lo que esperás, arreglá la config del proxy antes de continuar. No debuggees “despliegue” cuando es solo enrutamiento.

Task 6: Validar la verde directamente desde el contenedor proxy (evitar ruido externo)

cr0x@server:~$ docker exec -it bg-proxy sh -lc "wget -qO- http://app-green:8080/ | head -n 1"
hello from green

Qué significa: Nginx puede resolver y alcanzar la verde en la red interna.

Decisión: si falla la resolución de nombres, tenés un desacople de red (red equivocada, nombre de servicio equivocado o contenedores no adjuntos).

Task 7: Ejecutar test de config de Nginx antes de recargar (evitar cortes autoinfligidos)

cr0x@server:~$ docker exec -it bg-proxy nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Qué significa: la recarga no romperá el proxy con un error de sintaxis.

Decisión: si esto falla, no recargues. Arreglá la config primero. Un proxy roto es downtime instantáneo.

Task 8: Cambiar a la verde (un cambio de archivo + recarga)

cr0x@server:~$ sed -i 's/set \$upstream app-blue;/set $upstream app-green;/' nginx/snippets/upstream-active.conf
...output...
cr0x@server:~$ docker exec -it bg-proxy nginx -s reload
2026/01/03 09:22:48 [notice] 1#1: signal process started

Qué significa: Nginx aceptó la señal de recarga y arrancó nuevos workers con el nuevo objetivo de enrutamiento.

Decisión: verificá inmediatamente que el tráfico ahora golpee a la verde y observá las tasas de error unos minutos.

Task 9: Confirmar que la conmutación funcionó (y no quedó a medias)

cr0x@server:~$ curl -sS localhost/ | head -n 1
hello from green

Qué significa: las solicitudes ahora se enrutan a la verde.

Decisión: si ves resultados mezclados, puede que tengas múltiples instancias de proxy, un balanceador delante o caché que enmascara la realidad. Validá end-to-end.

Task 10: Vigilar logs 60 segundos tras la conmutación (canario barato)

cr0x@server:~$ docker logs -f --since=60s app-green
INFO listening on :8080
INFO GET / 200 3ms
INFO GET /api/orders 200 21ms

Qué significa: la verde está sirviendo tráfico real y devolviendo 200s.

Decisión: si ves 500s, timeouts o errores de BD: revertí ahora y luego debuggea con calma.

Task 11: Revertir a la azul (mismo mecanismo)

cr0x@server:~$ sed -i 's/set \$upstream app-green;/set $upstream app-blue;/' nginx/snippets/upstream-active.conf
...output...
cr0x@server:~$ docker exec -it bg-proxy nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
cr0x@server:~$ docker exec -it bg-proxy nginx -s reload
2026/01/03 09:24:10 [notice] 1#1: signal process started

Qué significa: el rollback está completo en cuanto al enrutamiento.

Decisión: si no podés revertir en menos de un minuto, tu proceso no es azul/verde—es “azul/verde-más-o-menos”. Arreglá el proceso, no las heroicidades.

Task 12: Inspeccionar presión de recursos (chequeo de realidad en un host)

cr0x@server:~$ docker stats --no-stream
CONTAINER   CPU %   MEM USAGE / LIMIT     MEM %   NET I/O         BLOCK I/O     PIDS
app-blue    0.10%   120MiB / 8GiB         1.46%   15MB / 18MB     5MB / 1MB     23
app-green   2.40%   620MiB / 8GiB         7.56%   120MB / 95MB    60MB / 4MB    45
bg-proxy    0.05%   20MiB / 8GiB          0.24%   135MB / 130MB   2MB / 1MB     5

Qué significa: la verde consume más. Eso puede estar bien, o puede ser el comienzo del final.

Decisión: si la memoria salta dramáticamente, buscá fugas, cambios de caché o límites faltantes. En un host único, “simplemente añadir réplicas” no es un plan.

Task 13: Verificar bindings de puertos (evitar exposición accidental y colisiones)

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}'
NAMES      PORTS
bg-proxy   0.0.0.0:80->80/tcp
app-blue   8080/tcp
app-green  8080/tcp

Qué significa: sólo el proxy liga el puerto 80 del host. Azul/verde son internos solamente.

Decisión: si ves 0.0.0.0:8080->8080/tcp en un contenedor de app, arreglalo. Creaste un punto de entrada secundario que evita tu conmutador.

Task 14: Depurar DNS y attachment de red (cuando el proxy “no puede alcanzar upstream”)

cr0x@server:~$ docker network inspect bg-net --format '{{json .Containers}}'
{"a1b2c3d4":{"Name":"app-blue","IPv4Address":"172.20.0.2/16"},"b2c3d4e5":{"Name":"app-green","IPv4Address":"172.20.0.3/16"},"c3d4e5f6":{"Name":"bg-proxy","IPv4Address":"172.20.0.4/16"}}

Qué significa: los tres contenedores están en la misma red. Los nombres deberían resolverse.

Decisión: si el proxy no está listado, adjuntalo a la red o arreglá la sección de redes en Compose.

Task 15: Confirmar digests de imágenes (asegurá que estás ejecutando lo que creés)

cr0x@server:~$ docker image inspect myapp:green --format '{{.Id}}'
sha256:7f0c1e9a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8

Qué significa: la etiqueta green apunta a un ID de imagen específico.

Decisión: si tu CI retaggea imágenes, basate en digests en las notas de release. “Green” no debería mutar silenciosamente durante un incidente.

Task 16: Detectar presión de disco por Docker rápido (antes de que sea downtime)

cr0x@server:~$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          42        8         18.4GB    9.1GB (49%)
Containers      16        3         1.2GB     900MB (75%)
Local Volumes   12        7         6.8GB     1.1GB (16%)
Build Cache     31        0         3.5GB     3.5GB

Qué significa: tenés espacio recuperable, especialmente en la caché de build.

Decisión: si el disco está ajustado, pruneá la caché de build en una ventana controlada. Si pruneás a ciegas durante un incidente, podrías borrar una imagen de rollback que todavía necesitás.

Tres micro-historias corporativas desde el campo

Incidente causado por una suposición errónea: “Escuchar equivale a estar sano”

Una empresa mediana ejecutaba una API interna de facturación en un solo host. La app estaba containerizada, el host era estable y los releases eran “rápidos”. Decidieron implementar azul/verde iniciando la verde en otro puerto y cambiando el upstream de Nginx.

La comprobación de salud era una conexión TCP. Si el puerto aceptaba conexión, la verde se declaraba buena. Eso parecía razonable: el proceso está arriba, ¿no?

Luego un release añadió una migración de inicio que calentaba cachés tirando un chunk de datos de la base. El proceso abrió el puerto pronto (default del framework), pero no estaba realmente listo para servir peticiones reales. Bajo carga devolvía 503s y timeouts mientras seguía “healthy”. Nginx felizmente mandó tráfico de producción a un servicio a medio despertar.

El outage no duró mucho porque la reversión fue rápida. El daño fue que todos perdieron confianza en el proceso de despliegue y empezaron a empujar cambios solo en horario de oficina. Eso no es fiabilidad; es programar por miedo.

Lo arreglaron cambiando el endpoint de salud para validar dependencias downstream: conectividad a BD, migraciones completas y una consulta trivial. El endpoint se mantuvo rápido y barato. La conmutación volvió a ser aburrida.

Optimización que se volvió contra ellos: “Compartamos todo para ahorrar disco”

Otra compañía ejecutaba un servicio de contenido con uploads de usuarios almacenados en disco local. Para “optimizar”, montaron el mismo volumen de uploads en azul y verde para evitar sincronizaciones y poder revertir instantáneamente.

Funcionó meses, hasta que un release cambió cómo se generaban y almacenaban las miniaturas. La verde comenzó a escribir archivos nuevos con nombres distintos y una máscara de permisos ligeramente diferente. La azul, aún funcionando, seguía sirviendo páginas antiguas que referenciaban miniaturas que ahora se sobrescribían o reemplazaban en vuelo.

El síntoma fue raro: no fue una caída total, sino imágenes rotas intermitentes y 403s ocasionales. Los tickets de soporte llegaron primero, luego ingeniería. El equipo persiguió headers de caché, comportamiento del CDN e incluso bugs del navegador. La causa raíz fue mundana: dos versiones escribiendo en el mismo directorio sin un contrato de compatibilidad.

La solución fue también mundana: directorios versionados para artefactos generados y un job en background para backfill. Los uploads compartidos permanecieron, pero los outputs derivados se versionaron y eventualmente migraron. La “optimización de disco” les ahorró unos gigabytes y les costó una semana de debugging.

Práctica aburrida pero correcta que salvó el día: “Conservá la imagen vieja y la ruta de rollback”

Una empresa regulada ejecutaba un host único en un segmento de red controlado para un gateway de integración legacy. Sin autoscaling, sin magia—solo gestión de cambios y un pager.

Implementaron azul/verde con un contenedor proxy y dos contenedores de app, pero también impusieron una regla aburrida: nunca podar imágenes en horario laboral, y siempre mantener la última imagen conocida buena fijada por digest en un mirror local de registro. A nadie le encantaba la regla. Parecía obstrucción.

Una tarde, una librería upstream publicó una minor release defectuosa que pasaba tests unitarios pero provocaba un crash en runtime bajo un handshake TLS específico. El contenedor verde arrancó, pareció healthy por poco tiempo y luego se cayó bajo tráfico real de clientes.

El rollback funcionó al instante. Sin drama. Pero lo que realmente salvó fue esto: el equipo pudo redeployar la imagen conocida buena incluso después de que CI moviera las tags, porque el digest se preservó localmente. No tuvieron que “reconstruir el commit de la semana pasada” bajo presión.

No pasó nada heroico, que es justamente el punto. El postmortem fue corto, técnico y no sobre sentimientos.

Guion de diagnóstico rápido

Cuando un despliegue azul/verde en un host único se va al traste, el cuello de botella suele ser uno de: enrutamiento, salud, recursos o estado. Revisá en este orden; está optimizado para “detener la hemorragia” primero.

1) Enrutamiento: ¿va el tráfico a donde creés que va?

  • Comprobá que el proxy esté vivo y sirviendo /healthz.
  • Revisá qué upstream está activo en el archivo snippet.
  • Verificá que la prueba de config de Nginx pase y que la recarga haya ocurrido.
cr0x@server:~$ curl -sS -i localhost/healthz | head -n 1
HTTP/1.1 200 OK

Decisión: si la salud del proxy falla, dejá de debuggear la app. Arreglá el proxy/contenedor/firewall del host primero.

2) Salud: ¿la verde está realmente sana, o solo “en ejecución”?

cr0x@server:~$ docker inspect -f '{{.State.Health.Status}}' app-green
healthy

Decisión: si está unhealthy, no conmutés. Si ya conmutaste, revertí y debuggeá la verde offline.

3) Recursos: ¿estás dejando al host sin recursos?

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:           7.7Gi       6.9Gi       210Mi       120Mi       610Mi       430Mi
Swap:          0B          0B          0B

Decisión: si la memoria disponible es baja y la verde consume más, vas camino al OOM killer. Revertí o añadí límites y capacidad.

4) Estado: ¿cambiaste algo irreversible?

Si la app usa una base de datos, verificá migraciones de esquema y compatibilidad. Si cambiaste formatos de datos, el rollback podría no restaurar el comportamiento.

cr0x@server:~$ docker logs --since=10m app-green | tail -n 20
INFO migration complete
INFO connected to db

Decisión: si las migraciones se ejecutaron y no son retrocompatibles, volver a desplegar el código puede no arreglarlo. Tu “despliegue” ahora es un incidente de datos.

Errores comunes: síntomas → causa raíz → solución

1) Síntoma: Nginx muestra “502 Bad Gateway” tras la conmutación

  • Causa raíz: el proxy no puede alcanzar la verde (red equivocada, nombre de contenedor distinto, la verde se cayó o la app escucha en otro puerto).
  • Solución: verificá attachment de red y nombre de servicio; confirmá que la verde está healthy; confirmá que la app escucha en 8080.
cr0x@server:~$ docker exec -it bg-proxy sh -lc "wget -S -qO- http://app-green:8080/health"
  HTTP/1.1 200 OK
  ...
ok

2) Síntoma: La verde está “healthy” pero los usuarios ven errores

  • Causa raíz: la health check es demasiado superficial (puerto abierto, no dependencias listas), o no representa rutas de petición reales.
  • Solución: hacé que la health check valide dependencias críticas y una petición representativa ligera. Mantenela rápida.

3) Síntoma: La conmutación funciona y luego el rendimiento colapsa 5–15 minutos después

  • Causa raíz: fuga de memoria, crecimiento de caché, cambio en pool de conexiones o amplificación de logs causando presión de I/O en disco.
  • Solución: revisá docker stats, memoria del host y volumen de logs; fijá límites de memoria en contenedores; rotá logs; investigá pools de conexión.

4) Síntoma: El rollback no arregla el problema

  • Causa raíz: la verde hizo un cambio de estado irreversible (migración de esquema, reescritura de datos), o el proxy sigue enrutando a la verde por config obsoleta o múltiples proxies.
  • Solución: confirmá el enrutamiento; si cambió el estado, usá una corrección hacia adelante o restaurá desde backup/snapshot. No podés “devolver” el tiempo con YAML.

5) Síntoma: Azul y verde sirven tráfico inesperadamente

  • Causa raíz: los contenedores de la app publicaron puertos del host, saltándose el proxy, o un balanceador externo apunta a ambos.
  • Solución: eliminá el publish de puertos en contenedores de app; asegurate de que solo el proxy esté expuesto; validá ruteo externo.

6) Síntoma: Los despliegues fallan intermitentemente con “bind: address already in use”

  • Causa raíz: intentaste ligar contenedores de app directamente a puertos del host para ambos colores.
  • Solución: dejá de ligar puertos de app al host. Ligá solo el proxy a puertos del host y enruta internamente.

7) Síntoma: La recarga de Nginx causa una breve caída en conexiones de larga duración

  • Causa raíz: websockets/streams y settings del proxy no afinados; la recarga es graciosa pero no mágica.
  • Solución: confirmá keepalive; para conexiones verdaderamente largas, considerá HAProxy con comportamiento de backend explícito, o aceptá una pequeña ventana de mantenimiento.

8) Síntoma: El disco se llena durante el despliegue

  • Causa raíz: crecimiento de logs, proliferación de imágenes, acumulación de caché de build.
  • Solución: configurá rotación de logs de Docker; pruneá con intención; mantené imágenes de rollback ancladas; monitorizá disco.

Listas de verificación / plan paso a paso

Checklist pre-vuelo (antes de tocar tráfico de producción)

  1. El proxy es el único servicio publicando 80/443 en el host.
  2. Azul y verde corren en la misma red Docker interna.
  3. Existen health checks que validan dependencias (no solo “puerto abierto”).
  4. Los logs tienen rotación (max-size, max-file).
  5. La ruta de rollback está probada: cambiá de vuelta a azul y recargá el proxy.
  6. Los cambios de estado están planificados: las migraciones son retrocompatibles o están separadas.
  7. Sabés cómo verificar “qué color está activo” usando una petición real.

Plan paso a paso de despliegue (azul/verde en un host)

  1. Construir/extraer la imagen green y etiquetarla claramente.
  2. Iniciar la green junto a la blue (sin puertos públicos en la green).
  3. Esperar salud: la green debe estar healthy.
  4. Smoke test de la green desde dentro del contenedor proxy (red y respuesta).
  5. Ejecutar test de config de Nginx antes de recargar.
  6. Cambiar archivo de upstream a la green.
  7. Recargar el proxy y confirmar que el tráfico en vivo va a la green.
  8. Observar logs, tasa de errores, latencia y recursos del host por unos minutos.
  9. Mantener la blue en ejecución hasta estar confiado (tu ventana de rollback).
  10. Tras la aceptación, detener la blue y conservar su imagen por al menos un ciclo de release.

Checklist de rollback (cuando la green te decepciona)

  1. Cambiar el archivo de upstream de vuelta a blue.
  2. Probar config de Nginx.
  3. Recargar Nginx.
  4. Verificar que el tráfico en vivo alcanza la blue.
  5. Capturar logs y métricas de la green para depuración.
  6. Decidir si mantener la green en ejecución para investigación o detenerla para liberar recursos.

Preguntas frecuentes

1) ¿Puedo hacer azul/verde sin un proxy inverso?

Podes, pero reinventarás un proxy inverso usando puertos del host, iptables o hacks de DNS. En un host único, un proxy explícito es la opción menos mala.

2) ¿Por qué no usar la publicación de puertos de Docker y simplemente intercambiar puertos?

Porque no podés ligar dos contenedores al mismo puerto del host, y el intercambio no es atómico a menos que agregues más maquinaria. Además, terminarás exponiendo ambas versiones en algún punto.

3) ¿La recarga de Nginx es realmente sin downtime?

Para solicitudes HTTP típicas, está cerca. La recarga de Nginx es graciosa, pero conexiones de larga duración y clientes peculiares aún pueden notar pequeños cortes. Diseñalo con eso en mente.

4) ¿Azul y verde deberían compartir la misma base de datos?

Usualmente sí, pero solo si tu estrategia de migraciones de esquema soporta compatibilidad durante la superposición. Si la green requiere una migración rompe-compatibilidad, necesitás un paso de datos planificado o ventana de downtime.

5) ¿Cuál es la manera más segura de manejar migraciones de esquema?

Preferí migraciones retrocompatibles: agregá columnas, evitá cambios destructivos y desplegá código que pueda manejar ambas formas. Ejecutá la limpieza destructiva luego.

6) ¿Cuánto tiempo debería mantener la blue después de la conmutación?

El tiempo suficiente para detectar las fallas que realmente aparecen en tu entorno: usualmente minutos a horas. Si las fallas suelen aparecer “a la mañana siguiente”, mantené la blue disponible más tiempo, según recursos.

7) ¿Puedo canaryear un pequeño porcentaje de tráfico en un solo host?

Sí, pero es más complejo. HAProxy facilita backends ponderados. Con Nginx se puede, pero tu config y observabilidad deben ser más robustas.

8) ¿Qué pasa con TLS en el proxy?

Terminá TLS en el proxy. Mantené los certificados en el host como mounts de solo lectura. No termines TLS en ambos contenedores de app a menos que disfrutes depurarlo.

9) ¿Necesito límites de recursos en contenedores para azul/verde?

En un host único, sí. Sin límites, dos versiones corriendo juntas pueden dejar sin recursos al host y hacer inútil el rollback porque todo está lento.

10) ¿Cómo demuestro qué versión sirvió una petición?

Devolvé un string de versión en un header de respuesta o en un endpoint de debug. Registralo. Cuando alguien dice “la green falla”, necesitás evidencia, no sensaciones.

Conclusión: próximos pasos prácticos

El azul/verde en un host único no busca la perfección. Busca eliminar las fallas más tontas: despliegues rotos, rollbacks lentos y “no sabemos qué está en vivo”. Si solo hacés tres cosas, hacé esto:

  1. Poné un proxy inverso al frente y hacelo el único punto de entrada público.
  2. Hacé que las health checks sean reales y condicioná la conmutación a ellas.
  3. Hacé de la conmutación y el rollback la misma operación en direcciones opuestas.

Luego, disciplinate con las partes poco sexys: rotación de logs, conservar imágenes de rollback y tratar los cambios de estado como eventos separados. Tus incidentes futuros seguirán ocurriendo. Solo serán más cortos, más limpios y menos teatrales.

← Anterior
Conjuntos de datos ZFS por carga de trabajo: el truco de gestión que evita el caos
Siguiente →
SERVFAIL de DNS desde el proveedor: cómo demostrar que el proveedor es el problema

Deja un comentario