Deploy: Cluster (RFC-037)#
Múltiplas réplicas stateless atrás de um load balancer. Todo o estado fica no Postgres e no S3 — qualquer réplica pode atender qualquer requisição sem afinidade de sessão.
Quando usar: alta disponibilidade, zero downtime em deploys, escala horizontal automática.
Pré-requisitos#
- Docker Compose com suporte a YAML anchors (versão ≥ 2.20)
- Postgres acessível por todos os containers
- Bucket S3 (ou MinIO local, como no exemplo)
Configuração#
cd examples/server
cp .env.cluster.example .env.cluster
Edite .env.cluster — todos os segredos devem ser idênticos em todas as réplicas:
POSTGRES_PASSWORD=<senha-segura>
AUTH_SECRET=<openssl rand -hex 32>
EDIT_TOKEN_SECRET=<openssl rand -hex 32>
ENCRYPTION_KEY=<openssl rand -base64 32>
# MinIO local (S3 compatível)
MINIO_USER=minioadmin
MINIO_PASSWORD=minioadmin
S3_BUCKET=sulfite-shares
Subindo com Docker Compose#
docker compose -f docker-compose.cluster.yml --env-file .env.cluster up --build
Isso sobe:
- Traefik (porta 8080) — load balancer com health checks
- MinIO (porta 9000) — S3 compatível local
- Postgres — banco compartilhado
- api1, api2, api3 — 3 réplicas do servidor
Verifique que todas as réplicas estão saudáveis:
curl http://localhost:8080/health # via Traefik (qualquer réplica)
Como funciona#
Cliente → Traefik :8080 → api1 / api2 / api3
↓ ↓
Postgres MinIO (S3)
Cada réplica é completamente stateless. O Traefik distribui as requisições em round-robin com health check automático — se uma réplica cair, o tráfego vai para as demais sem intervenção.
Stores e backends#
| Store | Backend | Env var |
|---|---|---|
| Relatórios | Postgres | STORAGE_BACKEND=postgres |
| Grupos de relatórios | Postgres | mesmo DATABASE_URL dos relatórios |
| Tokens JWT | Postgres | TOKEN_STORE_BACKEND=postgres |
| Job queue | Postgres | JOB_QUEUE_BACKEND=postgres |
| OTP codes | Postgres | OTP_STORE_BACKEND=postgres |
| Share links | Postgres | SHARE_LINK_STORE_BACKEND=postgres |
| Binários de share | S3/MinIO | SHARE_OUTPUT_STORE_BACKEND=s3 |
| Rate limit | Postgres | RATE_LIMIT_STORE_BACKEND=postgres |
| IPs banidos | Postgres | BANNED_IP_STORE_BACKEND=postgres |
| Conexões | Postgres ou hub remoto | CONNECTION_REGISTRY_BACKEND=postgres ou remote |
Cluster mode safety check#
SULFITE_CLUSTER_MODE=true ativa uma verificação no boot: o servidor recusa iniciar se qualquer store estiver configurado com backend
memory, file ou sqlite. Isso previne dados inconsistentes entre réplicas. Para conexões, use
postgres ou remote.
Exemplo de erro:
StateError: Cluster mode safety check failed:
- TOKEN_STORE_BACKEND=memory is not safe for multi-container deployments.
Set TOKEN_STORE_BACKEND=postgres.
Secrets compartilhados#
AUTH_SECRET, EDIT_TOKEN_SECRET e ENCRYPTION_KEY precisam ser idênticos em todas as réplicas. Um token gerado pela api1 precisa ser validável pela api2 e api3.
Migrações de grupos de relatórios#
Em cluster mode o servidor valida o schema no boot, mas não cria tabelas. Antes de subir as réplicas, aplique a migration de grupos de relatórios no Postgres compartilhado:
psql "$DATABASE_URL" \
-f packages/sulfite_server/migrations/005_report_groups.sql
Se report_groups, report_group_memberships ou
report_groups.color_hex estiverem ausentes, a réplica falha cedo com uma
mensagem de migration pendente.
S3 externo (AWS, Cloudflare R2, Backblaze)#
Substitua as variáveis MinIO pelas do seu provedor:
# AWS S3
S3_ENDPOINT=https://s3.us-east-1.amazonaws.com
S3_REGION=us-east-1
S3_BUCKET=sulfite-shares-prod
S3_ACCESS_KEY=AKIA...
S3_SECRET_KEY=...
# Cloudflare R2
S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com
S3_REGION=auto
Deploy em produção#
Para Render, Kubernetes ou ECS, veja Escala horizontal — inclui exemplos de
render.yaml e manifests Kubernetes.
Ver também#
- Escala horizontal (RFC-037) — arquitetura detalhada
- Configuração completa — todas as env vars