En un despliegue azul-verde mantienes dos copias de la aplicación: una (por ejemplo, la azul) atiende el tráfico y la otra (la verde) recibe la versión nueva. Cuando la versión nueva responde correctamente, el proxy cambia el tráfico de una a otra de golpe, sin cortar las peticiones en curso, y la versión anterior sigue en marcha para volver a ella en segundos si algo falla. En este tutorial montarás este esquema en Ubuntu 24.04 con dos contenedores Docker y Nginx como proxy inverso, y lo automatizarás con un script que despliega, comprueba la salud y revierte.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, y un usuario no root con privilegios
sudo. - Docker Engine instalado desde el repositorio oficial de Docker.
- Memoria suficiente para ejecutar dos instancias de tu aplicación a la vez, ya que durante el cambio conviven las dos.
- Opcionalmente, un dominio con un registro DNS A que apunte al servidor, si quieres añadir HTTPS al final.
Cómo funciona el esquema
| Elemento | Función |
|---|---|
Contenedor myapp-blue | Una de las dos copias, publicada solo en 127.0.0.1:8081 |
Contenedor myapp-green | La otra copia, publicada solo en 127.0.0.1:8082 |
/etc/nginx/conf.d/myapp-upstream.conf | Define a qué puerto (azul o verde) envía Nginx el tráfico |
/var/lib/bluegreen/myapp.active | Guarda qué color está activo |
Script bluegreen | Arranca la versión nueva en el color inactivo, comprueba su salud, reescribe el upstream y recarga Nginx |
nginx -s reload (o systemctl reload nginx) es lo que hace posible el cambio sin cortes: Nginx arranca procesos nuevos con la configuración actualizada y deja que los antiguos terminen las peticiones que estaban atendiendo.
Paso 1: Crear una aplicación de ejemplo
Para ver el cambio de versión necesitas una imagen que diga qué versión es. Si ya tienes la imagen de tu aplicación, puedes saltarte este paso y usarla en su lugar, ajustando el puerto interno en el paso 3.
Crea un directorio de trabajo con un Dockerfile:
mkdir -p ~/myapp && cd ~/myapp
nano Dockerfile
Añade este contenido. La imagen sirve con Nginx una página que muestra la versión indicada al construirla:
FROM nginx:stable-alpine
ARG VERSION=dev
RUN echo "myapp version ${VERSION}" > /usr/share/nginx/html/index.html
Construye dos versiones:
sudo docker build --build-arg VERSION=1 -t myapp:1 .
sudo docker build --build-arg VERSION=2 -t myapp:2 .
Comprueba que las dos imágenes existen:
sudo docker image ls myapp
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp 2 4c1d2e3f5a6b 5 seconds ago 48.2MB
myapp 1 9f8e7d6c5b4a 8 seconds ago 48.2MB
Paso 2: Configurar Nginx como proxy
Instala Nginx desde los repositorios de Ubuntu:
sudo apt update
sudo apt install nginx
Crea el archivo del upstream. Nginx incluye automáticamente todos los *.conf de /etc/nginx/conf.d/ en el bloque http, y el script reescribirá este archivo en cada cambio. De momento apunta al puerto del color azul:
sudo nano /etc/nginx/conf.d/myapp-upstream.conf
upstream myapp_backend {
server 127.0.0.1:8081;
}
Crea el sitio que usa ese upstream:
sudo nano /etc/nginx/sites-available/myapp
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name your_domain;
location / {
proxy_pass http://myapp_backend;
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;
}
}
Sustituye your_domain por tu dominio, si lo tienes. Desactiva el sitio por defecto, activa el nuevo y valida la configuración:
sudo rm /etc/nginx/sites-enabled/default
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Recarga Nginx y abre HTTP y HTTPS en UFW si el cortafuegos está activo:
sudo systemctl reload nginx
sudo ufw allow 'Nginx Full'
Importantepublica los contenedores siempre en
127.0.0.1, como hará el script. Docker añade sus propias reglas de iptables y un puerto publicado en todas las interfaces quedaría accesible desde Internet aunque UFW no lo permita.
Paso 3: Crear el script de despliegue
El script tiene tres órdenes: deploy IMAGEN arranca la imagen en el color inactivo, espera a que responda y cambia el tráfico; rollback devuelve el tráfico al otro color, que sigue en marcha; y status muestra el estado. Si la versión nueva no pasa la comprobación de salud, el tráfico no se toca.
Crea el script:
sudo nano /usr/local/sbin/bluegreen
Añade este contenido. Ajusta CONTAINER_PORT al puerto en el que escucha tu aplicación dentro del contenedor y HEALTH_PATH a su ruta de salud, si tiene una (por ejemplo /health):
#!/usr/bin/env bash
set -euo pipefail
APP="myapp"
CONTAINER_PORT=80
HEALTH_PATH="/"
UPSTREAM_FILE="/etc/nginx/conf.d/${APP}-upstream.conf"
STATE_DIR="/var/lib/bluegreen"
STATE_FILE="${STATE_DIR}/${APP}.active"
declare -A PORTS=([blue]=8081 [green]=8082)
usage() {
echo "Usage: $0 deploy IMAGE | rollback | status" >&2
exit 2
}
active_color() {
cat "$STATE_FILE" 2>/dev/null || echo "none"
}
other_color() {
if [[ "$1" == "blue" ]]; then echo "green"; else echo "blue"; fi
}
wait_healthy() {
local port="$1"
for _ in $(seq 1 30); do
if curl -fsS -o /dev/null "http://127.0.0.1:${port}${HEALTH_PATH}"; then
return 0
fi
sleep 2
done
return 1
}
switch_to() {
local color="$1"
local port="${PORTS[$color]}"
local backup="${STATE_DIR}/${APP}-upstream.conf.bak"
cp "$UPSTREAM_FILE" "$backup"
printf 'upstream %s_backend {\n server 127.0.0.1:%s;\n}\n' "$APP" "$port" > "$UPSTREAM_FILE"
if ! nginx -t -q; then
cp "$backup" "$UPSTREAM_FILE"
echo "nginx configuration test failed, previous upstream restored" >&2
exit 1
fi
systemctl reload nginx
echo "$color" > "$STATE_FILE"
echo "Traffic switched to ${color} (127.0.0.1:${port})"
}
deploy() {
local image="$1" active target port
active="$(active_color)"
target="$(other_color "$active")"
port="${PORTS[$target]}"
echo "Active: ${active}. Starting ${image} as ${APP}-${target} on 127.0.0.1:${port}"
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}:${CONTAINER_PORT}" "$image" >/dev/null
if ! wait_healthy "$port"; then
echo "${APP}-${target} failed the health check, traffic stays on ${active}" >&2
docker logs --tail 20 "${APP}-${target}" >&2 || true
docker rm -f "${APP}-${target}" >/dev/null
exit 1
fi
switch_to "$target"
}
rollback() {
local active previous
active="$(active_color)"
if [[ "$active" == "none" ]]; then
echo "Nothing has been deployed yet" >&2
exit 1
fi
previous="$(other_color "$active")"
if [[ "$(docker inspect -f '{{.State.Running}}' "${APP}-${previous}" 2>/dev/null)" != "true" ]]; then
echo "${APP}-${previous} is not running, cannot roll back" >&2
exit 1
fi
if ! wait_healthy "${PORTS[$previous]}"; then
echo "${APP}-${previous} is not healthy, cannot roll back" >&2
exit 1
fi
switch_to "$previous"
}
status() {
echo "Active color: $(active_color)"
docker ps -a --filter "name=${APP}-" --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
}
[[ $# -ge 1 ]] || usage
mkdir -p "$STATE_DIR"
case "$1" in
deploy) [[ $# -eq 2 ]] || usage; deploy "$2" ;;
rollback) rollback ;;
status) status ;;
*) usage ;;
esac
Puntos clave del script:
- El color de destino es siempre el que no está activo. En el primer despliegue no hay ninguno activo y se usa el azul.
- El contenedor del color de destino se elimina y se vuelve a crear con la imagen nueva. El contenedor activo no se toca.
wait_healthyespera hasta 60 segundos a que la aplicación responda con un código 2xx o 3xx. Si no lo hace, el contenedor nuevo se borra y el tráfico sigue donde estaba.- Antes de recargar Nginx se valida la configuración con
nginx -t; si falla, se restaura el upstream anterior. --restart unless-stoppedhace que ambos contenedores vuelvan a arrancar tras un reinicio del servidor, y el archivo de upstream conserva qué color recibe el tráfico.
Hazlo ejecutable solo por root, ya que controla Docker y Nginx:
sudo chmod 750 /usr/local/sbin/bluegreen
Paso 4: Hacer el primer despliegue
Despliega la versión 1. Como no hay ningún color activo, irá al azul:
sudo bluegreen deploy myapp:1
Active: none. Starting myapp:1 as myapp-blue on 127.0.0.1:8081
Traffic switched to blue (127.0.0.1:8081)
Comprueba que Nginx sirve la versión 1:
curl http://localhost/
myapp version 1
Paso 5: Desplegar una versión nueva sin cortes
Para comprobar que el cambio no pierde peticiones, abre una segunda sesión SSH en el servidor y lanza 300 peticiones seguidas, una cada 0,1 segundos, contando 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 se ejecuta, en la primera sesión despliega la versión 2, que irá al color verde:
sudo bluegreen deploy myapp:2
Active: blue. Starting myapp:2 as myapp-green on 127.0.0.1:8082
Traffic switched to green (127.0.0.1:8082)
Cuando termine el bucle de la segunda sesión, todas las respuestas deben ser 200:
300 200
Comprueba la versión que se sirve ahora y el estado de los contenedores:
curl http://localhost/
sudo bluegreen status
myapp version 2
Active color: green
NAMES IMAGE STATUS PORTS
myapp-green myapp:2 Up 40 seconds 127.0.0.1:8082->80/tcp
myapp-blue myapp:1 Up 3 minutes 127.0.0.1:8081->80/tcp
La versión 1 sigue en marcha en el color azul, lista para una reversión. El siguiente despliegue la sustituirá.
Paso 6: Revertir a la versión anterior
Si detectas un problema en la versión 2, devuelve el tráfico al color azul:
sudo bluegreen rollback
Traffic switched to blue (127.0.0.1:8081)
curl http://localhost/
myapp version 1
La reversión solo recarga Nginx, así que tarda lo que tarda la comprobación de salud: normalmente menos de un segundo.
Paso 7: Comprobar que una versión rota no recibe tráfico
La comprobación de salud protege frente a imágenes que no arrancan. Para probarlo, despliega una imagen que no sirve HTTP, como alpine, cuyo proceso termina nada más arrancar:
sudo bluegreen deploy alpine:3.20
Tras unos 60 segundos de intentos, el script aborta:
Active: blue. Starting alpine:3.20 as myapp-green on 127.0.0.1:8082
myapp-green failed the health check, traffic stays on blue
El sitio sigue sirviendo la versión activa:
curl http://localhost/
myapp version 1
Como el contenedor fallido se borra, en este punto ya no queda una versión anterior para rollback. Despliega de nuevo una versión buena para tener otra vez los dos colores en marcha.
Paso 8: Integrarlo en tu flujo de despliegue
Con las imágenes publicadas en un registro (Docker Hub, GitHub Container Registry o uno propio), un despliegue se reduce a una orden. Por ejemplo, desde un pipeline de CI que se conecte por SSH al servidor:
sudo bluegreen deploy ghcr.io/your_user/myapp:1.4.0
docker run descarga la imagen si no está en el servidor. Usa etiquetas de versión concretas en lugar de latest, para que cada despliegue y cada reversión se refieran a una imagen exacta.
Ten en cuenta dos limitaciones del patrón:
- Base de datos compartida: las dos versiones usan la misma base de datos. Las migraciones deben ser compatibles hacia atrás (por ejemplo, añadir columnas en un despliegue y borrar las antiguas en uno posterior) para que la reversión siga funcionando.
- Conexiones largas: WebSockets y descargas largas que ya estaban abiertas siguen conectadas al contenedor anterior hasta que se cierran. Por eso el script no detiene el color antiguo tras el cambio.
Para servir el sitio por HTTPS, instala Certbot con sudo apt install certbot python3-certbot-nginx y ejecuta sudo certbot --nginx -d your_domain. Certbot modifica el bloque server y no el archivo de upstream, así que el script sigue funcionando igual.
Solución de problemas
502 Bad Gateway justo después de configurar Nginx: aún no has hecho el primer despliegue y no hay nada escuchando en 127.0.0.1:8081. Ejecuta sudo bluegreen deploy con tu imagen.
Bind for 127.0.0.1:8081 failed: port is already allocated: otro proceso o contenedor usa ese puerto. Localízalo con sudo ss -ltnp | grep 8081 o sudo docker ps y libéralo, o cambia los puertos en PORTS.
La comprobación de salud falla con una imagen que funciona: CONTAINER_PORT no coincide con el puerto interno de la aplicación, o HEALTH_PATH devuelve un código 4xx o 5xx. Pruébalo a mano con curl -i http://127.0.0.1:8082/ mientras el contenedor está en marcha.
Conclusión
Tienes despliegues azul-verde en Ubuntu 24.04 con Docker y Nginx: cada versión nueva arranca junto a la actual, solo recibe tráfico si pasa la comprobación de salud y la anterior queda disponible para revertir con una recarga de Nginx. Como siguientes pasos puedes lanzar bluegreen deploy desde tu pipeline de CI, añadir a tu aplicación un endpoint /health que compruebe sus dependencias, o repartir un pequeño porcentaje del tráfico a la versión nueva con pesos en el bloque upstream antes del cambio completo.