Esta página cobre o stack-specific pra projetos Laravel. Pré-requisito: leia Standards de projeto primeiro — esta é complementar, não substituta.

Por que FrankenPHP

Recomendação default pra Laravel novo no server-stack: FrankenPHP como base do Dockerfile. Comparação rápida:
OpçãoProcessos no containerRobustezComplexidade do Dockerfile
php artisan serve1❌ single-thread, dev onlymínima
nginx + php-fpm + supervisord3✅ batalha-testadoalta (3 configs + entrypoint)
FrankenPHP1✅ stable, suporta workers Octanemínima
FrankenPHP é Caddy + PHP num único binário. Numa só linha de Dockerfile você tem servidor HTTP de produção + interpretador PHP. Sem nginx, sem php-fpm, sem supervisord. Trade-off: ecossistema menor (menos exemplos no Stack Overflow), mas o que existe é direto. Pra projetos novos no servidor pessoal, vale. Quando NÃO usar FrankenPHP: se já tem Dockerfile maduro com nginx+php-fpm que funciona e o projeto é grande/complexo, não vale converter só pra padronizar — pesa mais o risco de quebra do que o ganho de simplicidade.

Dockerfile recomendado

FROM dunglas/frankenphp:latest

# Extensões PHP comuns pra Laravel + Postgres
RUN install-php-extensions pdo_pgsql intl zip bcmath

# Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# Node (pra Vite build) — pode skip se projeto não usa front compilado
RUN apt-get update && apt-get install -y curl gnupg \
    && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Camadas cacheáveis
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist

COPY package.json package-lock.json ./
RUN npm ci

COPY . .

RUN npm run build
RUN composer dump-autoload --optimize --no-dev

# Comandos stateful em build time (sobrevivem a recreates)
RUN mkdir -p storage/app/public && php artisan storage:link
RUN chmod -R 775 storage bootstrap/cache

# FrankenPHP escuta HTTP em :80 (NPM termina TLS lá fora)
ENV SERVER_NAME=":80"
EXPOSE 80
SERVER_NAME=":80" faz o Caddy interno escutar HTTP-only na porta 80 — sem o auto-HTTPS do Caddy interferir, já que quem termina TLS é o NPM externamente.

TrustProxies obrigatório (Laravel 11+)

Se o projeto está em Laravel 11 ou 12, é obrigatório registrar trustProxies em bootstrap/app.php:
->withMiddleware(function (Middleware $middleware): void {
    $middleware->trustProxies(at: '*');
})
Por quê: o NPM termina TLS e encaminha pra dentro como HTTP. Sem TrustProxies configurado, Laravel olha pro request()->isSecure() (que é false) e gera URLs com http:// — o browser então bloqueia CSS/JS por mixed content quando a página foi servida via HTTPS. Em Laravel 10 ou anterior: o middleware vinha pré-configurado em app/Http/Middleware/TrustProxies.php no skeleton, então geralmente “simplesmente funciona” — mas vale conferir que $proxies = '*' está setado. Por que '*' é seguro aqui: o container do projeto não publica portas no host (docker-compose.yml sem ports:). Os únicos containers que conseguem alcançar a porta 80 do projeto são os que estão na server_net — na prática, só o NPM. Defesa por isolamento de rede.

Variáveis de ambiente típicas

.env.example mínimo pra Laravel no server-stack:
APP_NAME="<Nome do Projeto>"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://<deploy>.<dominio>

APP_LOCALE=pt_BR
APP_FALLBACK_LOCALE=pt_BR

LOG_CHANNEL=stack
LOG_LEVEL=warning

DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=<deploy>
DB_USERNAME=<deploy>
DB_PASSWORD=

SESSION_DRIVER=database
QUEUE_CONNECTION=database
CACHE_STORE=database

REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PASSWORD=
REDIS_PORT=6379

MAIL_MAILER=log
Notas:
  • APP_DEBUG=false mesmo em “dev server” porque erros expostos vazam paths e config. Pra debug pontual, ligue temporariamente.
  • SESSION/QUEUE/CACHE em database é simples e suficiente. Trocar pra redis é optimization, não requirement.
  • REDIS_HOST=redis aponta pro Redis compartilhado. Senha vem de infra/redis/.env.

Migrations portáveis

