Pular para o conteúdo

Deploy — Rede Cívica


Este documento descreve onde e como o MVP da Rede Cívica roda: a infraestrutura, a pipeline de CI/CD, os componentes em execução, o armazenamento, a configuração e a operação.

Nenhum valor real de ambiente entra neste repositório. Senhas, chaves, tokens, endereços internos e identificadores de recursos vivem nos secrets do Coolify e do GitHub. Os templates públicos estão nos repositórios de código.

Estado atual: o ambiente dev está no ar em dev.redecivica.com.br e o site de documentação em docs.redecivica.com.br. O ambiente de produção entra depois da validação de dev.


O código vive na org Rede-Civica do GitHub. O CI roda no GitHub Actions, publica imagens no GHCR e a VPS puxa essas imagens pelo Coolify. A VPS é uma HostGator NVMe 4, com 2 vCPU, 4 GB de RAM e 100 GB de NVMe, em São Paulo.

GitHub (org Rede-Civica)
mvp-api · mvp-web
│ push em develop ou main
GitHub Actions (lint, typecheck, test, build)
│ docker push
GHCR (ghcr.io/rede-civica/mvp-api, ghcr.io/rede-civica/mvp-web)
│ docker pull
VPS HostGator (São Paulo) · Coolify + Traefik
Traefik :443 (TLS automático)
└── web (nginx) ── /api/ ──> api (NestJS) ──> postgres
── /tiles/ ─> tiles (martin) ──> brasil.mbtiles (disco)
api ──> OCI Object Storage (mídia e backups em buckets separados)

Três decisões estruturam o desenho:

  • Mesma origem. Traefik serve o front e a API no mesmo domínio. O nginx do web faz proxy de /api/, /tiles/, /d6a/, /d6b/ e /admin/moderacao/. O navegador conversa com uma origem só.
  • Infraestrutura no Brasil. A VPS e o object storage ficam em São Paulo. Os processadores estrangeiros restantes são o Google (login) e a Esri (camada de satélite).
  • VPS única. Web, API, tiles e banco dividem a mesma máquina. É um ponto único de falha aceito na escala piloto.

Peça Onde Tecnologia
Front-end VPS, container web nginx servindo o bundle estático do Vite
API VPS, container api NestJS em Node 22
Assistência de texto VPS, container languagetool LanguageTool 6.8 self-hosted, pt-BR
Tiles VPS, container tiles martin 1.14.0 servindo brasil.mbtiles
Banco VPS, container postgres PostgreSQL 16
Site de documentação VPS, container docs nginx servindo o site estático gerado pelo Astro Starlight
Orquestração VPS Coolify com proxy Traefik
Imagens GHCR ghcr.io/rede-civica/mvp-api, mvp-web e mvp-docs
Mídia e backups OCI Object Storage API S3-compatible, região sa-saopaulo-1
DNS Registro.br zona redecivica.com.br

Os repos de código da org Rede-Civica são mvp-api (back-end), mvp-web (front-end) e docs (este repositório). O CI dos dois repos de código publica imagens no GHCR com o nome do repositório. Cada imagem carrega a tag sha do commit e uma tag por branch:

Branch Tags publicadas Ambiente
develop sha + dev dev
main sha + latest produção
pull request nenhuma (só valida o build)

A tag sha é imutável e permite voltar a uma versão exata. As tags dev e latest andam a cada push.


Dois ambientes, montados um de cada vez. O dev subiu primeiro; a produção entra depois da validação.

Ambiente Branch Tag Domínio Estado
dev develop dev dev.redecivica.com.br no ar
produção main latest app.redecivica.com.br depois da validação de dev

Cada ambiente tem o próprio conjunto de secrets, o próprio client OAuth, o próprio bucket e o próprio domínio. Nada é compartilhado entre os dois.


