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
- Orquestração Docker Compose de Produção: app/worker/scheduler/nginx/redis/mysql
- GitHub Actions CI/CD: Teste → Build → Deploy Totalmente Automatizados
- Health Checks e Monitoramento: Sentry Rastreamento de Erros / Laravel Telescope
- Estratégias de Migration de Banco de Dados e Planos de Backup
- Checklist de Deploy e Plano de Rollback
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.
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
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
# 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
# .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:
# Comando executado com sucesso
4. Pipeline CI/CD
(1) Workflow GitHub Actions Completo
# .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
// 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:
// Execução bem-sucedida
5. Health Checks e Monitoramento
(1) Endpoints de Health Check
// 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
// 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:
// 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
#!/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
// 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:
// 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
#!/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:
CONTAINER ID IMAGE STATUS
abc123 latest Up 2 hours
8. Exemplo Compreensivo: O Processo Completo de Lançamento do ShopMetrics
#!/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
📖 Resumo
- Orquestração Docker Compose de produção de 6 serviços: app (x2)/queue (x5)/scheduler/mysql/redis/nginx
- GitHub Actions CI/CD: cinco estágios — teste → segurança → build → deploy → verificar
- Endpoints de health check para DB/Redis/S3/Queue; verificação automatizada via CI/CD
- Sentry Rastreamento de Erros + Telescope Painel de Debug: Uma Dupla Rede de Segurança
- Backups diários do banco de dados para S3 + backups binlog em tempo real, retidos por 30 dias
- Checklist de Deploy em 15 Passos + 4 Cenários de Rollback: Deploy Não É Mais Questão de Sorte
📝 Exercícios
-
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.
-
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.
-
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.



