Deploy blue-green com Docker Compose para aplicações PHP monolito (Nginx, DB e healthcheck)

Deploy & DevOps · Por · Publicado em · 9 min de leitura

Conectores SFP e cabos de fibra óptica ligados a um equipamento de rack de rede
Imagem de infraestrutura e conectividade, adequada para tutoriais de deploy e operações. Foto: Brett Sayles / Pexels

Este tutorial ensina deploy blue-green com Docker Compose para aplicações PHP monolito. Você terá Nginx na frente, dois containers PHP-FPM (Blue e Green), um banco de dados compartilhado, healthchecks HTTP e um script para alternar o tráfego sem interrupção.

Pré-requisitos e arquitetura do blue-green com Compose

Blue-green mantém duas versões do app rodando: Blue (ativa) e Green (preparada). Trocamos o tráfego quando a Green estiver saudável. Se algo falhar, voltamos rapidamente para a Blue.

Neste deploy blue-green com Docker Compose para aplicações PHP monolito, você terá os componentes abaixo. Instale Docker e Compose antes de começar. Se precisar, siga: instalar Docker e Docker Compose.

A estratégia exige migrações compatíveis. Planeje mudanças no banco em duas etapas para permitir rollback. Nunca aplique migrações destrutivas sem plano de retorno.

  • Um Nginx único, expondo a porta 80.
  • Dois containers PHP-FPM: app_blue e app_green.
  • Um banco de dados compartilhado (ex.: MariaDB).
  • Um volume compartilhado para uploads/arquivos do app.
  • Healthchecks HTTP: /health, /health-blue e /health-green.

Estrutura de pastas do projeto

Organize os arquivos assim. Vamos versionar a infra junto do app. Ajuste nomes conforme seu projeto.

Criaremos um script para alternar o tráfego copiando a configuração de upstream ativa.

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 do monolito PHP (PHP-FPM)

Usaremos um Dockerfile enxuto para buildar dependências com Composer e executar PHP-FPM. Adapte extensões e comandos à sua aplicação. Para reduzir tamanho e camadas, veja também imagens Docker leves com 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"]

Configuração do Nginx com upstreams Blue/Green e endpoints de health

Nginx encaminha requisições ao upstream definido em upstream_active.conf. Criamos também rotas fixas para checar Blue e Green separadamente. A troca de tráfego alterna o arquivo de upstream ativo e recarrega o 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, DB, volumes e healthcheck

O Compose inicia Nginx, os dois apps e o banco. Usamos um volume compartilhado para uploads. O healthcheck do Nginx verifica os endpoints locais. Para publicar imagens em um registry privado, veja: registry 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 health check no PHP (com verificação do banco)

O health.php retorna 200 quando o app e o banco respondem. Também exibe versão e cor, úteis para validar a troca. Ajuste driver e DSN se usar outro banco (ex.: 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);

Subir ambiente, fazer o primeiro deploy (Blue) e preparar Green

Hora de subir os serviços, apontar o upstream ativo para Blue e validar. Depois, publicamos a Green, validamos e deixamos pronta para troca. Cuidado: se houver migrações, aplique-as de forma compatível com as duas versões.

# 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 .

Trocar tráfego e rollback seguro

Usaremos um script simples para alternar o upstream e recarregar o Nginx. No deploy blue-green com Docker Compose para aplicações PHP monolito, a troca é rápida e reversível. Para monitorar depois do deploy, veja Prometheus e Grafana com Docker Compose.

  • Script aceita blue ou green.
  • A troca é atômica do ponto de vista de Nginx: reload é rápido.
  • Rollback é apenas repetir a troca para a outra cor.
  • Mantenha migrações compatíveis com as duas versões.
# 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

Como confirmar que funcionou e solução de problemas

Confirme a troca inspecionando a saída do health. Ela deve mostrar a cor e a versão esperadas. Veja também logs e latência após o deploy. Alguns problemas comuns e correções:

Para automatizar tudo em CI/CD, integre o build, validação de health e switch no pipeline. Um exemplo prático: pipeline de deploy automático.

  • Verificação: curl http://SEU_HOST/health deve exibir {"color":"green","version":"..."} após a troca.
  • Uploads sumiram: garanta que ambos containers montam o mesmo volume de uploads (app_shared).
  • Sessões de usuário: use sessões compartilhadas (ex.: Redis) para evitar logout no switch.
  • Migrações: use etapas compatíveis com as duas versões; adicione colunas novas antes de usá-las no código.
  • Static assets: sirva por CDN/objeto ou monte um volume compartilhado para arquivos públicos gerados.
  • TLS: termine TLS no host ou adicione um proxy TLS na frente; este exemplo usa HTTP para simplificar.

Perguntas frequentes

Blue-green funciona sem Kubernetes?

Sim. Este tutorial usa apenas Docker Compose, Nginx e um script para alternar o upstream. Você mantém duas versões rodando e troca o tráfego com reload do Nginx.

Como faço rollback rápido?

Execute o script apontando para a cor anterior. Como ambas versões estão ativas, a reversão é imediata. Ainda assim, monitore erros e latência após o rollback.

E as migrações do banco no blue-green?

Aplique migrações compatíveis com as duas versões. Evite mudanças destrutivas até desligar a versão antiga. Teste o schema novo com a versão Green antes de trocar o tráfego.

Como lidar com sessões e cache?

Guarde sessões e cache em um backend compartilhado, como Redis. Isso evita que usuários percam a sessão na troca entre Blue e Green.

Posso usar PostgreSQL em vez de MariaDB?

Pode. Ajuste a imagem do banco, as variáveis de ambiente e o DSN no health.php (pdo_pgsql). O restante do fluxo blue-green permanece igual.

Como servir arquivos estáticos eficientemente?

Sirva assets estáticos por Nginx a partir de um volume compartilhado ou CDN. Gere os assets durante o build e disponibilize-os para ambas as versões antes da troca.

Quer adaptar o blue-green ao seu monolito PHP?

Posso ajudar a revisar seu Compose, planejar migrações seguras e automatizar o switch de tráfego.

Falar sobre deploy blue-green

Veja também