Cada repo tem um workflow build-image.yml disparado em push para develop ou main e em pull request. Os jobs:

  1. lint: ESLint. No web, também format:check.
  2. typecheck: tsc. Na API, com prisma generate antes.
  3. test: Jest na API e Vitest no web, com thresholds de cobertura.
  4. imagem: build sem push, apenas em pull request.
  5. imagem-dev e imagem-prod: build e push no GHCR, em push para develop ou main.

Os jobs de imagem dependem dos três gates. Gate vermelho bloqueia a publicação.

O build da imagem do web recebe as variáveis do client como build args, vindos dos secrets do repositório: VITE_GOOGLE_CLIENT_ID, um por ambiente. A imagem da API não tem segredo de build. Os valores de runtime chegam pelos secrets do Coolify.

Não existe trigger automático de deploy no fim do workflow. Depois que o CI publica a tag nova, o operador dispara o redeploy no painel do Coolify. A automação por webhook é passo planejado; quando entrar, a URL do webhook vive como secret do repositório, nunca no código.


O Coolify gerencia os containers, os volumes persistentes, os backups e os domínios. Ele roda na VPS com instâncias próprias de PostgreSQL e Redis para uso interno. O Traefik é o proxy reverso: escuta em 80 e 443, emite e renova o certificado TLS automaticamente (ACME HTTP-01) e roteia por label de container. Só os serviços com domínio configurado recebem rota.

Imagem multi-stage: node:22-alpine compila o bundle e nginx:alpine serve em porta interna 80. A configuração do nginx cobre:

  • Fallback de SPA (try_files para o index.html).
  • Cache imutável para /assets/ com hash no nome, sem cache para sw.js, registerSW.js e index.html.
  • .mjs servido como text/javascript, exigido pelo worker do MapLibre.
  • gzip para texto, JSON e wasm.
  • Proxy de /api/, /d6a/, /d6b/ e /admin/moderacao/ para o container api e de /tiles/ para o container tiles, com o prefixo /tiles removido antes do encaminhamento.

A resolução DNS dos upstreams usa o resolver do Docker, então o nginx sobe antes dos containers de origem.

Imagem multi-stage em node:22-slim (o onnxruntime exige glibc). No boot, o container aplica as migrations do Prisma com prisma migrate deploy e sobe o servidor. O health check do Docker chama GET /api/observability/health a cada 30s.

A configuração de runtime vem inteira de variáveis de ambiente registradas como secrets no Coolify. A lista completa, com placeholders, está em .env.dev.example e .env.production.example no repo mvp-api. Os nomes MINIO_* são mantidos no código por compatibilidade; os valores apontam para o bucket do OCI.

Os modelos de inferência ficam em um volume nomeado montado em /app/modelos-d1b: Whisper tiny e Florence-2 base na D-1b, classificador NSFW e detector de pessoa na D-1c. Os pesos são baixados do HuggingFace no primeiro uso de cada pipeline, cerca de 1,3 GB no total. O classificador NSFW ocupa cerca de 86 MB em disco e 100 MB residentes; o detector de pessoa, cerca de 42 MB em disco e 50 MB residentes. O volume sobrevive aos redeploys.

Os limites de recurso no painel são 2,5 GiB de memória e 1,5 vCPU. A VPS tem 4 GB de RAM e um swap de 2 GB. A conta de memória inclui API, modelos, PostgreSQL, Coolify/Traefik e martin. O classificador NSFW e o detector de pessoa usam dtype quantizado para conter a RAM. Com os três modelos carregados, o pico estimado fica em cerca de 1,7 GB, dentro do limite.

O docker-compose.vps-sim.yml reproduz essa conta no desenvolvimento: a API roda com mem_limit de 2,5 GiB e 1,5 vCPU e monta o mesmo volume de modelos. Se o OOM aparecer no sim, a válvula é D1B_DTYPE_MODELOS=fp16 ou desligar a detecção de pessoa com D1C_PESSOA_HABILITADA=false.

