prompt inicial do projeto

This commit is contained in:
2026-07-22 15:55:55 -03:00
commit 6fb920d333
19 changed files with 2937 additions and 0 deletions

292
docs/05-INFRA-DOCKER.md Normal file
View File

@@ -0,0 +1,292 @@
# 05 — Infraestrutura e Docker
> Piloto: um VPS com Compose. Produção: mesmo código, só configuração diferente. O backend é stateless desde o dia 1, então migrar é config, não reescrita.
## 1. Topologia
```
PILOTO (1 VPS — 8 vCPU, 16GB, link ≥ 1Gbps)
traefik · backend · postgres · livekit · coturn · minio · observabilidade
+ backup automático do Postgres com restore testado
PRODUÇÃO (mesmo compose, override de config)
├─ Postgres gerenciado (Neon / RDS) com réplica e PITR
├─ 2+ réplicas do backend atrás do Traefik
├─ LiveKit + coturn em máquina de banda dedicada
└─ MinIO → S3 (mesma API)
```
**Por que o piloto num VPS só é aceitável:** o volume real é de ~0,3 req/s, e a falha é mitigada por backup testado e pela contingência física obrigatória (`07-REQUISITOS-DE-CAMPO.md`). O que **não** é aceitável é o código depender de rodar em instância única — daí a regra de ser stateless.
## 2. `infra/docker-compose.yml`
```yaml
services:
traefik:
image: traefik:v3.3
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --certificatesresolvers.le.acme.tlschallenge=true
- --certificatesresolvers.le.acme.email=${ACME_EMAIL}
- --certificatesresolvers.le.acme.storage=/acme/acme.json
ports: ["80:80", "443:443"]
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_acme:/acme
restart: unless-stopped
postgres:
image: postgis/postgis:17-3.5
environment:
POSTGRES_DB: portaria
POSTGRES_USER: portaria
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
- ./postgres/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U portaria"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
backend:
image: ghcr.io/${GH_OWNER}/portaria-backend:${TAG:-latest}
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/portaria
SPRING_DATASOURCE_USERNAME: portaria
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
LIVEKIT_URL: ws://livekit:7880
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY}
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET}
STORAGE_ENDPOINT: http://minio:9000
STORAGE_ACCESS_KEY: ${MINIO_ROOT_USER}
STORAGE_SECRET_KEY: ${MINIO_ROOT_PASSWORD}
FCM_CREDENTIALS_PATH: /secrets/fcm.json
APNS_KEY_PATH: /secrets/apns.p8
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
volumes:
- ./secrets:/secrets:ro
depends_on:
postgres: { condition: service_healthy }
minio: { condition: service_started }
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/actuator/health/readiness"]
interval: 15s
timeout: 5s
retries: 5
start_period: 60s
labels:
- traefik.enable=true
- traefik.http.routers.api.rule=Host(`api.${DOMAIN}`)
- traefik.http.routers.api.tls.certresolver=le
- traefik.http.services.api.loadbalancer.server.port=8080
restart: unless-stopped
# SFU — host network é necessário: o LiveKit precisa das faixas UDP reais
livekit:
image: livekit/livekit-server:latest
command: --config /etc/livekit.yaml
network_mode: host
volumes:
- ./livekit/livekit.yaml:/etc/livekit.yaml:ro
restart: unless-stopped
coturn:
image: coturn/coturn:latest
network_mode: host
command: >
-n --log-file=stdout --min-port=49160 --max-port=49200
--realm=${DOMAIN} --use-auth-secret --static-auth-secret=${TURN_SECRET}
--external-ip=${PUBLIC_IP} --no-cli --no-tlsv1 --no-tlsv1_1
restart: unless-stopped
redis: # exclusivo do LiveKit e do rate limit
image: redis:7-alpine # NUNCA para dados que não podem se perder
command: redis-server --save "" --appendonly no
restart: unless-stopped
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- miniodata:/data
restart: unless-stopped
web-visitor:
image: ghcr.io/${GH_OWNER}/portaria-web-visitor:${TAG:-latest}
labels:
- traefik.enable=true
- traefik.http.routers.visitor.rule=Host(`v.${DOMAIN}`)
- traefik.http.routers.visitor.tls.certresolver=le
restart: unless-stopped
web-admin:
image: ghcr.io/${GH_OWNER}/portaria-web-admin:${TAG:-latest}
labels:
- traefik.enable=true
- traefik.http.routers.admin.rule=Host(`admin.${DOMAIN}`)
- traefik.http.routers.admin.tls.certresolver=le
restart: unless-stopped
volumes:
pgdata: {}
miniodata: {}
traefik_acme: {}
```
Observabilidade (`otel-collector`, `prometheus`, `grafana`, `loki`) fica em `docker-compose.observability.yml`, ativada por perfil.
**Domínios:** `v.` (visitante) curto de propósito — ele aparece impresso no QR e às vezes é digitado à mão.
## 3. LiveKit e coturn
`infra/livekit/livekit.yaml`:
```yaml
port: 7880
rtc:
tcp_port: 7881
port_range_start: 50000
port_range_end: 60000
use_external_ip: true
keys:
${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}
redis:
address: localhost:6379 # obrigatório com mais de uma instância
turn:
enabled: false # coturn cuida do TURN
room:
empty_timeout: 120
max_participants: 6
```
### Portas — abrir no firewall
| Porta | Protocolo | Serviço |
|---|---|---|
| 80, 443 | TCP | Traefik |
| 7880 | TCP | LiveKit signaling (atrás do Traefik) |
| 7881 | TCP | LiveKit ICE/TCP (fallback) |
| 5000060000 | UDP | Mídia WebRTC |
| 3478 | UDP/TCP | STUN/TURN |
| 4916049200 | UDP | Relay do coturn |
**Erro clássico:** esquecer a faixa UDP. A chamada conecta, a sinalização funciona, e o vídeo nunca aparece — sintoma que parece bug de aplicação e é firewall.
**Redis não é opcional** com múltiplas instâncias do LiveKit: sem ele, o estado das salas se divide silenciosamente entre nós e dois participantes da mesma sala não se enxergam.
## 4. Dimensionamento de banda
Uma chamada de portaria tem 2 participantes, vídeo em 480p a ~600kbps mais áudio. O SFU recebe e reenvia:
```
1 chamada ≈ 1,2 Mbps entrada + 1,2 Mbps saída ≈ 2,4 Mbps
```
| Chamadas simultâneas | Banda no SFU |
|---|---|
| 10 | ~24 Mbps |
| 50 | ~120 Mbps |
| 100 | ~240 Mbps |
| 200 | ~480 Mbps |
Chamadas de portaria duram 3060s. Um condomínio de 200 unidades gera ~50 visitas/dia com pico de 35 simultâneas. **Um VPS de 1Gbps atende com folga dezenas de condomínios.**
O que estoura banda é gravação (módulo `video_recording`): o LiveKit Egress soma outro fluxo por chamada e escreve no MinIO. Ative com quota por plano.
**Limite de resolução no servidor**, não no cliente: 480p é suficiente para reconhecer alguém na portaria, e 1080p multiplicaria o custo por quatro sem ganho de decisão.
## 5. Variáveis de ambiente
`infra/.env.example` (nunca commitar `.env`):
```bash
DOMAIN=portaria.exemplo.com.br
PUBLIC_IP=203.0.113.10
ACME_EMAIL=ops@exemplo.com.br
GH_OWNER=sua-org
TAG=latest
POSTGRES_PASSWORD=
LIVEKIT_API_KEY=
LIVEKIT_API_SECRET=
TURN_SECRET=
MINIO_ROOT_USER=
MINIO_ROOT_PASSWORD=
JWT_SIGNING_KEY=
```
Credenciais de FCM e APNs são arquivos em `./secrets/`, montados read-only. **Nada de secret em imagem.**
## 6. Observabilidade
Actuator expõe `/actuator/health` (liveness e readiness), `/actuator/prometheus` e `/actuator/info`. OpenTelemetry exporta traces do backend, do LiveKit e do Postgres.
**Dashboards obrigatórios no Grafana:**
1. **SLOs** — latência QR→toque (p50/p95/p99), taxa de atendimento, latência de entrega, abandono
2. **Fila e outbox** — profundidade de `scheduled_jobs`, idade do evento mais antigo não publicado, taxa de retry
3. **Notificações** — push enviados, entregues e falhados por plataforma; **tokens inválidos** (indicador precoce de morador que desinstalou)
4. **Mídia** — chamadas ativas, banda, falhas de conexão, uso do TURN
**Alertas:**
| Alerta | Condição | Severidade |
|---|---|---|
| Outbox atrasado | evento não publicado > 60s | crítico |
| Fila travada | job vencido > 30s | crítico |
| Push falhando | taxa de falha > 20% em 5min | crítico |
| LiveKit indisponível | circuit breaker aberto | crítico |
| Taxa de atendimento baixa | < 50% num condomínio por 7 dias | negócio |
| Disco do MinIO | > 80% | aviso |
Os dois últimos são de negócio, não de infraestrutura — viram tarefa de customer success, não plantão.
## 7. Backup e restore
```
Postgres pg_dump diário + WAL archiving retenção 30 dias
MinIO replicação de bucket ou snapshot diário retenção 30 dias
Segredos fora do servidor, em cofre
```
**Restore é testado mensalmente em ambiente separado, com o tempo medido.** Backup não verificado não é backup — e aqui perder o banco significa não só perder dados, mas deixar prédios inacessíveis.
## 8. CI/CD
```
push → GitHub Actions
├─ backend: ./gradlew build (unit + Testcontainers)
├─ shared: ./gradlew allTests (JVM + iOS)
├─ web: pnpm lint && pnpm test && pnpm build
│ └─ contraste + orçamento de bundle (falha se visitor > 200KB gzip)
├─ docker compose config (valida o compose)
└─ tag na main → build/push das imagens → deploy por SSH
```
O **orçamento de bundle do visitante é gate de CI**, não recomendação. A escolha de React em vez de Wasm foi motivada por TTI; sem gate automático, essa vantagem evapora em três sprints.
## 9. Modo degradado
Quando o circuit breaker do LiveKit abre, o backend rebaixa automaticamente e emite `DEGRADED_MODE` no WebSocket:
```
LiveKit fora ─► modo AUDIO
└─► modo FOTO_TEXTO (visitante manda foto, morador aprova)
└─► FILA_OPERADOR (resolução por telefone)
Backend fora ─► contingência física (botão + telefone do responsável)
documentada em 07-REQUISITOS-DE-CAMPO.md
```
O nível vigente aparece em `/actuator/health` e no painel admin. **Degradar em silêncio é pior que degradar** — o síndico precisa saber que o sistema está em modo reduzido antes de receber a reclamação.