Use Schema::Blueprint — funciona em MySQL, Postgres e SQLite igual. Evite DB::statement com SQL bruto. Padrões portáveis:
// Enum (Postgres faz CHECK constraint, MySQL faz ENUM nativo)
$table->enum('role', ['admin', 'rr', 'capgeral']);

// Inteiros pequenos
$table->unsignedTinyInteger('score')->nullable();
// (Mapeia pra smallint em Postgres — slight diferença de range, mas funcional)

// FKs com regras
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('regional_id')->nullable()->constrained()->nullOnDelete();

// Unique composto
$table->unique(['cap_id', 'referencia_mes']);
Anti-padrão:
// Não funciona em Postgres
DB::statement("ALTER TABLE users MODIFY COLUMN role ENUM('admin', 'rr') NOT NULL");
Se inevitável (caso de ALTER ENUM, que Blueprint não cobre bem), ramifique por driver:
$driver = DB::connection()->getDriverName();

if ($driver === 'mysql') {
    DB::statement("ALTER TABLE users MODIFY COLUMN role ENUM('admin', 'rr') NOT NULL");
} elseif ($driver === 'pgsql') {
    DB::statement('ALTER TABLE users DROP CONSTRAINT IF EXISTS users_role_check');
    DB::statement("ALTER TABLE users ADD CONSTRAINT users_role_check CHECK (role IN ('admin', 'rr'))");
}

Squash de migrations (quando vale)

Após muitas alterações de schema com refactors no caminho (renames, drops, pivots criados-e-removidos), a pasta database/migrations/ vira história arqueológica. Sintoma: ler todas as migrations não te diz o estado atual do schema. Quando squashar:
  • 15+ migrations com vários refactors
  • Vai migrar pra um banco novo (oportunidade natural)
  • Schema final é estável e tem chance baixa de mudar nas próximas semanas
Quando NÃO squashar:
  • Schema está em fluxo ativo (squash hoje, refactor amanhã = trabalho perdido)
  • Várias deploys em uso (cada um precisa do “manual migrations table populate” — overhead alto)
Procedimento (esboço):
  1. Ler todas as migrations e mapear o estado final do schema
  2. Criar N novas migrations (4-6 normalmente) representando esse estado, organizadas por domínio
  3. Apagar as antigas
  4. migrate:fresh em DB limpo pra validar
  5. Pra DBs existentes que tinham as antigas aplicadas, escrever um database/squash-transition.sql que substitui as entries da tabela migrations pelas novas (sem re-rodar nada — schema já está aplicado)
Detalhes do procedimento real estão nos commits do projeto piloto SistemaShalom.

Queue worker (quando aplicável)

Se o projeto usa Laravel Queues (php artisan queue:listen), separe em service distinto no docker-compose.yml:
services:
  app:
    # ... (config principal)
  
  queue:
    image: <deploy>-app:latest      # mesma imagem
    container_name: <deploy>-queue
    restart: unless-stopped
    command: ["php", "artisan", "queue:work", "--tries=3", "--max-time=3600"]
    volumes:
      - ./.env:/app/.env
      - ./storage:/app/storage
    depends_on:
      - app
    networks:
      - server_net
Por quê separar: restart independente, logs separados, escalabilidade futura, e o app principal não precisa saber de jobs (separation of concerns).

Storage e uploads

Bind mount ./storage:/app/storage pra persistir uploads, logs e cache de filesystem entre rebuilds. php artisan storage:link deve estar no Dockerfile (build time), não em entrypoint. Senão perde no recreate. O Dockerfile recomendado acima já inclui esse RUN.

Comandos comuns

Tudo via docker compose exec:
# Migrations
docker compose exec app php artisan migrate --force
docker compose exec app php artisan migrate:fresh --seed --force   # reset completo

# Seed
docker compose exec app php artisan db:seed --force

# Tinker (REPL)
docker compose exec app php artisan tinker

# Tests (Pest ou PHPUnit)
docker compose exec app php artisan test

# Cache (após mudar .env, rotas, views)
docker compose exec app php artisan config:clear
docker compose exec app php artisan route:clear
docker compose exec app php artisan view:clear

# Logs do FrankenPHP/Caddy
docker compose logs -f app

# Após mudança em código PHP/Blade, é necessário rebuild
# (não tem volume de código fonte por design)
docker compose up -d --build

Páginas relacionadas