Motor de revisão de português da assistência de texto (POST /api/assistencia/texto), self-hosted. Roda a imagem comunitária erikvl87/languagetool:6.8, com heap de 512 MB e limite de 768 MB, sem n-gramas e sem porta pública. A API o alcança pelo alias interno languagetool:8010, configurado em ASSISTENCIA_URL. O serviço é opcional para o restante do sistema: com ele fora do ar, a assistência responde disponivel: false e o envio do formulário segue normal. O compose versionado está em deploy/coolify/languagetool.yml do repo mvp-api.

PostgreSQL 16 criado pelo one-click do Coolify, com volume persistente próprio e senha forte gerada pelo painel. Não expõe porta pública; só a rede interna do Docker o alcança. A API conecta pelo hostname interno via DATABASE_URL.

O arquivo brasil.mbtiles, de cerca de 24,3 GiB, é gerado pelos scripts em scripts/tiles/ do repo mvp-api a partir do extrato do OpenStreetMap (Geofabrik), dos footprints da Microsoft GlobalMLBuildingFootprints (CDLA-Permissive-2.0) e do tema base.land_cover da Overture Maps (ODbL, derivado do ESA WorldCover), combinados na geração. O Brasil ocupa o z0–16 com os três grupos de dado. Os vizinhos da América do Sul ocupam o mesmo z0–16, gerados do extrato south-america-latest.osm.pbf recortado para excluir o polígono do Brasil, sem prédios e sem verde da Overture. A fusão dos tiles de fronteira decodifica os MVT e une as camadas por (camada, id), com a feição do Brasil prevalecendo. O z0–7 permanece como veio da cobertura de contexto anterior. O arquivo fica em um diretório da VPS e é montado somente leitura no container. O serviço roda martin 1.14.0 com o arquivo como argumento e responde o tileset brasil, esquema OpenMapTiles, z0 a 16. O app usa overzoom acima do maxzoom do tileset, até o zoom 19.

O MBTiles não vai para o object storage. As reimportações repetem o mesmo fluxo: gerar localmente, fundir os tiles dos vizinhos e copiar o arquivo para o diretório da VPS.

A camada base é derivada do OpenStreetMap e disponibilizada sob a ODbL, com a atribuição “© OpenStreetMap contributors” visível no app. O esquema de camadas é do OpenMapTiles (CC-BY), os prédios combinam o OpenStreetMap com a Microsoft Building Footprints (CDLA-Permissive-2.0), com a atribuição “Microsoft” visível no app, e a cobertura vegetal vem do tema base.land_cover da Overture Maps (ODbL), derivado do ESA WorldCover, com as atribuições “Overture Maps Foundation” e “ESA WorldCover / Copernicus” visíveis no app.

O guia mapa_e_tiles.md apresenta os conceitos, os formatos de dado e a cadeia completa, do extrato ao render.

O Coolify coloca os serviços na rede Docker coolify com os aliases api, tiles e languagetool. O nginx do web e a API usam esses aliases. O nome do container muda a cada deploy e nunca é usado para comunicação. Nos recursos declarados como Docker Compose, o alias é explícito no arquivo de deploy/coolify/ do repo mvp-api.


Mídia e backups do banco vivem no OCI Object Storage, S3-compatible, região de São Paulo. Cada ambiente tem buckets separados por uso: um para mídia e outro para backups.

  • Mídia: o navegador envia direto para o bucket por URL presigned gerada pela API. A API também lê a mídia pelo mesmo gateway para processar áudio e imagem. O bucket de mídia não guarda backup; o MBTiles fica fora.
  • Backups: o Coolify roda um dump diário do PostgreSQL (cron 0 2 * * *), mantém 4 cópias com teto de 10 GB e envia para o bucket dedicado de backups. O backup local fica desativado. O free tier de 20 GB cobre a escala piloto.
  • Separação de buckets: o cliente S3 da API é preso a um bucket, definido por MINIO_BUCKET. Com os dumps em bucket separado, uma falha de assinatura na leitura de mídia não alcança os backups. Dev usa rede-civica-dev (mídia) e rede-civica-dev-backups (backups). Produção repete com rede-civica e rede-civica-backups.
  • CORS: o gateway S3-compatible do OCI responde Access-Control-Allow-Origin: * por conta própria e não aceita regra por bucket nesta versão. O bucket é privado e o acesso depende de URL presigned. O MVP aceita o CORS aberto e registra a limitação. Se a restrição for exigida antes da produção, entra por proxy de apresentação (reaproveitando o MINIO_PUBLIC_ENDPOINT) ou media proxy na API.

