Topologia de deployment¶
Três ambientes, cada um com seu docker-compose.yml sob infra/. Os quatro processos do Garantora — api, worker, web e o banco/fila que eles usam — existem nos três; o que muda é o que roda em container, o que roda no host, e quem faz a borda.
| Ambiente | Projeto Compose | Domínio | Borda / TLS | Em container |
|---|---|---|---|---|
infra/local | garantora | localhost:8080 | Caddy em container, HTTP puro | db, redis, mailpit, caddy — api, worker, Vite e wiki rodam no host |
infra/dev-remote | garantora-devremote | dev.garantora.com.br | nginx nativo do host + certbot | api, worker, web, docs — Postgres, Redis e Mailpit são serviços nativos da VPS |
infra/prd | garantora-prd | app.garantora.com.br | Caddy dedicado, Let's Encrypt (HTTP-01) | db, redis, api, worker, web, caddy, backup |
worker é sempre a mesma imagem da api com outro comando (uv run arq app.worker.WorkerSettings); não tem Dockerfile próprio e não roda migration nem seed. Sem ele, a consulta de crédito fica PENDING e os jobs das 6h e das 7h não acontecem — sem erro nenhum.
Produção (infra/prd) — box srv01¶
flowchart TB
Internet(("Internet")) -->|"443/80"| Caddy["caddy\ncaddy:2-alpine\nTLS automático (Let's Encrypt)"]
Caddy -->|"/api* → api:8000"| Api["api\ninfra/prd/api.Dockerfile\nuvicorn — healthcheck GET /health"]
Caddy -->|"demais rotas → web:80\n(app na raiz)"| Web["web\ninfra/prd/web.Dockerfile\nnginx servindo o build estático"]
Api -->|"asyncpg"| Db[("db\npostgres:16-alpine\nhealthcheck pg_isready\nvolume postgres_data")]
Api -->|"enfileira"| Redis[("redis\nredis:7-alpine\nsó a fila do arq")]
Worker["worker\nmesma imagem da api\narq app.worker.WorkerSettings"] -->|"consome"| Redis
Worker -->|"asyncpg"| Db
Worker -->|"HTTPS"| APIFULL["API Full (externo)"]
Api -->|"SMTP STARTTLS"| SES["AWS SES (externo)"]
Backup["backup\npostgres:16-alpine\nbackup.sh em loop"] -->|"pg_dump"| Db
Backup --> BackupVol[("volume\npostgres_backups")] Todos os serviços caem na rede default do Compose (garantora-prd_default) e se descobrem por nome de serviço — não há networks: custom nem bind mount de código (só o Caddyfile e o backup.sh são montados, somente leitura). Banco e Redis não publicam porta: só a rede interna fala com eles. restart: unless-stopped, rotação de log (json-file, 3 × 10 MB) e limite de memória em todos. Só db e api têm healthcheck:; o caddy espera api: service_healthy, e o worker espera o db saudável e o redis iniciado.
O app fica na raiz, não sob /app: a imagem do web é construída com VITE_BASE_PATH=/, e o Caddyfile manda tudo que não é /api* para o web. Produção não expõe /mail nem /journeys, e aplica cabeçalhos de segurança (HSTS, nosniff, X-Frame-Options: DENY).
TLS é HTTP-01 puro — cada domínio precisa resolver publicamente para a box (srv01, 179.198.118.166) antes do primeiro request, porque o desafio usa a porta 80. Certificados persistem no volume caddy_data.
Backup — o serviço backup faz pg_dump comprimido a cada BACKUP_INTERVAL_SECONDS (padrão diário) e apaga os dumps com mais de BACKUP_RETENTION_DAYS (padrão 7). O deploy gera um dump extra antes de qualquer migration (make backup).
Migration e seed não rodam no boot (RN7 da spec 0028): a api sobe só o uvicorn, e os dois são passos explícitos do deploy.
Versão embutida — api, worker e web recebem os build args APP_VERSION e (api e worker) GIT_COMMIT. O /api/v1/health devolve os dois, e o web exibe a versão; vazios, valem "desconhecido" e o produto diz "versão não identificada" (spec 0043).
Deploy (/ship)¶
Deploy de produção é sempre o skill /ship, pedido do usuário — nunca à mão na box. O procedimento completo, com os códigos de saída do pré-voo e o rollback, está em .claude/skills/ship/SKILL.md; o resumo:
sequenceDiagram
participant U as Usuário
participant Skill as "skill ship"
participant Srv as "srv01 (/opt/garantora)"
participant Compose as "docker compose (infra/prd)"
U->>Skill: roda o skill ship
Skill->>Srv: pré-voo — origin/main × HEAD da box, commits e migrations pendentes, nota da versão
Skill->>U: mostra o que sobe e a versão X.Y.Z, pede confirmação
U->>Skill: confirma
Skill->>Srv: git pull --ff-only (ssh -A)
Skill->>Compose: make up — rebuild + recreate, com APP_VERSION e GIT_COMMIT
opt há migration pendente
Skill->>Compose: make backup (dump antes de migrar)
Skill->>Compose: make migrate
end
Skill->>Compose: make seed (sempre — permissão nova é dado de seed, não de schema)
Skill->>Srv: curl /api/v1/health (db, versão) + raiz pública (HTTP 200, TLS)
Skill->>U: só então cria e empurra a tag vX.Y.Z O /ship deploya origin/main e recusa se o main local estiver à frente (não faz push por você); recusa também sem a nota da versão em apps/web/src/versoes/X.Y.Z.md. O make up recria api, worker e web — o worker sobe com o mesmo código da API no mesmo deploy. O Caddy não é reiniciado à parte: o docker compose up -d --build só recria serviço cuja definição mudou.
Adicionar um domínio novo é mudança de infra, não de app: exige um bloco de site novo em infra/prd/Caddyfile (ou um novo serviço no docker-compose.yml), commitado em origin/main antes de rodar /ship — o skill não cria serviço nem edita Caddyfile por conta própria.
dev-remote — o único alvo do e2e¶
flowchart TB
Internet(("Internet")) -->|"443/80"| Nginx["nginx nativo da VPS\n+ certbot"]
subgraph Compose["Compose garantora-devremote (rede 172.29.5.0/24)"]
Api["api :8000\n(entrypoint: migrate + seed + uvicorn)"]
Worker["worker\nmesma imagem da api"]
Web["web :8081\nbuild estático, base /app/"]
Docs["docs :8082\nMkDocs (docs.garantora.com.br)"]
end
Nginx -->|"/api/*"| Api
Nginx -->|"/app/*"| Web
Nginx -->|"docs.garantora.com.br\n(Basic Auth)"| Docs
Nginx -->|"/mail/*"| Mailpit["Mailpit nativo"]
Nginx -->|"/journeys/*"| Disco["docs/journeys em disco"]
Api -->|"host.docker.internal"| PG[("PostgreSQL nativo\nbanco garantora_db")]
Api -->|"host.docker.internal"| RD[("Redis nativo\níndice /1")]
Worker --> PG
Worker --> RD
Api -->|"SMTP :1025"| Mailpit A VPS (145.223.94.55) é compartilhada com outros projetos: só o que carrega código do Garantora roda em container, publicado apenas em loopback; borda, banco, fila e captura de e-mail são serviços systemd nativos, configurados fora deste repositório. A rede do Compose tem sub-rede fixa (172.29.5.0/24) para o pg_hba.conf do host poder liberá-la nominalmente. Diferente da produção, aqui o entrypoint da api aplica as migrations e o seed a cada start.
As imagens embutem o código: o ambiente só reflete uma mudança depois do skill /devremote-rebuild, que reconstrói api e worker juntos (mesmo Dockerfile), web e docs conforme o que mudou, e grava a versão como "última publicada + N commits" (1.5.0 +7). apps/e2e roda sempre contra este ambiente, nunca contra produção nem contra uma stack isolada montada à mão.
Local (infra/local)¶
Só as dependências sobem em container (make local-up): Postgres, Redis, Mailpit e o Caddy, que serve tudo em http://localhost:8080 e alcança o host por host.docker.internal. A API (make api-dev), o worker (make worker-dev), o Vite (make web-dev) e a wiki Sphinx rodam no host, com hot reload — sem o worker-dev, a consulta de crédito e os jobs agendados ficam mudos também aqui.
| Path | Destino |
|---|---|
/api* | API no host, :8000 |
/app* | Vite no host, :5173 (base /app/) |
/wiki* | Sphinx no host, :8001 (prefixo removido) |
/mail* | Mailpit |
/ | redireciona para /app/ |
Ver infra/prd/README.md, infra/dev-remote/README.md e infra/local/README.md para o passo a passo operacional, e ../operations/deployment.md para o guia de uso do dia a dia.