404 Not Found

404 Not Found


nginx

Deploy do Projeto — Lançamento da Plataforma SaaS ShopMetrics

Entrar no ar não é o fim — é apenas o começo. Após o deploy, você ainda precisa monitorar, configurar alertas e fazer backup do sistema para garantir operação estável 24/7.

1. O Que Você Vai Aprender


2. Uma História Real do Dia de Lançamento

(1) Dor: Não percebemos que o banco de dados não havia sido migrado até após o deploy

Bob liderou a equipe do ShopMetrics no primeiro deploy — o código foi enviado, Docker estava rodando e Nginx estava configurado. Mas assim que Alice abriu o dashboard, recebeu um erro 500. Charlie verificou os logs e descobriu que a tabela analytics_reports recém-adicionada não existia no banco de dados — eles esqueceram de executar a migration. Para piorar, não haviam feito backup do banco de dados antes do deploy, então não tiveram outra opção senão executar a migration no ambiente de produção.

(2) Uma Solução para Sistematizar o Processo de Deploy

Deploy não é apenas "enviar código para produção" — é um checklist de 15 passos, e cada passo deve passar por verificação antes de prosseguir. CI/CD automatiza 10 desses passos, deixando apenas os health checks e validações de negócios para confirmação manual.

TEXT
Checklist: 15 passos, CI/CD automatiza 10, humano verifica 5
Resultado: 0 passos perdidos, 0 incidentes pós-deploy

(3) Resultado

Para o segundo deploy de Bob, um processo sistemático foi usado — deploy totalmente automatizado mais health checks — concluído em 3 minutos com zero erros.


3. Orquestração Docker Compose de Produção

(1) Visão Geral da Arquitetura de Produção

100%
flowchart TB
    subgraph Internet["Internet"]
        USER[Usuários]
        STRIPE[Stripe Webhooks]
    end

    subgraph LB["Load Balancer / CDN"]
        NGINX[Nginx Proxy Reverso]
    end

    subgraph App["Camada de Aplicação"]
        APP1[Container App 1]
        APP2[Container App 2]
    end

    subgraph Worker["Camada de Segundo Plano"]
        Q1[Queue Worker - High]
        Q2[Queue Worker - Default]
        SCHED[Agendador]
    end

    subgraph Data["Camada de Dados"]
        MASTER[(MySQL Primary)]
        REPLICA[(MySQL Replica)]
        REDIS[(Redis Cluster)]
        S3[(S3 Storage)]
    end

    subgraph Monitor["Observabilidade"]
        SENTRY[Sentry]
        TELESCOPE[Telescope]
        LOGS[Agregação de Logs]
    end

    USER --> NGINX
    STRIPE --> NGINX
    NGINX --> APP1
    NGINX --> APP2
    APP1 --> MASTER
    APP1 --> REDIS
    APP2 --> REDIS
    APP1 --> S3
    Q1 --> MASTER
    Q1 --> REDIS
    Q1 --> S3
    SCHED --> Q1
    APP1 --> REPLICA
    APP2 --> REPLICA
    APP1 --> SENTRY
    APP1 --> TELESCOPE

(2) Configuração Docker Compose de Produção

