infra + devops

Vários projetos numa VPS com Caddy, labels Docker e deploy por push

· Vinícius Levi

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

pastao que rodacomo atualiza
/opt/proxyCaddyquase nunca
/opt/entresentidosFastAPI + PostgreSQLpush na main
/opt/republicaFastify + MongoDB + app webdocker compose up
/opt/sitenginx com arquivos estáticosrsync dos arquivos
/opt/statusGatuseditar 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 503 na 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.