Residência de dados: a VPS e o bucket ficam no Brasil. HostGator e Oracle entram na lista de processadores da política de privacidade. Os processadores externos restantes são o Google (login) e a Esri (camada de satélite).


A zona redecivica.com.br é gerenciada no Registro.br. Cada ambiente tem um registro A apontando para o IP da VPS. O Traefik emite o certificado TLS automaticamente quando o recurso web com o domínio sobe. O HTTP redireciona para HTTPS.

O site de documentação tem registro A próprio de docs.redecivica.com.br para o mesmo IP, com o TLS emitido pelo mesmo Traefik. Ele é um container nginx separado, fora do domínio do app.

O mesmo domínio serve o app e a API. O nginx do web faz proxy de /api/, /tiles/, /d6a/, /d6b/ e /admin/moderacao/, então o navegador fala com uma origem só.


Nenhum valor real de ambiente é versionado. O repositório público carrega só templates:

  • mvp-api/.env.dev.example e mvp-api/.env.production.example: lista completa de variáveis com placeholders.
  • mvp-web/.env.example: variáveis do client, todas VITE_.

Os valores reais vivem em:

  • Secrets do Coolify: variáveis da API por ambiente (banco, JWT, OAuth, papéis de operação, storage, retenção).
  • Secrets do repositório no GitHub: build args do web e a futura URL de webhook.
  • Token de leitura de pacotes: o Coolify autentica no GHCR com um token de escopo read:packages, com expiração e rotação.

A imagem do web não carrega segredo de runtime. As variáveis VITE_ entram no bundle em tempo de build e são públicas por natureza. O client ID do Google é público; a camada base do mapa é servida por infraestrutura própria e o OpenStreetMap raster fica apenas como fallback de erro.


  • Health: a API expõe liveness em /api/observability/health e readiness em /api/observability/health/readiness, que checa banco, DLQ e storage. O container declara health check no Docker. Os outros serviços têm checagem própria no Coolify.
  • Rollback: redeploy de uma tag sha anterior no painel do Coolify. A tag é imutável, então a versão retornada é exata.
  • Restauração de backup: o drill de restauração do dump do bucket é rotina operacional planejada.
  • Logs: o driver do Docker rotaciona os logs como medida interina. O log estruturado em arquivo é evolução pendente.
  • Warm-up dos modelos: no primeiro uso de cada pipeline a API baixa os pesos para o volume. Aquecer antes de abrir tráfego é recomendado em ambiente novo.
  • Limites de recurso: definidos por serviço no painel do Coolify e aplicados a cada recreate.
  • Acesso ao host: SSH só por chave, com login de root por senha desabilitado, firewall restrito às portas necessárias e fail2ban ativo.
  • Instância única: locks e caches são em memória no MVP. A API roda em uma réplica. Escala horizontal exige Redis e fica para a Fase 2.

Os recursos do Coolify declarados como Docker Compose têm o arquivo versionado em deploy/coolify/ do repo mvp-api. O painel não lê o repositório: a alteração vale depois de colar o arquivo no recurso e redeployar.