YAML
# docker-compose.prod.yml
services:
  app:
    image: ghcr.io/bob/shopmetrics:latest
    restart: unless-stopped
    deploy:
      replicas: 2
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
    env_file:
      - .env.production
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - storage:/var/www/html/storage
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    networks:
      - internal

  queue-high:
    image: ghcr.io/bob/shopmetrics:latest
    restart: unless-stopped
    command: php artisan queue:work --queue=high --sleep=3 --tries=3 --max-time=3600
    deploy:
      replicas: 2
      resources:
        limits:
          memory: 256M
    env_file:
      - .env.production
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - storage:/var/www/html/storage
    networks:
      - internal

  queue-default:
    image: ghcr.io/bob/shopmetrics:latest
    restart: unless-stopped
    command: php artisan queue:work --queue=default --sleep=3 --tries=3 --max-time=3600
    deploy:
      replicas: 3
      resources:
        limits:
          memory: 256M
    env_file:
      - .env.production
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - storage:/var/www/html/storage
    networks:
      - internal

  scheduler:
    image: ghcr.io/bob/shopmetrics:latest
    restart: unless-stopped
    command: php artisan schedule:work --verbose
    env_file:
      - .env.production
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - internal

  mysql:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql
      - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf:ro
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASSWORD}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - internal

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: >
      redis-server
      --requirepass ${REDIS_PASSWORD}
      --maxmemory 512mb
      --maxmemory-policy allkeys-lru
      --appendonly yes
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - internal

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/nginx/production.conf:/etc/nginx/conf.d/default.conf:ro
      - ./docker/nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      app:
        condition: service_healthy
    networks:
      - internal

volumes:
  mysql_data:
  redis_data:
  storage:

networks:
  internal:
    driver: bridge

(1) ▶ Exemplo: Configuração .env.production do ShopMetrics

BASH
# .env.production
APP_NAME=ShopMetrics
APP_ENV=production
APP_KEY=base64:xxx
APP_DEBUG=false
APP_URL=https://shopmetrics.io
APP_VERSION=1.0.0

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=shopmetrics
DB_USERNAME=shopmetrics
DB_PASSWORD=${DB_PASSWORD}
DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}

CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=redis
REDIS_PASSWORD=${REDIS_PASSWORD}

FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=shopmetrics-reports

STRIPE_KEY=pk_live_xxx
STRIPE_SECRET=sk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx

SENTRY_LARAVEL_DSN=https://xxx@sentry.io/xxx
SENTRY_TRACES_SAMPLE_RATE=0.2

LOG_CHANNEL=daily
LOG_LEVEL=warning

Saída:

TEXT
# Comando executado com sucesso

4. Pipeline CI/CD

(1) Workflow GitHub Actions Completo

YAML
# .github/workflows/deploy-production.yml
name: Deploy para Produção

on:
  push:
    branches: [main]
  workflow_dispatch:  # Opção de gatilho manual

concurrency: production  # Apenas um deploy por vez

env:
  REGISTRY: ghcr.io
  IMAGE: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_DATABASE: shopmetrics_test
          MYSQL_USER: test
          MYSQL_PASSWORD: test
          MYSQL_ROOT_PASSWORD: test
        ports: ['3306:3306']
      redis:
        image: redis:7-alpine
        ports: ['6379:6379']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          coverage: xdebug
      - name: Instalar & Testar
        run: |
          composer install --no-progress
          cp .env.example .env
          php artisan key:generate
          php artisan test --parallel --coverage-text=coverage.txt
      - name: Upload cobertura
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage.txt

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: composer install --no-dev
      - name: Auditoria de segurança
        run: composer audit

  build:
    needs: [test, security]
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
            ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: deploy
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /opt/shopmetrics
            export IMAGE_TAG=${{ github.sha }}

            # Puxar imagem mais recente
            docker compose -f docker-compose.prod.yml pull app queue-high queue-default scheduler

            # Rolling update: iniciar novos containers
            docker compose -f docker-compose.prod.yml up -d --no-deps --scale app=3 app
            sleep 10

            # Health check dos novos containers
            for i in $(seq 1 5); do
              if curl -sf http://localhost:8080/health > /dev/null 2>&1; then
                echo "✓ Health check passou"
                break
              fi
              echo "Aguardando health check... ($i/5)"
              sleep 5
            done

            # Executar migrations
            docker compose -f docker-compose.prod.yml exec -T app php artisan migrate --force

            # Reiniciar workers (graceful)
            docker compose -f docker-compose.prod.yml exec -T app php artisan queue:restart

            # Reduzir containers antigos
            docker compose -f docker-compose.prod.yml up -d --scale app=2

            # Limpar e reconstruir caches
            docker compose -f docker-compose.prod.yml exec -T app php artisan optimize:clear
            docker compose -f docker-compose.prod.yml exec -T app php artisan optimize

            echo "✓ Deploy concluído: $IMAGE_TAG"

  verify:
    needs: deploy
    runs-on: ubuntu-latest
    steps:
      - name: Smoke test produção
        run: |
          STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://shopmetrics.io/health)
          if [ "$STATUS" != "200" ]; then
            echo "✗ Health check falhou: HTTP $STATUS"
            exit 1
          fi
          echo "✓ Health check de produção passou"

      - name: Notificar Slack em caso de sucesso
        if: success()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {"text":"✅ ShopMetrics deploy bem-sucedido: ${{ github.sha }}"}

      - name: Notificar Slack em caso de falha
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {"text":"🚨 ShopMetrics deploy FALHOU: ${{ github.sha }}"}

