Files
amare/openspec/changes/complete-foundation-parity/design.md
Manoel Freitas 7f4ea01f4b feat: prepare Dokploy staging and production deploy pipeline (#6)
Publish immutable FrankenPHP images to GHCR, auto-deploy staging after CI,
and promote the same digest to production with smoke, backup, and rollback docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 23:40:13 -03:00

7.2 KiB
Raw Permalink Blame History

Context

Fases 01 e provedores de produção (Resend + R2) estão em main com CI verde. OpenSpec ativo foi limpo: specs promovidas e changes arquivadas. Gaps remanescentes da Fase 0:

  • Critério de saída §18 exige hello-world em staging; só existe workflow CI.
  • docker-compose.yml sobe apenas PostgreSQL; app local usa artisan serve.
  • README declara PHP 8.5+; Docker/CI usam 8.4; Composer aceita ^8.3.
  • composer quality não roda npm audit; CI unit/feature usam coverage: none (SPEC §13.7 pede 80% Domain/Application).
  • User bloqueia inativos em canAccessPanel, mas não implementa MustVerifyEmail; reset de senha Filament não está validado por teste.
  • Destino de staging escolhido: VPS própria com Dokploy (não Railway).

Goals / Non-Goals

Goals:

  • Staging em Dokploy com a mesma imagem FrankenPHP por SHA para web, queue e scheduler.
  • Deploy automático em main após CI: build → GHCR → Dokploy API → migrate → health → smoke.
  • Produção via promoção manual da mesma digest SHA (alias :production), sem rebuild.
  • Backup PostgreSQL diário (retenção ≥14 dias), restore documentado e rollback por SHA.
  • Compose local com app FrankenPHP + Postgres (pendente fora da fatia deploy).
  • PHP 8.4 canônico em docs/Docker/CI (pendente).
  • Gates com npm audit e cobertura Domain/Application ≥ 80% (pendente).
  • E-mail verificado + reset de senha seguros no painel (ADM-01 / SPEC §12.1) (pendente).
  • Strict types em PHP próprio faltante (pendente).

Non-Goals:

  • Deploy automático em produção; provisionamento de VPS para clientes.
  • Fase 2 (briefing/CRM/E2E-01/02).
  • Redis, worker mode, CDN automation, signed private media.

Decisions

D1 — Dokploy Compose na VPS, imagem imutável no GHCR

GitHub Actions (após CI verde em main) constrói uma imagem ghcr.io/<owner>/<repo>:<git-sha> (+ alias :staging), faz push privado e chama POST /api/compose.deploy no Dokploy com x-api-key.

Compose compartilhado (docker-compose.deploy.yml) referencia ${APP_IMAGE} / IMAGE_TAG para web, queue, scheduler e job migrate one-shot (php artisan migrate --force). Dokploy mantém duas stacks Compose (staging e production) com IMAGE_TAG distinto. PostgreSQL é serviço Dokploy separado por ambiente (não na imagem da app).

Produção: workflow_dispatch com SHA + confirmação PRODUCTION move alias :production para o mesmo digest já publicado e dispara compose.deploy na stack de produção. Repo privado no GitHub Free não tem required reviewers de Environment; aprovação humana = disparo manual explícito (GitHub Pro opcional depois).

Alternativas rejeitadas: Railway (Pro para GHCR privado + desvio de plataforma); rebuild por serviço no Dokploy (quebra “mesma imagem” SPEC §14.3/§15.2); tag só :latest (rollback frágil); deploy automático direto em produção.

D2 — Processos e health

Serviço Comando
web entrypoint padrão FrankenPHP (CMD da imagem)
queue php artisan queue:work --sleep=2 --tries=3
scheduler php artisan schedule:work
migrate one-shot antes/ao lado do deploy

Healthcheck Docker/Dokploy e smoke pós-deploy usam GET /up (sem auth, sem secrets). Smoke mínimo: /up, home pública /, /admin/login respondem 200.

D3 — Secrets e providers

GitHub Actions guarda só orquestração: DOKPLOY_URL, API key, compose IDs, URLs públicas de smoke. Env Laravel (APP_KEY, DB, Resend, R2) vive somente no Dokploy, isolado por ambiente. Nunca embeds em layer. Local continua MAIL_MAILER=log e disco public.

D3b — Backup e restore

PostgreSQL staging/produção: backup diário via Dokploy → destino S3-compatible, retenção mínima 14 dias (SPEC §16.3). Restore documentado e testado em staging antes da primeira promoção a produção.

D4 — Compose local com app

Adicionar serviço app (build do Dockerfile) dependendo de postgres healthy, porta 8000, volumes só para storage local se útil. Documentar docker compose up como caminho preferido; artisan serve permanece opcional para iteração rápida sem rebuild.

D5 — PHP 8.4 canônico

Alinhar README, CI (setup-php 8.4) e Dockerfile ARG PHP_VERSION=8.4. Manter composer.json ^8.3 (compatibilidade de instalação), mas documentação e runtime canônicos = 8.4.

D6 — Quality gates

  • composer quality / job static: adicionar npm audit --omit=dev (ou política documentada equivalente) após npm ci onde assets forem necessários; falha bloqueia merge.
  • Jobs unit (e, se aplicável, coverage dedicada): habilitar cobertura e falhar se Domain+Application < 80%. Escopo limitado a app/Domain e app/Application (SPEC §13.7). Views/migrations/framework fora.

D7 — Auth parity

  • User implementa MustVerifyEmail; canAccessPanel exige ativo e e-mail verificado.
  • Seed local marca admin/assistant como verificados.
  • Habilitar fluxo de reset Filament/Laravel; feature tests: unverified denied, verified ok, reset request não revela existência de e-mail.
  • Correção pontual de declare(strict_types=1); em providers/arquivos próprios faltantes.

D8 — Rollback

Rollback = mover alias do ambiente (:staging ou :production) para tag SHA anterior no GHCR e compose.deploy. Falha de healthcheck/smoke impede promoção. Sem rebuild.

D9 — Trusted proxies atrás do Traefik

bootstrap/app.php confia em proxies (trustProxies(at: '*')) para honrar X-Forwarded-* do Traefik/Dokploy. Staging/produção usam SESSION_SECURE_COOKIE=true com HTTPS.

Risks / Trade-offs

  • [GHCR privado + pull na VPS] → configurar registry no Dokploy com PAT read:packages; documentar checklist.
  • [Migrate one-shot falha] → web/queue/scheduler dependem de migrate exit 0; manter migrations backward-compatible.
  • [GitHub Free sem required reviewers] → promoção humana via workflow_dispatch + input PRODUCTION; Pro opcional.
  • [Cobertura 80% com Domain quase vazio] → medir só namespaces existentes; baseline sobe conforme Fase 2 adiciona Domain (pendente).
  • [npm audit ruido]--omit=dev + allowlist documentada se necessário (pendente).
  • [E-mail verification em staging] → seed/users de staging com email_verified_at; Resend para reset real quando configurado (pendente).
  • [Compose local rebuild lento] → documentar serve opcional; CI permanece fonte FrankenPHP (pendente).

Migration Plan

  1. Fatia deploy: Compose deploy, workflows, smoke, trusted proxies, docs Dokploy/backup/rollback; CI verde.
  2. Criar projeto Dokploy + Postgres (staging + produção) + Compose apps; registrar GHCR.
  3. Primeiro push de imagem SHA → staging; smoke /up + home + login; testar rollback e backup/restore.
  4. Promoção manual para produção após domínio/TLS/APP_URL confirmados.
  5. Fatias restantes da change (auth/coverage/npm/Compose local/PHP docs) em PRs seguintes.
  6. Atualizar SPEC §18 Fase 0 apenas com itens comprovados; evidência no PR.

Open Questions

  • Domínio público exato do staging/produção (DNS) — preencher na implementação com valor do operador.
  • Se Dokploy Compose API exigir compose.update env para IMAGE_TAG a cada deploy: preferir aliases :staging/:production estáveis no Compose Dokploy para evitar rewrite de env.