Artigo 48 — Deploy e Ambiente de Produção
Módulo 10 · Semana 48 · Nível: Avançado
Introdução
Saber desenvolver é necessário, mas não suficiente. Um sistema que funciona perfeitamente no seu computador e nunca chega ao servidor de produção não tem valor. Deploy é o ato de levar o código do repositório para o servidor — e fazê-lo de forma segura, repetível e rastreável.
Configuração do servidor — NGINX + PHP-FPM
PHP-FPM (FastCGI Process Manager) é como o PHP roda em produção real: gerencia um pool de processos PHP separado do servidor web. NGINX recebe as requisições e as passa para o PHP-FPM processar:
# /etc/nginx/sites-available/minha-loja
server {
listen 80;
server_name minha-loja.com.br www.minha-loja.com.br;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name minha-loja.com.br www.minha-loja.com.br;
root /var/www/minha-loja/public;
index index.php;
# SSL — gerado com Let's Encrypt / Certbot
ssl_certificate /etc/letsencrypt/live/minha-loja.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/minha-loja.com.br/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
# Laravel: rota todas as requisições para index.php
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 300;
}
# Bloqueia acesso direto a arquivos sensíveis
location ~ /\.(env|git|htaccess) {
deny all;
}
# Cache de assets estáticos
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Logs
access_log /var/log/nginx/minha-loja.access.log;
error_log /var/log/nginx/minha-loja.error.log;
}
PHP-FPM — configuração do pool
; /etc/php/8.3/fpm/pool.d/minha-loja.conf
[minha-loja]
user = www-data
group = www-data
listen = /var/run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
; Gerenciamento de processos
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 8
pm.max_requests = 500 ; reinicia o processo após 500 requisições (evita memory leaks)
; Timeout
request_terminate_timeout = 60s
; Logs de slowlog
slowlog = /var/log/php-fpm/minha-loja.slow.log
request_slowlog_timeout = 5s
Deploy com zero downtime — script completo
#!/bin/bash
# deploy.sh — executa no servidor de produção
set -euo pipefail # para em qualquer erro
PROJETO_DIR="/var/www/minha-loja"
BRANCH="${1:-main}"
echo "🚀 Iniciando deploy — branch: $BRANCH"
cd $PROJETO_DIR
# 1. Ativa modo de manutenção (Laravel)
php artisan down --secret="token-secreto-de-bypass"
echo "✅ Modo de manutenção ativado"
# 2. Atualiza o código
git fetch origin
git reset --hard origin/$BRANCH
echo "✅ Código atualizado"
# 3. Instala dependências (sem dev, otimizado)
composer install --no-dev --optimize-autoloader --no-interaction
echo "✅ Dependências instaladas"
# 4. Roda migrations
php artisan migrate --force
echo "✅ Migrations executadas"
# 5. Otimizações do Laravel
php artisan config:cache # cache de configurações
php artisan route:cache # cache de rotas
php artisan view:cache # pre-compila templates Blade
php artisan event:cache # cache de event listeners
echo "✅ Cache otimizado"
# 6. Reinicia workers de fila (se houver)
php artisan queue:restart
echo "✅ Workers de fila reiniciados"
# 7. Recarrega PHP-FPM (graceful — sem derrubar requisições em andamento)
sudo systemctl reload php8.3-fpm
echo "✅ PHP-FPM recarregado"
# 8. Desativa modo de manutenção
php artisan up
echo "🎉 Deploy concluído com sucesso!"
GitHub Actions — CI/CD completo
# .github/workflows/deploy.yml
name: Testes e Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
testes:
name: Testes
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_DATABASE: teste
MYSQL_ROOT_PASSWORD: senha
ports: ['3306:3306']
options: --health-cmd="mysqladmin ping" --health-interval=10s
steps:
- uses: actions/checkout@v4
- name: Instalar PHP 8.3
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, pdo, pdo_mysql, gd, redis
coverage: xdebug
- name: Cache do Composer
uses: actions/cache@v3
with:
path: vendor
key: composer-${{ hashFiles('composer.lock') }}
- name: Instalar dependências
run: composer install --no-interaction
- name: Auditoria de segurança
run: composer audit
- name: Preparar ambiente de teste
run: |
cp .env.testing .env
php artisan key:generate
php artisan migrate --force
- name: Análise estática
run: vendor/bin/phpstan analyse --no-progress
- name: Estilo de código
run: vendor/bin/php-cs-fixer fix --dry-run --diff
- name: Testes com cobertura
run: vendor/bin/pest --coverage --min=75
deploy:
name: Deploy em Produção
needs: testes
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVIDOR_HOST }}
username: ${{ secrets.SERVIDOR_USER }}
key: ${{ secrets.SERVIDOR_SSH_KEY }}
script: |
cd /var/www/minha-loja
bash deploy.sh main
Docker para desenvolvimento padronizado
# Dockerfile
FROM php:8.3-fpm-alpine
# Extensões necessárias
RUN docker-php-ext-install pdo pdo_mysql opcache
RUN apk add --no-cache libpng-dev && docker-php-ext-install gd
# OPcache — melhora performance significativamente em produção
COPY opcache.ini /usr/local/etc/php/conf.d/
WORKDIR /var/www/html
COPY . .
RUN composer install --no-dev --optimize-autoloader
RUN chown -R www-data:www-data storage bootstrap/cache
# docker-compose.yml (desenvolvimento)
services:
app:
build: .
volumes:
- .:/var/www/html # monta código local
depends_on: [db, redis]
nginx:
image: nginx:alpine
ports: ['8000:80']
volumes:
- .:/var/www/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
db:
image: mysql:8.0
environment:
MYSQL_DATABASE: loja
MYSQL_ROOT_PASSWORD: senha
volumes:
- db_data:/var/lib/mysql
redis:
image: redis:7-alpine
volumes:
db_data:
Monitoramento e logs
<?php
// config/logging.php — Laravel
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'slack'],
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'level' => 'critical', // Apenas erros críticos vão para o Slack
],
],
// Uso — o mesmo que vimos na PSR-3:
Log::info('Pedido criado', ['id' => $pedido->id, 'total' => $pedido->total]);
Log::error('Falha no pagamento', ['pedido_id' => $id, 'erro' => $e->getMessage()]);
Log::critical('Banco de dados inacessível', ['exception' => $e]);
Resumo
| Etapa | Ferramenta |
|---|---|
| Servidor web | NGINX + PHP-FPM |
| HTTPS | Let's Encrypt + Certbot |
| Deploy | Script bash + GitHub Actions |
| Containerização | Docker + Docker Compose |
| CI/CD | GitHub Actions (testes → deploy) |
| Logs | Monolog + Laravel Log (diário + Slack crítico) |
Exercício da semana
-
Configure NGINX + PHP-FPM localmente com Docker. Crie um
docker-compose.ymlcom app, nginx, mysql e redis. Garanta que o Laravel funciona completamente nesse ambiente. -
Escreva o script
deploy.shcompleto. Teste localmente simulando as etapas. Adicione tratamento de erro: sephp artisan migratefalhar, o script deve reverter ogit resete reativar a aplicação. -
Configure o GitHub Actions com as etapas: composer audit, phpstan, php-cs-fixer, pest. Faça um PR intencional com erro e confirme que o CI falha.
-
Desafio: configure um pipeline de deploy com zero downtime real usando o padrão de diretórios
releases/: cada deploy cria um novo diretório com timestamp, um symlinkcurrentaponta para o release ativo, e os últimos 5 releases são mantidos para rollback instantâneo.
Referências
- NGINX Docs: https://nginx.org/en/docs/
- PHP-FPM Configuration: https://www.php.net/manual/pt_BR/install.fpm.configuration.php
- GitHub Actions: https://docs.github.com/en/actions
- Docker for PHP: https://hub.docker.com/_/php
- Let's Encrypt: https://letsencrypt.org/