Rotina, depois que o ambiente existe:

  1. Abrir pull request para develop no repo com a mudança. O CI roda os gates e valida o build da imagem.
  2. Fazer o merge. O CI roda os gates de novo, publica a tag dev e o sha do commit, aciona o deploy no Coolify e valida a saúde no domínio.
  3. Mudança em recurso Docker Compose (tiles e languagetool): editar o arquivo em deploy/coolify/, colar no painel do Coolify, salvar e redeployar o recurso.
  4. Validar no domínio: SPA respondendo, /api/observability/health com 200 e /tiles/brasil com o JSON do tileset.
  5. Para produção, repetir o fluxo em main, que publica latest, depois de validar o dev.

O provisionamento inicial de um ambiente, já feito para o dev, segue esta ordem:

  1. Projeto no Coolify com um environment.
  2. PostgreSQL one-click, com destino de backup no bucket.
  3. Recurso da API a partir da imagem, com os secrets do ambiente e o volume de modelos.
  4. Recurso do LanguageTool via Docker Compose, colando deploy/coolify/languagetool.yml do repo mvp-api.
  5. Recurso dos tiles via Docker Compose, colando deploy/coolify/tiles.yml, com o diretório do MBTiles montado.
  6. Recurso do web com o domínio e o TLS automático.
  7. Registro A no DNS antes de subir o recurso do web.
  8. Smoke test fim a fim: demanda com foto e áudio, login Google e tiles.

O site tem o próprio repositório e o próprio ciclo, sem passar pelos recursos do app:

  1. Merge ou push no master do repo docs.
  2. O workflow notificar-site.yml do docs dispara o build do site por repository_dispatch.
  3. O workflow do site roda os gates, clona o docs, gera as páginas e publica a imagem no GHCR com as tags sha e latest.
  4. O webhook do Coolify aciona o redeploy e a saúde é verificada em https://docs.redecivica.com.br/ e em uma página de conteúdo.
  5. Validar no domínio: home, uma página de SDS, o guia visual e a busca.

O rollback troca a imagem do recurso para a tag sha anterior no painel e volta para latest quando a correção entrar.


  • O nginx do web serve cabeçalhos de segurança em todas as respostas (HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy e CSP), com server_tokens off. A CSP libera a própria origem, o domínio de apresentação do bucket (connect-src para o upload presigned, img-src e media-src para a mídia lida pela D-7), o host cru do gateway OCI, a camada de satélite da Esri, os fallbacks de mapa e o script do Google usado no login. O domínio de apresentação de produção entra na mesma lista quando o bucket de produção for criado.
  • A API aplica CORS por allowlist (CORS_ORIGINS). Em produção, lista vazia significa nenhuma origem externa.
  • O rate limit é por IP do cliente. O nginx normaliza a cadeia X-Forwarded-For antes de repassar, o que fecha o vetor de spoofing com TRUST_PROXY=1.
  • As rotas de operação exigem papel ou IP de operação. metrics e readiness ficam restritos em produção, e o liveness segue público para o health check do container. Eventos e rebuilds exigem papel de operador.
  • A identidade anônima usa prova de posse do dispositivo (X-Device-Segredo) no vínculo com a conta Google. O login sem o segredo não transfere os dados do aparelho.
  • A fila de moderação fica atrás de JWT com papel de moderação, e o nginx faz proxy de /admin/moderacao/.

  • O deploy da API e do web é disparado ao fim do CI por webhook do Coolify. Os recursos Docker Compose (tiles e languagetool) continuam com deploy manual no painel, a partir do arquivo de deploy/coolify/.
  • A rotação de logs pelo driver do Docker é interina. O log estruturado está pendente.
  • A imagem da API tem cerca de 2,17 GB. Excluir dependências de desenvolvimento no estágio de produção corta perto de 1 GB.
  • O CORS do gateway do OCI responde * e não aceita regra por bucket. O MVP aceita e documenta. Restringir a origem exigiria proxy de apresentação ou media proxy.
  • O drill de restauração do banco está pendente.
  • A API roda em instância única. Locks e caches são em memória.
  • Front, API, tiles e banco dividem uma VPS. Uma queda derruba todos juntos. CDN na frente dos tiles e plano de resiliência do front são passos futuros.