Despliegue blue-green con Docker Compose para monolitos PHP (Nginx, BD y healthcheck)

Deploy & DevOps · Por · Publicado el · 9 min de lectura

Conectores SFP y cables de fibra óptica conectados a un equipo de rack de red
Imagen de infraestructura y conectividad, adecuada para tutoriales de despliegue y operaciones. Foto: Brett Sayles / Pexels

Este tutorial muestra cómo implementar un despliegue blue‑green con Docker Compose para un monolito PHP. Tendrás Nginx en front, dos contenedores PHP‑FPM (Blue y Green), una base de datos compartida, healthchecks HTTP y un script para cambiar el tráfico sin interrupciones.

Requisitos y arquitectura del blue‑green con Compose

Blue‑green mantiene dos versiones de la aplicación en ejecución: Blue (activa) y Green (lista). Redirigimos el tráfico cuando la Green está saludable. Si algo falla, volvemos rápidamente a la Blue.

En este despliegue blue‑green con Docker Compose para monolitos PHP tendrás los componentes que se muestran abajo. Instala Docker y Docker Compose antes de empezar. Si lo necesitas, sigue: instalar Docker y Docker Compose.

La estrategia requiere migraciones compatibles. Planifica cambios en la base de datos en dos pasos para permitir rollback. Nunca apliques migraciones destructivas sin un plan de retorno.

  • Un único Nginx, exponiendo el puerto 80.
  • Dos contenedores PHP‑FPM: app_blue y app_green.
  • Una base de datos compartida (ej.: MariaDB).
  • Un volumen compartido para uploads/archivos de la app.
  • Healthchecks HTTP: /health, /health-blue y /health-green.

Estructura de carpetas del proyecto

Organiza los archivos así. Versiona la infraestructura junto con la app. Ajusta los nombres según tu proyecto.

Crearemos un script para alternar el tráfico copiando la configuración del upstream activo.

project/
├── .env
├── docker-compose.yml
├── app/
│   ├── Dockerfile
│   ├── src/
│   │   ├── public/
│   │   │   └── health.php
│   │   └── index.php  # se usar front controller
├── nginx/
│   └── conf.d/
│       ├── nginx.conf
│       ├── upstream_blue.conf
│       └── upstream_green.conf
└── deploy/
    └── switch_traffic.sh

Dockerfile del monolito PHP (PHP‑FPM)

Usaremos un Dockerfile minimalista para construir dependencias con Composer y ejecutar PHP‑FPM. Adapta extensiones y comandos a tu aplicación. Para reducir tamaño y capas, consulta también imágenes Docker ligeras con multistage.

# app/Dockerfile
FROM php:8.2-fpm-alpine AS runtime

# Extensões comuns — ajuste conforme seu banco (pdo_mysql ou pdo_pgsql)
RUN apk add --no-cache git oniguruma-dev libzip-dev \
    && docker-php-ext-install pdo_mysql opcache

# Composer para instalar dependências quando houver composer.json
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html
COPY src/ .

# Instale dependências do PHP se existir composer.json
RUN if [ -f composer.json ]; then \
      composer install --no-interaction --no-dev --prefer-dist --optimize-autoloader; \
    fi \
    && chown -R www-data:www-data /var/www/html

EXPOSE 9000
CMD ["php-fpm", "-F"]

Configuración de Nginx con upstreams Blue/Green y endpoints de health

Nginx dirige las peticiones al upstream definido en upstream_active.conf. También creamos rutas dedicadas para comprobar Blue y Green por separado. El cambio de tráfico sustituye el archivo de upstream activo y recarga Nginx.

# nginx/conf.d/nginx.conf

# Upstreams fixos para health dos dois lados
upstream php_blue  { server app_blue:9000; }
upstream php_green { server app_green:9000; }

# Upstream ativo — definido por upstream_active.conf (criado pelo switch)
# Este arquivo deve declarar: upstream php_upstream { server app_blue:9000; } ou app_green
include /etc/nginx/conf.d/upstream_active.conf;