(1) ▶ Exemplo: Estratégia de Migration de Banco de Dados do ShopMetrics

PHP
// Padrões de migration seguros para produção

// ✅ SEGURO: Adicionar coluna (não-breaks)
Schema::table('orders', function (Blueprint $table) {
    $table->string('shipping_method')->nullable()->after('status');
    $table->index('shipping_method');
});

// ✅ SEGURO: Adicionar índice (concurrent no PostgreSQL)
// MySQL: Usar ALGORITHM=INPLACE para criação de índice sem bloqueio
DB::statement('ALTER TABLE orders ADD INDEX idx_orders_shipping (shipping_method) ALGORITHM=INPLACE');

// ⚠️ CUIDADO: Remover coluna (migration em dois passos)
// Passo 1: Marcar como depreciado (deploy primeiro, remover referências no código)
Schema::table('orders', function (Blueprint $table) {
    // Manter coluna antiga, adicionar nova coluna
    $table->renameColumn('shipping_method', 'deprecated_shipping_method');
});

// Passo 2: Remover após o código não referenciar mais (próximo deploy)
Schema::table('orders', function (Blueprint $table) {
    $table->dropColumn('deprecated_shipping_method');
});

// ❌ PERIGOSO: Alterar tipo de coluna (bloqueia tabela)
// Usar dois passos: adicionar nova coluna → migrar dados → excluir coluna antiga
Schema::table('orders', function (Blueprint $table) {
    $table->unsignedBigInteger('total_cents_new')->nullable()->after('total_cents');
});

// Migração de dados em uma migration separada
DB::statement('UPDATE orders SET total_cents_new = total_cents WHERE total_cents_new IS NULL');

Schema::table('orders', function (Blueprint $table) {
    $table->dropColumn('total_cents');
    $table->renameColumn('total_cents_new', 'total_cents');
});

Saída:

TEXT
// Execução bem-sucedida

5. Health Checks e Monitoramento

(1) Endpoints de Health Check

PHP
// routes/web.php
Route::get('/health', function () {
    $start = microtime(true);
    $checks = [];

    // Verificação de banco de dados
    try {
        DB::connection()->getPdo();
        $checks['database'] = 'ok';
    } catch (\Throwable $e) {
        $checks['database'] = 'fail: ' . $e->getMessage();
    }

    // Verificação Redis
    try {
        Cache::put('health_check', 'ok', 10);
        $checks['redis'] = Cache::get('health_check') === 'ok' ? 'ok' : 'fail';
    } catch (\Throwable $e) {
        $checks['redis'] = 'fail: ' . $e->getMessage();
    }

    // Verificação S3
    try {
        Storage::disk('s3')->put('health_check.txt', 'ok');
        $checks['storage'] = Storage::disk('s3')->get('health_check.txt') === 'ok' ? 'ok' : 'fail';
    } catch (\Throwable $e) {
        $checks['storage'] = 'fail: ' . $e->getMessage();
    }

    // Verificação de fila
    try {
        $size = Queue::size('default');
        $checks['queue'] = $size < 10000 ? "ok (size: {$size})" : "warn (size: {$size})";
    } catch (\Throwable $e) {
        $checks['queue'] = 'fail: ' . $e->getMessage();
    }

    $latency = round((microtime(true) - $start) * 1000, 2);
    $allOk = collect($checks)->every(fn ($v) => str_starts_with($v, 'ok'));

    return response()->json([
        'status' => $allOk ? 'healthy' : 'unhealthy',
        'checks' => $checks,
        'latency_ms' => $latency,
        'version' => config('app.version'),
        'timestamp' => now()->toIso8601String(),
    ], $allOk ? 200 : 503);
});

