En un despliegue blue-green mantienes dos copias de tu aplicación: una (por ejemplo, blue) atiende el tráfico real mientras la otra (green) recibe la versión nueva. Cuando la versión nueva está arrancada y responde bien, cambias el tráfico de golpe hacia ella, y la antigua queda parada en reserva para volver atrás en segundos si algo falla. En este tutorial montarás este esquema en Ubuntu 24.04 con Docker para ejecutar los dos entornos y Nginx como proxy inverso que decide cuál de ellos recibe el tráfico, y automatizarás el cambio con un script de despliegue.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
- Un usuario no root con privilegios
sudo. - Docker Engine instalado desde el repositorio oficial. Si aún no lo tienes, sigue la guía de instalación de Docker en Ubuntu 24.04.
- El puerto 80 accesible (con UFW activo lo abrirás en el paso 1).
Como aplicación de ejemplo se usará la imagen oficial de Nginx en dos versiones (nginx:1.26-alpine y nginx:1.27-alpine). En tu caso serán dos versiones de la imagen de tu aplicación.
Cómo funciona el cambio de tráfico
El esquema de esta guía tiene tres piezas:
| Pieza | Función |
|---|---|
Contenedor app-blue | Escucha en 127.0.0.1:8081 |
Contenedor app-green | Escucha en 127.0.0.1:8082 |
| Nginx en el host | Escucha en el puerto 80 y reenvía al upstream definido en active.conf |
active.conf es un enlace simbólico que apunta a blue.conf o a green.conf. Cambiar de entorno consiste en cambiar el enlace y ejecutar systemctl reload nginx. En una recarga, Nginx arranca procesos nuevos con la configuración nueva y deja que los antiguos terminen las peticiones en curso antes de salir, así que no se corta ninguna conexión.
Paso 1: Instalar Nginx y abrir el puerto 80
Instala Nginx desde los repositorios de Ubuntu:
sudo apt update
sudo apt install nginx
Si usas UFW, permite el tráfico HTTP:
sudo ufw allow 'Nginx HTTP'
Desactiva el sitio por defecto, que también escucha en el puerto 80:
sudo rm /etc/nginx/sites-enabled/default
Paso 2: Arrancar el entorno blue
Arranca la versión actual de la aplicación como entorno blue. Publicar el puerto solo en 127.0.0.1 evita que el contenedor sea accesible desde Internet saltándose Nginx (Docker publica puertos por delante de las reglas de UFW):
docker run -d --name app-blue --restart unless-stopped -p 127.0.0.1:8081:80 nginx:1.26-alpine
Comprueba que responde:
curl -sI http://127.0.0.1:8081/ | grep -i '^server'
Server: nginx/1.26.3
Paso 3: Configurar Nginx con blue y green
Crea un directorio para las definiciones de cada entorno:
sudo mkdir -p /etc/nginx/bluegreen
Crea la definición del entorno blue:
sudo nano /etc/nginx/bluegreen/blue.conf
upstream app_active {
server 127.0.0.1:8081;
keepalive 16;
}
Y la del entorno green:
sudo nano /etc/nginx/bluegreen/green.conf
upstream app_active {
server 127.0.0.1:8082;
keepalive 16;
}
Ambos archivos definen el mismo upstream con distinto destino. Crea el enlace active.conf apuntando a blue:
sudo ln -sfn /etc/nginx/bluegreen/blue.conf /etc/nginx/bluegreen/active.conf
Ahora crea el sitio que usa ese upstream:
sudo nano /etc/nginx/sites-available/app
include /etc/nginx/bluegreen/active.conf;
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
location / {
proxy_pass http://app_active;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header X-Upstream $upstream_addr always;
}
}
proxy_http_version 1.1y la cabeceraConnectionvacía permiten reutilizar conexiones con el backend gracias akeepalive.- La cabecera
X-Upstreamindica qué backend ha atendido cada petición. Es útil para verificar el cambio; puedes quitarla en producción.
Si tienes un dominio, sustituye server_name _; por server_name your_domain; y añade HTTPS con Certbot como harías en cualquier sitio de Nginx.
Activa el sitio, valida la sintaxis y recarga:
sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/app
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Comprueba que el tráfico llega a blue:
curl -sI http://localhost/ | grep -i '^x-upstream'
X-Upstream: 127.0.0.1:8081
Paso 4: Crear el script de despliegue
El despliegue siempre sigue los mismos pasos: detectar el entorno activo, arrancar la versión nueva en el otro, esperar a que responda, cambiar el enlace y recargar Nginx. Es un buen caso para un script, porque evita cambiar el tráfico a un entorno que no ha arrancado bien.
sudo nano /usr/local/bin/bluegreen-deploy
#!/usr/bin/env bash
# Despliega una imagen en el entorno inactivo y le cambia el tráfico.
set -euo pipefail
IMAGE="${1:?Uso: bluegreen-deploy <imagen>}"
APP_PORT="${APP_PORT:-80}"
HEALTH_PATH="${HEALTH_PATH:-/}"
CONF_DIR=/etc/nginx/bluegreen
active="$(basename "$(readlink -f "$CONF_DIR/active.conf")" .conf)"
if [[ "$active" == "blue" ]]; then
target=green
port=8082
else
target=blue
port=8081
fi
echo "Entorno activo: $active. Desplegando $IMAGE en $target (puerto $port)."
docker pull "$IMAGE"
docker rm -f "app-$target" >/dev/null 2>&1 || true
docker run -d --name "app-$target" --restart unless-stopped \
-p "127.0.0.1:$port:$APP_PORT" "$IMAGE"
echo "Esperando a que $target responda en $HEALTH_PATH..."
for attempt in $(seq 1 30); do
if curl -fsS -o /dev/null "http://127.0.0.1:$port$HEALTH_PATH"; then
break
fi
if [[ "$attempt" -eq 30 ]]; then
echo "El entorno $target no responde. El tráfico sigue en $active." >&2
exit 1
fi
sleep 2
done
ln -sfn "$CONF_DIR/$target.conf" "$CONF_DIR/active.conf"
if ! nginx -t 2>/dev/null; then
ln -sfn "$CONF_DIR/$active.conf" "$CONF_DIR/active.conf"
echo "La configuración de Nginx no es válida. Cambio revertido." >&2
exit 1
fi
systemctl reload nginx
echo "Tráfico cambiado a $target. $active sigue en marcha para un posible rollback."
Si tu aplicación escucha en otro puerto dentro del contenedor o tiene un endpoint de salud, pásalos como variables, por ejemplo APP_PORT=3000 HEALTH_PATH=/health. El script usa curl -f, que falla con cualquier respuesta 4xx o 5xx, así que solo cambia el tráfico si el endpoint devuelve un código correcto.
Hazlo ejecutable:
sudo chmod 755 /usr/local/bin/bluegreen-deploy
Paso 5: Desplegar la versión nueva
Para comprobar que no se pierde ninguna petición, abre una segunda sesión SSH y lanza 300 peticiones seguidas durante unos 30 segundos, agrupando los códigos de respuesta:
for i in $(seq 1 300); do curl -s -o /dev/null -w '%{http_code}\n' http://localhost/; sleep 0.1; done | sort | uniq -c
Mientras corre, despliega la versión nueva desde la primera sesión:
sudo bluegreen-deploy nginx:1.27-alpine
Entorno activo: blue. Desplegando nginx:1.27-alpine en green (puerto 8082).
...
Esperando a que green responda en /...
Tráfico cambiado a green. blue sigue en marcha para un posible rollback.
Cuando termine el bucle de la segunda sesión, todas las respuestas deben ser 200:
300 200
Comprueba que el tráfico va ahora a green:
curl -sI http://localhost/ | grep -i '^x-upstream'
X-Upstream: 127.0.0.1:8082
Y que ambos entornos siguen en marcha:
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES IMAGE PORTS
app-green nginx:1.27-alpine 127.0.0.1:8082->80/tcp
app-blue nginx:1.26-alpine 127.0.0.1:8081->80/tcp
Paso 6: Hacer rollback
Si la versión nueva da problemas, volver atrás es cambiar el enlace al entorno anterior, que sigue arrancado con la versión que funcionaba:
sudo ln -sfn /etc/nginx/bluegreen/blue.conf /etc/nginx/bluegreen/active.conf
sudo nginx -t && sudo systemctl reload nginx
curl -sI http://localhost/ | grep -i '^x-upstream'
X-Upstream: 127.0.0.1:8081
Cuando confirmes que la versión nueva es estable, puedes parar el entorno inactivo para liberar memoria. El siguiente despliegue lo volverá a crear:
docker stop app-blue
Paso 7: Tener en cuenta la base de datos
El cambio de tráfico es instantáneo, pero durante unos segundos las dos versiones conviven y, tras un rollback, la versión antigua vuelve a usar la base de datos que ya ha tocado la nueva. Por eso las migraciones deben ser compatibles con ambas versiones. La técnica habitual se llama expandir y contraer:
- Expandir: la versión nueva solo añade (columnas nuevas que admiten
NULLo tienen valor por defecto, tablas nuevas). La versión antigua sigue funcionando porque ignora lo que no conoce. - Desplegar: la versión nueva escribe en las columnas antiguas y en las nuevas.
- Contraer: en un despliegue posterior, cuando ya no vas a volver a la versión antigua, eliminas las columnas que dejaron de usarse.
Nunca renombres ni borres una columna en el mismo despliegue que cambia el código que la usa. Ejecuta las migraciones antes de cambiar el tráfico y solo si son de tipo "expandir".
Tampoco guardes sesiones de usuario en la memoria de la aplicación: tras el cambio, los usuarios perderían la sesión. Guárdalas en Redis, en la base de datos o en cookies firmadas.
El mismo patrón en Kubernetes
Si tu aplicación corre en Kubernetes, la idea es idéntica: dos Deployments (por ejemplo con las etiquetas version: blue y version: green) y un Service cuyo selector decide a cuál van los pods. El cambio de tráfico es modificar ese selector:
kubectl patch service app -p '{"spec":{"selector":{"app":"app","version":"green"}}}'
Antes de cambiar, comprueba que el Deployment nuevo está listo con kubectl rollout status deployment/app-green.
Solución de problemas
nginx: [emerg] duplicate upstream "app_active": Nginx está cargando blue.conf y green.conf a la vez. Asegúrate de que solo se incluye active.conf y de que el directorio bluegreen no está dentro de conf.d/ ni de sites-enabled/.
El script falla con El entorno green no responde: revisa los logs del contenedor con docker logs app-green. Comprueba también que APP_PORT coincide con el puerto en el que escucha la aplicación dentro del contenedor y que HEALTH_PATH existe.
Aparecen errores 502 durante el cambio: suele ocurrir si detienes el entorno antiguo inmediatamente después de recargar Nginx. Espera a que terminen las peticiones en curso antes de pararlo; el script lo deja en marcha a propósito.
X-Upstream no aparece en la respuesta: comprueba que el sitio app está enlazado en sites-enabled y que no queda otro sitio con default_server en el puerto 80 (sudo nginx -T | grep default_server).
Conclusión
Has montado un despliegue blue-green con dos entornos en Docker y Nginx como conmutador, has automatizado el cambio con una comprobación de salud previa y has comprobado que no se pierde ninguna petición durante el cambio ni durante el rollback. Como siguientes pasos puedes:
- Añadir HTTPS al sitio de Nginx con Certbot.
- Llamar a
bluegreen-deploydesde tu pipeline de CI/CD tras construir y publicar la imagen. - Añadir un endpoint
/healtha tu aplicación que compruebe también la conexión con la base de datos y usarlo comoHEALTH_PATH.