server {
    listen 80;
    server_name _;

    # Health ativo (aponta para php_upstream definido via include)
    location = /health {
        fastcgi_pass php_upstream;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME /var/www/html/public/health.php;
    }

    # Healths dedicados
    location = /health-blue {
        fastcgi_pass php_blue;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME /var/www/html/public/health.php;
    }
    location = /health-green {
        fastcgi_pass php_green;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME /var/www/html/public/health.php;
    }

    # Roteie tudo para seu front controller
    location / {
        try_files $uri /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass php_upstream; # definido no include
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
    }
}

# nginx/conf.d/upstream_blue.conf
upstream php_upstream { server app_blue:9000; }

# nginx/conf.d/upstream_green.conf
upstream php_upstream { server app_green:9000; }

docker-compose.yml: Blue/Green, BD, volúmenes y healthcheck

El Compose arranca Nginx, ambas apps y la base de datos. Usamos un volumen compartido para uploads. El healthcheck de Nginx comprueba los endpoints locales. Para publicar imágenes en un registry privado, consulta: registro Docker privado.

# docker-compose.yml
version: "3.9"

services:
  nginx:
    image: nginx:1.25-alpine
    depends_on:
      - app_blue
      - app_green
    ports:
      - "80:80"
    volumes:
      - ./nginx/conf.d/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./nginx/conf.d/upstream_active.conf:/etc/nginx/conf.d/upstream_active.conf:ro
      # Diretório de uploads/arquivos compartilhados
      - app_shared:/var/www/html/storage
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost/health >/dev/null && wget -qO- http://localhost/health-blue >/dev/null && wget -qO- http://localhost/health-green >/dev/null"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks: [web]

  app_blue:
    build:
      context: ./app
    container_name: app_blue
    environment:
      APP_COLOR: blue
      APP_VERSION: v1
      DB_HOST: db
      DB_NAME: app
      DB_USER: app
      DB_PASS: secret
    volumes:
      - app_shared:/var/www/html/storage
    networks: [web]

  app_green:
    build:
      context: ./app
    container_name: app_green
    environment:
      APP_COLOR: green
      APP_VERSION: v2
      DB_HOST: db
      DB_NAME: app
      DB_USER: app
      DB_PASS: secret
    volumes:
      - app_shared:/var/www/html/storage
    networks: [web]

  db:
    image: mariadb:10.11
    environment:
      MARIADB_DATABASE: app
      MARIADB_USER: app
      MARIADB_PASSWORD: secret
      MARIADB_ROOT_PASSWORD: rootsecret
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks: [web]

volumes:
  db_data:
  app_shared:

networks:
  web:

Endpoint de healthcheck en PHP (con verificación de BD)

El health.php devuelve 200 cuando la app y la base de datos responden. También muestra versión y color, útiles para validar el cambio. Ajusta driver y DSN si usas otra BD (ej.: PostgreSQL).

<?php
// app/src/public/health.php
header('Content-Type: application/json');

$color   = getenv('APP_COLOR') ?: 'unknown';
$version = getenv('APP_VERSION') ?: 'unknown';
$dbHost  = getenv('DB_HOST') ?: 'db';
$dbName  = getenv('DB_NAME') ?: 'app';
$dbUser  = getenv('DB_USER') ?: 'app';
$dbPass  = getenv('DB_PASS') ?: 'secret';

$status = [
  'ok' => true,
  'color' => $color,
  'version' => $version,
  'db' => 'unknown'
];

try {
  $pdo = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4", $dbUser, $dbPass, [
    PDO::ATTR_TIMEOUT => 2,
  ]);
  $pdo->query('SELECT 1');
  $status['db'] = 'up';
} catch (Throwable $e) {
  http_response_code(503);
  $status['ok'] = false;
  $status['db'] = 'down';
  $status['error'] = 'db';
}

echo json_encode($status);

Levantar el entorno, hacer el primer deploy (Blue) y preparar Green

Es hora de arrancar los servicios, apuntar el upstream activo a Blue y validar. Luego publicamos la Green, la validamos y la dejamos lista para el cambio. Atención: si hay migraciones, aplícalas de forma compatible con ambas versiones.

# 1) Gerar upstream ativo inicial (Blue)
cp -f nginx/conf.d/upstream_blue.conf nginx/conf.d/upstream_active.conf

# 2) Subir stack
docker compose up -d --build db app_blue app_green nginx