(2) Comparação de Ferramentas de Monitoramento

Ferramenta Tipo Plano Gratuito Casos de Uso
Sentry Rastreamento de Erros 5K eventos/mês Captura de Exceções + Rastreamento de Performance
Laravel Telescope Painel de Debug Gratuito Desenvolvimento/Debug (Acesso restrito em produção)
Prometheus + Grafana Monitoramento de Métricas Auto-hospedado Gratuito Métricas de Sistema + Dashboards Personalizados
UptimeRobot Monitoramento de Disponibilidade 50 monitores Alertas de Health Check HTTP
Algolia/Meilisearch Busca de Logs Gratuito Limitado Agregação e Busca de Logs

(1) ▶ Exemplo: Configuração Sentry e Contexto Personalizado do ShopMetrics

PHP
// config/sentry.php
return [
    'dsn' => env('SENTRY_LARAVEL_DSN'),
    'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE', 0.2),
    'send_default_pii' => false,
    'environment' => app()->environment(),
    'release' => config('app.version'),
];

// app/Exceptions/Handler.php
class Handler extends ExceptionHandler
{
    public function report(Throwable $e): void
    {
        if (app()->bound('sentry') && $this->shouldReport($e)) {
            \Sentry\configureScope(function (\Sentry\State\Scope $scope) {
                if ($user = auth()->user()) {
                    $scope->setUser([
                        'id' => $user->id,
                        'email' => $user->email,
                        'tenant_id' => $user->tenant_id,
                    ]);
                }

                $scope->setTag('app_version', config('app.version'));
                $scope->setExtra('request_url', request()->url());
                $scope->setExtra('request_method', request()->method());
            });
        }

        parent::report($e);
    }
}

// app/Providers/AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        if (app()->environment('local')) {
            Telescope::listenStorageNotifications();
        }

        Telescope::filter(function (IncomingEntry $entry) {
            if (app()->environment('local')) return true;
            return $entry->isReportableException() ||
                   $entry->isFailedJob() ||
                   $entry->isScheduledTask() ||
                   $entry->hasMonitoredTag();
        });
    }
}

Saída:

TEXT
// Execução bem-sucedida

6. Plano de Backup de Banco de Dados

(1) Estratégia de Backup

Tipo de Backup Frequência Retenção Local de Armazenamento Tempo de Recuperação
Backup Completo Diariamente às 3:00 30 dias S3 (off-site) 30-60 minutos
Backup Incremental Horário 7 dias S3 (off-site) 60-120 minutos
Binlog Tempo real 7 dias Local + S3 PITR a qualquer momento

(2) Script de Backup Automático

BASH
#!/bin/bash
# backup-mysql.sh — Backup diário do MySQL para S3
set -e

DB_NAME="shopmetrics"
S3_BUCKET="s3://shopmetrics-backups/mysql"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="/tmp/shopmetrics_${DATE}.sql.gz"

echo "→ Iniciando backup: ${DATE}"

# Dump com snapshot consistente (sem lock)
mysqldump \
  --host=mysql \
  --user=${DB_USERNAME} \
  --password=${DB_PASSWORD} \
  --single-transaction \
  --routines \
  --triggers \
  --quick \
  ${DB_NAME} | gzip > ${BACKUP_FILE}

