--- title: "Despliegues azul-verde con Docker y Nginx en Ubuntu 24.04" description: "Monta despliegues azul-verde con dos contenedores Docker y Nginx en Ubuntu 24.04: cambia de versión sin cortes, con comprobación de salud y reversión rápida." category: "CI/CD DevOps" language: es source: https://cubepath.com/es/docs/ci-cd-devops/implementaciones-azul-verde-con-docker-y-nginx updated: 2026-09-25 --- # Despliegues azul-verde con Docker y Nginx en Ubuntu 24.04 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`: ```bash 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: ```dockerfile FROM nginx:stable-alpine ARG VERSION=dev RUN echo "myapp version ${VERSION}" > /usr/share/nginx/html/index.html ``` Construye dos versiones: ```bash 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: ```bash sudo docker image ls myapp ``` ```output 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: ```bash 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: ```bash sudo nano /etc/nginx/conf.d/myapp-upstream.conf ``` ```nginx upstream myapp_backend { server 127.0.0.1:8081; } ``` Crea el sitio que usa ese upstream: ```bash sudo nano /etc/nginx/sites-available/myapp ``` ```nginx 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: ```bash sudo rm /etc/nginx/sites-enabled/default sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ sudo nginx -t ``` ```output 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: ```bash sudo systemctl reload nginx sudo ufw allow 'Nginx Full' ``` > Importante: publica 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: ```bash 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`): ```bash #!/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_healthy` espera 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-stopped` hace 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: ```bash 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: ```bash sudo bluegreen deploy myapp:1 ``` ```output 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: ```bash curl http://localhost/ ``` ```output 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: ```bash 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: ```bash sudo bluegreen deploy myapp:2 ``` ```output 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`: ```output 300 200 ``` Comprueba la versión que se sirve ahora y el estado de los contenedores: ```bash curl http://localhost/ sudo bluegreen status ``` ```output 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: ```bash sudo bluegreen rollback ``` ```output Traffic switched to blue (127.0.0.1:8081) ``` ```bash curl http://localhost/ ``` ```output 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: ```bash sudo bluegreen deploy alpine:3.20 ``` Tras unos 60 segundos de intentos, el script aborta: ```output 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: ```bash curl http://localhost/ ``` ```output 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: ```bash 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.