infra + devops
Vários projetos numa VPS com Caddy, labels Docker e deploy por push
Este site, a API do EntreSentidos, a demo do RepublicApp com o MongoDB dela e a página de status rodam na mesma VM: 4 vCPUs e 8 GB de RAM. Cada projeto é independente, tem seu próprio domínio com HTTPS e pode ser atualizado sem tocar nos outros. A peça que faz isso funcionar tem umas 30 linhas.
Um proxy só, configurado pelos próprios projetos
As portas 80 e 443 pertencem a um único container: o caddy-docker-proxy. Ele observa o Docker e monta
a configuração do Caddy a partir dos labels dos containers ligados à rede proxy. Para publicar um
serviço, o projeto não edita nada fora da própria pasta:
services:
api:
networks: [default, proxy]
labels:
caddy: republica-api.viniciuslevi.dev.br
caddy.reverse_proxy: "{{upstreams 3000}}"
networks:
proxy:
external: true
O Caddy vê o label, emite o certificado do Let's Encrypt e começa a encaminhar o tráfego. Com um registro DNS curinga
(*.viniciuslevi.dev.br), criar um subdomínio novo é só escrever o label.
Nenhuma porta publicada além do proxy
Os serviços não usam ports:. Isso tem um motivo que pega muita gente: portas publicadas pelo Docker
passam por cima do ufw, porque o Docker escreve as próprias regras de iptables. Um
ports: ["5432:5432"] esquecido deixa o banco aberto para a internet mesmo com o firewall "bloqueando".
Sem ports:, o banco só existe na rede interna do projeto, e a API só é alcançável pelo proxy.
Uma pasta por projeto
| pasta | o que roda | como atualiza |
|---|---|---|
/opt/proxy | Caddy | quase nunca |
/opt/entresentidos | FastAPI + PostgreSQL | push na main |
/opt/republica | Fastify + MongoDB + app web | docker compose up |
/opt/site | nginx com arquivos estáticos | rsync dos arquivos |
/opt/status | Gatus | editar o YAML |
Deploy por push, com o mínimo de privilégio
O EntreSentidos tem deploy contínuo: cada push na main roda os testes no GitHub Actions e, se passarem,
entra na VM por SSH e executa um script que atualiza o código, reconstrói a imagem e espera a API ficar saudável.
O detalhe importante é como o Actions entra. Ele usa um usuário deploy cuja chave, no
authorized_keys, tem um comando forçado: ela só consegue executar o script de deploy. Se a chave vazar
dos secrets do GitHub, o pior que alguém consegue fazer é um deploy da main. A VM lê o repositório com
uma deploy key somente leitura.
Um projeto não pode derrubar os outros
Dividir a máquina tem um risco: um serviço sobrecarregado leva todos junto. O caso real aqui é a transcrição de voz do EntreSentidos, que roda o Whisper na CPU. Algumas pessoas mandando áudio ao mesmo tempo ocupariam os 4 núcleos. Três ajustes resolveram:
api:
cpus: 2 # no máximo metade da máquina
mem_limit: 2g
- Limite de CPU e memória em todo container de demo (o Mongo do RepublicApp, por exemplo, tem 512 MB).
- Rate limit próprio para o endpoint de voz, menor que o do chat.
- No máximo uma transcrição por vez; as excedentes recebem
503na hora em vez de enfileirar.
O básico que não aparece
- SSH só por chave. Login com senha desligado e root apenas por chave. Uma VPS nova recebe tentativas de senha em minutos.
- Monitoramento. O Gatus confere cada serviço a cada 2 minutos, incluindo a validade dos certificados. A seção "No ar agora" da página inicial lê esse status ao vivo.
- Limitação honesta. O monitor roda na mesma VM: se a máquina inteira cair, ele cai junto. O próximo passo é um check externo.
O que eu faria igual de novo
Proxy configurado por labels foi a decisão que mais simplificou o resto. Cada projeto carrega a própria configuração de publicação, e adicionar o quarto serviço deu o mesmo trabalho que o primeiro. Para uma VM pessoal, isso é mais simples que Kubernetes e bem mais organizado que um nginx editado à mão.