# Upload para S3
aws s3 cp ${BACKUP_FILE} ${S3_BUCKET}/daily/shopmetrics_${DATE}.sql.gz

# Limpar arquivo local
rm -f ${BACKUP_FILE}

# Limpar backups com mais de 30 dias
aws s3 rm ${S3_BUCKET}/daily/ --recursive --exclude "*" --include "shopmetrics_*" \
  --query "Contents[?LastModified<='$(date -d '-30 days' +%Y-%m-%d)'].Key"

echo "✓ Backup concluído: shopmetrics_${DATE}.sql.gz"

# Verificar integridade do backup
LATEST=$(aws s3 ls ${S3_BUCKET}/daily/ | tail -1 | awk '{print $4}')
aws s3 cp ${S3_BUCKET}/daily/${LATEST} /tmp/verify.sql.gz
if gzip -t /tmp/verify.sql.gz; then
    echo "✓ Integridade do backup verificada"
else
    echo "✗ Verificação de integridade do backup FALHOU"
    exit 1
fi
rm -f /tmp/verify.sql.gz

(1) ▶ Exemplo: Agendamento de Backup Automático do ShopMetrics

PHP
// routes/console.php
use Illuminate\Support\Facades\Schedule;

// Backup diário do banco de dados
Schedule::command('shopmetrics:backup-database')
    ->dailyAt('03:00')
    ->onOneServer()
    ->withoutOverlapping()
    ->emailOutputOnFailure('ops@shopmetrics.io');

// Sincronização horária do armazenamento S3 (para arquivos de relatório)
Schedule::command('shopmetrics:sync-storage-backup')
    ->hourly()
    ->onOneServer();

// Teste semanal de integridade do backup
Schedule::command('shopmetrics:verify-backup')
    ->weeklyOn(Schedule::SUNDAY, '04:00')
    ->onOneServer()
    ->emailOutputOnFailure('ops@shopmetrics.io');

// app/Console/Commands/BackupDatabase.php
class BackupDatabase extends Command
{
    protected $signature = 'shopmetrics:backup-database';
    protected $description = 'Backup do banco de dados MySQL para S3';

    public function handle(): int
    {
        $filename = 'shopmetrics_' . now()->format('Ymd_His') . '.sql.gz';
        $tempPath = storage_path('app/backups/' . $filename);

        $this->info('Iniciando backup do banco de dados...');

        $command = sprintf(
            'mysqldump --host=%s --user=%s --password=%s --single-transaction %s | gzip > %s',
            config('database.connections.mysql.host'),
            config('database.connections.mysql.username'),
            config('database.connections.mysql.password'),
            config('database.connections.mysql.database'),
            $tempPath
        );

        $exitCode = Process::run($command)->exitCode();

        if ($exitCode !== 0) {
            $this->error('Backup falhou!');
            return self::FAILURE;
        }

        $s3Path = 'mysql/daily/' . $filename;
        Storage::disk('s3-backup')->put($s3Path, file_get_contents($tempPath));
        unlink($tempPath);

        $this->info("Backup enviado: {$s3Path}");
        return self::SUCCESS;
    }
}

Saída:

TEXT
// Execução bem-sucedida

7. Checklist de Deploy e Plano de Rollback

(1) Checklist de Lançamento

# Item de Verificação Comando/Ação Automação Atende Padrão
1 Código passou nos testes php artisan test CI 0 falhas
2 Auditoria de segurança passou composer audit CI 0 vulnerabilidades
3 Build da imagem Docker docker build CI Build bem-sucedido
4 Puxar imagem mais recente docker compose pull CD Imagem existe
5 Iniciar novo container docker compose up -d CD Container rodando
6 Health check passou curl /health CD HTTP 200
7 Migration do Banco de Dados migrate --force CD 0 erros
8 Aquecimento de Cache config:cache + route:cache CD Comando bem-sucedido
9 Reiniciar Worker da Fila queue:restart CD Worker Reiniciado
10 Limpar containers antigos docker image prune CD Espaço em disco normal
11 Smoke Test de Negócio Teste Manual de Fluxos Principais Manual Funcionalidade Normal
12 Monitoramento confirmado Sem anomalias no Sentry/Telescope Manual 0 erros em 5 min
13 Verificação de Logs Nenhum ERROR Manual Sem novas exceções
14 Validação de Performance Dashboard carrega < 500 ms Manual p95 < 500 ms
15 Notificação à Equipe Notificação Slack/Email Automático Enviado

