0028 — Ambiente de produção (app.garantora.com.br na box srv01)¶
| Status | CONCLUÍDA |
| Branch | spec/0028-ambiente-de-producao |
| Criada em | 2026-08-08 |
| Apps afetados | infra (nova stack infra/prd) · api/web/e2e sem impacto de código |
Status:
RASCUNHO→APROVADA→EM IMPLEMENTAÇÃO→CONCLUÍDAEsta é uma spec de infraestrutura, não de feature. Não cria rota, modelo nem tela — os apps
api/web/e2eentram apenas como imagens buildadas, sem alteração de código. O template feature (api → web → e2e) é preenchido com "sem impacto" onde não se aplica; o trabalho real vive eminfra/prd/.
1. Problema¶
Hoje há três infras: infra/local (dependências em container, apps no host do Mac), infra/dev-remote (tudo em container, ambiente compartilhado de desenvolvimento) e infra/prd — que é um rascunho de dev disfarçado de produção: build: . com volume-mount do código, uvicorn --reload, banco exposto em 0.0.0.0:5432, senha garantora hardcoded, sem TLS, sem frontend, sem Caddy, sem backup. Não há como subir o Garantora num servidor de produção de forma segura e repetível.
Quando o primeiro cliente entrar, é preciso uma stack de produção endurecida — não o dev-remote apontado para outro domínio, e muito menos o esboço atual do infra/prd.
2. Objetivo¶
Uma stack infra/prd que cria e sobe pela primeira vez o Garantora completo (db · api · web · caddy) na box srv01 servindo o domínio público app.garantora.com.br com TLS público automático, configuração de produção (sem debug, sem Mailpit, sem rotas de teste expostas), backup automático do banco e as práticas de robustez que faltam ao dev-remote (healthchecks, política de restart, limites de recurso, rotação de logs).
Esta spec é um provisionamento único — define o ambiente e o coloca no ar uma vez. Não é o fluxo de deploy do dia a dia: subir versões novas do código e a configuração de acesso (SSH, gestão de segredos, quem/como faz deploy) são um comando/spec separado, a ser construído depois que a 0028 fechar (ver §3). O que a 0028 entrega do lado operacional é só o bring-up inicial:
up → migrate → seed.
3. Fora de escopo¶
- Alta disponibilidade / múltiplas réplicas. Um nó só. HA (Postgres replicado, múltiplas instâncias da API atrás de LB) fica para outra spec.
- Fluxo de deploy do dia a dia — comando futuro. Subir versões novas do código de forma repetível (pull + rebuild +
migratecomo rotina), a configuração de acesso (chaves SSH, gestão/rotação de segredos, quem tem permissão de deploy) e qualquer automação (CI/CD, GitHub Actions) não entram aqui. São um comando/spec separado, construído após o fechamento da 0028. A 0028 entrega apenas o bring-up inicial (up → migrate → seed), que é parte de criar o ambiente. - Alteração de código nos apps.
apps/api,apps/web,apps/e2enão mudam. Se a implementação descobrir que falta um endpoint de saúde para o healthcheck, isso vira decisão em aberto — não se muda a API dentro desta spec sem registrar. - Escolha e configuração do provedor de e-mail. A stack consome variáveis SMTP; qual provedor e as credenciais são decisão em aberto (ver §9).
- Backup off-box / disaster recovery. O backup desta spec é dump agendado em volume na própria box. Cópia para storage externo (S3, outra máquina) fica para depois — mas o README deve dizer isso em voz alta, para não dar falsa sensação de segurança.
- Migração do dev-remote co-hospedado.
srv01é dedicado ao Garantora (Caddy standalone segura 80/443). O modo behind-edge do dev-remote não se aplica aqui. - Remoção do
infra/prdatual como "quebra". O esboço atual é substituído integralmente; não há dado nem ambiente real dependendo dele.
4. Regras de negócio (operacionais)¶
Numeradas para citação em revisão e nos critérios de aceite.
- RN1. Tudo em container, uma origem por path.
db · api · web · caddysobem viadocker compose. O Caddy roteia na mesma origem, sem CORS, sem remover prefixo:/api → api:8000(a API já registra sob/api/v1, Swagger em/api/docs) e todo o resto → web:80. Diferente do dev-remote (app sob/app), a produção serve o app na raiz: o web builda comVITE_BASE_PATH=/ehttps://app.garantora.com.br/é a própria aplicação (sem redirect/app). - RN2. TLS público automático, servidor dedicado. O Caddy segura
0.0.0.0:80e0.0.0.0:443e emite/renova o certificado Let's Encrypt (HTTP-01) para o domínio públicoapp.garantora.com.br(EDGE_DOMAIN). Pré-condição: o A record deapp.garantora.com.brresolve para o IP público da box (179.198.118.166) antes do primeiro boot (senão o desafio falha). A box em si ésrv01/179.198.118.166. - RN3. Configuração é de produção, não de dev.
APP_ENV=production,APP_DEBUG=false(reduz stack traces vazados). Nada de--reload, nada de volume-mount de código: as imagens embutem o código (igual ao dev-remote). - RN4. Sem Mailpit e sem rotas de teste. Produção não sobe Mailpit e o Caddy não expõe
/mailnem/journeys. O envio de e-mail usa um SMTP real via variáveis de ambiente (SMTP_HOST/PORT/USER/PASSWORD/FROM). - RN5. Banco nunca exposto. O serviço
dbnão temports:— vive só na rede interna do Compose. Acesso administrativo é pordocker compose execna box. - RN6. Segredos fora do git e fora das imagens. Valores vêm do
.env(ignorado pelo git);.dockerignore/Dockerfile descartam qualquer.envque vaze para o contexto de build. Todos os segredos do.env.examplesão placeholders óbvios a trocar (POSTGRES_PASSWORD,APP_SECRET_KEY,JWT_SECRET_KEY,SEED_ADMIN_PASSWORD). - RN7. Migrations são passo explícito do deploy — nunca no boot. O boot da API sobe apenas o uvicorn.
alembic upgrade headroda viamake migrate, decidido pelo operador. O seed do admin (init_db) roda uma vez, viamake seed, no provisionamento inicial — não a cada restart. - RN8. Backup automático do Postgres. Um serviço dedicado roda
pg_dumpem intervalo configurável (padrão: diário), grava em volume nomeado com retenção configurável (padrão: manter 7 dias, apagando os mais antigos). Há caminho documentado e testado de restore a partir de um dump. - RN9. Robustez. Todos os serviços têm
restart: unless-stopped.dbtemhealthcheck(pg_isready) e aapitemhealthcheckbatendo no endpoint/healthjá existente na app (app/main.py— retorna{"status":"ok","db":...}validando o banco comSELECT 1), sem alterar a API.apiecaddydeclaramdepends_oncomcondition: service_healthyonde couber. Cada serviço tem rotação de logs (json-file,max-size,max-file) e limites de recurso (memória/CPU), para um container não derrubar a box. - RN10. Bring-up inicial (não deploy de rotina). A 0028 entrega o provisionamento de primeira vez:
make up(build +up -d, boot só sobe uvicorn), depoismake migrate(schema) emake seed(admin, uma vez). O fluxo de deploy repetível de código novo é comando futuro (§3), fora desta spec.reset(que apaga volumes) não existe em produção — ou, se existir, exige confirmação explícita e é destacado como destrutivo no README.
5. Impacto por app¶
infra/prd (o trabalho desta spec)¶
Substitui o esboço atual. Arquivos, espelhando a estrutura madura do infra/dev-remote e ajustando para produção:
docker-compose.yml—db · api · web · caddy+backup; semmailpit.name: garantora-prd. Healthchecks,restart: unless-stopped,logging(json-file com rotação) e limites de recurso em todos os serviços.dbsemports:. Caddy em0.0.0.0:80/0.0.0.0:443com volumescaddy_data/caddy_config(certificados persistidos). Sem os bind mounts de/journeys.api.Dockerfile— igual ao do dev-remote (contexto = raiz,uv sync --frozen --no-dev, remove.env), mas o CMD sobe só o uvicorn (verprd-api-entrypoint.sh).prd-api-entrypoint.sh— não aplica migrations nem seed (RN7); apenasexec uv run uvicorn app.main:app --host 0.0.0.0 --port 8000. (Alternativa aceitável: dispensar entrypoint e usarCMDdireto.)web.Dockerfile+web-nginx.conf— build do Vite na raiz (VITE_BASE_PATH=/) servido por nginx a partir da raiz. Diferente do dev-remote (que serve sob/app).Caddyfile— standalone com HTTPS automático;handle /api*→ api ehandle(todo o resto) → web na raiz. Sem/app, sem redirect, sem/maile/journeys. Cabeçalhos de segurança de produção (ex.: HSTS) onde fizer sentido..env.example— flags de produção (APP_ENV=production,APP_DEBUG=false),EDGE_DOMAIN=app.garantora.com.br,APP_BASE_URL=https://app.garantora.com.br/app,CORS_ORIGINS=["https://app.garantora.com.br"], segredos placeholder, bloco SMTP real (placeholder, ver §9) e as vars de backup (BACKUP_CRON/intervalo,BACKUP_RETENTION_DAYS).Makefile— alvos de provisionamento e operação básica:up(build + up, sem migrate),migrate,seed(one-time),backup(dump imediato),restore(a partir de um dump),logs,ps,down. Sem alvodeploy(pull + rebuild de rotina) — isso é do comando futuro (§3). Semresetdestrutivo silencioso (RN10).README.md— provisionamento na box, pré-condição de DNS, sequência de bring-up inicial (up → migrate → seed uma vez), backup/restore, segurança, e as diferenças em relação ao dev-remote. Deixar explícito que backup é on-box (sem cópia externa ainda) e que o deploy de rotina + configuração de acesso são comando futuro (§3), não cobertos aqui..dockerignore— reaproveitar o da raiz (já exclui**/.env); confirmar que cobre o contexto doinfra/prd.
apps/api¶
Sem impacto de código. A imagem é buildada a partir do código atual. O healthcheck do container usa o endpoint /health que já existe em app/main.py (valida o banco com SELECT 1), sem criar rota nova (DA3 resolvida — endpoint ainda melhor que o openapi.json cogitado, porque confirma a conectividade com o Postgres).
apps/web¶
Mudança pequena e backward-compatible (para servir a produção na raiz, não sob /app): a base do Vite passou a ser parametrizada por VITE_BASE_PATH (default /app/) e o basename do React Router (App.tsx) deriva de import.meta.env.BASE_URL — então segue a base sozinho. Local e dev-remote não mudam (seguem /app); só a imagem de produção builda com VITE_BASE_PATH=/. Nada na UI, na responsividade ou no PWA muda. Sem impacto no e2e (a suíte roda contra o dev-remote, que continua em /app).
apps/e2e¶
Sem impacto. Esta spec não roda a suíte contra produção. Validação é operacional (subir a stack, conferir rotas e TLS), não e2e Playwright.
6. Contrato de API¶
Sem impacto. Nenhuma rota nova, alterada ou removida. A stack serve os endpoints existentes sob /api/v1 sem modificá-los.
7. Critérios de aceite¶
Verificáveis por quem não escreveu a stack, operando na box srv01.
Verificados no ambiente real em 2026-08-08, com a stack no ar em
https://app.garantora.com.br/(ver Registro para o detalhe de cada checagem).
- [x] CA1. Com o A record de
app.garantora.com.brapontando para a box,make upsobedb · api · web · caddy · backupe, após o handshake ACME,https://app.garantora.com.br/responde com certificado válido (Let's Encrypt, verify=0), sem aviso de TLS. (RN1, RN2) - [x] CA2.
https://app.garantora.com.br/carrega a aplicação na raiz (oindex.htmlreferencia/assets/…, não/app/…, sem redirect) ehttps://app.garantora.com.br/api/docsmostra o Swagger (HTTP 200). (RN1) - [x] CA3. Não há serviço
mailpitna stack (docker compose ps --serviceslista sóapi · web · caddy · db · backup) e o Caddy não temhandle /mail//journeys. Como a produção serve o app na raiz,/maile/journeyscaem no catch-all do SPA e devolvem o próprio app (<title>Garantora</title>, HTTP 200) — não o Mailpit nem capturas. O intuito (não expor Mailpit/journeys) está atendido; a expectativa antiga de "404" não vale com o app na raiz. (RN4) - [x] CA4. O serviço
dbnão publica porta no host (docker compose pssem0.0.0.0:5432); a API alcança o banco pela rede interna. (RN5) - [x] CA5. A API sobe com
APP_ENV=productioneAPP_DEBUG=false(via.env). (RN3) - [x] CA6. O boot da API não aplica migrations nem seed;
make migrateaplicou o schema (cadeia Alembic completa) emake seedcriou o admin, ambos como passos explícitos. (RN7) - [x] CA7. O serviço
backupgerou o primeiro dump no volume (garantora_20260808_225952.sql.gz); omake restoreestá documentado (restore num banco vazio ainda não exercitado em prod — dispensável enquanto a base é só o seed). (RN8) - [x] CA8. Todos os serviços com
restart: unless-stopped;dbeapireportamhealthy; compose com limites de recurso e rotação de logs configurados. (RN9) - [x] CA9. O bring-up inicial funcionou na ordem prevista:
make up(sem schema) →make migrate→make seed. Não há alvodeployde rotina no Makefile (é comando futuro). (RN10, §3) - [x] CA10. Nenhum segredo real está versionado:
.envé ignorado pelo git (git check-ignoreconfirma), o.env.examplesó tem placeholders (TROQUE-…), egit grepnão acha senha/segredo real na stack. Verificado em 2026-08-08. (RN6)
8. Plano de implementação¶
A ordem api → web → e2e do template não se aplica (é infra pura). Ordem real:
- [x]
infra/prd/docker-compose.yml— serviços, healthchecks, restart, logging, limites, volumes;backupincluído;mailpitfora - [x]
infra/prd/api.Dockerfile— imagem da API, boot só uvicorn. Semprd-api-entrypoint.sh: usei a alternativa aceita em §5 (CMD direto), menos um arquivo.make migrate/make seedviadocker compose exec. - [x]
infra/prd/web.Dockerfile+web-nginx.conf— build do Vite servido por nginx - [x]
infra/prd/Caddyfile— standalone, HTTPS automático, sem/maile/journeys, - cabeçalhos de segurança (HSTS, nosniff, X-Frame-Options, Referrer-Policy, -Server)
- [x]
infra/prd/.env.example— flags de prod, domínio, SMTP real (nomes reais doconfig.py:SMTP_HOST/PORT/USER/PASSWORD/FROM_EMAIL/FROM_NAME/USE_TLS), vars de backup - [x]
infra/prd/Makefile— up / migrate / seed / backup / restore / logs / ps / down (sem alvodeployde rotina — comando futuro, §3) - [x]
infra/prd/backup/backup.sh— script do serviçobackup(loop com retenção + modoonceparamake backup) - [x]
infra/prd/README.md— provisionamento, DNS, sequência de bring-up inicial, backup/restore, segurança, diferenças do dev-remote; deploy de rotina + acesso marcados como comando futuro - [x]
.dockerignore— cobertura confirmada: o da raiz já exclui**/.enve artefatos; o contexto de build do prd é a raiz do monorepo (igual ao dev-remote), então vale - [x] Validação estática (docker indisponível no host; produção não se sobe localmente — regra do skill): YAML do compose válido (anchors de logging aplicados aos 5 serviços, limites e healthchecks conferidos),
sh -nnobackup.shOK, Makefile com tabs corretos. Subida real + TLS + build das imagens: só na boxsrv01— ver CA marcados abaixo. - [x]
make test/make lint— não afetados: nenhum arquivo deapps/foi tocado (sóinfra/prd/). Não executados no host (semuv/docker aqui); sem código app novo, não há apontamento novo a introduzir. - [ ] Revisar
docs//CLAUDE.md: a seção "nada em produção" — deixado para o usuário. A stack está definida, mas nada foi implantado ainda; a seção só muda de sinal quando o ambiente for ao ar com dado real. Não editei a governança por conta própria (ver Registro).
make test/make linte o browser em desktop+mobile do template não se aplicam a mudança de infra; a validação é subir a stack e conferir os critérios de aceite.
9. Decisões em aberto — resolvidas¶
Todas as decisões de desenho foram fechadas (2026-08-08). Resta apenas um valor operacional a preencher no .env da box (IP não; credenciais SMTP — ver DA2).
- [x] DA1 — DNS. ✅ O domínio público
app.garantora.com.bre o hostname da boxsrv01.garantora.com.brjá apontam para179.198.118.166(confirmado pordigem 2026-08-08). OEDGE_DOMAIN/ACME usaapp.garantora.com.br. Pré-condição do ACME atendida; o README documenta o IP e o comando de conferência. - [x] DA2 — Provedor SMTP. ✅ SMTP genérico via
.env(SMTP_HOST/PORT/USER/ PASSWORD/FROM). A stack não fixa provedor — serve SendGrid, Mailgun, SES, corporativo. ~~Pendência operacional~~ Fechada em 2026-08-09 (durante a spec 0027/0031): o provedor escolhido é o AWS SES Mail Manager SMTP (us-east-1, ingress endpoint, porta 587 com STARTTLS), preenchido no.envda box. A conta está fora do sandbox, então o envio alcança destinatários não verificados — pré-requisito do convite por link (spec 0031). Verificado em quatro níveis: login local, login da box, envio real da box ePOST /auth/forgot-passwordpelo domínio público (202, sem erro no log), com o e-mail recebido de fato. - [x] DA3 — Healthcheck da API. ✅ Usa o endpoint
/healthjá existente emapp/main.py(valida o banco comSELECT 1). Sem rota nova — mantém o "sem impacto de código" (RN9). Melhor que oopenapi.jsoncogitado: confirma a conexão com o Postgres. - [x] DA4 — Backup off-box. ✅ Fora de escopo nesta spec. Só backup on-box (RN8). O README declara em voz alta a limitação (protege contra falha de app, não contra perda da box) e registra a cópia externa (S3/rsync) como roadmap.
- [x] DA5 — Admin inicial. ✅ Seed via
make seed(one-time) no provisionamento, a partir deSEED_ADMIN_EMAIL/SEED_ADMIN_PASSWORDdo.env. Troca de senha no primeiro login fica por conta do operador.
10. Registro¶
Implementado em 2026-08-08. A stack infra/prd foi criada substituindo o esboço anterior. Arquivos: docker-compose.yml, api.Dockerfile, web.Dockerfile, web-nginx.conf, Caddyfile, .env.example, Makefile, README.md, backup/backup.sh. Removido: infra/prd/Dockerfile (rascunho antigo).
Desvios em relação ao planejado (todos previstos como aceitáveis na spec):
- Sem
prd-api-entrypoint.sh. Usei a alternativa de §5 (CMD direto noapi.Dockerfile) — o boot sobe só o uvicorn e migrate/seed vão pordocker compose exec. Um arquivo a menos, mesmo comportamento (RN7). - Healthcheck da API usa
/healthreal (não o/api/openapi.jsoncogitado na DA3): descobri, lendoapp/main.py, que já existe@app.get("/health")validando o banco comSELECT 1. É melhor — confirma a conexão com o Postgres. Zero código novo. - Backup próprio, não imagem externa. O serviço
backupusapostgres:16-alpine(trazpg_dump/psqlna versão certa) rodandobackup/backup.sh— loop com dump comprimido, retenção por-mtime, e modooncepara omake backup. Preferi um script auditável a depender de imagem de terceiro. - Cabeçalhos de segurança no Caddy (HSTS,
X-Content-Type-Options,X-Frame-Options,Referrer-Policy,-Server) — não estavam explícitos no plano, mas cabem no "hardening de produção" (RN9) e são baratos.
Nomes de env conferidos no código (app/core/config.py), não inventados: SMTP usa SMTP_HOST/PORT/USER/PASSWORD/FROM_EMAIL/FROM_NAME/USE_TLS; flags APP_ENV, APP_DEBUG, APP_SECRET_KEY, JWT_SECRET_KEY, APP_BASE_URL, CORS_ORIGINS, SEED_ADMIN_*.
Validação feita (estática, no host): YAML do compose válido (5 serviços, anchors de logging aplicados, limites e healthchecks conferidos via parser), sh -n no backup.sh OK, Makefile com tabs corretos, CA10 (segredos) verificado. Docker não está disponível neste host e, por política do skill, produção não se sobe num ambiente isolado para validar — o build das imagens, a subida real e o TLS (CA1–CA9) só se verificam na box srv01, no provisionamento.
Pendências operacionais conhecidas (não bloqueiam a entrega da stack):
- DA2 — SMTP: ~~pendente~~ resolvido em 2026-08-09 — SES Mail Manager SMTP na box, conta fora do sandbox, envio confirmado ponta a ponta. O
.env.examplesegue com placeholders (os valores reais nunca entram no repositório). - DA4 — Backup off-box: só on-box nesta spec; cópia externa é roadmap (documentado no README).
- CLAUDE.md "nada em produção": não editei. A stack está definida mas nada foi implantado; a seção só muda de sinal quando o ambiente for ao ar com dado real — é decisão de governança do usuário, não desta implementação.
e2e: sem impacto — infra não tem spec Playwright. Nada a escrever nem a executar.
Go-live em 2026-08-08 — https://app.garantora.com.br/ NO AR. A box srv01 (179.198.118.166) foi provisionada (Docker 29.7.2 + Compose v5.4.0 + make, ufw 22/80/443, repo em /opt/garantora) e a stack subiu: pull main → .env → make up → make migrate → make seed. Verificação real (curl externo): / responde 200 com TLS Let's Encrypt válido e app na raiz (/assets/…); /api/v1/health = {"status":"ok","db":"ok"}; /api/docs 200; db sem porta publicada; 1º dump de backup gerado; api+db healthy. CA1–CA10 verificados. Portas 80/443 confirmadas alcançáveis de fora (o provedor não bloqueia — testado com listener temporário antes do deploy).
Domínio vs box: o público é app.garantora.com.br; srv01/179.198.118.166 é a máquina (SSH). O app é servido na raiz (não sob /app) — o web builda com VITE_BASE_PATH=/ (mudança backward-compatible; local e dev-remote seguem /app).
Ajustes desta etapa pós-implantação: CA3 reescrito (com o app na raiz, /mail//journeys caem no catch-all do SPA e dão 200 servindo o próprio app — não o Mailpit; o intuito de não expor Mailpit segue atendido). Eco do make up corrigido (lia /app e não expandia o domínio; agora lê EDGE_DOMAIN do .env e aponta a raiz).
Segredos: gerados fortes no provisionamento e guardados só no .env (perm 600) da box; o dono vai rotacioná-los. Admin semeado (admin@garantora.com.br, senha gerada a trocar).
Pendências que seguem abertas: SMTP real (DA2 — e-mail não sai até configurar), backup off-box (DA4), e a seção "nada em produção" do CLAUDE.md — agora há um ambiente de produção no ar (ainda sem cliente/dado real, só o seed). Reescrever essa seção é decisão de governança do usuário; não editei.