# 3) Verificar saúde
curl -s http://localhost/health | jq .
curl -s http://localhost/health-blue | jq .
curl -s http://localhost/health-green | jq .

# 4) Popular DB, seeds e migrações (exemplo genérico)
# docker compose exec app_blue php artisan migrate --force   # se usar Laravel
# docker compose exec app_blue php artisan db:seed --force

# 5) Preparar versão Green (atualize código e re-build)
# Atualize app/src/ com sua nova versão
docker compose build app_green

# 6) Validar saúde da Green
curl -s http://localhost/health-green | jq .

Cambiar el tráfico y rollback seguro

Usaremos un script sencillo para alternar el upstream y recargar Nginx. En este despliegue blue‑green con Docker Compose para monolitos PHP, el cambio es rápido y reversible. Para monitorizar después del deploy, consulta Prometheus y Grafana con Docker Compose.

  • El script acepta blue o green.
  • El cambio es atómico desde Nginx: reload es rápido.
  • El rollback es simplemente repetir el cambio a la otra color.
  • Mantén las migraciones compatibles con ambas versiones.
# deploy/switch_traffic.sh
#!/usr/bin/env bash
set -euo pipefail
COLOR="${1:-}"
if [[ "$COLOR" != "blue" && "$COLOR" != "green" ]]; then
  echo "Uso: $0 blue|green"; exit 1
fi
cp -f "nginx/conf.d/upstream_${COLOR}.conf" "nginx/conf.d/upstream_active.conf"
docker compose exec -T nginx nginx -s reload
echo "Tráfego direcionado para: $COLOR"

# Exemplos de uso:
# bash deploy/switch_traffic.sh green   # troca para Green
# bash deploy/switch_traffic.sh blue    # rollback para Blue

Cómo confirmar que funcionó y resolución de problemas

Confirma el cambio inspeccionando la salida del health. Debe mostrar el color y la versión esperados. Revisa también logs y latencia tras el deploy. Algunos problemas comunes y correcciones:

  • Verificación: curl http://TU_HOST/health debe mostrar {"color":"green","version":"..."} después del cambio.
  • Los uploads desaparecen: asegúrate de que ambos contenedores montan el mismo volumen de uploads (app_shared).
  • Sesiones de usuario: usa sesiones compartidas (ej.: Redis) para evitar cierres de sesión al cambiar.
  • Migraciones: realiza pasos compatibles con ambas versiones; añade columnas nuevas antes de usarlas en el código.
  • Assets estáticos: sírvelos desde CDN/objeto o monta un volumen compartido para archivos públicos generados.
  • TLS: termina TLS en el host o añade un proxy TLS delante; este ejemplo usa HTTP para simplificar.

Preguntas frecuentes

¿Funciona blue‑green sin Kubernetes?

Sí. Este tutorial usa solo Docker Compose, Nginx y un script para alternar el upstream. Mantienes dos versiones en ejecución y cambias el tráfico con un reload de Nginx.

¿Cómo hago un rollback rápido?

Ejecuta el script apuntando al color anterior. Como ambas versiones están activas, la reversión es inmediata. Aun así, monitorea errores y latencia tras el rollback.

¿Y las migraciones de base de datos en blue‑green?

Aplica migraciones compatibles con ambas versiones. Evita cambios destructivos hasta que la versión antigua esté fuera de servicio. Prueba el esquema nuevo con la versión Green antes de cambiar el tráfico.

¿Cómo manejar sesiones y cache?

Almacena sesiones y cache en un backend compartido, como Redis. Así evitas que los usuarios pierdan sesión al alternar entre Blue y Green.

¿Puedo usar PostgreSQL en vez de MariaDB?

Puedes. Ajusta la imagen de la base de datos, las variables de entorno y el DSN en health.php (pdo_pgsql). El resto del flujo blue‑green permanece igual.

¿Cómo servir archivos estáticos de forma eficiente?

Sirve los assets estáticos con Nginx desde un volumen compartido o desde una CDN. Genera los assets en el build y ponlos disponibles para ambas versiones antes de cambiar el tráfico.

¿Quieres adaptar el blue‑green a tu monolito PHP?

Puedo ayudar a revisar tu Compose, planificar migraciones seguras y automatizar la conmutación de tráfico.

Hablar sobre despliegue blue‑green

Ver también