(2) Plano de Rollback

Cenário Método de Detecção Procedimento de Rollback Tempo de Recuperação
Health Check Falhou Detecção Automatizada CI/CD docker compose pull <tag-anterior> + up < 1 minuto
Migration Falhou Logs CI/CD migrate:rollback + Rollback de Código < 5 minutos
Erro de Lógica de Negócio Feedback do Usuário/Monitoramento Rollback Blue-Green para Slot Anterior < 30 segundos
Corrupção de Banco de Dados Alertas de Monitoramento Restore de Backup S3 + Replay Binlog 30-120 minutos

(1) ▶ Exemplo: Script de Rollback do ShopMetrics

BASH
#!/bin/bash
# rollback.sh — Script de rollback de emergência
set -e

COMPOSE_FILE="docker-compose.prod.yml"
BACKUP_DIR="/opt/shopmetrics/backups"

echo "=== ROLLBACK DE EMERGÊNCIA ==="

# Obter tag da imagem anterior
PREVIOUS_TAG=$(cat ${BACKUP_DIR}/last_successful_deploy_tag.txt)
CURRENT_TAG=$(cat ${BACKUP_DIR}/current_deploy_tag.txt)

echo "Atual: ${CURRENT_TAG}"
echo "Revertendo para: ${PREVIOUS_TAG}"

# Passo 1: Definir tag da imagem para versão anterior
export IMAGE_TAG=${PREVIOUS_TAG}

# Passo 2: Puxar imagem anterior
docker compose -f ${COMPOSE_FILE} pull app queue-high queue-default scheduler

# Passo 3: Iniciar containers da versão anterior
docker compose -f ${COMPOSE_FILE} up -d --no-deps --scale app=2 app
docker compose -f ${COMPOSE_FILE} up -d --no-deps queue-high queue-default scheduler

# Passo 4: Aguardar e verificar
sleep 10
if curl -sf http://localhost:8080/health > /dev/null; then
    echo "✓ Health check passou após rollback"
else
    echo "✗ Health check FALHOU após rollback — INTERVENÇÃO MANUAL NECESSÁRIA"
    exit 1
fi

# Passo 5: Verificar se rollback de migration de BD é necessário
echo "⚠ Verificar se migrations de banco de dados precisam de rollback:"
echo "  Executar: docker compose -f ${COMPOSE_FILE} exec -T app php artisan migrate:status"
echo "  Se necessário: docker compose -f ${COMPOSE_FILE} exec -T app php artisan migrate:rollback --step=N"

# Passo 6: Atualizar tag atual
echo "${PREVIOUS_TAG}" > ${BACKUP_DIR}/current_deploy_tag.txt

# Passo 7: Notificar equipe
echo "⚠ ROLLBACK CONCLUÍDO — Notificar equipe imediatamente"
echo "  Revertido de ${CURRENT_TAG} para ${PREVIOUS_TAG}"

Saída:

TEXT
CONTAINER ID   IMAGE     STATUS    
abc123         latest    Up 2 hours

8. Exemplo Compreensivo: O Processo Completo de Lançamento do ShopMetrics

BASH
#!/bin/bash
# ============================================
# Compreensivo: Script completo de deploy do ShopMetrics
# Abrange: backup → deploy → migrate → verify → plano de rollback
# ============================================

set -euo pipefail

