Pular para conteúdo

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.