DEPLOY_TAG=${1:-$(git rev-parse --short HEAD)}
COMPOSE_FILE="docker-compose.prod.yml"
BACKUP_DIR="/opt/shopmetrics/backups"
SLACK_WEBHOOK="${SLACK_WEBHOOK_URL}"

log() { echo "[$(date +%H:%M:%S)] $1"; }
notify() { curl -s -X POST "${SLACK_WEBHOOK}" -H 'Content-type: application/json' --data "{\"text\":\"$1\"}" > /dev/null 2>&1 || true; }

# Pré-deploy: Backup
log "→ Passo 1/10: Backup do banco de dados"
php artisan shopmetrics:backup-database
log "✓ Backup concluído"

# Pré-deploy: Salvar estado atual
log "→ Passo 2/10: Salvando estado atual"
cat ${BACKUP_DIR}/current_deploy_tag.txt > ${BACKUP_DIR}/last_successful_deploy_tag.txt 2>/dev/null || true
echo "${DEPLOY_TAG}" > ${BACKUP_DIR}/current_deploy_tag.txt
log "✓ Estado salvo (revertendo para: $(cat ${BACKUP_DIR}/last_successful_deploy_tag.txt 2>/dev/null || echo 'N/A'))"

# Deploy: Puxar e iniciar
log "→ Passo 3/10: Puxando imagem ${DEPLOY_TAG}"
export IMAGE_TAG=${DEPLOY_TAG}
docker compose -f ${COMPOSE_FILE} pull app queue-high queue-default scheduler

log "→ Passo 4/10: Iniciando novos containers"
docker compose -f ${COMPOSE_FILE} up -d --no-deps --remove-orphans app queue-high queue-default scheduler

# Health check com retry
log "→ Passo 5/10: Health check"
for i in $(seq 1 6); do
    if curl -sf http://localhost:8080/health > /dev/null 2>&1; then
        log "✓ Health check passou"
        break
    fi
    if [ $i -eq 6 ]; then
        log "✗ Health check falhou após 30s — REVERTENDO"
        bash /opt/shopmetrics/rollback.sh
        notify "🚨 ShopMetrics deploy FALHOU (health check) — revertido"
        exit 1
    fi
    log "  Aguardando... ($i/6)"
    sleep 5
done

# Migrar
log "→ Passo 6/10: Executando migrations"
docker compose -f ${COMPOSE_FILE} exec -T app php artisan migrate --force
if [ $? -ne 0 ]; then
    log "✗ Migration falhou — REVERTENDO"
    bash /opt/shopmetrics/rollback.sh
    notify "🚨 ShopMetrics deploy FALHOU (migration) — revertido"
    exit 1
fi
log "✓ Migrations concluídas"

# Otimizar
log "→ Passo 7/10: Armazenando em cache"
docker compose -f ${COMPOSE_FILE} exec -T app php artisan optimize:clear
docker compose -f ${COMPOSE_FILE} exec -T app php artisan optimize
docker compose -f ${COMPOSE_FILE} exec -T app php artisan queue:restart
log "✓ Caches reconstruídos"

# Verificação pós-deploy
log "→ Passo 8/10: Smoke tests"
SMOKE_PASS=true
for endpoint in "/health" "/api/v1/plans"; do
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://shopmetrics.io${endpoint}")
    if [ "$STATUS" -ge 400 ]; then
        log "✗ Smoke test falhou: ${endpoint} → HTTP ${STATUS}"
        SMOKE_PASS=false
    fi
done
if [ "$SMOKE_PASS" = false ]; then
    log "✗ Smoke tests falharam — REVERTENDO"
    bash /opt/shopmetrics/rollback.sh
    notify "🚨 ShopMetrics deploy FALHOU (smoke test) — revertido"
    exit 1
fi
log "✓ Smoke tests passaram"

# Limpeza
log "→ Passo 9/10: Limpeza"
docker image prune -f > /dev/null
log "✓ Imagens antigas limpas"

# Sucesso
log "→ Passo 10/10: Verificação final"
sleep 5
ERRORS=$(docker compose -f ${COMPOSE_FILE} logs --since 2m app 2>&1 | grep -c "ERROR" || true)
if [ "${ERRORS}" -gt 5 ]; then
    log "⚠ Alta taxa de erros detectada: ${ERRORS} erros em 2 minutos"
    notify "⚠️ ShopMetrics deploy concluído mas com alta taxa de erros: ${ERRORS} erros"
else
    log "✓ Deploy bem-sucedido: ${DEPLOY_TAG}"
    notify "✅ ShopMetrics deploy bem-sucedido: ${DEPLOY_TAG}"
fi

log "=== DEPLOY CONCLUÍDO ==="

❓ Perguntas Frequentes

P Devemos usar Docker Compose ou Kubernetes em ambiente de produção?
R Docker Compose é suficiente para até 5 servidores, pois simplifica operações. Migre para Kubernetes quando tiver mais de 5 servidores ou precisar de auto-scaling. ShopMetrics começou com Docker Compose e considerará Kubernetes quando o número de usuários ultrapassar 10.000.
P O que devo fazer se uma migration falhar no ambiente de produção?
R No CI/CD, uma migration falhada automaticamente bloqueia o deploy e não troca o tráfego. Basta reverter para a versão de código anterior (o schema do banco de dados permanece compatível com a versão antiga, pois as migrations seguem o princípio de "adicionar primeiro, depois excluir").
P O plano gratuito do Sentry é suficiente?
R 5.000 eventos por mês é suficiente para os estágios iniciais. Ao exceder esse limite, você pode mudar para um plano de pagamento por uso ou auto-hospedar o Sentry (open source e gratuito, mas precisará manter seu próprio servidor). Recomendamos manter o Sentry sempre habilitado em ambientes de produção.
P Quanto tempo leva para fazer backup e restaurar um banco de dados?
R Uma restauração completa de um banco de dados de 10GB leva aproximadamente 30 minutos (incluindo download do S3 e importação). Habilitar binlog permite recuperação point-in-time (PITR), com tempo de recuperação de aproximadamente 60-120 minutos. Execute um exercício de recuperação pelo menos uma vez por mês.
P Como saber quando escalar?
R Monitore as seguintes métricas: CPU > 70% por 5 minutos consecutivos, backlog de fila > 1.000 e latência p95 > 500 ms. Configure limiares de alerta; quando acionados, primeiro investigue se otimização é possível. Só escale após confirmar que é um problema de capacidade.
P O que acontece com o banco de dados após um rollback?
R Siga o princípio de migration "adicionar primeiro, depois excluir" — a nova versão apenas adiciona colunas sem modificar as existentes, então a estrutura do banco de dados permanece compatível após reverter o código. Operações de exclusão devem ser adiadas por pelo menos uma versão. Assim, um rollback afeta apenas o código, não o banco de dados.

📖 Resumo


📝 Exercícios

  1. Exercício Básico (⭐): Escreva uma configuração Docker Compose de produção para o ShopMetrics que inclua seis serviços: app, queue-worker, scheduler, mysql, redis e nginx. Todos os serviços devem incluir health checks e políticas de restart.

  2. Exercício Avançado (⭐⭐): Escreva um workflow GitHub Actions completo: teste → auditoria de segurança → build Docker → deploy → smoke test → notificação Slack. Inclua lógica de rollback automático em caso de falha no health check.

  3. Desafio (⭐⭐⭐): Design um sistema abrangente de operações e manutenção para o ShopMetrics — (1) Endpoints de health check cobrindo DB/Redis/S3/Queue/S3-backup (2) Scripts automatizados de backup + recuperação do banco de dados + exercícios mensais de recuperação (3) Sentry rastreamento de erros + contexto personalizado (4) Script automatizado de checklist de deploy em 15 passos (5) Scripts de rollback (código + banco de dados). Escreva um manual de operações em formato Markdown.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%