O que separa código que roda do produto que sobrevive: DNS, servidores, edge, secrets, backups, LGPD, incidentes, custo. Vinte e quatro capítulos sobre tudo que o livro de engenharia não cobriu — porque é território adjacente, igualmente essencial.
Antes de servidor, antes de aplicação, antes de qualquer coisa: o tráfego encontrar o que você construiu. DNS, proxy, certificados, email. As quatro coisas que parecem triviais e quebram silenciosamente.
Quando você digita um endereço no navegador e ele responde, uma sequência de meia dúzia de sistemas acontece em milissegundos. Conhecer essa sequência não é luxo — é o que diferencia quem debuga incidentes em minutos de quem passa madrugada chutando soluções.
A maioria dos desenvolvedores aprende a internet pelo lado de cima — HTTP, APIs, JSON. Esse capítulo trata do lado de baixo, da parte que normalmente "simplesmente funciona" até o dia que para. DNS, TCP, TLS — cada um pode quebrar de forma silenciosa e cada um tem ferramentas próprias de diagnóstico. Vou cobrir o que vale e omitir o que só importa pra quem opera infraestrutura de telecom.
Em 1969, a ARPANET conectou quatro universidades nos EUA. Computadores se identificavam por número. Conforme a rede cresceu nos anos 70, alguém precisou manter um arquivo chamado HOSTS.TXT com a lista de máquinas e seus endereços. Toda máquina baixava essa lista periodicamente do Stanford Research Institute.
Em 1983, com algumas centenas de hosts, o HOSTS.TXT estava prestes a colapsar. Paul Mockapetris propôs o DNS — Domain Name System. Em vez de uma lista central, uma hierarquia distribuída: cada nível responsável por uma parte. A ideia foi formalizada nas RFCs 882 e 883 (depois substituídas por 1034 e 1035, ainda em uso).
Em 1985, surgiram os primeiros domínios .com: symbolics.com (Symbolics, Inc.) foi o primeiro, registrado em 15 de março. Em 1989, o Brasil registrou o .br e criou o que viraria o registro.br em 1995, operado pelo NIC.br (sem fins lucrativos, modelo único no mundo).
Em 1998, a ICANN foi criada para coordenar os domínios globais (gTLDs). Em 2010, com o esgotamento iminente do IPv4, IPv6 começou a ganhar tração — em 2026, ainda coexistem, mas IPv6 já é maioria do tráfego de mobile no mundo. Em 2018, a RFC 8484 padronizou o DNS over HTTPS (DoH); em 2024, Cloudflare e Google reportam >15% do tráfego DNS já criptografado.
O DNS hoje é praticamente invisível — exatamente como deveria ser. Quando vira problema, é dor; quando funciona, ninguém percebe. Esse capítulo é sobre o quando vira problema.
Você abre o navegador, digita https://meuapp.com.br/pedidos, pressiona Enter. O que acontece nos próximos 200ms?
# Etapa 1 — DNS lookup (tipicamente 10-50ms na primeira vez, 0ms se cacheado) Navegador: "qual o IP de meuapp.com.br?" → Resolver local (do seu ISP ou Cloudflare 1.1.1.1) → Root nameservers (".") → TLD nameservers (".br") → Authoritative nameservers (da Cloudflare, por exemplo) ← "meuapp.com.br = 104.21.42.17" # Etapa 2 — TCP handshake (1 round-trip, ~20-100ms dependendo de distância) Navegador → Servidor: SYN Servidor → Navegador: SYN-ACK Navegador → Servidor: ACK # Conexão TCP estabelecida # Etapa 3 — TLS handshake (1-2 round-trips, ~40-200ms) Navegador → Servidor: "falo TLS 1.3, suporto essas cipher suites" Servidor → Navegador: "OK, aqui meu certificado, vamos usar X" Navegador: valida certificado, deriva chaves # Conexão criptografada estabelecida # Etapa 4 — HTTP request (1 round-trip, ~20-100ms) Navegador → Servidor: GET /pedidos HTTP/2 Host: meuapp.com.br ... Servidor → Navegador: HTTP/2 200 OK + corpo # Total: tipicamente 200-500ms na PRIMEIRA visita. # Visitas subsequentes: 50-150ms (DNS cacheado, conexão pode ser reaproveitada).
Cada uma dessas etapas pode falhar de jeitos diferentes, e cada uma tem ferramenta própria de diagnóstico. Erro de DNS retorna "site não encontrado". Erro de TCP retorna "connection refused" ou timeout. Erro de TLS retorna "certificado inválido". Erro de HTTP retorna status code. Saber em qual etapa o erro está é metade do trabalho de debug.
DNS é um sistema distribuído de tradução de nomes em endereços. Não há servidor central — há uma hierarquia onde cada nível conhece o próximo. Isso é o que permite a internet escalar para bilhões de domínios sem colapsar.
.br é gerenciado pelo NIC.br; .com pela Verisign.
Consulta para meuapp.com.br, sem nada cacheado:
1. Stub resolver → Recursive (1.1.1.1): "qual o A de meuapp.com.br?" 2. Recursive → Root (a.root-servers.net): "qual o A de meuapp.com.br?" ← "não sei, mas o .br é com esses nameservers" 3. Recursive → .br TLD (a.dns.br): "qual o A de meuapp.com.br?" ← "não sei, mas meuapp.com.br tem esses nameservers (kara.ns.cloudflare.com, etc)" 4. Recursive → Authoritative (Cloudflare): "qual o A de meuapp.com.br?" ← "104.21.42.17, TTL 300s" 5. Recursive → Stub: "104.21.42.17" (Recursive guarda em cache por TTL segundos) 6. Stub → Navegador: "104.21.42.17" (Sistema operacional pode cachear também) Daqui pra frente, qualquer consulta nos próximos 300s pega do cache — não passa por esses 4 níveis de novo.
Isso parece complexo mas acontece em milissegundos. E em condições normais, 90% das consultas batem direto no cache do recursive sem ir até a authoritative. É o que torna o DNS rápido na prática.
Um domínio é, na verdade, um conjunto de registros DNS. Cada tipo de registro serve a um propósito. Os que você precisa conhecer:
| Tipo | O que faz | Exemplo de valor |
|---|---|---|
A | Aponta nome para endereço IPv4 | 104.21.42.17 |
AAAA | Aponta nome para endereço IPv6 | 2606:4700:3033::ac43:b1e9 |
CNAME | Alias — aponta nome para outro nome | app.meudominio.com.br → meudominio.com.br |
MX | Servidor de email do domínio | mail.meudominio.com.br (priority 10) |
TXT | Texto arbitrário — usado para SPF, DKIM, verificações | v=spf1 include:_spf.google.com ~all |
NS | Quais nameservers respondem por esse domínio | kara.ns.cloudflare.com |
CAA | Quais autoridades podem emitir certificados pra esse domínio | 0 issue "letsencrypt.org" |
SRV | Serviço específico em porta específica (raro hoje) | _sip._tcp 10 60 5060 sipserver |
PTR | Reverso — IP para nome (configurado pelo dono do IP) | 17.42.21.104.in-addr.arpa |
CNAME parece o registro mais útil — "aponta esse nome pra esse outro nome". Mas tem duas armadilhas que pegam quase todo iniciante:
www.meusite.com tem CNAME, ele não pode ter A, MX, TXT etc. nesse mesmo nome. Por isso você não usa CNAME no domínio raiz (meusite.com) — esse precisa ter MX para email.meusite.com como CNAME viola o padrão. Cloudflare e alguns DNS modernos implementam "CNAME flattening" (resolvem internamente e devolvem o A final), o que contorna a limitação. Mas se você usa registro.br direto, não pode CNAME no apex — precisa A com IP fixo, ou usar nameservers de provedor que faça flattening.
Vários provedores DNS criaram registros próprios pra resolver o problema do apex: ANAME (DNSimple), ALIAS (DNS Made Easy, Cloudflare flatten). Funcionam como CNAME mas no apex. Não são padrão — funcionam só dentro do seu provedor DNS. Se você precisa apontar meusite.com direto para serviço externo (Vercel, Netlify), use o provedor DNS que oferece flattening ou aceite usar IPs fixos.
TTL (Time To Live) é o número de segundos que um recursive resolver pode cachear uma resposta antes de perguntar de novo. É configurado por registro, no servidor authoritative.
Implicação prática: quando você muda um registro, a mudança não propaga imediatamente. Resolvers ao redor do mundo têm a versão antiga em cache até o TTL anterior expirar.
# Cenário comum: vou mudar IP do meu servidor. # IP atual: 1.2.3.4, TTL atualmente 86400 (24h). # Sem planejamento: 14h00 - mudo registro A para 5.6.7.8 14h01 - alguns usuários veem novo IP (cache do resolver expira logo) 22h00 - maioria já vê novo IP 14h00+24h - todos veem novo IP # Com planejamento: 1 dia ANTES da mudança: - mudo TTL de 86400 para 300 (5 minutos) - espero 24 horas (que era o TTL antigo) para garantir que todos os resolvers pegaram a nova versão com TTL curto 14h00 - mudo IP para 5.6.7.8 14h05 - praticamente todos veem novo IP 1 dia DEPOIS, se quiser: - aumento TTL de volta para 3600 ou 86400
| TTL típico | Quando usar |
|---|---|
| 60s (1 min) | Muito baixo. Útil só antes/durante mudanças planejadas. Aumenta carga nos seus nameservers. |
| 300s (5 min) | Bom para serviços ativos que podem mudar (failover, mudança de IP planejada). Janela de propagação aceitável. |
| 3600s (1h) | Padrão razoável para A/AAAA em sistemas com baixa taxa de mudança. |
| 86400s (24h) | Bom para registros que raramente mudam: MX, TXT (SPF/DKIM), CAA. Reduz consultas. |
| 604800s (7 dias) | NS records do TLD podem ter TTL alto. Dentro do seu domínio raramente faz sentido. |
Para registrar um domínio você precisa de um registrar — empresa autorizada pelo órgão responsável pelo TLD. Para .com.br, é o registro.br. Para .com, são dezenas de empresas (GoDaddy, Namecheap, Cloudflare Registrar, Porkbun, etc.).
Modelo único no mundo: o NIC.br é uma entidade sem fins lucrativos, e registro.br é o registrar ÚNICO para .br. Não há "GoDaddy do .br". Você compra direto deles.
Vantagens:
Limitações:
Para domínios genéricos (.com, .dev, .io, .app), você escolhe entre muitos. Em 2026, três opções honestas:
Evite GoDaddy: cheio de upsells, interface confusa, histórico de incidentes de segurança. Funciona, mas há alternativas melhores.
Essa separação (registrar para registro, provedor para DNS) é a configuração mais flexível e robusta. Você pode trocar de provedor DNS sem mexer no registrar; pode trocar de registrar mantendo DNS.
Quando você compra um domínio, ele vem com nameservers do próprio registrar. Pra usar Cloudflare (ou outro), você precisa mudar os nameservers no painel do registrar.
# Antes:
# meudominio.com.br
# NS: a.dnsbr.registro.br
# NS: b.dnsbr.registro.br
# (DNS gerenciado pelo registro.br)
# Você abre conta na Cloudflare, adiciona o domínio, ela gera:
# Nameservers atribuídos: kara.ns.cloudflare.com, walt.ns.cloudflare.com
# No painel do registro.br, em "DNS":
# Substituir os NS antigos pelos da Cloudflare.
# Depois:
# meudominio.com.br
# NS: kara.ns.cloudflare.com
# NS: walt.ns.cloudflare.com
# (DNS agora gerenciado pela Cloudflare)
Após a troca, o TLD (.br) passa a delegar consultas para os nameservers da Cloudflare. Você gerencia registros A/MX/TXT/etc no painel da Cloudflare a partir dali.
dig +trace pra confirmar.Quando algo não funciona, essas ferramentas dizem o que está realmente acontecendo. Aprenda cada uma.
# Consulta simples (A record) $ dig meudominio.com.br # Consulta tipo específico $ dig meudominio.com.br MX $ dig meudominio.com.br TXT $ dig meudominio.com.br NS # Consulta direto num resolver específico (útil pra testar Cloudflare vs ISP) $ dig @1.1.1.1 meudominio.com.br $ dig @8.8.8.8 meudominio.com.br # Trace completo — mostra a hierarquia inteira (root → TLD → authoritative) $ dig +trace meudominio.com.br # Saída curta, só o valor $ dig +short meudominio.com.br 104.21.42.17 # Consulta sem cache (pergunta direto ao authoritative) $ dig @kara.ns.cloudflare.com meudominio.com.br
Funciona em Windows nativo (dig nem sempre). Saída mais simples, menos info técnica. Use quando dig não está disponível.
host meudominio.com.br retorna A, MX, IPv6 em uma linha cada. Bom pra checagem rápida.
# macOS $ sudo dscacheutil -flushcache $ sudo killall -HUP mDNSResponder # Linux (systemd-resolved) $ sudo systemd-resolve --flush-caches # Windows > ipconfig /flushdns # Navegador (Chrome): chrome://net-internals/#dns
Cliente liga: "o site não abre mais." Sem método, você passa horas. Com método, em 5 minutos você sabe se é DNS, rede, certificado ou aplicação.
$ dig +short meuapp.com.br
# Caso A: retorna IP → DNS OK, problema é em outra camada
# Caso B: retorna nada → problema é DNS, vai para passo 2
$ dig +trace meuapp.com.br # Vai mostrar a cadeia inteira: root → .br → cloudflare.com → seu nameserver. # Se quebra em algum ponto, mostra exatamente onde. $ dig NS meuapp.com.br # Confirma quais nameservers o TLD .br tem registrado para você. # Eles batem com o que você configurou na Cloudflare?
$ ping 104.21.42.17 # Caso A: ping responde → conectividade OK, vai para passo 4 # Caso B: timeout → problema é de rede/firewall $ traceroute 104.21.42.17 # Mostra onde a rota está quebrando. $ nc -vz 104.21.42.17 443 # Testa se a porta 443 (HTTPS) está aceitando conexão.
$ curl -v https://meuapp.com.br # Mostra detalhes do TLS handshake + resposta HTTP. # Procure por: # - "SSL certificate problem" → cert expirado/inválido # - "Connection refused" → servidor não escuta # - "Could not resolve host" → DNS de novo # - "200 OK" → tudo OK, problema era do cliente! $ openssl s_client -connect meuapp.com.br:443 -servername meuapp.com.br # Detalhes completos do certificado: validade, emissor, SAN.
Resultado prático: em ~5 minutos você sabe exatamente em qual camada está o problema, e cada camada tem responsável e ferramenta diferentes. Sem esse método, você fica chutando — "será o servidor? a aplicação? o Cloudflare?" — e perde tempo.
Painéis de DNS são confusos. Editar o A do www achando que é o do apex, ou vice-versa. Sempre confira o "nome" do registro: vazio/@ é o apex; www é o subdomínio.
"Vou mudar o IP amanhã, e o TTL está em 86400 (24h)." Mudança vai levar até 24h pra propagar tudo. Reduza TTL pra 300 com 24h de antecedência.
Configurar CNAME em meudominio.com direto no registro.br. Não funciona — registro.br não faz CNAME flattening. Mova para provedor que faz (Cloudflare) ou use A com IP fixo.
Trocou nameservers para Cloudflare, mas esqueceu de copiar os MX records. Email para de funcionar silenciosamente. Sempre faça inventário completo dos registros antes de mudar nameservers.
"Mudei o registro mas meu computador ainda mostra o IP antigo." O resolver do seu sistema/navegador tem cache próprio. Use dig @1.1.1.1 para consultar diretamente, e flush do cache local quando precisar.
"Comprei o domínio na Cloudflare, mas o DNS está no registro.br." Lugares diferentes. Registrar é onde o domínio existe; provedor DNS é onde os registros vivem. Painéis diferentes, podem ser empresas diferentes.
dig meudominio.com.br e recebe o IP correto. O que isso te diz?"Pegue um domínio que você possui (ou um conhecido, tipo google.com) e levante todos os registros principais usando dig: A, AAAA, MX, TXT, NS. Anote o TTL de cada um. O que você descobriu sobre o domínio?
$ dig +short google.com A # 142.250.190.78 (varia por região) $ dig +short google.com AAAA # 2607:f8b0:4004:c1b::65 $ dig +short google.com MX # 10 smtp.google.com. $ dig +short google.com TXT # "v=spf1 include:_spf.google.com ~all" # "google-site-verification=..." # (vários registros TXT) $ dig +short google.com NS # ns1.google.com. ... ns4.google.com. # Para ver TTLs, use sem +short: $ dig google.com # Coluna entre o nome e o tipo é o TTL em segundos.
Observações típicas: TTLs variam (Google usa TTL baixo nos A para load balancing global; alto nos NS); MX aponta para servidor próprio; TXT inclui SPF.
Você vai trocar a VPS amanhã: IP atual 1.2.3.4, novo IP 5.6.7.8. TTL do A record está em 3600 (1h). Cliente quer downtime mínimo. Descreva o plano em fases, incluindo os comandos dig que você usaria para confirmar cada etapa.
Plano em 4 fases:
Fase 1 — 2h antes da migração (mínimo):
dig @1.1.1.1 meusite.com e verificar a coluna TTL — deve estar baixando de 300 para 0 e renovando.Fase 2 — Preparação do novo servidor:
5.6.7.8 e validar manualmente acessando direto pelo IP.Fase 3 — Mudança do A record:
1.2.3.4 para 5.6.7.8.dig +short meusite.com em vários resolvers (@1.1.1.1, @8.8.8.8, @9.9.9.9) ou usar dnschecker.org.Fase 4 — Limpeza:
Downtime real esperado: zero. O que existe é uma janela de minutos onde ambos os servidores precisam estar funcionais (alguns clientes ainda batem no antigo).
Rode dig +trace github.com e analise a saída. Em quais níveis a resolução passa? Quantos nameservers o GitHub usa? Os TTLs no caminho são diferentes — por quê?
A saída de +trace mostra três níveis de delegação:
Por que TTLs diferentes? Quanto mais "alto" na hierarquia (root, TLD), menos as coisas mudam — TTL alto reduz consultas globais. Quanto mais perto da aplicação (A record do github.com), mais flexibilidade pode ser útil — GitHub usa TTL baixo para conseguir mudar IP rapidamente em caso de incidente ou load balancing.
Cenário: você administra app.empresa.com.br. Cliente reporta que o site "não abre" para alguns usuários, mas você acessa normalmente do seu computador. Você suspeita de DNS. Descreva passo a passo o que vai investigar, quais comandos roda, e o que cada resultado descarta ou confirma.
Hipóteses iniciais: propagação inconsistente, cache desatualizado em alguns ISPs, registro com TTL muito baixo causando lookup demorado, ou ataque/sequestro DNS direcionado.
Passo 1 — Resposta em vários resolvers públicos:
$ dig @1.1.1.1 app.empresa.com.br
$ dig @8.8.8.8 app.empresa.com.br
$ dig @9.9.9.9 app.empresa.com.br
$ dig @208.67.222.222 app.empresa.com.br # OpenDNS
Se todos retornam mesmo IP → DNS está consistente; problema é outro. Se diferem → propagação inconsistente.
Passo 2 — Verificar nameservers authoritative diretamente:
$ dig NS empresa.com.br +short
# Lista nameservers
$ dig @ns1.dnsprovider.com app.empresa.com.br
$ dig @ns2.dnsprovider.com app.empresa.com.br
Verifica que os authoritative servers todos respondem consistentemente.
Passo 3 — Usar dnschecker.org:
Mostra resposta em ~30 cidades pelo mundo. Identifica regiões com cache antigo.
Passo 4 — Pedir ao usuário com problema:
nslookup app.empresa.com.br dele?cat /etc/resolv.conf no Linux/Mac)dig @1.1.1.1 app.empresa.com.br — bypassa o resolver do ISPDiagnósticos possíveis:
@1.1.1.1 resolve esse caso.Cloudflare é a ferramenta de maior alavancagem que existe para quem opera infra solo ou em time pequeno. Faz o trabalho de CDN, WAF, DDoS protection, DNS, certificado SSL, e analytics — tudo no plano gratuito. Quem não usa, está deixando dinheiro e segurança na mesa.
Você vai usar Cloudflare. A questão é se vai usar bem ou superficial. Esse capítulo cobre o que importa: setup correto desde o primeiro dia, configuração de proxy, SSL/TLS sem se queimar, regras úteis, e quando o gratuito basta e quando vale pagar. Vou ser honesto: cobre tudo o que projetos solo/pequenos precisam dela. Quando você cresce, paga por features específicas — mas isso é problema para depois.
Em 2009, Matthew Prince, Lee Holloway e Michelle Zatlyn fundaram a Cloudflare. A ideia inicial veio de um projeto anterior — o Project Honey Pot, que rastreava spammers. Eles perceberam que dava pra fazer isso em escala: proxy reverso global que protegeria sites e aceleraria conteúdo.
Em 2010, lançaram com modelo gratuito ousado: qualquer um podia usar CDN + proteção básica de graça. Em 2014, lançaram Cloudflare DNS — primeiro a oferecer DNS gerenciado grátis com performance global. Em 2017, o resolver público 1.1.1.1 (parceria com APNIC) virou referência de resolver rápido e privacy-first.
Em 2017-2019, a Cloudflare começou a montar a "plataforma": Workers (edge computing, 2017), Workers KV, Pages (hosting estático, 2020), R2 (storage S3-compatible sem custo de egress, 2022), D1 (SQLite distribuído, 2023). De CDN, virou plataforma serverless de borda.
Hoje (2026), Cloudflare atende ~20% do tráfego web global, opera em mais de 320 cidades, e processa mais de 50 milhões de HTTP requests por segundo. O plano gratuito segue. O modelo é freemium honesto: gratuito é gratuito de verdade; planos pagos adicionam features específicas para empresas que crescem.
Em 2024-2025, com a explosão de AI bots fazendo scraping, a Cloudflare lançou ferramentas específicas para bloquear AI crawlers (AI Bot Management, AI Audit). Em junho de 2025, anunciou o "Pay-per-crawl" — modelo onde sites podem cobrar AI bots por acesso. Vai se desenvolvendo a parte mais recente da história.
Confusão comum: "Cloudflare é CDN, certo?" É, mas é mais. Em essência, Cloudflare é um proxy reverso global. Quando você ativa Cloudflare num domínio, todo o tráfego passa por ela antes de chegar ao seu servidor.
# Sem Cloudflare: Usuário → DNS resolve → IP do seu servidor → Sua app responde # Com Cloudflare (proxy ativado): Usuário → DNS resolve → IP da Cloudflare (edge) → Cloudflare faz tudo abaixo, e só então chama seu servidor: ┌──────────────────────────┐ │ • Termina TLS (HTTPS) │ │ • Bloqueia bots óbvios │ │ • Aplica WAF (regras) │ │ • Serve cache se tiver │ │ • Aplica rate limit │ │ • Loga analytics │ └────────────┬─────────────┘ ↓ IP do seu servidor → Sua app responde
Implicações:
O processo completo, do nada à proteção ativa, leva 15-30 minutos. Vou passar cada passo.
Vá em cloudflare.com, crie conta gratuita. Email + senha. Ative 2FA imediatamente — sua conta Cloudflare controla seu domínio inteiro, então tratamento de segurança é nível "conta de banco".
No dashboard, "Add site". Digite seu domínio (ex: meudominio.com.br). Cloudflare:
kara.ns.cloudflare.com e walt.ns.cloudflare.com).dig completo no domínio atual e compare com o que a Cloudflare importou. Adicione o que faltou.
No painel do registro.br (ou Porkbun, ou onde quer que seu domínio esteja registrado), substitua os nameservers atuais pelos dois da Cloudflare. Salva. Aguarda propagação — pode levar minutos a horas.
A Cloudflare verifica automaticamente. Quando os nameservers .br confirmarem que os NS oficiais são os deles, o site fica "Active" no painel. Daí pra frente, você gerencia DNS pelo painel da Cloudflare.
Cada registro DNS na Cloudflare tem um ícone de "nuvem": laranja (proxy ativo) ou cinza (DNS apenas, sem proxy). Por default, A/AAAA/CNAME ficam laranja.
ssh.meusite.com), serviços não-HTTP.@ (apex) → IP do servidorwww → IP do servidorapp → IP do servidorapi → IP do servidormail, smtp, imapssh, vpn, dbEsse é o passo onde mais gente erra. Detalhamento próprio adiante.
A regra geral: se é HTTP/HTTPS, deixe laranja. Se é qualquer outro protocolo, deixe cinza.
Por quê: o proxy da Cloudflare só sabe falar HTTP/HTTPS. Se você ativar laranja em ssh.meusite.com apontando para seu servidor SSH, o SSH para de funcionar (Cloudflare não sabe encaminhar SSH — ela bloqueia tudo que não é HTTP).
Casos comuns:
| Registro | Tipo | Proxy | Por quê |
|---|---|---|---|
@ (apex) | A | Laranja | HTTP/HTTPS do site principal |
www | A ou CNAME | Laranja | HTTP/HTTPS |
app, api | A | Laranja | HTTP/HTTPS |
| Registros MX | MX | (não se aplica) | Email, não HTTP |
mail, smtp | A | Cinza | Servidor de email, protocolos SMTP/IMAP |
ssh, console | A | Cinza | SSH precisa do IP real |
| Validação de domínio (Google, etc) | TXT | (não se aplica) | Não é A/CNAME |
ssh.meusite.com? Use Cloudflare Tunnel (gratuito, capítulo futuro) ou simplesmente conecte por IP direto via VPN/wireguard. Expor SSH via nome cinza é OK em desenvolvimento, mas em produção tem alternativas melhores.
Cloudflare oferece 4 modos de SSL/TLS no menu SSL/TLS → Overview. Cada um define como o tráfego é criptografado em duas pontas: usuário ↔ Cloudflare, e Cloudflare ↔ seu servidor.
# Off # Usuário → Cloudflare: HTTP (não criptografado) # Cloudflare → Servidor: HTTP # Estado: inaceitável em 2026. Não use. # Flexible # Usuário → Cloudflare: HTTPS (criptografado) # Cloudflare → Servidor: HTTP (não criptografado) # Estado: PIOR que Off — dá falsa sensação de segurança ao usuário # enquanto tráfego ainda passa em claro internamente. Não use. # Full # Usuário → Cloudflare: HTTPS # Cloudflare → Servidor: HTTPS, mas Cloudflare NÃO valida certificado # Estado: aceita certificado self-signed. Útil para setup, mas vulnerável # a MITM dentro da rede do servidor. Não use em produção. # Full (strict) # Usuário → Cloudflare: HTTPS # Cloudflare → Servidor: HTTPS, com VALIDAÇÃO do certificado # Estado: o único modo aceitável em produção. Sempre esse.
Parece contra-intuitivo, mas é verdade. Em Off, o navegador do usuário avisa "site não seguro" — usuário sabe que algo não está bem. Em Flexible, o cadeado verde aparece para o usuário, mas o tráfego entre Cloudflare e seu servidor é HTTP puro — qualquer intermediário na rede (provedor da VPS, datacenter, qualquer hop) pode ler/modificar. Pior dos mundos: segurança visual, sem segurança real.
Você precisa de certificado válido no seu servidor. Três opções:
Configuração recomendada para projetos novos: use Cloudflare Origin Certificate. Simples, válido por 15 anos, sem dor de renovação. Vamos cobrir detalhes operacionais nos Caps 4 e 11.
Em SSL/TLS → Edge Certificates, ative Always Use HTTPS. Isso força redirect de qualquer requisição HTTP para HTTPS no edge da Cloudflare. Sem isso, alguém que acesse http://meudominio.com.br bate em HTTP — você pode até estar com Full strict, mas o link inicial fica fora do túnel.
HSTS (HTTP Strict Transport Security) é header que diz ao navegador "nunca mais aceite HTTP desse domínio, sempre HTTPS, por X meses". Cloudflare oferece configuração simples em SSL/TLS → Edge Certificates → HSTS.
Cuidado: uma vez ativado com max-age grande (ex: 1 ano), você fica preso. Se algum dia tiver problema de HTTPS e quiser servir HTTP temporariamente, navegadores não vão deixar. Ative só depois de meses de operação estável. Para projetos novos, deixe HSTS desligado por algum tempo.
Cloudflare permite criar regras que executam no edge: redirects, mudanças de cache, headers, bloqueios. Há duas APIs: Page Rules (mais antiga, sendo descontinuada) e Rulesets (nova, mais poderosa).
No plano gratuito, você tem 3 Page Rules. Use bem.
# Regra 1: Redirect apex para www (ou vice-versa)
# Match: meudominio.com.br/*
# Action: Forwarding URL → 301 → https://www.meudominio.com.br/$1
# Regra 2: Cache agressivo para assets estáticos
# Match: *meudominio.com.br/static/*
# Action: Cache Level → Cache Everything
# Edge Cache TTL → 1 month
# Regra 3: Bypass cache em endpoints dinâmicos
# Match: *meudominio.com.br/api/*
# Action: Cache Level → Bypass
Rulesets (Configuration Rules, Cache Rules, Origin Rules) substituem Page Rules e são mais flexíveis. No plano gratuito você ganha algumas delas; vale entrar em Rules → Overview e ver o que está disponível.
Cache é onde Cloudflare entrega valor desproporcional. Por default, ela já cacheia muitas extensões de arquivo: .css, .js, .jpg, .png, fontes. Mas tem comportamentos importantes a entender.
Cloudflare tem uma lista de extensões "estáticas" que ela cacheia automaticamente, respeitando headers do servidor (Cache-Control, Expires). HTML não é cacheado por default — porque normalmente é dinâmico.
Toda resposta passada pela Cloudflare tem header cf-cache-status:
HIT — serviu do cache. Ótimo.MISS — primeira vez, pegou do servidor, salvou no cache.EXPIRED — tinha em cache, mas expirou. Vai buscar de novo.BYPASS — não cacheou (por configuração ou headers).DYNAMIC — Cloudflare considerou dinâmico, não cacheia.$ curl -I https://meudominio.com.br/static/main.css HTTP/2 200 content-type: text/css cf-cache-status: HIT # servido do cache do edge age: 8472 cache-control: public, max-age=2592000
Para sites com conteúdo predominantemente público e estático (blog, marketing site, documentação), você pode usar Page Rule com Cache Level: Cache Everything + TTL longo. Toda página fica em cache.
Para apps com usuários autenticados, NÃO use. Cache vai servir conteúdo de um usuário para outro. Catastrófico.
Quando você atualiza um arquivo e quer derrubar o cache: vá em Caching → Configuration → Purge Cache. Pode purgar tudo (use com cuidado) ou URLs específicas. API também existe — útil pra integrar no seu CI/CD.
No plano gratuito, Cloudflare já entrega:
Em Security → Settings → Security Level:
Cloudflare Turnstile é alternativa ao reCAPTCHA do Google. Gratuita, privacy-friendly, funciona melhor para usuário (geralmente passa sem clique). Em Turnstile, gere site key e secret key. Implementa em formulários (signup, login) como faria com reCAPTCHA.
Honestamente: para 90% dos projetos individuais e startups iniciais, o plano gratuito basta. O que muda no Pro ($20/mês), Business ($200/mês), Enterprise (custom):
| Feature | Free | Pro | Business+ |
|---|---|---|---|
| DDoS protection | Ilimitado | Ilimitado | Ilimitado |
| SSL/TLS | ✓ | ✓ | ✓ |
| WAF Managed Rules | Básico | OWASP Core Rule Set | Custom rules |
| Cache Rules | 10 | 25 | 50+ |
| Page Rules | 3 | 20 | 50+ |
| Image Optimization (Polish) | — | ✓ | ✓ |
| Mirage (image speed) | — | ✓ | ✓ |
| Argo Smart Routing | — | $5/mo addon | $5/mo addon |
| Cache Reserve | — | — | $5/mo addon |
| Custom WAF rules | — | — | ✓ |
| SLA | — | — | 100% uptime |
Quando vale o Pro:
Para start, comece no gratuito. Mude quando dor concreta justificar.
Você ativou proxy laranja. Cloudflare protege. Mas seu servidor ainda está exposto em IP público — se alguém descobrir o IP real, conecta direto, bypassa Cloudflare inteira.
A defesa: configure firewall do servidor para aceitar HTTP/HTTPS apenas dos ranges de IP da Cloudflare. Conexões diretas (sem passar pela Cloudflare) são bloqueadas.
# Cloudflare publica os ranges atualizados em: # https://www.cloudflare.com/ips-v4 # https://www.cloudflare.com/ips-v6 # Script para configurar ufw aceitando só Cloudflare: #!/bin/bash ufw default deny incoming # SSH primeiro (cuidado pra não se trancar fora) ufw allow 22/tcp # HTTP/HTTPS apenas dos IPs da Cloudflare for ip in $(curl -s https://www.cloudflare.com/ips-v4); do ufw allow from $ip to any port 80,443 proto tcp done for ip in $(curl -s https://www.cloudflare.com/ips-v6); do ufw allow from $ip to any port 80,443 proto tcp done ufw enable ufw status numbered
Vamos cobrir firewall em profundidade no Cap 8. Por agora, registre: ativar Cloudflare proxy não é suficiente sem fechar o servidor para o resto do mundo.
Você acabou de comprar meuapp.com.br no registro.br. Tem uma VPS na Hetzner com IP 5.6.7.8. Aplicação FastAPI rodando em porta 8000 atrás de Nginx na porta 80. Vamos configurar Cloudflare corretamente.
meuapp.com.br registrado no registro.br5.6.7.8, Nginx servindo na 80, app na 8000 atrás delemeuapp.com.brkara.ns.cloudflare.com, walt.ns.cloudflare.comNo painel da Cloudflare, adicionar/confirmar:
Tipo Nome Valor Proxy
A @ 5.6.7.8 Laranja
A www 5.6.7.8 Laranja
A ssh 5.6.7.8 Cinza # SSH precisa IP real
No painel do registro.br, ir em "DNS" do domínio, substituir nameservers pelos da Cloudflare. Salvar. Aguardar propagação (~15min a algumas horas).
Aguardar Cloudflare confirmar "Active". Daí:
Importante: Flexible é estado temporário. Próximo passo cura.
Na Cloudflare: SSL/TLS → Origin Server → Create Certificate. Aceitar defaults (15 anos, ECC). Gerar.
Copiar conteúdo do Certificate e Private Key. No servidor:
# Salvar: sudo nano /etc/ssl/certs/cloudflare-origin.pem # colar certificate sudo nano /etc/ssl/private/cloudflare-origin.key # colar private key sudo chmod 600 /etc/ssl/private/cloudflare-origin.key # Configurar Nginx para HTTPS: sudo nano /etc/nginx/sites-enabled/meuapp # Dentro: server { listen 443 ssl http2; server_name meuapp.com.br www.meuapp.com.br; ssl_certificate /etc/ssl/certs/cloudflare-origin.pem; ssl_certificate_key /etc/ssl/private/cloudflare-origin.key; location / { proxy_pass http://localhost:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } sudo nginx -t && sudo systemctl reload nginx
Agora que servidor tem cert válido, na Cloudflare: SSL/TLS → Overview → Full (strict). Testar: curl -v https://meuapp.com.br — deve funcionar sem warning, cert válido.
Configurar ufw aceitando 80/443 só dos ranges Cloudflare (script da seção anterior). Manter porta 22 aberta para seu IP, vamos cuidar no Cap 7 e 8.
Resultado: domínio com HTTPS válido, IP do servidor escondido, DDoS protection ativo, bots básicos bloqueados, edge cache funcionando, custo R$ 0/mês. Tempo total: 1 a 2 horas se for sua primeira vez.
Tráfego usuário→Cloudflare criptografado, mas Cloudflare→servidor em HTTP puro. Cadeado aparece, mas a segurança é teatro. Sempre Full (strict).
Ativou proxy no registro smtp ou ssh. SSH para de funcionar; email vira problema. Lembre da regra: HTTP/HTTPS → laranja; resto → cinza.
Troca de nameservers, esquece de configurar MX na Cloudflare, email para. Sempre adicione registros completos ANTES de mudar nameservers no registrar.
Cloudflare escondendo IP real. Mas se atacante descobrir o IP (vazamento por email, headers, histórico), conecta direto. Sempre feche firewall pra aceitar 80/443 só de IPs Cloudflare.
Ativou HSTS com max-age 1 ano no primeiro mês de operação. Tem problema de cert, quer voltar para HTTP temporariamente — não consegue. HSTS depois de meses estáveis.
Configurou Page Rule "Cache Everything" no site inteiro. Cache do edge serve página de um usuário para outro. Vazamento de sessão.
Conta Cloudflare controla seu DNS inteiro. Sequestro da conta = sequestro do domínio = redirecionamento de email, fake site, tudo. 2FA é obrigatório, não opcional.
meuapp.com.br com proxy laranja. Modo SSL é Flexible. O cadeado aparece no navegador do usuário. Por que essa configuração é problemática?"Para cada registro DNS, decida proxy laranja (proxied) ou cinza (DNS only):
@ A 5.6.7.8 (apex apontando para servidor web)mail A 5.6.7.8 (subdomínio para servidor SMTP)api A 5.6.7.8 (subdomínio da API REST)db A 5.6.7.8 (Postgres, para acesso direto)blog CNAME meublog.ghost.io (blog hospedado em Ghost)_dmarc TXT "v=DMARC1..." (registro de DMARC)vpn A 5.6.7.8 (servidor wireguard na porta 51820)Você herdou um projeto. Os usuários reclamam que veem "Aviso de Segurança" eventualmente. Cloudflare está ativa. O que você verifica? Liste comandos e configurações.
Investigação:
$ openssl s_client -connect meuapp.com.br:443 -servername meuapp.com.br # Verificar: emissor (deveria ser Cloudflare Inc ECC CA-3 ou similar), # datas (notBefore, notAfter), CN/SAN cobrindo o domínio. $ curl -vI https://meuapp.com.br 2>&1 | grep -E "SSL|cert|server" # Procurar mensagens de erro de cert.
meuapp.com.br, outros www.meuapp.com.br. O certificado cobre ambos? Universal SSL cobre apex + 1 nível; subdomínios extras precisam configuração.Você tem um site Astro (build estático) servido via Cloudflare. Estrutura:
/, /sobre, /posts/* — HTML gerado no build, raramente muda/_astro/* — JS/CSS com hash no nome (ex: main.abc123.js)/api/* — proxy para serverless function, sempre dinâmico/sitemap.xml — gerado, muda quando publica post novoDescreva estratégia de cache: 3 Page Rules para usar bem o limite gratuito.
# Page Rule 1 — Assets imutáveis (com hash no nome) # Match: *meusite.com.br/_astro/* # Setting: Cache Level → Cache Everything # Setting: Edge Cache TTL → 1 year (max disponível) # Setting: Browser Cache TTL → 1 year # Justificativa: hash no nome significa que mudança de arquivo # gera nome novo. Cache eterno é seguro. # Page Rule 2 — Bypass cache em endpoints dinâmicos # Match: *meusite.com.br/api/* # Setting: Cache Level → Bypass # Justificativa: sempre dinâmico, cache só atrapalha. # Page Rule 3 — Cache de páginas HTML # Match: meusite.com.br/* # Setting: Cache Level → Cache Everything # Setting: Edge Cache TTL → 1 hour # Setting: Browser Cache TTL → 5 minutes # Justificativa: páginas mudam raramente; 1h no edge é OK. # Browser TTL menor permite que mudanças apareçam em ~5min. # Quando publicar post novo: purge manual. # OBS: para o sitemap.xml, não precisa regra própria — # a Regra 3 vai cobrir. Quando publicar, faz purge. # Resultado: ~95% das requests servidas direto do edge, # servidor de origem raramente tocado.
Observação: Page Rules estão sendo descontinuadas em favor de Cache Rules / Rulesets. A lógica é a mesma, sintaxe diferente.
Você herda projeto onde Cloudflare está configurada. Liste 10 checks que você faria pra verificar se está configurada corretamente, e o que cada check valida.
| # | Check | O que valida |
|---|---|---|
| 1 | SSL/TLS Mode | Está em "Full (strict)"? Outros modos são problema. |
| 2 | Always Use HTTPS | Ativo? Garante redirect de HTTP para HTTPS. |
| 3 | 2FA na conta Cloudflare | Ativado para todos os admins? |
| 4 | Audit log de mudanças DNS | Account → Audit Log. Confere mudanças recentes não autorizadas. |
| 5 | Proxy laranja nos registros HTTP | Todos os registros A/CNAME que apontam para web estão proxied? |
| 6 | Proxy cinza em SMTP/SSH | Registros não-HTTP não estão laranja. |
| 7 | Firewall do servidor aceita só Cloudflare IPs | Testa conexão direta no IP do servidor — deveria recusar 80/443. |
| 8 | Certificate Validity | Universal SSL ativo, sem warnings. Origin Certificate (se usado) ainda válido. |
| 9 | Bot Fight Mode | Security → Bots. Ativado se faz sentido para o uso. |
| 10 | Cache Rules / Page Rules | Não tem "Cache Everything" em rota com conteúdo personalizado por usuário? |
| 11 | Email Routing ou MX | Email funciona? dig MX mostra registros corretos? |
| 12 | DNSSEC | Cloudflare oferece DNSSEC gratuito. Ativado? Adicionado ao registrar? |
Email é o protocolo mais subestimado da internet. Parece simples — "manda mensagem, chega" — e na verdade tem três camadas de autenticação, sistemas de reputação opacos, e mil formas silenciosas de quebrar. Quem não trata com cuidado, ou não consegue enviar email, ou consegue mas ninguém recebe.
Você vai precisar enviar email — confirmação de cadastro, reset de senha, notificações, faturas. E vai precisar receber email no seu domínio — contato@meusite.com.br, suporte@meusite.com.br. Esse capítulo cobre os dois lados sem você precisar virar especialista em SMTP. Vou ser direto sobre o que vale e o que é dor desnecessária.
Email é mais antigo que a web. Ray Tomlinson enviou o primeiro email entre máquinas em 1971, na ARPANET. Escolheu o caractere @ para separar usuário e máquina — escolha pragmática que sobreviveu 55 anos. Em 1982, a RFC 821 padronizou o SMTP (Simple Mail Transfer Protocol), que ainda é a base de toda transmissão de email hoje.
Nos anos 90 e início dos 2000, spam virou epidemia. SMTP foi projetado em uma era de confiança mútua entre instituições; não tinha autenticação nenhuma. Qualquer servidor podia enviar qualquer email se passando por qualquer pessoa. Spammers usaram exatamente isso.
A resposta veio em três camadas, ao longo de duas décadas. SPF (Sender Policy Framework, RFC 4408 em 2006, atualizado em RFC 7208 em 2014): autoriza quais servidores podem enviar email pelo seu domínio. DKIM (DomainKeys Identified Mail, RFC 6376 em 2011): assina criptograficamente cada email, provando que veio do servidor autorizado. DMARC (RFC 7489 em 2015): política que diz o que fazer quando SPF ou DKIM falham, e reporta tentativas de fraude.
Em fevereiro de 2024, Google e Yahoo começaram a exigir SPF + DKIM + DMARC de qualquer remetente que enviar mais de 5000 emails por dia para usuários deles. Em 2025, Microsoft adotou requisitos similares. Quem não tem esses três configurados, simplesmente não entrega email no Gmail/Outlook. Não vai pro spam — não chega.
Em paralelo, rodar servidor de email próprio virou inviável para a maioria. IPs novos têm "reputação zero" — provedores grandes (Gmail, Outlook) marcam como spam por padrão. Levar reputação para um nível decente leva meses de envio consistente e gera custo e expertise que não compensa. Por isso, em 2026, a recomendação universal é: não rode servidor SMTP próprio. Use serviços especializados.
Três fatos que explicam quase toda dor:
contato@meusite.com.br é um problema; enviar de noreply@meusite.com.br é outro. Frequentemente usa serviços diferentes.
Quando alguém envia email para voce@meusite.com.br, o servidor SMTP do remetente faz consulta DNS:
$ dig MX meusite.com.br +short 10 mail.meusite.com.br. 20 mail2.meusite.com.br.
Os números (10, 20) são prioridades. Menor número = maior prioridade. O remetente tenta o de prioridade 10 primeiro; se falhar, tenta o 20. Permite redundância.
Os valores apontam para nomes — o remetente vai resolver esses nomes em IPs e fazer SMTP. Importante: MX records apontam para nomes, não para IPs diretamente.
| Cenário | MX records |
|---|---|
| Google Workspace | 1 ASPMX.L.GOOGLE.COM.5 ALT1.ASPMX.L.GOOGLE.COM.5 ALT2.ASPMX.L.GOOGLE.COM.... |
| Microsoft 365 | 0 meusite-com-br.mail.protection.outlook.com. |
| Zoho Mail | 10 mx.zoho.com.20 mx2.zoho.com.50 mx3.zoho.com. |
| Fastmail | 10 in1-smtp.messagingengine.com.20 in2-smtp.messagingengine.com. |
| Cloudflare Email Routing | 1 isaac.mx.cloudflare.net. (e outros) |
| Migadu (recomendado para BR) | 10 aspmx1.migadu.com.20 aspmx2.migadu.com. |
Se você não pretende receber email no domínio (só envia), pode configurar MX null:
# Null MX (RFC 7505): diz explicitamente "não aceito email aqui" MX 0 . # Servidores remetentes que respeitam RFC entendem que devem rejeitar # o email imediatamente, sem retry. Reduz spam para você.
SPF (Sender Policy Framework) é um registro TXT no DNS que lista quais servidores podem enviar email em nome do seu domínio. Quando um servidor de destino recebe email "de" algo@meusite.com.br, ele consulta o SPF e verifica se o servidor que está enviando está autorizado.
# Exemplo de SPF — registro TXT no apex do domínio: meusite.com.br. TXT "v=spf1 include:_spf.google.com include:sendgrid.net ~all" # Decomposição: # v=spf1 → versão 1 (única que existe na prática) # include:_spf.google.com → autoriza servidores listados pelo Google # include:sendgrid.net → autoriza servidores do SendGrid # ~all → tudo o mais: softfail (suspeito mas aceita)
| Mecanismo | Significado |
|---|---|
include:dominio | Importa o SPF de outro domínio (forma mais comum) |
ip4:1.2.3.4 | Autoriza IP específico |
ip4:1.2.3.0/24 | Autoriza range de IPs |
a | Autoriza o IP do registro A do próprio domínio |
mx | Autoriza os IPs dos servidores MX |
-all | Hard fail: rejeita o resto |
~all | Soft fail: aceita mas marca como suspeito |
?all | Neutro: sem opinião |
+all | Aceita qualquer um (NUNCA use — quebra o sentido do SPF) |
SPF impõe um máximo de 10 consultas DNS para resolver tudo. Cada include:, a, mx conta. Excedendo, o registro inteiro é considerado inválido (PermError). Use uma ferramenta como dmarcian.com/spf-survey para verificar.
Só pode haver UM SPF por domínio. Adicionou novo provedor de email e criou novo TXT? Inválido. Combine tudo num registro só.
Cada string TXT tem limite de 255 chars. SPFs longos viram problema. A solução é dividir em strings separadas dentro do mesmo registro (sintaxe DNS aceita), ou usar redirect.
# Recebe no Google Workspace, envia transacional pelo Resend: "v=spf1 include:_spf.google.com include:_spf.resend.com ~all" # Recebe no Migadu, envia transacional pelo SES da AWS: "v=spf1 include:_spf.migadu.com include:amazonses.com ~all" # Não envia email do domínio (só recebe): "v=spf1 -all"
DKIM adiciona assinatura digital a cada email enviado. O servidor de envio assina o conteúdo com uma chave privada; o servidor de recebimento busca a chave pública no DNS e valida.
# 1. Você configura serviço (Resend, SendGrid, Google, etc). # 2. Ele te dá um TXT record para adicionar no seu DNS: selector1._domainkey.meusite.com.br. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA..." # 3. Quando o serviço envia email "de" meusite.com.br, adiciona header: DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=meusite.com.br; s=selector1; h=from:to:subject:date; bh=...; b=... # 4. Servidor de recebimento faz: - lê "d=meusite.com.br" e "s=selector1" - consulta DNS: dig TXT selector1._domainkey.meusite.com.br - obtém a chave pública - valida assinatura # Se válido: email confirmadamente vem de servidor autorizado. # Se inválido: marcado como suspeito ou rejeitado.
selector1, google, k1, etc). Você pode ter vários simultaneamente.DMARC junta SPF e DKIM e define o que fazer quando algum deles falha. É o registro mais importante hoje (e o mais frequentemente esquecido).
# DMARC vai no subdomínio _dmarc: _dmarc.meusite.com.br. TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@meusite.com.br; pct=100; adkim=s; aspf=s" # Decomposição: # v=DMARC1 → versão # p=quarantine → política: o que fazer com falhas # rua=mailto:dmarc@meusite.com.br → onde mandar relatórios agregados (diários) # pct=100 → aplica a 100% dos emails # adkim=s → DKIM em modo strict # aspf=s → SPF em modo strict
p=none: não faz nada com falhas, mas reporta. Use no início para monitorar sem bloquear.p=quarantine: manda falhas para spam. Recomendado quando você tem confiança nos seus SPF/DKIM.p=reject: rejeita falhas no servidor. Maior nível, recomendado quando tudo estiver maduro.p=none com rua= apontando para email seu. Monitora relatórios. Ajusta SPF e DKIM até zero falhas.p=quarantine; pct=10. Manda 10% das falhas para spam. Observe se quebra algo legítimo.pct para 50, depois 100.p=reject com pct=100. Maturidade total.p=reject sem ter SPF e DKIM cobrindo todos os serviços que enviam emails do seu domínio, emails legítimos vão sumir. Algum SaaS que envia notificações em seu nome (Stripe, Pipedrive, suporte ao cliente) — todos precisam estar autorizados. Por isso o fluxo começa com p=none e relatórios.
Os relatórios são XML, enviados diariamente. Lê na mão é doloroso. Serviços que processam pra você:
rua, processa, manda relatório semanal legível por email.Para projeto solo/pequeno: comece com Postmark DMARC Monitoring. Simples, eficaz, grátis.
Quando sua aplicação envia email (confirmação de cadastro, reset de senha, notificações, faturas), você usa um serviço de email transacional. Esses serviços já têm IPs com boa reputação, gerenciam SPF/DKIM, escalam, e expõem API simples.
| Serviço | Free tier | Preço acima | Notas |
|---|---|---|---|
| Resend | 3.000/mês | $20/mês = 50k | API moderna, foco em devs, melhor experiência de onboarding. Recomendado. |
| AWS SES | 62.000/mês (se enviado de EC2) | $0.10 por 1.000 | Barato, escalável. Setup mais complexo. Sem painel próprio (você precisa montar). |
| Postmark | 100/mês | $15/mês = 10k | Focado em transacional. Excelente entregabilidade. Premium pequeno-médio. |
| SendGrid | 100/dia | $20/mês = 50k | Tradicional, robusto. Painel cheio. Twilio comprou. |
| Mailgun | (sem free permanente) | $15/mês = 10k | API boa. Bem documentado. |
| Brevo (ex-Sendinblue) | 300/dia | €19/mês = 20k | Mistura transacional + marketing. Europeu. |
import resend import os resend.api_key = os.environ["RESEND_API_KEY"] def enviar_confirmacao(email_destino: str, link_confirmacao: str): response = resend.Emails.send({ "from": "noreply@meusite.com.br", "to": email_destino, "subject": "Confirme seu cadastro", "html": f""" <h1>Bem-vindo!</h1> <p>Clique para confirmar: <a href="{link_confirmacao}">Confirmar</a></p> """, }) return response["id"] # id do envio, útil pra rastrear
contato@meusite.com.br
Diferente do transacional, "email pessoal" no domínio é onde você recebe emails (e responde). Quem visita seu site e usa contato@ precisa de uma caixa real.
| Provedor | Preço por usuário/mês | Notas |
|---|---|---|
| Cloudflare Email Routing | Grátis | Só forwarding (não tem caixa própria). Encaminha para seu Gmail/Outlook pessoal. |
| Migadu | $19/ano (Mini) ou $90/ano (Standard) | Preço por domínio, não por usuário. Múltiplas caixas no mesmo plano. Excelente custo-benefício. |
| Fastmail | $5/mês básico | Reputação altíssima, foco em privacidade, interface excepcional. |
| Zoho Mail | Grátis até 5 usuários (1 domínio) | Free tier muito generoso. Painel datado mas funcional. |
| Google Workspace | $7+/mês | Gmail no seu domínio. Caro mas familiar. |
| Microsoft 365 | $6+/mês | Outlook no seu domínio. Bom se você usa o ecossistema MS. |
contato@ que cai no meu Gmail pessoal: Cloudflare Email Routing. Grátis. 5 minutos de setup. Cap 3.9.
Lançado pela Cloudflare em 2021, gratuito, simples e suficiente para muito caso: configura contato@meusite.com.br para encaminhar para seunome@gmail.com. Não tem caixa própria — emails chegam no seu Gmail/Outlook pessoal.
contato@meusite.com.br → seuemail@gmail.com.*@meusite.com.br não definido vai pra um endereço fallback.contato@meusite.com.br precisa de outro caminho (configurar Gmail para enviar como ou usar serviço de email transacional).
No Gmail: Configurações → Contas e Importação → Enviar email como → Adicionar outro endereço de email. Use SMTP de um serviço transacional (Resend, SES, SendGrid). Daí você responde no Gmail e o destinatário vê contato@meusite.com.br como remetente.
"Mandei email mas não chegou" é um dos pesadelos clássicos. Ferramentas que ajudam:
Acessa o site, copia um endereço único que ele gera (algo tipo test-abc123@mail-tester.com), manda email pra esse endereço. Em segundos, te dá score de 0-10 com análise detalhada: SPF, DKIM, DMARC, conteúdo, blacklists. Excelente primeira ferramenta.
Bom pra verificar registros publicados. Em particular: SuperTool permite consultar MX, SPF, DKIM, DMARC, blacklists de uma vez.
Se você envia muito para Gmail, vale ativar. Mostra estatísticas de entregabilidade, reputação do IP, taxa de spam reportada pelos usuários. Diagnóstico premium e grátis.
Email recebido tem dezenas de headers que contam a história de como chegou. No Gmail: abra email → menu três pontos → "Mostrar original". Headers importantes:
Authentication-Results: mx.google.com;
dkim=pass header.i=@meusite.com.br header.s=resend
spf=pass (google.com: domain of bounces@resend.dev) smtp.mailfrom=bounces@resend.dev
dmarc=pass (p=QUARANTINE sp=NONE) header.from=meusite.com.br
Received-SPF: pass
Return-Path: <bounces@resend.dev>
From: "Eu" <noreply@meusite.com.br>
DKIM-Signature: v=1; a=rsa-sha256; ...
# O que olhar:
# - dkim=pass / dkim=fail → assinatura DKIM verificada?
# - spf=pass / spf=fail → SPF passou?
# - dmarc=pass / dmarc=fail → DMARC passou?
# - Authentication-Results → resumo do que Gmail validou
Se seu IP de envio caiu em blacklist (Spamhaus, SpamCop, Barracuda), emails serão rejeitados por muitos destinos. mxtoolbox.com/blacklists.aspx verifica em ~100 listas de uma vez. Se você usa serviço transacional (Resend, SES), o IP é deles — preocupação não é sua, em geral.
Você tem meuapp.com.br na Cloudflare. Quer: receber email em contato@ (Cloudflare Email Routing → seu Gmail), e enviar email transacional da app via Resend.
meuapp.com.brTipo Nome Valor Proxy # Cloudflare Email Routing — recebimento MX @ isaac.mx.cloudflare.net (priority 36) — MX @ amir.mx.cloudflare.net (priority 50) — MX @ linda.mx.cloudflare.net (priority 90) — # SPF — autoriza tanto Cloudflare Email (recebimento via SRS) quanto Resend (envio) # Importante: combine TUDO num único TXT no apex TXT @ v=spf1 include:_spf.mx.cloudflare.net include:_spf.resend.com ~all # DKIM do Resend TXT resend._domainkey p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB... # DMARC — começa em modo monitoramento TXT _dmarc v=DMARC1; p=none; rua=mailto:dmarc-reports@meuapp.com.br # Bounce handler do Resend MX send feedback-smtp.us-east-1.amazonses.com (priority 10) TXT send v=spf1 include:amazonses.com ~all
No painel Cloudflare → Email Routing → Routes:
contato@meuapp.com.br → seunome@gmail.comdmarc-reports@meuapp.com.br → seunome@gmail.com (para relatórios DMARC)*@meuapp.com.br → seunome@gmail.com (opcional)Cloudflare envia email de confirmação para seunome@gmail.com — você precisa clicar para verificar.
$ curl -X POST 'https://api.resend.com/emails' \ -H 'Authorization: Bearer re_SUA_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "from": "noreply@meuapp.com.br", "to": "test-abc123@mail-tester.com", "subject": "Teste", "text": "Hello" }' # Acessa mail-tester.com e vê o score. # Objetivo: 10/10 com SPF pass, DKIM pass, DMARC pass.
De qualquer email externo (seu Gmail pessoal, por exemplo), envie para contato@meuapp.com.br. Deve chegar no Gmail destino em segundos. Reply funciona? Vai sair de seunome@gmail.com (limitação do forwarding puro — para responder como contato@, configure Gmail SMTP via Resend).
Com p=none, você recebe relatórios. Estude por 2 semanas — confirma que todos os serviços que enviam emails do domínio estão autorizados. Daí muda para:
_dmarc TXT v=DMARC1; p=quarantine; pct=10; rua=mailto:dmarc-reports@meuapp.com.br # Em mais 2 semanas → pct=50, depois pct=100. # Por fim → p=reject.
Resultado: domínio recebendo email no contato@ (via Cloudflare, grátis), aplicação envia transacional via Resend (3000/mês grátis), autenticação SPF + DKIM + DMARC ativa, monitoramento de fraude funcionando. Custo: R$ 0/mês.
Adicionou Resend. Mais tarde, adicionou Google Workspace e criou novo TXT SPF. Resultado: SPF inválido (regra é um por domínio). Sempre combine.
Configurou p=reject no primeiro dia. Algum SaaS que envia notificações em seu nome (Stripe receipts, talvez) não está no SPF — emails legítimos viraram pó. Sempre comece com p=none.
Serviços transacionais usam subdomínio para tratamento de bounces (ex: send.meuapp.com.br). Esse subdomínio precisa de SPF próprio. Sem ele, bounces falham — você não sabe quais emails não foram entregues.
"Posso fazer Postfix na minha VPS." Pode, mas seu IP novo tem reputação zero. Maioria das mensagens cairá em spam. Levar IP a reputação decente leva meses. Use serviço especializado.
Cada include: conta. Adicionou 6 serviços, cada um com 2-3 includes internos — estourou. SPF inválido (PermError). Verifique com dmarcian.com.
Domínio puramente de envio (subdomínio send.meuapp.com.br). Não declara MX null. Recebe spam, bounces, etc. Configure MX 0 . explicitamente.
SPF/DKIM cobrem meusite.com.br. Aplicação envia From: noreply@outro-dominio.com. DMARC alinha o From com o domínio autenticado — falha de alinhamento. Sempre envie do domínio que você configurou.
p=reject no DMARC. Qual o caminho seguro?"O que cada SPF abaixo significa?
v=spf1 -allv=spf1 +allv=spf1 include:_spf.google.com ~allv=spf1 a mx ~allv=spf1 ip4:1.2.3.4 ip4:5.6.7.0/24 -allPense em um projeto seu (ou imaginário). Liste todos os serviços que enviam email "em nome" do seu domínio. Para cada um, identifique se precisa estar no SPF/DKIM.
Lista típica de um SaaS pequeno (cada um geralmente exige SPF + DKIM):
Lição: a maioria das pessoas subestima quantos serviços enviam em seu nome. Auditar a lista é parte essencial de configurar SPF certo.
Cenário: você envia email de noreply@meusite.com.br via Resend. Usuários reclamam que email vai pra spam no Gmail. Como você investiga?
Investigação em ordem:
Authentication-Results:
Você usa Google Workspace há 2 anos para meusite.com.br. Decidiu migrar para Migadu por custo. Descreva o plano de migração, incluindo passos para minimizar perda de email durante a transição e o que fazer com DMARC.
Plano em fases:
Fase 1 — Preparação (1-2 semanas antes):
Fase 2 — Migração de dados (opcional, mas recomendado):
Fase 3 — Reduzir TTL do MX (24h antes do switch):
Fase 4 — Switch MX:
include:_spf.google.com por include:_spf.migadu.comp=quarantine ou p=none — evita rejeitar emails legítimos durante transiçãoFase 5 — Validação (próximas 24-48h):
Fase 6 — Limpeza (1-2 semanas depois):
p=reject quando confiar 100%Downtime esperado: zero. Risco principal é alguns emails ficarem no Google um pouco mais durante propagação — o forwarding configurado na Fase 1 mitiga isso.
HTTPS deixou de ser opcional há tempo — em 2026 é tabela. Mas quem só sabe "instalei Let's Encrypt" não está pronto pra quando algo dá errado: certificado não renovou, navegador mostra erro estranho, ACME falhou no domingo. Entender o protocolo evita madrugadas debugando o óbvio.
Esse capítulo cobre o que importa operacionalmente: como TLS funciona o suficiente pra debugar, como funciona o sistema de certificados, como Let's Encrypt automatiza tudo, e por que renovação automática quebra silenciosamente — o erro mais comum em produção. Vou ser pragmático: não vou ensinar criptografia de ponta-a-ponta, vou ensinar a operar.
Em 1994, a Netscape inventou o SSL (Secure Sockets Layer) para proteger transações no recém-nascido e-commerce. SSL 1.0 nunca foi lançado publicamente — era cheio de falhas. SSL 2.0 saiu em 1995. SSL 3.0 em 1996.
Em 1999, a IETF padronizou TLS 1.0 (Transport Layer Security) — basicamente SSL 3.1 renomeado para separar da empresa Netscape. TLS 1.1 (2006) e TLS 1.2 (2008) trouxeram melhorias incrementais. Em 2018, depois de quase 10 anos de trabalho, saiu TLS 1.3 — reformulação maior, mais rápida, mais segura, sem cipher suites legados.
O ecossistema de certificados foi historicamente caro e arcaico. CAs (Certificate Authorities) tradicionais como Verisign, DigiCert, GoDaddy cobravam de $50 a $1000/ano por certificado. Emissão envolvia papelada, esperar 24-72h, validação manual.
Em 2012, a EFF (Electronic Frontier Foundation) e a Mozilla iniciaram o projeto Let's Encrypt: CA gratuita, automatizada, aberta. Em abril de 2016, Let's Encrypt saiu do beta. Em 2026, emite mais de 500 milhões de certificados ativos — maior CA do mundo de longe. Em 2024, surgiu uma alternativa: ZeroSSL e Buypass (ambas free, suportam o mesmo protocolo).
Em paralelo, ferramentas como Caddy (2015) e plataformas como Cloudflare automatizaram a obtenção de certificados a ponto de "HTTPS" virar default sem configuração. Hoje, em 2026, certificado pago só faz sentido em casos raríssimos (EV certs, alguns ambientes corporativos). Para 99% dos casos, Let's Encrypt + automação é o caminho.
TLS resolve três problemas simultaneamente: identidade (o servidor é quem diz ser), confidencialidade (terceiros não conseguem ler) e integridade (terceiros não conseguem modificar).
# TLS 1.3 (versão atual recomendada) — 1 round-trip: Cliente Servidor │ │ │ ClientHello │ │ - "falo TLS 1.3" │ │ - cipher suites que aceito │ │ - chave pública efêmera (Diffie-Hellman) │ │ - server_name = "meuapp.com.br" (SNI) │ │ ─────────────────────────────────────────→ │ │ │ │ ServerHello │ │ - escolho TLS 1.3 │ │ - escolho cipher X │ │ - chave pública dele │ │ - certificado │ │ - assinatura provando que ele é │ │ dono da chave do certificado │ │ ←───────────────────────────────────────── │ │ │ │ Cliente: │ │ 1. Valida certificado (cadeia até root CA)│ │ 2. Deriva chaves de sessão (DH ephemeral) │ │ 3. Começa a falar HTTPS criptografado │ │ ─────────────────────────────────────────→ │ │ │ │ [HTTP request/response criptografado] │ │ ←──────────────────────────────────────────→│
Um IP pode hospedar centenas de sites. No ClientHello, o cliente envia SNI — o nome do site que ele quer acessar (meuapp.com.br). O servidor usa isso pra escolher o certificado certo. Sem SNI (TLS antigo), seria impossível ter múltiplos certs num único IP.
Implicação operacional: configurações como curl --resolve e proxies precisam mandar SNI correto. Erro de SNI causa "certificado errado retornado".
| Aspecto | TLS 1.2 (2008) | TLS 1.3 (2018) |
|---|---|---|
| Round-trips no handshake | 2 | 1 (ou 0 com session resumption) |
| Cipher suites | ~40 (muitas vulneráveis ou lentas) | 5 (todas seguras e modernas) |
| Perfect Forward Secrecy | Opcional | Obrigatória |
| Algoritmos antigos (RC4, 3DES, SHA-1) | Suportados (vulneráveis) | Banidos |
| Performance | Boa | Significativamente melhor |
| Suporte navegadores | 100% | 100% desde 2020 |
Certificado é arquivo no formato X.509. Contém:
meuapp.com.br, www.meuapp.com.br, etc).Not Before e Not After — período de validade.# Ver certificado de um site: $ openssl s_client -connect meuapp.com.br:443 -servername meuapp.com.br </dev/null \ | openssl x509 -noout -text # Saída resumida: Certificate: Data: Version: 3 (0x2) Serial Number: 04:a1:... Signature Algorithm: ecdsa-with-SHA256 Issuer: C=US, O=Let's Encrypt, CN=E5 Validity Not Before: Mar 8 14:23:11 2026 GMT Not After : Jun 6 14:23:10 2026 GMT Subject: CN=meuapp.com.br Subject Public Key Info: ... X509v3 extensions: X509v3 Subject Alternative Name: DNS:meuapp.com.br, DNS:www.meuapp.com.br # Versão curta — só validade: $ echo | openssl s_client -connect meuapp.com.br:443 -servername meuapp.com.br 2>/dev/null \ | openssl x509 -noout -dates notBefore=Mar 8 14:23:11 2026 GMT notAfter=Jun 6 14:23:10 2026 GMT
Seu certificado é assinado pela Let's Encrypt. Mas o navegador não confia diretamente nela — confia em uma root CA (raíz). A Let's Encrypt é, na verdade, uma intermediate CA assinada por uma root.
# Cadeia típica de um certificado Let's Encrypt em 2026: ISRG Root X1 (root) ← confiada pelo navegador │ ↓ assina Let's Encrypt R3/R10/E5 (intermediate) │ ↓ assina meuapp.com.br (seu certificado) # O servidor envia: SEU CERT + INTERMEDIATE. # Navegador valida intermediate contra root que ele já confia.
Configuração comum: o servidor envia o seu cert mais a intermediate (a "chain" ou "fullchain"). Servir apenas o seu cert sem a intermediate é erro comum: muitos navegadores fazem AIA fetching (buscam intermediate por conta), mas alguns clientes (curl strict, libs HTTP de linguagens) falham com "certificate chain incomplete".
Antes do Let's Encrypt, obter certificado envolvia: gerar CSR, mandar para CA, esperar 1-3 dias, receber email, instalar manualmente. Renovação era manual. Esquecimento causava expiração e site fora.
ACME (Automatic Certificate Management Environment, RFC 8555) é o protocolo que automatiza tudo. Cliente (certbot, Caddy, etc) conversa com a CA via API HTTP. Provisionamento e renovação viram script.
A CA precisa confirmar que você controla o domínio. Para isso, ela emite um challenge: você prova controle de uma das duas formas (em 2026, com algumas variações):
http://seudominio.com/.well-known/acme-challenge/TOKEN. Mais comum. Funciona se você tem servidor HTTP rodando._acme-challenge.seudominio.com. Mais flexível — funciona mesmo sem servidor HTTP exposto, e é a única forma de obter certificado wildcard.# Cliente ACME (certbot, etc) inicia: 1. Cliente: "quero certificado para meuapp.com.br" → CA (Let's Encrypt) 2. CA: "prove que você controla. Coloque o token TOKEN no caminho /.well-known/acme-challenge/TOKEN do site." → Cliente 3. Cliente: cria arquivo TOKEN no path certo, com conteúdo específico. → Servidor web (Nginx, Caddy, etc) serve esse arquivo. 4. Cliente: "feito, valida" → CA 5. CA: faz HTTP GET para http://meuapp.com.br/.well-known/acme-challenge/TOKEN Encontra arquivo? Conteúdo bate? OK. 6. CA: emite certificado. → Cliente 7. Cliente: salva cert + chave privada local. Recarrega servidor web. # Tempo total típico: 10-30 segundos. # Validade do certificado: 90 dias (Let's Encrypt). # Renovação automática: 30 dias antes de vencer.
Let's Encrypt emite certificados por 90 dias (e, desde 2025, está testando certificados de 6 dias). Por quê tão curto?
A indústria está convergindo para certificados ainda mais curtos. CABForum aprovou em 2025 baseline para certificados de até 47 dias. Em 2-3 anos, certificados longos serão exceção.
| Tipo | O que valida | Tempo | Preço | Recomendação |
|---|---|---|---|---|
| DV (Domain Validation) | Que você controla o domínio | Automático | Grátis (Let's Encrypt) | Default. Use sempre, exceto casos especiais. |
| OV (Organization Validation) | Que organização existe | Dias | $50-300/ano | Pouco valor visual. Raro. |
| EV (Extended Validation) | Empresa registrada, processo formal | 1-3 semanas | $200-1000/ano | Browsers tiraram o "selo verde" em 2019. Sem benefício para 99% dos casos. |
meuapp.com.br.meuapp.com.br, www.meuapp.com.br, app.meuapp.com.br. Padrão moderno.*.meuapp.com.br — cobre qualquer subdomínio com um nível. NÃO cobre a.b.meuapp.com.br nem o apex.Wildcard parece sempre vantajoso, mas tem trade-off:
Recomendação: use cert individual por subdomínio quando você tem poucos. Use wildcard quando tem dezenas (multi-tenant, por exemplo). Ferramentas modernas (Caddy) tornam ambos triviais.
Caddy (criado por Matt Holt em 2015) é servidor web que faz HTTPS automaticamente. Você fala "quero servir meuapp.com.br"; ele:
Sem configuração de SSL. Sem certbot. Sem cron. Apenas funciona.
# Caddyfile — configuração completa de Caddy # para um app FastAPI rodando em localhost:8000 meuapp.com.br { reverse_proxy localhost:8000 } # Isso é tudo. Caddy: # - Obtém cert Let's Encrypt automaticamente # - Redireciona HTTP para HTTPS automaticamente # - Configura TLS 1.3 com defaults sãos # - Renova cert antes de expirar (30 dias) # - Reinicia transparente # Múltiplos sites: meuapp.com.br, www.meuapp.com.br { reverse_proxy localhost:8000 } api.meuapp.com.br { reverse_proxy localhost:3000 } # Wildcard com Cloudflare DNS provider: *.meuapp.com.br { tls { dns cloudflare {env.CLOUDFLARE_API_TOKEN} } reverse_proxy localhost:8000 }
Vamos cobrir reverse proxies em detalhe no Cap 11. Por enquanto, registre: Caddy é a opção mais simples e moderna para HTTPS automático.
certbot (da EFF) é a ferramenta original de ACME. Funciona com qualquer servidor (Nginx, Apache, etc). Mais boilerplate que Caddy, mas é o padrão histórico.
# Instalação (Ubuntu): $ sudo apt install certbot python3-certbot-nginx # Obter certificado para domínio rodando Nginx: $ sudo certbot --nginx -d meuapp.com.br -d www.meuapp.com.br # Certbot: # - faz HTTP-01 challenge # - obtém cert # - modifica config Nginx adicionando HTTPS # - configura redirect HTTP→HTTPS # - testa config # - reinicia Nginx # - configura cron pra renovar automaticamente # Verificar renovação está funcionando (dry run): $ sudo certbot renew --dry-run # Forçar renovação (raro, só pra teste): $ sudo certbot renew --force-renewal # Listar certificados existentes: $ sudo certbot certificates # Obter wildcard (precisa DNS-01 com plugin do provedor): $ sudo apt install python3-certbot-dns-cloudflare $ sudo certbot certonly --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ -d '*.meuapp.com.br' -d meuapp.com.br
Onde certbot salva:
/etc/letsencrypt/live/meuapp.com.br/fullchain.pem — cert + intermediate/etc/letsencrypt/live/meuapp.com.br/privkey.pem — chave privada/etc/letsencrypt/renewal/meuapp.com.br.conf — config de renovaçãoConfigurou renovação automática. Funciona por meses. Então — sem você notar — para de funcionar. Cert expira. Site fora do ar com erro de cert.
Causas comuns:
Renovação usa HTTP-01 challenge — Let's Encrypt acessa http://meuapp.com.br/.well-known/acme-challenge/.... Se você:
/.well-known/Renovação falha. Próximas tentativas no cron também. Cert expira.
Certbot grava em /var/log/letsencrypt/. Disco cheio → certbot falha em escrever log → renovação não roda.
Por algum motivo (manutenção, upgrade do SO, container reiniciado com state perdido), o cron de renovação não existe mais. Ou existe mas o serviço cron está parado.
Let's Encrypt tem limites: 50 certs por domínio raiz por semana, 5 erros por hora. Se você renovou 50× testando algo, fica bloqueado temporariamente.
openssl x509 em todos os domínios, alertando se faltar < 14 dias.#!/bin/bash # check_certs.sh — alerta se cert vai expirar em < 14 dias # Rodar via cron diariamente. DOMAINS=("meuapp.com.br" "api.meuapp.com.br" "blog.meuapp.com.br") ALERT_DAYS=14 NOTIFY_URL="https://ntfy.sh/meu-canal-pessoal" # ou Slack webhook, etc for domain in "${DOMAINS[@]}"; do expiry=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null \ | openssl x509 -noout -enddate \ | cut -d= -f2) expiry_epoch=$(date -d "$expiry" +%s) now_epoch=$(date +%s) days_left=$(( (expiry_epoch - now_epoch) / 86400 )) if [ "$days_left" -lt "$ALERT_DAYS" ]; then curl -d "⚠️ Cert de $domain expira em $days_left dias" "$NOTIFY_URL" fi done
No TLS normal, só o servidor prova identidade. Em mTLS (mutual TLS), o cliente também apresenta certificado, e o servidor valida.
Casos de uso:
mTLS é setup pesado. Para projetos solo/pequenos quase nunca vale. Cloudflare Access (a partir do plano gratuito limitado) oferece autenticação SSO sem precisar gerenciar certs cliente. Menciono pra você conhecer o termo; setup detalhado fica fora do escopo desse capítulo.
Sábado, 22h. Cliente reporta: tentou comprar pelo site, navegador mostra "Sua conexão não é privativa" em vermelho. Você abre — confirma. Vamos diagnosticar e resolver.
$ echo | openssl s_client -connect meuapp.com.br:443 -servername meuapp.com.br 2>/dev/null \ | openssl x509 -noout -dates notBefore=Dec 8 14:23:11 2025 GMT notAfter=Mar 8 14:23:10 2026 GMT # Cert expirou há 2 dias. Confirmado.
$ sudo certbot certificates # Lista cert expirado. $ sudo journalctl -u snap.certbot.renew --since "1 month ago" # Logs do timer de renovação. # OU em logs do certbot direto: $ sudo tail -100 /var/log/letsencrypt/letsencrypt.log # Procurar mensagens de erro nas últimas semanas. # Causa comum encontrada: # "Detail: Fetching http://meuapp.com.br/.well-known/acme-challenge/XYZ: # Connection refused"
O erro "Connection refused" na porta 80 sugere que ela está fechada. Verificar:
$ sudo ufw status Status: active To Action From -- ------ ---- 22/tcp ALLOW Anywhere 443/tcp ALLOW Anywhere # Achei. Há 1 mês alguém endureceu o firewall pra aceitar só 443. # Esqueceu que renovação usa HTTP-01 na porta 80.
# Reabrir porta 80 temporariamente $ sudo ufw allow 80/tcp # Forçar renovação agora $ sudo certbot renew --force-renewal # Aguardar 30-60s # Recarregar Nginx (certbot tenta fazer automaticamente) $ sudo systemctl reload nginx # Verificar $ echo | openssl s_client -connect meuapp.com.br:443 -servername meuapp.com.br 2>/dev/null \ | openssl x509 -noout -dates # notAfter agora 90 dias no futuro. OK.
Em vez de manter porta 80 aberta, migrar para DNS-01 challenge — não precisa de porta 80:
# Instalar plugin DNS $ sudo apt install python3-certbot-dns-cloudflare # Criar credentials $ sudo nano /etc/letsencrypt/cloudflare.ini # Conteúdo: dns_cloudflare_api_token = SEU_TOKEN_AQUI $ sudo chmod 600 /etc/letsencrypt/cloudflare.ini # Reemitir cert via DNS-01 $ sudo certbot certonly --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ -d meuapp.com.br -d www.meuapp.com.br \ --force-renewal # Agora pode fechar porta 80 com segurança $ sudo ufw delete allow 80/tcp
Tempo total resolução: 30 minutos. Cliente esperando, downtime real desde a expiração. Lição: monitoramento de cert validity é prevenção barata para problema caro.
Configurou só seu cert, sem a chain. Browser via AIA buscando intermediate funciona; curl --strict, lib HTTP de várias linguagens falham com "unable to get local issuer certificate". Sempre sirva fullchain.pem.
Como vimos no Cap 2. Cert no Cloudflare ↔ usuário OK, mas Cloudflare ↔ servidor em HTTP puro. Inaceitável.
Server config legado aceita versões antigas. Vulnerabilidades conhecidas. Padrões PCI bloqueiam. Force TLS 1.2 mínimo, idealmente 1.3.
Renovação automática "deveria funcionar". Não funciona em algum mês. Site fora 12 horas. Monitor independente é obrigatório.
Cert cobre meuapp.com.br mas não www. Usuário acessa com www, vê erro. SAN explícito ou wildcard resolve.
Submeteu domínio para HSTS preload list. Algum mês depois, tem problema de cert, quer voltar para HTTP — não consegue. Lista de preload é praticamente irreversível. Pense duas vezes.
privkey.pem num repo. Mesmo privado, vaza eventualmente (forks, leaks, ex-colaborador). Cert compromisado. Revogue, gere nova, e use gitleaks para evitar repetição.
Use openssl s_client para inspecionar o certificado de 3 sites diferentes (sugestões: github.com, banco do brasil, gov.br). Identifique para cada um:
$ openssl s_client -connect github.com:443 -servername github.com </dev/null 2>/dev/null # Pontos relevantes na saída: # - Server certificate # subject=CN=github.com # issuer=C=US, O=DigiCert Inc, CN=DigiCert Global G2 ... # - X509v3 Subject Alternative Name: # DNS:github.com, DNS:www.github.com # - Protocol : TLSv1.3 # - Cipher : TLS_AES_128_GCM_SHA256 # - SSL-Session: ... # - Verify return code: 0 (ok) # Observações típicas: # - github.com: usa DigiCert, TLS 1.3, cipher GCM moderno # - bb.com.br: pode usar TLS 1.2 ou 1.3 # - gov.br: tipicamente DigiCert ou similar pago, TLS 1.2/1.3 # Versão curta da consulta: $ echo | openssl s_client -connect github.com:443 -servername github.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
Escreva um Caddyfile para o seguinte cenário:
meuapp.com.br e www.meuapp.com.br → app FastAPI em localhost:8000api.meuapp.com.br → outra app em localhost:9000 com header X-Real-IP propagadoadmin.meuapp.com.br → app em localhost:7000 protegido por autenticação básicastatic.meuapp.com.br → servir arquivos do diretório /var/www/static com cache de 1 ano# Site principal — apex + www meuapp.com.br, www.meuapp.com.br { reverse_proxy localhost:8000 } # API com headers explícitos api.meuapp.com.br { reverse_proxy localhost:9000 { header_up X-Real-IP {remote_host} header_up X-Forwarded-Proto {scheme} } } # Admin com basic auth # Gerar hash: caddy hash-password admin.meuapp.com.br { basic_auth { admin $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG } reverse_proxy localhost:7000 } # Static com cache longo static.meuapp.com.br { root * /var/www/static file_server header Cache-Control "public, max-age=31536000, immutable" } # Caddy faz HTTPS automático em TODOS os domínios acima. # Redirect HTTP→HTTPS automático. # Renovação a cada 60 dias (margem de 30 dias).
Cliente reporta: "alguns visitantes do meu site veem aviso de cert inválido, outros não." Você acessa do seu computador, parece funcionar. Como investiga?
Hipóteses (em ordem de probabilidade):
$ curl -v https://meuapp.com.br 2>&1 | grep -E "^\*" # Olhar quantos certificados são apresentados na chain. $ openssl s_client -connect meuapp.com.br:443 -servername meuapp.com.br -showcerts </dev/null # Devem aparecer pelo menos 2 certs: o seu + a intermediate. # Se aparece só 1, é o problema.
# Sem SNI: $ openssl s_client -connect meuapp.com.br:443 # Com SNI: $ openssl s_client -connect meuapp.com.br:443 -servername meuapp.com.br # Devem retornar o mesmo cert. Se difere, há problema de SNI.
ssllabs.com/ssltest mostra cache OCSP.# Submeter em ssllabs.com/ssltest — análise completa, identifica
# problemas que dig/openssl não mostram diretamente.
Implemente um script Python que:
""" check_certs.py — monitora validade de certs TLS. Rodar via cron diariamente: 0 9 * * * /usr/bin/python3 /opt/scripts/check_certs.py """ import ssl import socket import json import sys from datetime import datetime, timezone from pathlib import Path import urllib.request import logging logging.basicConfig(level=logging.INFO, format="%(message)s") log = logging.getLogger() DOMAINS_FILE = Path("/opt/scripts/domains.txt") ALERT_DAYS = 14 NTFY_URL = "https://ntfy.sh/seu-canal-privado" TIMEOUT = 10 def get_cert_expiry(domain: str) -> datetime: with socket.create_connection((domain, 443), timeout=TIMEOUT) as sock: ctx = ssl.create_default_context() with ctx.wrap_socket(sock, server_hostname=domain) as ssock: cert = ssock.getpeercert() expiry_str = cert["notAfter"] # Formato: 'Jun 6 14:23:10 2026 GMT' expiry = datetime.strptime(expiry_str, "%b %d %H:%M:%S %Y %Z") return expiry.replace(tzinfo=timezone.utc) def notify(message: str): try: req = urllib.request.Request(NTFY_URL, data=message.encode(), method="POST") urllib.request.urlopen(req, timeout=5) except Exception as e: log.error(json.dumps({"event": "notify_failed", "error": str(e)})) def check_domain(domain: str) -> dict: try: expiry = get_cert_expiry(domain) days_left = (expiry - datetime.now(timezone.utc)).days result = { "event": "cert_check", "domain": domain, "expires_at": expiry.isoformat(), "days_left": days_left, "status": "ok" if days_left >= ALERT_DAYS else "warning", } log.info(json.dumps(result)) if days_left < ALERT_DAYS: notify(f"⚠️ Cert de {domain} expira em {days_left} dias ({expiry.date()})") return result except Exception as e: result = { "event": "cert_check_error", "domain": domain, "error": str(e), "status": "error", } log.error(json.dumps(result)) notify(f"❌ Falha ao verificar cert de {domain}: {e}") return result def main(): if not DOMAINS_FILE.exists(): log.error(f"Domains file não encontrado: {DOMAINS_FILE}") sys.exit(1) domains = [d.strip() for d in DOMAINS_FILE.read_text().splitlines() if d.strip() and not d.startswith("#")] results = [check_domain(d) for d in domains] # Status final para exit code (útil em CI/health checks) has_errors = any(r["status"] != "ok" for r in results) sys.exit(1 if has_errors else 0) if __name__ == "__main__": main()
Arquivo domains.txt ao lado:
meuapp.com.br
www.meuapp.com.br
api.meuapp.com.br
blog.meuapp.com.br
# Comentários começam com #
# admin.meuapp.com.br (desativado)
Cron: 0 9 * * * /usr/bin/python3 /opt/scripts/check_certs.py (todo dia às 9h).
Onde a aplicação vai morar. Escolha do provedor, primeiro setup, fechamento de SSH, firewall, atualizações, logs. Seis capítulos para o servidor que não vira manchete por estar comprometido.
Provedor errado custa de 3 a 10 vezes mais do que precisaria, com performance pior. Provedor certo custa pouco, performa bem, e quando você precisa de mais, deixa você crescer sem trauma. Não é decisão definitiva — você pode migrar — mas começar bem economiza meses de trabalho.
Esse capítulo é opinado. Vou comparar os principais provedores com critérios honestos, ignorando o marketing. Os preços e specs são de 2026 (verifique site oficial no dia da decisão — VPS é mercado que muda). O objetivo: você sair daqui sabendo o que escolher e por quê, sem ler 15 reviews superficiais.
Nos anos 90, hospedar site significava: comprar servidor físico, levar pra datacenter (colocation), pagar aluguel de rack, contratar banda. Custo de entrada alto, complexidade técnica grande. Só empresas grandes tinham infra própria.
Em 2001, surgiu o conceito de VPS (Virtual Private Server) — múltiplos servidores virtuais em hardware físico compartilhado, via tecnologias como Virtuozzo, Xen, KVM. Reduziu custo de entrada para $20-50/mês. Linode (2003), Slicehost (2006), depois DigitalOcean (2011) com sua experiência de "droplet em 55 segundos" democratizaram acesso. VPS virou commodity.
Em paralelo, surgiu a cloud pública: AWS EC2 (2006), Google Cloud (2008), Azure (2010). Não é só VPS — é catálogo gigante de serviços gerenciados (banco, fila, ML, etc) com pricing por uso. Mais flexível, mais caro, mais complexo. Hoje, AWS sozinha tem >200 serviços diferentes.
Nos anos 2010, novos players reformaram VPS com pricing agressivo: Hetzner (alemão, expandindo cloud em 2017), Vultr, OVH, Contabo. Diferença de preço para AWS/GCP em workloads simples chegou a 5-10×.
Em 2015-2020, surgiu o "serverless" e PaaS modernos — Vercel, Netlify, Cloud Run, Render, Fly.io. Você paga por execução, sem servidor pra gerenciar. Excelente para casos específicos (sites estáticos, funções stateless), mas pricing escala mal em workloads constantes.
Em 2026, a paisagem é: VPS pra trabalho pesado constante (apps, bancos, workers); PaaS pra simplicidade ou casos específicos (sites estáticos, edge functions); cloud pública pra empresas grandes ou necessidades específicas (compliance, integração com produtos gerenciados, AI/ML em GPUs). Cada um tem seu lugar — e usar o errado custa caro.
VPS = Virtual Private Server. Você aluga uma máquina virtual num servidor físico compartilhado com outros clientes (mas isolada por hypervisor). Recebe acesso root via SSH e pode fazer o que quiser dentro dela.
Em ordem de importância para 90% dos casos:
| Provedor | Plano entry | Preço (2026) | Latência BR | Notas |
|---|---|---|---|---|
| Hetzner | CX22: 2 vCPU, 4GB, 40GB SSD | €3.79–4.59/mês (sobe pra €7.99 em abril 2026) | 200ms+ (DE/FI), ~130ms (US East) | Preço/performance imbatível em 2026. Suporte limitado. Recomendado para maior parte dos projetos. |
| Hetzner CAX11 | 2 vCPU ARM, 4GB, 40GB | €3.79/mês | Mesmo | ARM Ampere — performance excelente, ~17% mais barato. Use se sua stack roda ARM (a maioria roda). |
| DigitalOcean | Basic: 1 vCPU, 1GB, 25GB | $6/mês | ~130ms (NYC), 30-40ms (BR — quando tinha; verifique região atual) | Excelente UX, docs incríveis. Caro relativamente. Suporte mediano. |
| Vultr | Regular: 1 vCPU, 1GB, 25GB | $6/mês | ~30ms (São Paulo) | TEM datacenter em SP! Importante para apps com usuários brasileiros. |
| Contabo | VPS S: 4 vCPU, 8GB, 100GB | €5.50/mês | ~200ms (DE), ~30ms (BR — eles têm SP) | Mais recursos pelo preço. Performance variável (overselling), reputação mista. Vale se preço é crítico. |
| Oracle Cloud Free | 4 ARM vCPU, 24GB RAM (always free) | $0 (sim, zero) | ~50ms (SP) | Generoso mas com cuidado: a Oracle "recupera" instances ociosas e bloqueio de conta sem aviso é comum. Não use para produção crítica. |
| OVH | VPS Starter: 1 vCPU, 2GB | €3.50/mês | ~200ms (FR), ~80ms (CA) | Francês, tradicional. UX antiga. Sólido em performance. |
| Hostinger VPS | KVM 1: 1 vCPU, 4GB, 50GB | $6/mês (anual) | ~30ms (BR — eles têm SP) | Tem datacenter BR. Preço bom no anual. UX de hosting compartilhado (não é DevOps-friendly). |
| AWS Lightsail | 1 vCPU, 1GB, 40GB | $5/mês (com data transfer incluso) | ~30ms (SP) | "VPS da AWS" — preço fixo, sem surpresa de billing. Datacenter em SP. Bom meio-termo entre VPS puro e cloud completa. |
| Linode (Akamai) | Nanode: 1 vCPU, 1GB, 25GB | $5/mês | ~130ms (NYC) | Tradicional, sólido. Sem datacenter BR. Foi comprado pela Akamai em 2022 — qualidade pré-aquisição. |
Para apps com usuários brasileiros, latência da conexão entre o usuário e o servidor é frequentemente o fator mais perceptível de performance. Latências típicas de São Paulo (verifique a sua com ping):
| Datacenter | Latência típica | Experiência |
|---|---|---|
| São Paulo (BR) | 5-30 ms | Instantânea. Web responsiva, jogos, tudo OK. |
| Miami (US East) | 110-130 ms | Aceitável para web. Chat sente leve atraso. |
| Virginia (US East) | 120-150 ms | Aceitável. Padrão da indústria pra LATAM. |
| Califórnia (US West) | 180-220 ms | Notável. APIs ficam mais lentas perceptivelmente. |
| Frankfurt (Alemanha) | 200-220 ms | Notável. Apps interativos sentem. |
| Helsinki (Finlândia) | 230-260 ms | Sente bastante. Aceitável para APIs em background. |
| Singapura | 320-380 ms | Lento. Evite para usuários BR. |
Para muitos apps (especialmente os web tradicionais), latência de origin não importa tanto quanto se imagina, se tiver Cloudflare bem configurado na frente. Para apps real-time (chat, jogos, colaborativo), origin próximo é essencial.
Free tier é marketing. Sempre tem custos escondidos. Os maiores:
Conclusão pragmática: free tier é bom para aprender e experimentar. Para projetos reais, escolha provedor com preço fixo previsível ($5-7/mês). Surpresa de billing destrói muito mais que economia inicial.
VPS é o caminho certo para 95% dos projetos solo/pequenos. Sair pra AWS/GCP só faz sentido quando você bate em problemas específicos:
Antes de migrar pra cloud, considere abordagens híbridas:
"Qual VPS comprar para começar?" — pergunta clássica. Resposta: menor que você acha, e cresça quando precisar.
Você vai lançar SaaS de gestão financeira para PMEs brasileiras. Stack: FastAPI + Postgres + Redis + worker. Estimativa inicial: 50-200 usuários no primeiro semestre. Vamos passar pelo raciocínio.
Hetzner Helsinki: €4/mês, mas 250ms para usuário BR. Cloudflare na frente mitiga (cache + edge), mas API calls não-cacheadas vão sentir.
Hetzner Ashburn (US East): €4/mês, ~130ms. Bom compromisso. Cloudflare na frente vira ~50ms percebido para grande parte do conteúdo.
Vultr São Paulo: $6/mês mas só 1GB RAM. ~30ms latência. Caro relativo, mas localizado.
AWS Lightsail SP: $5-10/mês com mais RAM. Latência local. Datacenter da AWS no Brasil — bom para compliance LGPD.
Contabo SP: €5.50, 4 vCPU, 8GB. Caro-relativo é bom. Performance variável reportada.
Para esse caso, considere as duas melhores opções:
Recomendação: começar com Lightsail SP pela tranquilidade de "está no Brasil" para o primeiro contrato B2B. Migrar para Hetzner quando crescer e LGPD compliance ficar mais maduro.
Custo total mensal: ~$35-40 + Cloudflare/Resend grátis. Cabe orçamento de MVP.
Quando reavaliar:
EC2 t3.small + RDS + S3 + ELB = $80-150/mês pra MVP que faria em €5 no Hetzner. Cloud pública vale quando você usa o ecossistema; pra "EC2 + 1 banco", VPS ganha de longe.
AWS free 12 meses, depois vira $50-200/mês silenciosamente. Cliente pega conta surpresa. Sempre calcule "quanto custa depois do free expirar".
"Vou começar com 8 cores, 32GB, NVMe rápido pra não ter problema." Gasta 10× mais por ano. Servidor fica ocioso. Comece pequeno, escala vertical é trivial.
VPS mais barato em Singapura para app de usuários brasileiros. Latência 350ms+ — UX terrível. Localização é frequentemente mais importante que economia de $2.
Provedor com VPS de $3/mês mas $0.20/GB de egress. App com vídeo/imagens: conta vira $200 num mês ruim. Hetzner incluindo 20TB resolve esse caso.
Comprou VPS sem ver o painel. Painel ruim significa horas perdidas em operações triviais (snapshots, snapshots, restore). Vale fazer signup e brincar com 1 VPS de $4/mês antes de comprometer.
É generoso, mas conta pode sumir sem aviso. Para hobby e laboratório, ótimo. Para algo que paga seu aluguel, sério não.
De sua máquina (ou da máquina de um usuário típico do seu projeto), meça latência para os datacenters mais relevantes via ping. Anote números reais. Compare com tabela do capítulo. Use:
# Latência: $ ping -c 10 google.com # referência local $ ping -c 10 hetzner.com # DE $ ping -c 10 aws.amazon.com # US # Para testar latência específica a datacenters, # use looking glass dos provedores: # - hetzner: https://hel1-speed.hetzner.com (Helsinki) # - hetzner: https://nbg1-speed.hetzner.com (Nuremberg) # - hetzner: https://ash-speed.hetzner.com (Ashburn US) # - vultr: https://lg-sjo.vultr.com, lg-sao.vultr.com, etc
Resultados típicos de São Paulo, Brasil:
Variação de até 20% é normal — depende do horário, ISP, rota. Para decisão de datacenter, foque na média de várias medições.
Você precisa hospedar: 1 app Python web, 1 Postgres 5GB, ~500GB egress/mês, snapshots diários. Calcule custo mensal nessas opções:
| Opção | Componente | Custo/mês |
|---|---|---|
| 1. Hetzner | CX22 (Ashburn ou Helsinki) | €4 (~$4.30) |
| Snapshot em R2 (5GB) | $0.08 | |
| Egress 500GB (incluso até 20TB) | $0 | |
| Total: | ~$4.40/mês | |
| 2. Lightsail SP | Plano 2GB | $10 |
| Snapshots automáticos | $2/mês | |
| Egress: 3TB inclusos | $0 | |
| Total: | ~$12/mês | |
| 3. DigitalOcean | Droplet $6 | $6 |
| Backup automático (+20%) | $1.20 | |
| Egress: 1TB incluso | $0 | |
| Total: | ~$7.20/mês | |
| 4. AWS EC2 | t3.small | ~$17 |
| EBS 20GB | $2 | |
| EBS snapshots | $3 | |
| Egress 500GB ($0.09/GB) | $45 | |
| Total: | ~$67/mês | |
Lição: 15× de diferença entre opções "equivalentes" para o usuário final. Egress é o assassino oculto da AWS. Para 95% dos projetos pequenos, AWS pura é gasto que não se justifica.
Para cada cenário, recomende provedor e justifique em 2-3 frases:
Você está há 8 meses no DigitalOcean ($12/mês). Decidiu migrar para Hetzner ($4/mês) — economia anual de ~$100. Tem: app FastAPI, Postgres 15GB, Redis, 50GB de arquivos em volume. Faça o plano de migração detalhado, incluindo o que NÃO migrar, riscos, e cronograma realista.
Antes de tudo: vale a pena?
Economia: $96/ano. Custo de migração: tempo, risco, possível downtime. Para projeto solo ativo, talvez sim. Para SaaS com clientes pagantes, considere: o tempo gasto vale mais que $96 economizados? Frequentemente não. Migre só se há outro motivo (latência, suporte, features) — preço sozinho raramente justifica.
Plano (assumindo decisão tomada):
Fase 1 — Preparação (semana 1):
Fase 2 — Sincronização contínua (semana 2):
rsync contínuo do volume DO para Hetzner (ou pular pra S3/R2 como destino comum)Fase 3 — Cutover (madrugada de domingo):
Fase 4 — Validação (próximos dias):
Riscos:
O que NÃO migrar:
Cronograma realista: 2-3 semanas de calendário, ~10-15 horas de trabalho efetivo. Considerando seu tempo a $50/hora, custo "real" da migração: $500-750 — vs economia de $96/ano. Conclusão: pra projeto solo, migrar por preço só vale se for hobby ou se Hetzner trouxer outros benefícios. Para projeto comercial, escolha bem desde o início.
ssh root@ip ao servidor utilizável. Usuário não-root, locale, swap, hostname, fail2ban. O setup que cabe em uma tarde mas economiza horas depois.
Provedor entregou o servidor: IP público, senha de root, acesso por SSH. Daqui até "servidor utilizável" tem 30-60 minutos de trabalho que ninguém deveria pular. Quem pula, paga em dor depois — fuso horário errado em logs, root exposto na internet, swap inexistente em momento crítico.
Esse capítulo é a receita do setup inicial mínimo, em ordem. Cada passo justificado, cada comando explicado. Não vou ensinar Linux do zero — assumo que você sabe ler um terminal. Vou cobrir exatamente o que importa: o que distingue um servidor que vai aguentar produção de um que vai virar manchete em fórum de incidentes.
Nos anos 2000, provisionar servidor era ritual. SysAdmins escreviam scripts bash gigantescos, mantidos em pastas pessoais, replicados via cópia e edição. Cada admin tinha "seu" jeito. Variação enorme entre servidores nominalmente idênticos. Bugs sutis, dependências de quem instalou.
Em 2005-2010, ferramentas de configuração: Puppet (2005), Chef (2009), Salt (2011), depois o vencedor pragmático Ansible (2012, sem agente, usa SSH). Provisionamento virou código declarativo, idempotente, versionado em Git.
Em paralelo, surgiu o conceito de cloud-init (2008, Canonical/Ubuntu) — script executado automaticamente no primeiro boot da VPS, baseado em metadata fornecido pelo provedor. Hoje, todo provedor decente suporta: você cria VPS via API com YAML de configuração inicial, e ela já nasce provisionada.
Em 2014-2016, virou padrão criar VPS via Terraform (HashiCorp): infra como código declarativa. Hoje, ambientes profissionais provisionam VPS + DNS + firewall + tudo via terraform apply. Para projeto solo, é overkill — cloud-init basta. Para time, vale.
Esse capítulo cobre o caminho manual (ssh, configurar) primeiro, porque entender o que cada passo faz é essencial. Depois mostra como automatizar. Quem só copia/cola Ansible sem entender quebra na primeira surpresa.
Provedor te deu: IP público (ex: 5.6.7.8), usuário root, senha temporária. Ou (Hetzner, AWS, alguns outros): só IP + chave SSH que você forneceu na criação.
# Conectar pela primeira vez (senha): $ ssh root@5.6.7.8 The authenticity of host '5.6.7.8 (5.6.7.8)' can't be established. ED25519 key fingerprint is SHA256:abc123... Are you sure you want to continue connecting (yes/no/[fingerprint])? yes root@5.6.7.8's password: # Daí, prompt: root@vps-nome:~#
Aquele aviso "authenticity can't be established" é importante. Você está aceitando a chave do servidor sem confirmação. Em teoria, alguém poderia estar fazendo MITM. Na prática, em rede normal, raro. Provedores sérios mostram a fingerprint esperada no painel — vale conferir uma vez. Após aceitar, fica em ~/.ssh/known_hosts; conexões futuras só prosseguem se a fingerprint bate.
# Sempre primeiro passo num servidor novo: # apt update → busca lista de pacotes atualizados # apt upgrade → instala atualizações disponíveis (sem mudar versão major) # apt dist-upgrade → idem mas pode promover ou remover pacotes (use com critério) # apt update # apt upgrade -y # Em Ubuntu/Debian. Outras distros: dnf (Fedora/RHEL), zypper (openSUSE), etc. # Pode reiniciar o kernel: # [ -f /var/run/reboot-required ] && reboot
Em Ubuntu da Hetzner ou DigitalOcean, frequentemente já vem bem atualizado, mas faça o ritual mesmo. Em casos raros aparece kernel novo — reinicia.
Rodar como root é a configuração padrão dos provedores. É terrível: qualquer comando ruim destrói o sistema. Toda operação que vier de fora (script malicioso, vulnerabilidade na app) tem privilégio total.
Crie um usuário "humano" com sudo:
# Como root: # adduser admin # Vai pedir senha (escolha forte), depois algumas perguntas opcionais — pula com Enter. # usermod -aG sudo admin # Agora 'admin' pode executar com sudo. # Verificar: # su - admin # sudo whoami # Pedirá a senha. Deve retornar 'root'.
Se você ainda não fez login com chave (só senha), agora é hora. Da sua máquina local:
# Se você ainda não tem chave SSH local: $ ssh-keygen -t ed25519 -C "seu-email@dominio.com" # Aceite o caminho padrão (~/.ssh/id_ed25519). Senha opcional mas recomendada. # Copiar chave pública para o servidor (no usuário admin): $ ssh-copy-id admin@5.6.7.8 # Vai pedir senha do admin. Após isso, pode entrar sem senha. # Testar: $ ssh admin@5.6.7.8 # Sem prompt de senha = sucesso.
A partir desse momento, você não precisa mais do usuário root direto. Não desligue ainda — vamos hardenizar SSH no próximo capítulo. Mas use admin com sudo para o resto desse setup.
admin com sudo funcionando. Confirme que tudo funciona antes de fechar a sessão root. Se algo deu errado e você fechou root, pode ficar trancado fora do servidor — exigindo console via painel do provedor para corrigir.
Servidor com locale errado faz Python imprimir UnicodeDecodeError em horário aleatório. Servidor com timezone errado faz logs aparecerem 3-9h depois do "horário real" — debug vira inferno.
$ locale LANG=C.UTF-8 LANGUAGE= LC_ALL= $ timedatectl Local time: Wed 2026-05-21 14:23:11 UTC Universal time: Wed 2026-05-21 14:23:11 UTC RTC time: Wed 2026-05-21 14:23:11 Time zone: Etc/UTC (UTC, +0000) System clock synchronized: yes NTP service: active
# Gerar locales (caso precise pt_BR):
# locale-gen en_US.UTF-8 pt_BR.UTF-8
# Definir locale padrão para inglês UTF-8 (recomendado em servidores):
# update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
# Por que inglês? Erros e logs em inglês são pesquisáveis.
# Português dá tradução ruim de stack traces e mensagens de sistema.
Convenção sólida: servidor em UTC, aplicação converte para timezone do usuário. Logs em UTC são comparáveis globalmente; conversão na hora de mostrar pro usuário.
# Manter UTC (recomendado): # timedatectl set-timezone UTC # Se você prefere horário local (não recomendado para servidor de produção, # mas pode fazer sentido para servidor pessoal): # timedatectl set-timezone America/Sao_Paulo # Listar timezones disponíveis: # timedatectl list-timezones | grep -i sao # America/Sao_Paulo
Se você escolher UTC, lembre que ferramentas como journalctl mostrarão horários em UTC. journalctl --since "1 hour ago" ainda funciona porque é relativo. Para visualizar em horário local, use TZ=America/Sao_Paulo journalctl ... ou configure seu cliente local.
Hostname é o nome que aparece no prompt e nos logs. Default é algo genérico (ubuntu-2gb-hil1); útil mudar para algo que faça sentido pra você.
# Mudar hostname permanente: # hostnamectl set-hostname meuapp-prod-01 # Adicionar mapeamento local (recomendado): # nano /etc/hosts # Adicionar linha: 127.0.1.1 meuapp-prod-01 # Convenção saudável: nome-ambiente-numero # - meuapp-prod-01 # - meuapp-staging-01 # - meuapp-db-01 # # Permite logs e dashboards identificarem origem facilmente.
Swap é espaço em disco que o kernel usa quando RAM acaba. VPS modernas (especialmente Hetzner, Vultr) vêm sem swap. Pra projetos pequenos, isso pode ser problema: aplicação consome todo RAM em momento de pico, Linux mata processos arbitrariamente (OOM killer), serviço cai.
Habilitar swap não é solução para problema de memória — mas é colchão para picos curtos. Compromisso útil em servidores pequenos.
# Criar arquivo de swap de 2GB: # fallocate -l 2G /swapfile # chmod 600 /swapfile # mkswap /swapfile # swapon /swapfile # Tornar persistente entre reboots: # echo '/swapfile none swap sw 0 0' >> /etc/fstab # Ajustar 'swappiness' (0-100, default 60): # Menor = usa menos swap, prefere RAM. # Para servidor: 10 é comum. # echo 'vm.swappiness=10' >> /etc/sysctl.conf # sysctl -p # Verificar: $ free -h total used free shared buff/cache available Mem: 3.8Gi 240Mi 3.0Gi 0.0Ki 540Mi 3.4Gi Swap: 2.0Gi 0B 2.0Gi
unattended-upgrades é pacote do Debian/Ubuntu que aplica patches de segurança automaticamente, durante a noite. Configurado, você dorme sabendo que CVEs sendo divulgadas hoje estarão patcheadas amanhã.
Vamos cobrir em detalhe no Cap 9 (Updates e supply chain). Por enquanto, basta ativar com configuração default:
# Instalar (geralmente já vem em Ubuntu): # apt install -y unattended-upgrades apt-listchanges # Ativar interativamente (responda Yes): # dpkg-reconfigure -plow unattended-upgrades # Verificar: # systemctl status unattended-upgrades # Configuração avançada vai no Cap 9.
O servidor "limpo" do provedor tem o mínimo. Ferramentas que economizam horas depois:
# apt install -y \
curl wget \ # downloads
git \ # version control
vim nano \ # editores (escolha um)
htop \ # monitoring de CPU/RAM interativo
iotop \ # monitoring de I/O
ncdu \ # 'du' visual — qual diretório está cheio
tmux \ # multiplexer (sessões persistentes)
jq \ # processamento de JSON na linha de comando
rsync \ # sync de arquivos
net-tools \ # netstat e amigos
dnsutils \ # dig, nslookup, host
fail2ban \ # proteção contra brute-force (Cap 7)
ufw \ # firewall simples (Cap 8)
logrotate # rotação de logs (Cap 10)
Boa prática: documentar essa lista num script de provisionamento. Reinstalar em servidor novo vira ./provision.sh — 30 segundos em vez de 30 minutos.
Antes de configurar dashboards e observabilidade séria (Cap 10), saiba os comandos básicos para inspecionar saúde do servidor:
# RAM e swap em tempo real: $ free -h # Uso de CPU/RAM por processo (interativo): $ htop # Pressione 'F6' pra ordenar por colunas; 'q' pra sair. # Espaço em disco: $ df -h # Coluna 'Use%' > 80% começa a preocupar; > 95% é incêndio. # Onde está cheio dentro de um diretório (interativo): $ sudo ncdu /var/log # Top processos por CPU/memória: $ ps aux --sort=-%mem | head $ ps aux --sort=-%cpu | head # Carga média (load average): $ uptime 14:35:22 up 12 days, 3:14, 1 user, load average: 0.15, 0.12, 0.10 # Load entre 0 e número de CPUs = OK. # > número de CPUs = sistema sobrecarregado. # Conexões de rede ativas: $ ss -tunlp # t=TCP, u=UDP, n=numeric, l=listening, p=process # Quem está logado: $ w
Esses comandos não substituem observabilidade séria, mas são primeira linha de diagnóstico. Quando alarme dispara, você ssh, roda htop e df, e em 30 segundos sabe se é CPU, RAM, ou disco.
Esse capítulo cobriu setup manual. Para fazer o mesmo de forma reproduzível, duas opções principais:
Maioria dos provedores (Hetzner, AWS, DO, Vultr) aceita um YAML de cloud-init na criação da VPS. Ela boota já configurada.
#cloud-config timezone: UTC hostname: meuapp-prod-01 users: - name: admin sudo: "ALL=(ALL) NOPASSWD:ALL" shell: /bin/bash ssh_authorized_keys: - ssh-ed25519 AAAAC3...sua-chave-publica package_update: true package_upgrade: true packages: - curl - git - htop - vim - fail2ban - ufw - unattended-upgrades runcmd: - ufw default deny incoming - ufw allow 22/tcp - ufw --force enable - sed -i 's/^PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config - systemctl restart ssh - fallocate -l 2G /swapfile - chmod 600 /swapfile - mkswap /swapfile - swapon /swapfile - echo '/swapfile none swap sw 0 0' >> /etc/fstab
Crie VPS via API/painel com esse YAML. Em 1-2 minutos, servidor está pronto sem você logar nele.
Para configurações complexas, evolutivas, multi-servidor, Ansible. Playbook YAML descreve estado desejado; rodar é idempotente.
- hosts: meuapp_prod become: true tasks: - name: Atualizar todos os pacotes apt: upgrade: dist update_cache: true - name: Instalar essenciais apt: name: - curl - git - htop - vim - fail2ban state: present - name: Configurar timezone timezone: name: UTC - name: Habilitar swap command: "swapon /swapfile" when: ansible_swaptotal_mb < 100 # Roda com: ansible-playbook -i inventory setup.yml # Idempotente — só faz mudanças quando necessário.
Para projeto solo, cloud-init basta. Para gerenciar múltiplos servidores ao longo do tempo, Ansible vence. Para infra grande, Terraform + Ansible.
Você acabou de comprar Hetzner CX22 em Ashburn. Acabou de receber email com IP 5.6.7.8. Vamos do nada ao servidor pronto pra Cap 7 (SSH hardening).
# Hetzner já recebeu sua chave SSH na criação, então: $ ssh root@5.6.7.8 The authenticity of host... yes # Já dentro: root@cx22-test:~# uname -a Linux cx22-test 6.8.0-31-generic ...
# apt update && apt upgrade -y # Aguarda. Hetzner Ubuntu já vem decente; updates típicos: 20-60 pacotes. # [ -f /var/run/reboot-required ] && reboot # Se kernel atualizou, reboota. Espera 30s, ssh de novo.
# adduser admin # Senha forte. Confirma. Enter no resto. # usermod -aG sudo admin # Copiar chave SSH para admin: # mkdir -p /home/admin/.ssh # cp ~/.ssh/authorized_keys /home/admin/.ssh/ # chown -R admin:admin /home/admin/.ssh # chmod 700 /home/admin/.ssh # chmod 600 /home/admin/.ssh/authorized_keys # Em outra janela, validar: $ ssh admin@5.6.7.8 # Sem prompt de senha. Sucesso. admin@cx22-test:~$ sudo whoami [sudo] password for admin: ... root # Funciona.
$ sudo timedatectl set-timezone UTC $ sudo hostnamectl set-hostname meuapp-prod-01 $ sudo nano /etc/hosts # Editar a linha 127.0.1.1 para o novo nome. # Confirmar: $ timedatectl # Time zone: Etc/UTC $ hostname meuapp-prod-01
$ sudo fallocate -l 2G /swapfile $ sudo chmod 600 /swapfile $ sudo mkswap /swapfile $ sudo swapon /swapfile $ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab $ echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf $ sudo sysctl -p # Confirmar: $ free -h # Swap: 2.0Gi
$ sudo apt install -y curl git vim htop iotop ncdu tmux jq \
rsync net-tools dnsutils ufw fail2ban logrotate \
unattended-upgrades apt-listchanges
$ sudo dpkg-reconfigure -plow unattended-upgrades
# Diálogo: "Automatically download and install stable updates?" Yes
$ uname -a; uptime; free -h; df -h # Verifica tudo numa hora. $ systemctl status fail2ban unattended-upgrades # Ambos active (running) $ id uid=1000(admin) gid=1000(admin) groups=1000(admin),27(sudo) # admin no grupo sudo. OK.
Servidor pronto para Cap 7 (hardening de SSH).
Próximos passos pendentes (próximos capítulos):
"Sou só eu, é mais rápido como root." Até a hora que rm -rf com path errado destrói o sistema. Use admin + sudo. É 5 segundos a mais que economiza tragédia.
Mudou configs de SSH "rapidão" enquanto logado como root, fechou. Não consegue logar como admin (chave SSH não copiada, sudo sem senha, etc). Trancado fora. Sempre teste em janela paralela ANTES de fechar.
Logs em "horário de Brasília", banco com timestamps em outro fuso, app em outro. Confusão garantida quando você atende incidente. UTC sempre no servidor; converta na hora de mostrar pro usuário.
Postgres começou a swapar. Performance caiu 10×. Queries que rodavam em ms passam a rodar em segundos. Para banco em produção, dimensione RAM bem; swap é último recurso emergencial.
VPS recém-criada tem imagem de meses atrás. CVEs conhecidas não patcheadas. Setup completo sem update primeiro = janela de exposição.
3 meses depois, precisa criar servidor staging idêntico. "Como configurei mesmo?" Sem nota, sem script, sem playbook — refaz tudo do zero, esquece algumas coisas, ambientes divergem.
Escreva um checklist em ordem de TODOS os passos de provisionamento inicial cobertos nesse capítulo. Cada item em uma linha, com comando associado quando aplicável.
# Provisionamento inicial — VPS Ubuntu
[ ] SSH como root pela primeira vez
ssh root@<ip>
[ ] Atualizar pacotes
apt update && apt upgrade -y
[ -f /var/run/reboot-required ] && reboot
[ ] Criar usuário admin
adduser admin
usermod -aG sudo admin
[ ] Copiar chave SSH para admin
mkdir -p /home/admin/.ssh
cp ~/.ssh/authorized_keys /home/admin/.ssh/
chown -R admin:admin /home/admin/.ssh
chmod 700 /home/admin/.ssh
chmod 600 /home/admin/.ssh/authorized_keys
[ ] Validar login admin em janela paralela
ssh admin@<ip>
sudo whoami # deve retornar root
[ ] Configurar timezone
sudo timedatectl set-timezone UTC
[ ] Configurar hostname
sudo hostnamectl set-hostname meuapp-prod-01
sudo sed -i 's/^127.0.1.1.*/127.0.1.1 meuapp-prod-01/' /etc/hosts
[ ] Habilitar swap (VPS com ≤4GB)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
[ ] Instalar pacotes essenciais
sudo apt install -y curl git vim htop iotop ncdu tmux jq \
rsync net-tools dnsutils ufw fail2ban logrotate \
unattended-upgrades
[ ] Ativar unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
[ ] Validar
systemctl status fail2ban unattended-upgrades
id # admin no grupo sudo
[ ] Documentar
Notas: IP, hostname, propósito, data, chave SSH usada
[ ] Snapshot inicial
Painel do provedor: criar snapshot 'baseline'
Compre uma VPS (Hetzner CX22 €4 ou Lightsail SP $5 — cabe no bolso). Faça TODO o setup do capítulo. Documente o tempo total e cada problema encontrado. Tire snapshot ao final.
O que tipicamente trava na primeira vez:
chown -R admin:admin. Sem dono certo, SSH ignora arquivo./etc/fail2ban/jail.local não existe ainda. Inicia mesmo sem; vai cobrir no Cap 7.hash -r ou abrir nova sessão para PATH atualizar.Tempo realista da primeira vez: 60-90 minutos com leitura e dúvidas. Segunda vez: 30-45 min. Quinta vez: 15 minutos ou virou script.
Escreva um arquivo de cloud-init que automatize todo esse setup. Quando aplicado na criação de VPS Hetzner, o servidor já deveria boot com tudo configurado.
#cloud-config hostname: meuapp-prod-01 timezone: UTC preserve_hostname: false users: - name: admin sudo: "ALL=(ALL) NOPASSWD:ALL" groups: sudo shell: /bin/bash ssh_authorized_keys: - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...SUA_CHAVE_PUBLICA admin@laptop package_update: true package_upgrade: true package_reboot_if_required: true packages: - curl - git - vim - htop - iotop - ncdu - tmux - jq - rsync - net-tools - dnsutils - ufw - fail2ban - logrotate - unattended-upgrades - apt-listchanges write_files: - path: /etc/sysctl.d/99-swappiness.conf content: "vm.swappiness=10" runcmd: # Swap 2GB - fallocate -l 2G /swapfile - chmod 600 /swapfile - mkswap /swapfile - swapon /swapfile - echo '/swapfile none swap sw 0 0' >> /etc/fstab - sysctl -p /etc/sysctl.d/99-swappiness.conf # Habilitar unattended-upgrades automaticamente - echo 'unattended-upgrades unattended-upgrades/enable_auto_updates boolean true' | debconf-set-selections - dpkg-reconfigure -fnoninteractive unattended-upgrades # Firewall básico (só SSH; Cap 8 ajusta) - ufw default deny incoming - ufw default allow outgoing - ufw allow 22/tcp - ufw --force enable final_message: "Servidor pronto. SSH em admin@$ip"
Como usar: ao criar VPS Hetzner pela web, há campo "User data" — cola esse YAML. Outras opções: via Terraform, via API HCloud CLI (hcloud server create --user-data-from-file).
Você ssh num VPS de produção pequeno (2GB RAM). Aplicação está lenta. Logs não mostram nada óbvio. Faça um diagnóstico estruturado usando apenas comandos cobertos nesse capítulo. Em quais sinais você foca?
Diagnóstico em ordem (~5 min):
1. Carga geral:
$ uptime 14:35:22 up 3 days, load average: 3.50, 2.80, 1.90 # Em VPS de 2 cores: 3.50 = sobrecarga real. Tendência crescente (vs 1.90 há 15min).
2. Memória e swap:
$ free -h
total used free shared buff/cache available
Mem: 1.9Gi 1.7Gi 90Mi 5.0Mi 120Mi 70Mi
Swap: 2.0Gi 1.5Gi 500Mi
# RAM lotada. Swap ativo agressivamente (1.5GB usado). SWAPPING — explica lentidão.
3. Quem está comendo memória:
$ ps aux --sort=-%mem | head -5 USER PID %CPU %MEM VSZ RSS COMMAND postgres 1234 15.2 35.4 ... postgres: idle python 5678 8.5 28.2 ... gunicorn worker python 5679 8.1 27.8 ... gunicorn worker # Postgres + 2 workers Python = ~90% da RAM. # Provavelmente subiram número de workers ou queries pesadas no banco.
4. I/O (swap mata disco):
$ sudo iotop
# Vê processos que estão fazendo muito I/O. Em swap thrashing, kswapd aparece alto.
5. Disco:
$ df -h /dev/sda1 40G 38G 2.0G 95% / # 95% cheio! Causa secundária de problemas (Postgres pode ter dificuldade de gravar). $ sudo ncdu /var/log # Vê onde está cheio: /var/log/postgresql/postgresql-14-main.log com 8GB. Logrotate não configurado direito.
Diagnóstico final:
Mitigações imediatas:
Solução real: migrar para VPS maior (CX32 com 8GB) ou separar Postgres em VPS dedicada. Tudo isso será mais fácil com observabilidade do Cap 10 — mas no aperto, esses comandos de 30 segundos diagnosticam.
SSH é a única porta de entrada do seu servidor. Bem configurada, é praticamente intransponível. Mal configurada, é o caminho mais provável de comprometimento. A diferença é meia hora de trabalho e uns 5 ajustes de configuração — feitos uma vez, esquecidos para sempre.
Esse capítulo cobre o que importa: como SSH realmente autentica (modelo de chaves), como gerar chaves direito em 2026, quais ajustes no servidor valem (e quais são teatro), fail2ban configurado de verdade, e o que fazer quando você se tranca fora (porque vai acontecer). Vou ignorar mitos populares e focar em prática.
Nos anos 80, conectar remotamente em máquina Unix significava telnet — protocolo sem criptografia, senha em texto puro, qualquer pessoa na rota lia tudo. Nos anos 80 isso era aceitável porque a internet era pequena e amistosa. Nos 90 já não era.
Em 1995, Tatu Ylönen, pesquisador da Universidade Tecnológica de Helsinki, sofreu ataque de password sniffing na rede da universidade. Em resposta, escreveu SSH (Secure Shell). Liberou como software livre. Adoção foi rapidíssima — em 2 anos, ~20 mil servidores usando.
Em 1996, Ylönen fundou empresa (SSH Communications Security) e SSH 1 virou produto comercial. Em 1999, o projeto OpenSSH (do OpenBSD) reescreveu SSH como software realmente livre, sob licença BSD. Em 2026, OpenSSH é a implementação padrão em praticamente todo Linux.
Em 2006, SSH 2 (protocolo) foi padronizado nas RFCs 4250-4256. SSH 1 tinha vulnerabilidades conhecidas e foi deprecado. Em 2014, vulnerabilidade séria (Heartbleed em OpenSSL, que SSH usa) recalibrou a atenção da indústria com gestão de chaves e bibliotecas.
Em 2025, OpenSSH removeu suporte a chaves DSA (consideradas fracas). Versões recentes incentivam ed25519 (curva elíptica moderna) sobre RSA legado. Em 2026, o estado da arte é: ed25519, sem senha de root, sem login por password.
SSH tem múltiplos modos de autenticação. Os dois que importam:
Usuário digita senha; servidor compara com /etc/shadow. Vulnerável a:
Você tem um par de chaves: privada (fica na sua máquina, nunca sai dela) e pública (colocada no servidor, em ~/.ssh/authorized_keys).
# Fluxo de autenticação por chave: 1. Cliente: "oi, sou o user admin no servidor X. tenho essas chaves públicas pra oferecer." → Servidor 2. Servidor verifica se alguma das chaves oferecidas está em /home/admin/.ssh/authorized_keys 3. Servidor: "prove que você TEM a chave privada da chave X. eis um desafio aleatório, assine ele." → Cliente 4. Cliente: assina o desafio com chave privada local (chave privada nunca sai do cliente) → Servidor 5. Servidor verifica assinatura usando a chave pública. Válida? Login autorizado. # Vantagem chave: o servidor nunca recebe sua chave privada. # Mesmo se atacante intercepta tudo, não consegue forjar autenticação.
| Tipo | Tamanho típico | Status 2026 | Recomendação |
|---|---|---|---|
| DSA | 1024 bits | Removido do OpenSSH em 2025 | Não use. |
| RSA 1024 | 1024 bits | Considerada fraca | Substitua. |
| RSA 2048 | 2048 bits | Aceitável, mas legado | OK pra compatibilidade antiga; prefira ed25519. |
| RSA 4096 | 4096 bits | Segura | Funciona, mais lenta. Sem razão pra escolher hoje. |
| ECDSA (P-256) | 256 bits | OK | Aceitável, mas ed25519 é melhor. |
| ed25519 | 256 bits | Estado da arte | Padrão recomendado. |
ed25519 vence em tudo: mais rápida, mais segura (na curva 25519, sem números mágicos suspeitos), chaves curtas e legíveis. Único motivo pra ainda usar RSA é compatibilidade com sistemas pré-2014 — raro.
# Gerar chave ed25519: $ ssh-keygen -t ed25519 -C "voce@laptop" Generating public/private ed25519 key pair. Enter file in which to save the key (/home/voce/.ssh/id_ed25519): Enter passphrase (empty for no passphrase): Enter same passphrase again: # Arquivos gerados: # ~/.ssh/id_ed25519 → chave privada (NUNCA compartilhe) # ~/.ssh/id_ed25519.pub → chave pública (esta vai pro servidor)
Quando gera a chave, pedem passphrase. Polêmica entre comodidade e segurança:
ssh-agent vem com sistema operacional. macOS: ssh-add --apple-use-keychain guarda no Keychain. Linux com gnome-keyring: automático. Windows com OpenSSH: ssh-add com agent rodando.
Boa prática: chave separada por contexto (trabalho, pessoal, servidores específicos).
$ ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_pessoal -C "voce@pessoal" $ ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_trabalho -C "voce@trabalho" $ ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_servidores -C "voce@servidores" # Configuração de qual usar em qual host vai em ~/.ssh/config (próxima seção).
A pública é segura para compartilhar — esse é o ponto. Coloque em:
~/.ssh/authorized_keys dos servidores que você acessa.
Sua chave pública vai começar com ssh-ed25519 e ter ~80 caracteres. Pode publicar no GitHub público, no Twitter — não há risco. Privada é a única que precisa segredo absoluto.
O arquivo principal de configuração do servidor SSH é /etc/ssh/sshd_config. Para Ubuntu/Debian modernos, prefira criar arquivo em /etc/ssh/sshd_config.d/ que sobrescreve defaults — é mais limpo e sobrevive a upgrades.
# Hardening recomendado em 2026 # Aplica-se em adição às configs default do Debian/Ubuntu # --- Desabilitar root login --- PermitRootLogin no # --- Desabilitar autenticação por senha --- PasswordAuthentication no ChallengeResponseAuthentication no KbdInteractiveAuthentication no # --- Forçar apenas chaves --- PubkeyAuthentication yes AuthenticationMethods publickey # --- Limitar usuários que podem entrar via SSH (opcional, recomendado) --- AllowUsers admin # --- Timeouts agressivos contra conexões ociosas --- ClientAliveInterval 300 ClientAliveCountMax 2 # Se cliente não responder em 300s × 2, conexão é fechada. # --- Reduzir tentativas antes de desconectar --- MaxAuthTries 3 LoginGraceTime 30 # --- Desabilitar features que raramente são úteis --- X11Forwarding no AllowAgentForwarding no AllowTcpForwarding no # Atenção: TcpForwarding às vezes é útil (port forwarding). # Se você usa SSH tunnels, deixe 'yes'. # --- Banner opcional --- Banner /etc/issue.net # Conteúdo do arquivo deve avisar que acesso é restrito (efeito legal em algumas jurisdições).
# Testar config antes de reiniciar (CRÍTICO): $ sudo sshd -t # Sem output = OK. Se houver erro de sintaxe, mostra. # Reiniciar serviço: $ sudo systemctl restart ssh # IMPORTANTE: mantenha sua sessão atual aberta! # Abra outra janela e teste novo login antes de fechar a atual. $ ssh admin@5.6.7.8 # Funcionou? OK, agora pode fechar a primeira sessão.
Linha AuthenticationMethods publickey força que SÓ chave seja aceita. Combinada com PasswordAuthentication no, deixa o servidor impermeável a brute force de senha — bot pode tentar 1 milhão de vezes, sempre vai falhar sem chave válida.
Você pode exigir múltiplos fatores: AuthenticationMethods publickey,password obriga as duas. Para servidores muito críticos, isso é melhor — mas adiciona fricção. Padrão 2026 para projetos solo: só chave basta, se bem cuidada.
Sem ~/.ssh/config, você digita ssh -i ~/.ssh/id_ed25519_servidores -p 22 admin@5.6.7.8 toda vez. Com config, vira ssh meuapp.
# Defaults para todos os hosts Host * AddKeysToAgent yes IdentitiesOnly yes ServerAliveInterval 60 ServerAliveCountMax 5 # Servidor de produção Host meuapp HostName 5.6.7.8 User admin Port 22 IdentityFile ~/.ssh/id_ed25519_servidores # Staging Host meuapp-staging HostName 1.2.3.4 User admin IdentityFile ~/.ssh/id_ed25519_servidores # GitHub via SSH Host github.com HostName github.com User git IdentityFile ~/.ssh/id_ed25519_pessoal # Bastion / jump host (pra acessar máquinas internas via uma exposta) Host db-interno HostName 10.0.1.5 User admin ProxyJump meuapp IdentityFile ~/.ssh/id_ed25519_servidores
$ ssh meuapp # entra direto $ ssh meuapp-staging # outro servidor $ scp arquivo.tar.gz meuapp:/tmp/ # scp funciona com aliases $ rsync -av ./local/ meuapp:/var/www/ # rsync também $ ssh db-interno # conecta via meuapp como bastion
Mesmo com password authentication desligado, bots vão bater na sua porta SSH milhares de vezes por dia. Logs ficam barulhentos, há um pequeno custo de CPU. fail2ban resolve: monitora logs, detecta padrões de ataque, e bloqueia IPs via firewall.
fail2ban vem com configurações em /etc/fail2ban/jail.conf. Não edite esse — crie /etc/fail2ban/jail.local com overrides.
[DEFAULT] # Quanto tempo banir (segundos) bantime = 3600 # Janela para contar tentativas findtime = 600 # Quantas tentativas antes do ban maxretry = 5 # IPs que nunca devem ser banidos (seu IP de casa, escritório): ignoreip = 127.0.0.0/8 ::1 SEU.IP.DE.CASA # Backend para detectar mudanças nos logs backend = systemd [sshd] enabled = true port = ssh filter = sshd logpath = %(sshd_log)s maxretry = 3 bantime = 86400 # SSH banido por 24h (mais rigor que default)
$ sudo systemctl restart fail2ban # Ver status: $ sudo fail2ban-client status Status |- Number of jail: 1 `- Jail list: sshd # Detalhes do jail sshd: $ sudo fail2ban-client status sshd Status for the jail: sshd |- Filter | |- Currently failed: 3 | |- Total failed: 847 | `- File list: /var/log/auth.log `- Actions |- Currently banned: 12 |- Total banned: 156 `- Banned IP list: 1.2.3.4 ... # Desbanir um IP manualmente (caso seja você): $ sudo fail2ban-client unban 5.6.7.8 # Ver logs do fail2ban: $ sudo journalctl -u fail2ban -f
Resposta honesta: com password auth desligada, fail2ban é mais sobre reduzir ruído nos logs do que segurança real. Brute force de chave SSH é matematicamente inviável. Mas:
Use. Não é "linha defensiva crítica" mas é higiene.
Conselho popular: "mude a porta SSH de 22 para 2222 ou outra coisa, vai reduzir ataques". Verdade pela metade.
nmap. Não muda nada.# Em sshd_config.d/99-hardening.conf, adicionar: Port 22 Port 2347 # SSH escutará em ambas as portas durante transição. # Validar: $ sudo sshd -t $ sudo systemctl restart ssh # Abrir firewall pra nova porta: $ sudo ufw allow 2347/tcp # Testar (em outra janela): $ ssh -p 2347 admin@5.6.7.8 # Funcionou? Edita ~/.ssh/config pro Port 2347. # Depois de 24h+ confiantes, remover linha "Port 22" e fechar firewall: $ sudo ufw delete allow 22/tcp
Servidor de time tem múltiplas pessoas com acesso SSH. Modelo errado: criar usuário deploy com senha que todo mundo compartilha. Catastrófico — quando alguém sai, você troca a senha, todo mundo tem que atualizar; logs não distinguem quem fez o quê.
# Para cada pessoa do time: $ sudo adduser maria $ sudo usermod -aG sudo maria $ sudo mkdir -p /home/maria/.ssh $ echo "ssh-ed25519 AAAAC3... maria@laptop" | sudo tee /home/maria/.ssh/authorized_keys $ sudo chown -R maria:maria /home/maria/.ssh $ sudo chmod 700 /home/maria/.ssh $ sudo chmod 600 /home/maria/.ssh/authorized_keys # Quando alguém sai: $ sudo userdel -r maria # Remove conta + home directory. # Logs mostram cada pessoa explicitamente: $ sudo journalctl _SYSTEMD_UNIT=ssh.service -p info ... sshd[1234]: Accepted publickey for maria from 200.150.x.x port ...
Quando passa de 5-10 pessoas, fica chato. Opções:
Para projetos solo/pequenos (1-5 devs), modelo "1 usuário por pessoa, chave SSH, gerenciado manualmente" basta. Faz a transição quando dor justificar.
Vai acontecer pelo menos uma vez. Configuração errada, chave perdida, firewall fechado demais. O que fazer:
Todo provedor decente oferece console KVM no painel — acesso direto ao servidor, como se você estivesse fisicamente com teclado conectado. Bypassa SSH inteiro.
Console precisa de senha de usuário (que normalmente você não definiu se entrou só por chave!). Boa prática preventiva: defina senha forte para admin ANTES de hardenizar SSH. Senha só será usada via console, em emergência.
# Definir senha do admin (preventivo): $ sudo passwd admin New password: ... Retype: ... # Guarda essa senha no seu gerenciador de senhas. # Vai usar apenas via console em emergência.
Se nem console funciona (cenário raro: kernel quebrou no boot), rescue mode: provedor reinicia VPS num sistema de "resgate" — pequeno Linux que monta seu disco como volume. Você corrige arquivo problemático manualmente.
Documente o processo do seu provedor antes de precisar. Saber onde clicar quando o site está fora é diferença entre 10 minutos e 2 horas.
Continuando do Cap 6: VPS Hetzner CX22 provisionada, admin com sudo, chave SSH copiada. Vamos hardenizar.
$ sudo passwd admin
# Senha forte (gerador de senhas do seu gerenciador). Guarda no gerenciador.
# Será usada apenas via console KVM em emergência.
$ sudo nano /etc/ssh/sshd_config.d/99-hardening.conf # Conteúdo: PermitRootLogin no PasswordAuthentication no ChallengeResponseAuthentication no PubkeyAuthentication yes AuthenticationMethods publickey AllowUsers admin ClientAliveInterval 300 ClientAliveCountMax 2 MaxAuthTries 3 LoginGraceTime 30 X11Forwarding no
$ sudo sshd -t
# Sem output = OK. Se aparecer erro, NÃO reinicie SSH ainda.
$ sudo systemctl restart ssh
Agora abra OUTRA janela do terminal:
$ ssh admin@5.6.7.8
# Entrou? Excelente. Pode fechar a sessão original.
# Não entrou? Use a sessão original para reverter:
# sudo rm /etc/ssh/sshd_config.d/99-hardening.conf
# sudo systemctl restart ssh
$ sudo nano /etc/fail2ban/jail.local # Conteúdo: [DEFAULT] bantime = 3600 findtime = 600 maxretry = 5 ignoreip = 127.0.0.0/8 ::1 [sshd] enabled = true maxretry = 3 bantime = 86400 $ sudo systemctl restart fail2ban $ sudo fail2ban-client status sshd
$ nano ~/.ssh/config # Adicionar: Host meuapp HostName 5.6.7.8 User admin IdentityFile ~/.ssh/id_ed25519 # Testar: $ ssh meuapp # Funcionou — agora você usa 'ssh meuapp' em vez do IP.
# Tentativa de login como root deve falhar: $ ssh root@5.6.7.8 Permission denied (publickey). # Tentativa com senha (de outra máquina sem chave) deve falhar: $ ssh admin@5.6.7.8 Permission denied (publickey). # Não pede senha — só aceita chave. # fail2ban funcionando: $ sudo fail2ban-client status sshd # Active. # Logs limpos: $ sudo journalctl -u ssh --since today | grep -i fail # Em algumas horas, vai começar a aparecer tentativas falhas vindas # de bots — confirma que fail2ban está vendo (e banindo).
Resultado: servidor com SSH endurecido em ~30 minutos. Brute force inviável. Logs limpos via fail2ban. Acesso de emergência via console preservado. Próximo capítulo: firewall — fechar tudo o que não precisa estar aberto.
Mexer em sshd_config é "lock-yourself-out" potencial. Sem janela paralela aberta para testar, você pode ficar sem acesso e precisar de console KVM (que talvez não esteja configurado).
SSH agora exige chave. Console KVM exige senha. Você não tem senha (entrou só por chave). Se chave SSH der problema, fica travado. Defina senha forte de emergência ANTES.
Laptop roubado, ladrão tem acesso direto a todos seus servidores. Passphrase + ssh-agent é compromisso saudável — comodidade no dia, segurança na perda.
Chave única para GitHub + servidores pessoais + cliente A + cliente B. Comprometeu uma vez (laptop antigo, repositório vazado), comprometeu todos. Chaves por contexto, idealmente.
chmod 644 no authorized_keys em vez de 600. SSH detecta — refuso por segurança. Mensagem confusa "Authentication failed" sem motivo aparente. Sempre 600 + ~/.ssh com 700.
Acreditar que porta 2347 esconde o serviço. Bots têm masscan; encontram em segundos. Sem chave-only, ainda é vulnerável. Security through obscurity isolado falha.
"Vamos usar uma chave compartilhada do time, fica mais fácil." Quando alguém sai, todo mundo precisa trocar. Logs perdem rastreabilidade. Um usuário por pessoa, sempre.
PasswordAuthentication no e PermitRootLogin no no SSH. Reiniciou o serviço. Agora não consegue entrar como admin — chave parece não estar sendo aceita. O que você verifica primeiro?"Na sua máquina local (se ainda não fez): gere uma chave ed25519 com passphrase. Configure ssh-agent. Crie ~/.ssh/config com pelo menos 2 hosts. Documente os comandos exatos que você usou.
# Gerar chave $ ssh-keygen -t ed25519 -C "voce@laptop-pessoal" -f ~/.ssh/id_ed25519 # Quando pedir passphrase, use senha forte (gerador). Guarda no gerenciador. # Verificar ssh-agent $ ssh-add -l # Se "The agent has no identities", adicionar: $ ssh-add ~/.ssh/id_ed25519 # Pede a passphrase uma vez; depois fica em memória. # Em macOS, para guardar no Keychain: $ ssh-add --apple-use-keychain ~/.ssh/id_ed25519 # Criar config $ nano ~/.ssh/config # Exemplo: Host * AddKeysToAgent yes IdentitiesOnly yes ServerAliveInterval 60 Host meuapp HostName 5.6.7.8 User admin IdentityFile ~/.ssh/id_ed25519 Host github.com User git IdentityFile ~/.ssh/id_ed25519 # Permissões importam: $ chmod 600 ~/.ssh/config # Testar: $ ssh meuapp
Aplique todo o hardening desse capítulo numa VPS sua. Antes/depois, faça as seguintes verificações:
ssh root@ip falhassh -o PreferredAuthentications=password admin@ip falhafail2ban-client status sshd mostrando IPs banidosPontos onde tipicamente trava:
~/.ssh/authorized_keys (precisa 600) ou no diretório .ssh (precisa 700). Logs em /var/log/auth.log mostram exatamente.Seu sócio precisa ter acesso SSH ao servidor de produção. Descreva o processo completo, do "ele me manda a chave pública dele" até "ele consegue logar". Inclua boas práticas (não compartilhar chaves, logs por pessoa).
Processo:
$ ssh-keygen -t ed25519 -C "socio@laptop"
$ cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3...... socio@laptop
$ sudo adduser socio $ sudo usermod -aG sudo socio $ sudo mkdir -p /home/socio/.ssh $ echo "ssh-ed25519 AAAAC3...... socio@laptop" | sudo tee /home/socio/.ssh/authorized_keys $ sudo chown -R socio:socio /home/socio/.ssh $ sudo chmod 700 /home/socio/.ssh $ sudo chmod 600 /home/socio/.ssh/authorized_keys # Atualizar AllowUsers se você restringiu: $ sudo nano /etc/ssh/sshd_config.d/99-hardening.conf # Mudar: AllowUsers admin socio $ sudo sshd -t $ sudo systemctl restart ssh
$ ssh socio@5.6.7.8
$ sudo whoami
root # sudo funciona
Boas práticas:
passwd).sudo userdel -r socio remove usuário e diretório home — limpo.journalctl _SYSTEMD_UNIT=ssh.service | grep socio mostra acessos dele especificamente.De propósito, em ambiente de teste: trave-se fora do seu próprio servidor (ex: mude SSH para porta 9999 mas esqueça de abrir no firewall). Em seguida, recupere via console KVM. Documente o processo passo a passo. Atenção: faça em VPS de teste, não em produção.
Cenário simulado:
Você muda Port 22 para Port 9999 no sshd_config, reinicia SSH, mas esqueceu ufw allow 9999. SSH atual cai porque você fechou a sessão. Tenta reconectar: timeout.
Recuperação via console:
meuapp-prod-01 login: admin
Password: ******** # a senha que você definiu preventivamente
admin@meuapp-prod-01:~$
Console aceita login por senha mesmo com SSH só-chave configurado.
# Opção 1: abrir porta no firewall $ sudo ufw allow 9999/tcp # Opção 2: reverter a porta no sshd $ sudo nano /etc/ssh/sshd_config.d/99-hardening.conf # Comentar a linha Port 9999 $ sudo sshd -t $ sudo systemctl restart ssh # Confirma porta escutando: $ sudo ss -tlnp | grep ssh LISTEN 0 128 0.0.0.0:22 ...
$ ssh admin@5.6.7.8
# Funcionou. Pode fechar console.
Por que esse exercício é importante: a primeira vez que você precisa do console KVM em produção, é situação estressante. Praticar em ambiente de teste te dá familiaridade — sabe onde fica no painel, sabe que precisa de senha, sabe como navegar. Treinamento que economiza minutos críticos.
Servidor exposto na internet sem firewall é casa com a porta da frente aberta. Configurar firewall direito é meia hora de trabalho — pra esconder serviços que não precisam estar públicos, limitar acesso aos que precisam, e bloquear o IP real mesmo com Cloudflare na frente. Defesa em camadas começa aqui.
Esse capítulo cobre ufw (a ferramenta que você vai usar 95% do tempo), iptables o suficiente pra debug, security groups da nuvem (camada paralela em AWS/GCP), e a integração com Cloudflare que esconde seu IP do mundo. Vou ser pragmático: vai sair daqui com o servidor fechado.
Em 1989, surgiram os primeiros firewalls comerciais — caixas físicas separando "dentro" e "fora" da rede corporativa. Em 1995, o Linux ganhou ipfwadm, primeira ferramenta de packet filtering nativa. Em 1998, ipchains substituiu. Em 1999, com o kernel 2.4, surgiu o iptables + Netfilter — base do firewall do Linux por mais de 20 anos.
Em 2014, nftables foi introduzido como sucessor moderno do iptables. Mais consistente, mais performático, sintaxe mais legível. Distribuições começaram a migrar: Debian 10 (2019), RHEL 8 (2019), Ubuntu 22.04 (2022). Em 2026, nftables é o backend padrão na maioria, mas comandos iptables continuam funcionando via camada de compatibilidade.
Em paralelo, surgiram ferramentas de alto nível para configurar regras sem digitar incantações: UFW (Uncomplicated Firewall, 2008, Ubuntu) e firewalld (2011, Red Hat). Em 2026, ufw é praticamente padrão em Debian/Ubuntu — o que esse capítulo vai cobrir.
Em paralelo, cloud pública trouxe outro modelo: security groups e VPC firewalls. Regras gerenciadas pelo provedor, aplicadas antes do tráfego chegar ao servidor. AWS Security Groups (2009), GCP firewall rules (2014), Hetzner Cloud Firewall (2020). Funcionam em paralelo com firewall do sistema operacional — defense in depth.
Firewall examina cada pacote de rede que entra/sai e decide aceitar ou rejeitar baseado em regras. Em essência, três coisas:
Ponto mais importante de toda a configuração: política default. Define o que acontece quando nenhuma regra explícita bate. Duas filosofias:
Firewall moderno é stateful: lembra das conexões em andamento. Você diz "permite SSH entrar" — automaticamente as respostas saindo são permitidas (mesma conexão, fluxo bidirecional). Sem stateful, teríamos que configurar regras para cada direção. Com stateful, configuramos só "novas conexões".
Pra servidor moderno em cloud, há tipicamente 3 camadas de firewall agindo:
Internet
↓
┌─────────────────────────────────────────┐
│ Camada 1: WAF / DDoS protection │ ← Cloudflare, AWS Shield
│ (filtragem em aplicação, ataques L7) │
└────────────────┬────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Camada 2: Cloud firewall (security │ ← AWS Security Groups,
│ group) — filtragem por porta/IP │ Hetzner Cloud Firewall,
│ │ gerenciado pelo provedor
└────────────────┬────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Camada 3: Firewall do SO (ufw/iptables) │ ← Configurado dentro da VPS
│ — última linha antes da aplicação │
└────────────────┬────────────────────────┘
↓
Aplicação
Cada camada filtra independentemente. Configuração em uma não substitui a outra. Recomendação: todas as três configuradas para servidores expostos. Para projetos solo:
ufw (Uncomplicated Firewall) é wrapper amigável sobre iptables/nftables. Sintaxe legível, comandos coerentes. Para 95% dos casos, é tudo que você precisa.
# Já instalado em Ubuntu/Debian. Caso não: $ sudo apt install ufw # Status: $ sudo ufw status Status: inactive # Configurar defaults ANTES de habilitar: $ sudo ufw default deny incoming $ sudo ufw default allow outgoing
# ANTES de habilitar, abra SSH — senão você se tranca fora: $ sudo ufw allow 22/tcp comment 'SSH' # Liberar HTTP/HTTPS (vamos ajustar depois pra só Cloudflare): $ sudo ufw allow 80/tcp comment 'HTTP' $ sudo ufw allow 443/tcp comment 'HTTPS' # Habilitar: $ sudo ufw enable Command may disrupt existing ssh connections. Proceed with operation (y|n)? y Firewall is active and enabled on system startup # Status: $ sudo ufw status numbered Status: active To Action From -- ------ ---- [ 1] 22/tcp ALLOW IN Anywhere # SSH [ 2] 80/tcp ALLOW IN Anywhere # HTTP [ 3] 443/tcp ALLOW IN Anywhere # HTTPS [ 4] 22/tcp (v6) ALLOW IN Anywhere (v6) [ 5] 80/tcp (v6) ALLOW IN Anywhere (v6) [ 6] 443/tcp (v6) ALLOW IN Anywhere (v6)
Note que ufw cria regras para IPv4 e IPv6 automaticamente — bom.
# Permitir só de um IP específico (útil pra SSH restrito): $ sudo ufw allow from 200.150.1.2 to any port 22 proto tcp # Permitir de um range: $ sudo ufw allow from 200.150.0.0/24 to any port 22 proto tcp # Bloquear IP específico (lista negra): $ sudo ufw deny from 198.51.100.100 # Deletar regra pelo número: $ sudo ufw status numbered $ sudo ufw delete 3 # deleta regra #3 # Deletar regra pela descrição: $ sudo ufw delete allow 80/tcp # Resetar tudo (cuidado!): $ sudo ufw reset # Desabilitar (mantém regras, só não aplica): $ sudo ufw disable # Modo verboso — vê detalhes (chains, contadores): $ sudo ufw status verbose # Ver logs de tráfego bloqueado: $ sudo tail -f /var/log/ufw.log # Habilitar logging (default é off): $ sudo ufw logging on # Níveis: off, low, medium, high, full. 'low' geralmente é o suficiente.
ufw vem com perfis pré-configurados para serviços comuns:
$ sudo ufw app list Available applications: Nginx Full Nginx HTTP Nginx HTTPS OpenSSH ... # Permitir usando o profile: $ sudo ufw allow 'Nginx Full' # Equivale a: ufw allow 80/tcp + ufw allow 443/tcp
Útil pra clareza nas regras ('Nginx Full' em vez de "80/tcp e 443/tcp"), mas funciona igual.
Você ativou Cloudflare proxy (Cap 2). O mundo vê IPs da Cloudflare; seu IP real está escondido — desde que ninguém descubra. Como atacante pode descobrir? Vazamento em email, header HTTP indiscreto, histórico de DNS, scanning de range do provedor.
Defesa final: aceitar HTTP/HTTPS no servidor APENAS dos ranges de IP da Cloudflare. Conexões diretas, mesmo se descobrirem o IP, são rejeitadas pelo firewall.
Cloudflare publica os ranges atualizados em URLs públicas:
Atualizados periodicamente (semestralmente, em média). Você precisa atualizar seu firewall quando mudar.
#!/bin/bash # cloudflare_ufw.sh # Configura ufw para aceitar HTTP/HTTPS apenas de IPs Cloudflare. # Rodar como root. set -e # 1. Remover regras genéricas de HTTP/HTTPS, se existirem: ufw delete allow 80/tcp 2>/dev/null || true ufw delete allow 443/tcp 2>/dev/null || true # 2. Buscar ranges atuais echo "Baixando ranges IPv4 da Cloudflare..." IPV4_RANGES=$(curl -fsSL https://www.cloudflare.com/ips-v4) echo "Baixando ranges IPv6 da Cloudflare..." IPV6_RANGES=$(curl -fsSL https://www.cloudflare.com/ips-v6) # 3. Adicionar regra por range: for ip in $IPV4_RANGES; do ufw allow from "$ip" to any port 80,443 proto tcp comment "Cloudflare" done for ip in $IPV6_RANGES; do ufw allow from "$ip" to any port 80,443 proto tcp comment "Cloudflare" done # 4. Recarregar ufw reload echo "Pronto. ufw status:" ufw status numbered
Cloudflare muda ranges raramente, mas muda. Opções:
# Em /etc/cron.d/cloudflare-ufw, rodar todo dia 1 do mês às 4h: 0 4 1 * * root /opt/scripts/cloudflare_ufw.sh >> /var/log/cloudflare-ufw.log 2>&1
http://seudomínio/.well-known/acme-challenge/... direto, não passando pela Cloudflare. Restringir porta 80 só pra Cloudflare quebra a renovação. Soluções: (1) usar DNS-01 challenge (recomendado), (2) usar Cloudflare Origin Certificate (sem Let's Encrypt no servidor), (3) manter porta 80 aberta para todos. Veja Cap 4.
ufw cobre 95% dos casos, mas há configurações onde você precisa baixar para iptables (que é o que ufw usa por baixo). Casos:
# Listar regras (com numeração): $ sudo iptables -L -v -n --line-numbers # Listar regras NAT: $ sudo iptables -t nat -L -v -n # Adicionar regra (exemplo: bloquear IP): $ sudo iptables -I INPUT -s 198.51.100.0/24 -j DROP # Deletar regra: $ sudo iptables -D INPUT 3 # deleta regra 3 da chain INPUT # Salvar regras (para persistir reboot): $ sudo apt install iptables-persistent $ sudo netfilter-persistent save
Em sistemas modernos, comandos iptables podem estar usando nftables por baixo (transparente). Para usar diretamente:
# Listar tudo: $ sudo nft list ruleset # Adicionar regra: $ sudo nft add rule inet filter input ip saddr 198.51.100.100 drop
Para projetos solo, raramente vai precisar. Mas conhecer o nome ajuda quando aparece em docs e fóruns.
Em AWS, GCP, Azure, e provedores modernos como Hetzner Cloud, há firewall gerenciado pela plataforma — aplicado antes do tráfego chegar à VPS.
resource "hcloud_firewall" "prod" { name = "prod-firewall" rule { direction = "in" protocol = "tcp" port = "22" source_ips = ["200.150.1.2/32"] # só meu IP } rule { direction = "in" protocol = "tcp" port = "443" source_ips = ["173.245.48.0/20", ...] # ranges Cloudflare } }
Sim, vale ter ambos. Razões:
Trade-off: dois lugares pra atualizar quando muda regra. Aceitável.
VPS moderna vem com IPv4 + IPv6 público. ufw configurado cria regras para ambos automaticamente. Mas há armadilhas:
:: (todos IPv6) mas você só fechou 0.0.0.0 (IPv4) no firewall. Vulnerável via IPv6.$ sudo ss -tlnp # Vê tudo escutando, tanto v4 quanto v6. # Coluna "Local Address:Port": # 0.0.0.0:80 → escuta em IPv4 todas as interfaces # *:80 → escuta em IPv4 + IPv6 todas # [::]:80 → escuta em IPv6 todas # 127.0.0.1:5432 → escuta só localhost (Postgres bem configurado) # Se vê serviço em 0.0.0.0 ou *, considere se deveria estar exposto.
Idealmente, serviços de aplicação (banco, cache) bind em 127.0.0.1 ou IP privado da rede interna — não no IP público. Defense in depth no nível da aplicação.
ufw padrão libera todo o tráfego de saída. Você poderia restringir — "servidor só pode acessar internet via porta 443" — limitando dano de comprometimento. Trade-offs:
Continuando da configuração do Cap 7: Hetzner CX22, SSH endurecido, aplicação web rodando. Vamos fechar tudo o que não precisa estar aberto.
$ sudo ss -tlnp Netid State Local Address:Port Process tcp LISTEN 0.0.0.0:22 sshd tcp LISTEN 0.0.0.0:80 caddy tcp LISTEN 0.0.0.0:443 caddy tcp LISTEN 127.0.0.1:8000 gunicorn # app, OK só local tcp LISTEN 0.0.0.0:5432 postgres # PROBLEMA: Postgres exposto!
Postgres não deveria estar acessível externamente. Configurar para escutar só em localhost:
$ sudo nano /etc/postgresql/16/main/postgresql.conf # Procurar/mudar: listen_addresses = 'localhost' $ sudo systemctl restart postgresql $ sudo ss -tlnp | grep 5432 tcp LISTEN 127.0.0.1:5432 postgres # Agora só local. Bom.
$ sudo ufw default deny incoming $ sudo ufw default allow outgoing $ sudo ufw allow 22/tcp comment 'SSH' $ sudo ufw allow 80/tcp comment 'HTTP - liberado pra ACME challenge' $ sudo ufw allow 443/tcp comment 'HTTPS' $ sudo ufw enable
# Já que vamos restringir, podemos rodar script: $ sudo nano /opt/scripts/cloudflare_ufw.sh # Cola conteúdo da seção anterior. $ sudo chmod +x /opt/scripts/cloudflare_ufw.sh $ sudo /opt/scripts/cloudflare_ufw.sh # Confirma: $ sudo ufw status numbered | grep Cloudflare | head [ 3] 80,443/tcp ALLOW IN 173.245.48.0/20 # Cloudflare [ 4] 80,443/tcp ALLOW IN 103.21.244.0/22 # Cloudflare ...
$ sudo nano /etc/cron.d/cloudflare-ufw 0 4 1 * * root /opt/scripts/cloudflare_ufw.sh >> /var/log/cloudflare-ufw.log 2>&1
No painel da Hetzner Cloud:
Hetzner gerencia atualizações de IPs Cloudflare automaticamente quando você usa o preset deles.
# Tentar conectar direto no IP da VPS (não pelo DNS que vai pra Cloudflare): $ curl -v http://5.6.7.8/ # Esperado: timeout. Firewall bloqueia. $ curl -v https://5.6.7.8/ -k # Esperado: timeout. # Via Cloudflare: $ curl https://meuapp.com.br/ # Funciona. Tráfego válido passa. # Postgres está exposto? $ nc -zv 5.6.7.8 5432 nc: connect to 5.6.7.8 port 5432 (tcp) failed: Connection timed out # Bom. Banco protegido.
Resultado: servidor com 3 camadas de defesa (Cloudflare, Hetzner firewall, ufw), Postgres bind em localhost, SSH endurecido (do Cap 7). Conexão direta no IP é rejeitada; tráfego válido passa pela cadeia. Próximo capítulo: updates automáticos.
Roda ufw enable sem antes ufw allow 22. Conexão SSH atual ainda funciona (stateful), mas próxima vai falhar. Você não percebe enquanto está logado; quando sair, fica trancado fora.
0.0.0.0Defaults de muitos pacotes bindam em todas as interfaces. Sem firewall, qualquer um na internet pode tentar conectar — credenciais default, tentativas de brute force, vazamento de dados. Bind em localhost ou IP privado sempre.
"Quero só HTTPS!" Fecha porta 80. Próxima renovação de cert via HTTP-01 falha. Cert expira. Cap 4 cobriu — use DNS-01 ou Origin Certificate.
Configurou ufw para v4. Serviço bind em :: (todos IPv6). Acessível via IPv6, vulnerável. ufw moderno faz v4+v6, mas sempre verifique com ss -tlnp.
Configurou uma vez há 2 anos. Cloudflare adicionou novos ranges. Alguns POPs novos tentam acessar seu servidor, são bloqueados. Tráfego legítimo cai. Cron mensal resolve.
"Tenho security group, não preciso de ufw." Bug ou misconfiguração no SG (acontece) → servidor exposto sem proteção interna. Camadas redundantes valem.
"Vou bloquear ping, ninguém precisa." ICMP não é só ping — inclui Path MTU Discovery. Bloquear pode quebrar conexões em redes com MTU diferente. Permita ICMP echo-request (ou pelo menos ICMP "unreachable").
meuapp.com.br. Modo SSL Full strict configurado. IP da VPS é 5.6.7.8. Mas atacante descobriu o IP real e está tentando conectar direto em http://5.6.7.8 e https://5.6.7.8. Sem firewall configurado no servidor, o que acontece?"Numa VPS sua, liste TODOS os serviços escutando em portas. Para cada um, identifique: porta, interface (0.0.0.0 vs localhost), e se deveria estar exposto.
$ sudo ss -tlnp Netid State Local Address:Port Process tcp LISTEN 0.0.0.0:22 sshd # SSH — exposto, OK tcp LISTEN 0.0.0.0:80 caddy # HTTP — exposto, OK tcp LISTEN 0.0.0.0:443 caddy # HTTPS — exposto, OK tcp LISTEN 127.0.0.1:5432 postgres # OK — só localhost tcp LISTEN 127.0.0.1:6379 redis # OK — só localhost tcp LISTEN 0.0.0.0:8000 gunicorn # PROBLEMA: app exposta direto # Deveria estar em 127.0.0.1 # com Caddy na frente
Checklist do que esperar:
0.0.0.0: OK, são públicos por design.Configure ufw num servidor com app web + banco interno. Requisitos:
200.150.1.2)10.0.0.5)# Reset (cuidado: se tiver SSH ativo, NÃO faça reset sem janela paralela) $ sudo ufw default deny incoming $ sudo ufw default allow outgoing # SSH só do IP de casa $ sudo ufw allow from 200.150.1.2 to any port 22 proto tcp comment 'SSH' # HTTP/HTTPS pra todos (Cloudflare cuida no Cap 2) $ sudo ufw allow 80/tcp comment 'HTTP' $ sudo ufw allow 443/tcp comment 'HTTPS' # Postgres só pra IP interno específico $ sudo ufw allow from 10.0.0.5 to any port 5432 proto tcp comment 'Postgres interno' # Habilitar $ sudo ufw enable $ sudo ufw status numbered # Resultado: # To Action From # 22/tcp ALLOW IN 200.150.1.2 (SSH) # 80/tcp ALLOW IN Anywhere (HTTP) # 443/tcp ALLOW IN Anywhere (HTTPS) # 5432/tcp ALLOW IN 10.0.0.5 (Postgres interno) # (Tudo mais: bloqueado por default deny)
Cuidados:
10.0.0.5 só funciona se ambos servidores estão na mesma rede privada (Hetzner Cloud networks, AWS VPC, etc).Adapte o script do capítulo para incluir uma "trapdoor" — uma porta de emergência que só você (do seu IP de casa) pode usar pra emergencialmente acessar HTTP no servidor mesmo se Cloudflare cair. Documente como usar.
#!/bin/bash ## cloudflare_ufw.sh — configurar ufw para HTTP/HTTPS Cloudflare-only, ## com trapdoor pro IP do admin (você). ## ## USO DA TRAPDOOR: ## - Acesso normal: navegue pelo domínio (passa pela Cloudflare) ## - Emergência (Cloudflare fora): acesse direto via IP da VPS ## curl https://5.6.7.8/ -H "Host: meuapp.com.br" -k ## - Funciona porque firewall libera HTTP/HTTPS do MEU_IP_FIXO ## ## Para isso funcionar, ADMIN_IP precisa ser estático e atualizado neste script. set -e ADMIN_IP="200.150.1.2" # Atualize se mudar # Limpar regras antigas relacionadas: ufw status numbered | grep -E "Cloudflare|Admin trapdoor" | awk -F"[][]" '{print $2}' | sort -rn | while read num; do echo "y" | ufw delete "$num" done # Adicionar regras Cloudflare: echo "Configurando ranges Cloudflare..." for ip in $(curl -fsSL https://www.cloudflare.com/ips-v4); do ufw allow from "$ip" to any port 80,443 proto tcp comment "Cloudflare" done for ip in $(curl -fsSL https://www.cloudflare.com/ips-v6); do ufw allow from "$ip" to any port 80,443 proto tcp comment "Cloudflare" done # Trapdoor — meu acesso direto: echo "Adicionando trapdoor para $ADMIN_IP..." ufw allow from "$ADMIN_IP" to any port 80,443 proto tcp comment "Admin trapdoor" ufw reload ufw status numbered echo "OK. Trapdoor ativo para $ADMIN_IP."
Como usar em emergência:
# Cloudflare está fora. Acessar direto via IP: $ curl -k -H "Host: meuapp.com.br" https://5.6.7.8/ # Ou, modificando /etc/hosts temporariamente: $ echo "5.6.7.8 meuapp.com.br" | sudo tee -a /etc/hosts $ curl https://meuapp.com.br/ # resolve pra IP direto agora # Lembre de remover essa linha quando Cloudflare voltar.
Trade-off honesto: trapdoor adiciona vetor de exposição (mais um IP confiado direto). Vale se seu IP de casa é estático. Se IP dinâmico do ISP, alternativa melhor é VPN/Tailscale entrando direto na VPS.
Você assumiu administração de um servidor. Conduza uma auditoria completa do firewall e exposição de serviços. Liste pelo menos 15 verificações que você faria, agrupando por categoria (kernel/SO, ufw/iptables, serviços expostos, Cloudflare integration, IPv6).
Auditoria estruturada:
A. Política de firewall (4 checks):
$ sudo ufw status verbose
# 1. ufw está ativo? "Status: active"
# 2. Default incoming é deny? "Default: deny (incoming)"
# 3. Logging está habilitado? "Logging: on (low)"
# 4. Há comentários nas regras? (qualidade de manutenção)
B. Serviços expostos (4 checks):
$ sudo ss -tlnp # 5. Algum serviço em 0.0.0.0 que não deveria? (banco, app) # 6. Postgres/Redis/Mongo em localhost? # 7. Aplicação atrás de reverse proxy (em 127.0.0.1, não exposto)? $ sudo ss -ulnp # 8. UDP exposto? DNS recursivo aberto? NTP exposto? (vetores de amplificação DDoS)
C. IPv6 (2 checks):
# 9. Regras ufw cobrem v6? (status mostra "(v6)" nas regras) # 10. Algum serviço em [::]: que não tem regra equivalente? $ sudo ss -tlnp | grep "::"
D. Integração Cloudflare (3 checks):
$ sudo ufw status | grep Cloudflare | wc -l # 11. Há regras restringindo HTTP/HTTPS aos ranges Cloudflare? $ cat /etc/cron.d/cloudflare-ufw 2>/dev/null || echo "sem cron" # 12. Há cron pra atualizar os ranges? $ ls /opt/scripts/cloudflare_ufw.sh 2>/dev/null # 13. Script de atualização presente e atualizado?
E. Camadas paralelas (2 checks):
curl http://IP-DIRETO deveria timeoutar; pelo domínio deveria funcionar.F. Higiene operacional (3 checks):
sudo fail2ban-client status sshdsudo tail -1000 /var/log/ufw.log | sort | uniq -c/etc/ufw? Histórico de comandos auditado?Conclusão da auditoria: 18 checks numerados. Cada um identifica problema concreto ou confirma higiene. Pode evoluir para um script de auditoria automatizado em sistemas maduros.
O servidor que você esqueceu de atualizar é o servidor que vai ser invadido. Não por um gênio do mal com exploit zero-day — por um script automatizado varrendo a internet atrás de uma CVE pública de oito meses atrás que você nunca aplicou. Patching é a defesa mais barata e mais negligenciada que existe.
Este capítulo é sobre manter o software do servidor atualizado sem quebrar produção no meio da madrugada. Vamos cobrir o que é uma CVE e como priorizar, configurar unattended-upgrades direito (não só ligar — configurar), lidar com reboots e patches de kernel, e estender o pensamento para a supply chain: as dependências da sua aplicação, que hoje são um vetor de ataque tão real quanto o SSH aberto. O objetivo é simples: patches de segurança aplicados automaticamente, com janela de quebra controlada e visibilidade do que aconteceu.
Nos anos 1990 e início dos 2000, atualizar um servidor era um ato manual e ocasional. Admins rodavam apt-get upgrade quando lembravam — frequentemente nunca. O resultado foi uma era de worms automatizados: Code Red (2001) infectou 359 mil máquinas em 14 horas explorando uma falha do IIS que tinha patch havia um mês. SQL Slammer (2003) dobrava de tamanho a cada 8,5 segundos explorando uma vulnerabilidade do SQL Server corrigida seis meses antes.
A lição entrou no DNA da segurança: a maioria dos ataques não usa zero-days; usa falhas conhecidas e não corrigidas. Em resposta, surgiu a infraestrutura de divulgação coordenada. O sistema CVE (Common Vulnerabilities and Exposures) nasceu em 1999 no MITRE, dando um identificador único a cada falha. Em 2005 veio o CVSS, um placar numérico de severidade.
No Debian/Ubuntu, o pacote unattended-upgrades (2009) automatizou a aplicação de patches de segurança. Livepatch (Canonical, 2016) e kpatch (Red Hat) trouxeram patching de kernel sem reboot. O problema "esqueci de atualizar" virou, em grande parte, um problema resolvido — para quem configura.
A fronteira se moveu. A partir de 2020, a ameaça mais comentada deixou de ser o pacote do sistema e passou a ser a supply chain: o código de terceiros que sua aplicação puxa. O SolarWinds (2020) comprometeu 18 mil organizações via uma atualização envenenada. O backdoor do xz/liblzma (CVE-2024-3094, descoberto em março de 2024) quase colocou uma porta dos fundos no SSH de meio mundo Linux — plantada por um mantenedor paciente ao longo de dois anos. Patching, em 2026, é sobre o sistema e sobre tudo que ele importa.
Antes de automatizar, vale entender o vocabulário, porque ele aparece em todo aviso de segurança que você vai ler.
Uma CVE é um número único para uma vulnerabilidade específica, no formato CVE-ANO-NÚMERO — por exemplo CVE-2024-3094. Não diz nada sobre gravidade por si só; é só um nome estável para que todo mundo (fornecedor, pesquisador, scanner, você) esteja falando da mesma falha.
O CVSS (Common Vulnerability Scoring System) dá uma nota de 0.0 a 10.0. A faixa importa mais que o número exato:
| Faixa CVSS | Severidade | Postura prática |
|---|---|---|
| 9.0 – 10.0 | Crítica | Patch hoje. Acorde de madrugada se preciso. |
| 7.0 – 8.9 | Alta | Patch em dias, não semanas. |
| 4.0 – 6.9 | Média | Ciclo normal de updates. |
| 0.1 – 3.9 | Baixa | Quando der; agrupe com outros. |
O conceito que organiza tudo é a janela de exposição: o tempo entre o patch ficar disponível e você aplicá-lo. Durante essa janela, a falha é pública (logo, conhecida pelos atacantes) e o seu servidor segue vulnerável. O objetivo de todo o capítulo é encolher essa janela ao máximo — idealmente para "horas" no caso de segurança, sem precisar de você acordado.
Patch publicado Você aplica
│ │
▼ ▼
──────┼───────────────────────┼──────────► tempo
│◄─── janela de ────────►│
│ exposição │
│ │
atacantes já sabem você protegido
da falha (é pública)
Objetivo: encolher essa janela.
Segurança → automatizada (horas).
Demais → ciclo controlado (dias).
Nem todo update é igual. Tratar todos da mesma forma leva ou à paralisia (com medo de quebrar) ou à imprudência (atualiza tudo às cegas). O modelo sadio separa em três baldes:
-security. Aplicar automaticamente, rápido. Risco de quebra baixo, risco de não aplicar alto.
A regra de ouro: automatize segurança, controle o resto. Patches de segurança quase nunca quebram nada (o mantenedor faz backport mínimo da correção para a versão estável). Updates funcionais e upgrades maiores, sim. É por isso que a configuração padrão do unattended-upgrades só pega segurança — e é assim que deve ficar na maioria dos servidores.
Esse é o coração do capítulo. O pacote já vem no Ubuntu, mas vale instalar e — principalmente — configurar. Ligar sem configurar deixa pontas soltas (não reinicia serviços, não avisa de nada, não limpa kernels antigos).
$ sudo apt update $ sudo apt install unattended-upgrades apt-listchanges # Habilitar via diálogo interativo (cria 20auto-upgrades): $ sudo dpkg-reconfigure --priority=low unattended-upgrades # Conferir se está ativo: $ systemctl status unattended-upgrades $ cat /etc/apt/apt.conf.d/20auto-upgrades APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1";
O arquivo 50unattended-upgrades é onde mora a decisão. Por padrão ele só aplica segurança — bom. Mas há quatro linhas que você quase sempre quer ajustar:
// Quais origens atualizar — manter SÓ security é o conservador: Unattended-Upgrade::Allowed-Origins { "${distro_id}:${distro_codename}-security"; "${distro_id}ESMApps:${distro_codename}-apps-security"; "${distro_id}ESM:${distro_codename}-infra-security"; }; // 1. Remover dependências órfãs após o upgrade: Unattended-Upgrade::Remove-Unused-Dependencies "true"; // 2. Limpar kernels antigos (senão /boot enche e quebra): Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; // 3. Reiniciar serviços afetados sem perguntar // (libssl atualizada → reinicia quem usa, sem reboot): Unattended-Upgrade::Automatic-Reboot "false"; $nrconf{restart} = "a"; // via /etc/needrestart/needrestart.conf // 4. Avisar por email se algo der errado: Unattended-Upgrade::Mail "voce@exemplo.com"; Unattended-Upgrade::MailReport "on-change";
libssl é atualizada, os processos que já estavam rodando continuam usando a versão antiga em memória até serem reiniciados. O patch foi aplicado no disco, mas seu Nginx ainda roda o código vulnerável. O pacote needrestart resolve isso reiniciando os serviços afetados. Em /etc/needrestart/needrestart.conf, defina $nrconf{restart} = 'a'; (automático) em servidores — caso contrário ele trava o unattended-upgrade num prompt interativo que ninguém vai responder.
Antes de confiar, faça um dry-run. Ele mostra exatamente o que seria atualizado sem mexer em nada:
# Simular — não aplica, só lista: $ sudo unattended-upgrade --dry-run --debug # Rodar de verdade, agora, manualmente: $ sudo unattended-upgrade -v # Ver o que já foi feito automaticamente: $ cat /var/log/unattended-upgrades/unattended-upgrades.log $ zcat /var/log/unattended-upgrades/unattended-upgrades.log.*.gz
Liga unattended-upgrades e esquece. Serviços não reiniciam (patch só vale após reboot manual). /boot enche de kernels e quebra o próximo update. Nenhum email quando falha. Falsa sensação de segurança.
Só security automático. needrestart reinicia serviços afetados. Kernels antigos limpos. Email on-change avisa o que foi feito. Você sabe que está protegido e por quê.
Um problema fica de fora do needrestart: o próprio kernel. Patch de kernel só passa a valer depois de um reboot — não há como reiniciar "o kernel" sem reiniciar a máquina. E aqui mora uma tensão real: rebootar derruba a aplicação (downtime), mas não rebootar deixa o servidor rodando um kernel vulnerável indefinidamente.
# Se este arquivo existe, há reboot pendente: $ ls /var/run/reboot-required $ cat /var/run/reboot-required.pkgs # quais pacotes pediram # Kernel rodando vs kernel instalado: $ uname -r # o que está em execução $ ls /boot/vmlinuz-* # o que está instalado
Automatic-Reboot "true" e Automatic-Reboot-Time "04:00". Simples, mas derruba a app — só serve se o downtime de 1 minuto às 4h é aceitável.$ sudo pro attach <SEU-TOKEN> # token em ubuntu.com/pro $ sudo pro enable livepatch $ canonical-livepatch status # kernel: 6.8.0-x # fully-patched: true # → CVEs de kernel corrigidas em memória, sem reboot
Até aqui falamos do sistema operacional. Mas a maior superfície de ataque moderna não é o apt — é tudo que sua aplicação importa: pacotes npm, módulos pip, gems, crates, imagens Docker base. Cada dependência é código de um estranho que você roda com os privilégios da sua aplicação. Uma única lib comprometida na sua árvore de dependências é um comprometimento da sua aplicação.
python-requests vs requests, colour vs color). Você erra a digitação, instala o do atacante.event-stream (2018), baixado milhões de vezes, ganhou um roubador de carteiras de Bitcoin.xz (2024): um colaborador construiu confiança por dois anos antes de inserir o backdoor. Difícil de detectar porque a fonte parecia limpa — o payload estava em arquivos de teste binários.FROM alguma-imagem:latest sem fixar digest. O mantenedor da imagem (ou quem comprometeu a conta dele) injeta algo.# 1. Lockfiles SEMPRE versionados e instalados a partir deles: $ npm ci # usa package-lock.json, não package.json $ pip install -r requirements.txt # com versões fixas + hashes $ uv sync --frozen # respeita uv.lock # 2. Auditar vulnerabilidades conhecidas na árvore: $ npm audit $ pip-audit # pip install pip-audit # 3. Fixar imagens Docker por digest, não por tag mutável: # RUIM: FROM node:20 # BOM: FROM node:20@sha256:abc123... (imutável) # 4. Escanear a imagem antes de subir: $ trivy image meuapp:latest # CVEs nas camadas
package-lock.json, poetry.lock, uv.lock ou Cargo.lock registra a versão exata de cada dependência e subdependência, muitas vezes com hash criptográfico. Sem ele, npm install hoje e amanhã podem trazer árvores diferentes — e a de amanhã pode incluir a versão envenenada que acabou de ser publicada. Versione o lockfile e instale a partir dele em produção (npm ci, não npm install). É a diferença entre build reproduzível e roleta-russa.
Manualmente acompanhar CVEs de todas as suas dependências é inviável. Ferramentas como Dependabot (GitHub, grátis) e Renovate abrem pull requests automáticos quando uma dependência tem update — marcando quais são correções de segurança. Você revisa, o CI testa, e você faz merge. A vigilância vira parte do fluxo em vez de uma tarefa que ninguém faz.
version: 2 updates: - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" # Agrupar updates menores num PR só, pra não inundar: groups: minor-patch: update-types: ["minor", "patch"] - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly"
Há um ponto que escapa de muita gente: o servidor roda runtimes (Node, Python, PostgreSQL) que também recebem CVEs, e nem sempre vêm pelo apt. Se você instalou Node via NodeSource, Python via deadsnakes, ou roda tudo em contêiner, o unattended-upgrades do sistema pode não estar cobrindo esses.
Allowed-Origins inclui o repositório, ou atualize-os manualmente no seu ciclo.Automação cega tem limites. Não coloque updates funcionais (não-segurança) em automático num servidor de produção crítico sem CI/staging — uma mudança de comportamento de pacote pode quebrar a app sem ninguém perceber até o cliente reclamar. Não faça upgrade de versão maior do SO automaticamente, nunca. E em servidores com SLA rígido, prefira reboot manual coordenado a reboot automático às cegas. A regra "automatize segurança, controle o resto" existe justamente por isso.
Automação sem monitoramento é só um jeito mais silencioso de falhar — o mesmo princípio do Cap 4 sobre renovação de TLS. Você precisa de visibilidade de que o patching está, de fato, acontecendo. O mínimo viável:
| Sinal | Como verificar | O que indica |
|---|---|---|
| Updates aplicados | Email on-change do unattended-upgrades | Patching ativo e funcionando |
| Falhas no upgrade | Log em /var/log/unattended-upgrades/ | Algo travou (prompt, disco, lock) |
| Reboot pendente | /var/run/reboot-required | Kernel patcheado, aguardando reboot |
/boot cheio | df -h /boot | Kernels antigos não limpos → próximo update quebra |
| CVEs nas deps | Dependabot / pip-audit / npm audit no CI | Vulnerabilidade conhecida na aplicação |
# Quantos pacotes de segurança pendentes? $ apt list --upgradable 2>/dev/null | grep -c security # unattended-upgrades rodou nas últimas 24h? $ grep "$(date +%Y-%m-%d)" \ /var/log/unattended-upgrades/unattended-upgrades.log # Reboot pendente? $ [ -f /var/run/reboot-required ] && echo "REBOOT NECESSÁRIO" \ || echo "ok" # /boot tem espaço? $ df -h /boot
Você assumiu uma VPS (Ubuntu 24.04) com a aplicação de uma startup. Ninguém configurou updates. apt list --upgradable mostra 40 pacotes pendentes, 12 deles de segurança. O /boot está em 78%. A app é um SaaS B2B com uns minutos de downtime toleráveis de madrugada.
$ apt list --upgradable 2>/dev/null | grep security $ df -h /boot $ uname -r ; ls /boot/vmlinuz-* $ ls /var/run/reboot-required 2>/dev/null # Entender o estado: o que está pendente, há reboot acumulado?
# Em janela de baixo tráfego, snapshot antes (provedor) por segurança: $ sudo apt update $ sudo unattended-upgrade -v # aplica só o repo de security # needrestart deve reiniciar os serviços afetados
$ sudo apt autoremove --purge
$ df -h /boot # conferir que baixou de 78%
$ sudo apt install unattended-upgrades needrestart # Editar 50unattended-upgrades: Remove-Unused-Kernel, # Mail on-change, needrestart automático (seção 9.4) $ sudo nano /etc/apt/apt.conf.d/50unattended-upgrades $ sudo nano /etc/needrestart/needrestart.conf # $nrconf{restart}='a' # Reboot automático às 4h (downtime tolerável neste caso): # Automatic-Reboot "true"; Automatic-Reboot-Time "04:00"; $ sudo unattended-upgrade --dry-run --debug # validar
# No repositório da aplicação: $ npm audit # ou pip-audit # Habilitar Dependabot (.github/dependabot.yml) # Fixar imagem base Docker por digest
Resultado: 12 patches de segurança aplicados e serviços reiniciados; /boot liberado; unattended-upgrades configurado com needrestart, limpeza de kernel, email on-change e reboot às 4h; supply chain da app sob vigilância do Dependabot. A janela de exposição caiu de "8 meses" para "horas". Próximo capítulo: logs e o início da observabilidade — para enxergar o que está acontecendo no servidor.
Habilita o serviço mas não configura needrestart. Patches são aplicados no disco, mas os processos em memória seguem com a versão vulnerável da lib até um reboot que nunca vem. Proteção no papel, não na prática.
/boot enche e o próximo update quebraKernels antigos não são removidos. /boot chega a 100%, e a próxima instalação de kernel falha pela metade — às vezes deixando o boot inconsistente. Ative Remove-Unused-Kernel-Packages e monitore df -h /boot.
Coloca ${distro_codename}-updates no automático "pra ficar tudo atualizado". Uma mudança de comportamento de pacote quebra a app de madrugada, sem ninguém acordado. Automatize só -security; o resto passa por CI/staging.
npm install em vez de npm ci no deployinstall pode resolver a árvore de dependências diferente do lockfile, puxando versões novas (potencialmente envenenadas) sem você notar. Em produção, instale sempre a partir do lockfile: npm ci, uv sync --frozen, pip install --require-hashes.
FROM imagem:latest sem fixar digestA tag latest é mutável: o que você buildou hoje não é o que vai buildar amanhã. Além de builds não-reproduzíveis, você fica exposto a uma imagem base comprometida. Fixe por @sha256: e atualize deliberadamente.
"Apliquei tudo, estou protegido." Mas o kernel novo só roda após reboot. uname -r ainda mostra o antigo, vulnerável. Ou habilite Livepatch, ou agende reboot — não deixe meses de kernels acumularem sem nunca reiniciar.
Despriorizar uma CVE "média" 5.5 que está sendo ativamente explorada (no KEV da CISA) num serviço seu exposto, enquanto corre atrás de uma "crítica" 9.8 num pacote que você nem tem. Score orienta; exposição real e exploração ativa decidem.
unattended-upgrades num servidor há três meses e o log confirma que patches de segurança vêm sendo aplicados todo dia. Mesmo assim, um pentest aponta que o servidor está vulnerável a uma CVE crítica da libssl corrigida há semanas. Qual a explicação mais provável?Numa VPS sua, descubra: quantos pacotes de segurança estão pendentes, se o unattended-upgrades está ativo, se há reboot pendente, e quanto espaço resta em /boot. Resuma o estado em uma frase: "protegido / parcial / negligenciado".
$ sudo apt update $ apt list --upgradable 2>/dev/null | grep -c security # → nº de pacotes de segurança pendentes $ systemctl is-enabled unattended-upgrades $ cat /etc/apt/apt.conf.d/20auto-upgrades # Unattended-Upgrade "1" = ativo $ [ -f /var/run/reboot-required ] && echo PENDENTE || echo ok $ df -h /boot
Como interpretar:
/boot com folga, sem reboot acumulado há muito tempo./boot apertado, ou needrestart não configurado.Num servidor de teste, configure unattended-upgrades para: aplicar só segurança, reiniciar serviços afetados automaticamente, limpar kernels antigos, e enviar email só quando houver mudança. Valide com um dry-run.
$ sudo apt install unattended-upgrades needrestart apt-listchanges $ sudo dpkg-reconfigure --priority=low unattended-upgrades # /etc/apt/apt.conf.d/50unattended-upgrades Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; Unattended-Upgrade::Remove-Unused-Dependencies "true"; Unattended-Upgrade::Mail "voce@exemplo.com"; Unattended-Upgrade::MailReport "on-change"; # Allowed-Origins: manter SÓ as linhas -security # /etc/needrestart/needrestart.conf $nrconf{restart} = 'a'; # automático, sem prompt # Validar: $ sudo unattended-upgrade --dry-run --debug
Critério de sucesso: o dry-run lista apenas pacotes do repositório -security, não trava em nenhum prompt, e o 50unattended-upgrades não tem nenhuma linha de -updates descomentada. Mande um sudo unattended-upgrade -v e confira o log para fechar o ciclo.
Pegue um repositório real seu (Node, Python ou ambos). Audite as dependências por CVEs conhecidas, verifique se o lockfile está versionado, e configure Dependabot para abrir PRs semanais agrupando patches menores.
# 1. Auditar: $ npm audit # Node $ pip install pip-audit && pip-audit # Python # 2. Lockfile versionado? $ git ls-files | grep -E "package-lock.json|uv.lock|poetry.lock" # Se vazio: o lockfile NÃO está no git. Corrija.
version: 2 updates: - package-ecosystem: "npm" directory: "/" schedule: { interval: "weekly" } groups: minor-patch: update-types: ["minor", "patch"]
O que observar: o npm audit classifica por severidade — priorize as high/critical que estão em dependências diretas (mais fáceis de atualizar). Vulnerabilidades em subdependências transitivas às vezes exigem subir a dependência pai. O Dependabot agrupado evita inundar você de PRs minúsculos enquanto isola as atualizações maiores para revisão individual.
Você precisa desenhar a política de patching para a infraestrutura de uma empresa: três tipos de servidor — (A) API de produção com SLA de 99,9%, (B) workers de processamento batch que toleram downtime, (C) servidor interno de ferramentas. Para cada um, defina: o que automatizar, estratégia de reboot, e o que monitorar. Justifique as diferenças.
Princípio comum aos três: segurança automatizada via unattended-upgrades com needrestart configurado; updates funcionais sempre via CI/staging; upgrade de versão maior do SO nunca automático. O que varia é a estratégia de reboot e o rigor do monitoramento.
A · API de produção (SLA 99,9%):
-security. needrestart automático para reiniciar serviços sem reboot da máquina./boot, e CVEs de dependências no CI bloqueando deploy. É o servidor onde a janela de exposição importa mais.B · Workers batch (downtime tolerável):
-security, igual.Automatic-Reboot "true" numa janela sem jobs agendados (ex.: 4h). O downtime curto é aceitável; simplicidade ganha de complexidade. Drene a fila antes, se possível./boot. Menos crítico que (A) porque o reboot automático já resolve o kernel.C · Servidor interno de ferramentas:
O insight da entrevista: a diferença entre os perfis não está em se patcheia (todos patcheiam segurança automaticamente), mas em como reinicia e quanto observa — função direta do custo do downtime e da criticidade. E o cuidado de não tratar "interno" como "ignorável". Um bom candidato menciona rolling restart para (A) e o risco de movimento lateral em (C).
Todo servidor gera logs. A diferença entre quem opera bem e quem opera no escuro não é ter logs — é conseguir responder perguntas com eles. "Por que o site caiu às 3h14?" tem resposta em segundos para quem sabe onde olhar, e é um mistério de horas para quem só sabe que "tem um log em algum lugar".
Este capítulo fecha a Parte II ensinando a enxergar dentro do servidor. Vamos cobrir onde os logs realmente moram em um Linux moderno, dominar o journald (a peça central que quase ninguém aprende direito), configurar rotação para não encher o disco, entender níveis e structured logging, e — o mais importante — passar de "eu tenho logs" para "eu consigo responder perguntas". No fim, um panorama honesto de quando vale centralizar logs num serviço externo e quando é cedo demais para isso. É a fundação de observabilidade sobre a qual a Parte III vai construir Sentry, uptime e métricas.
Por décadas, log em Unix foi sinônimo de arquivos de texto em /var/log. O syslog, formalizado nos anos 1980 a partir do Sendmail de Eric Allman, definiu o protocolo: cada serviço manda mensagens com uma facility (de onde vem) e uma severity (quão grave), e um daemon as escreve em arquivos. syslogd, depois rsyslog (2004) e syslog-ng (1998), reinaram. A caixa de ferramentas era grep, tail -f, awk.
O modelo tinha limites incômodos: texto sem estrutura (cada serviço inventava seu formato), sem metadados confiáveis, rotação como peça separada (logrotate), e nenhuma forma fácil de correlacionar "todos os logs deste processo durante este boot".
Em 2010, com o systemd, veio o journald — um log binário, indexado, com metadados ricos por mensagem (qual unit, qual PID, qual usuário, qual boot). De repente "me mostre os erros do nginx desde o último boot" virou um comando só. A maioria das distribuições modernas (Ubuntu, Debian, Fedora) usa journald como base, frequentemente com rsyslog em paralelo por compatibilidade.
Em paralelo, a indústria cunhou o termo observabilidade (emprestado da teoria de controle): a capacidade de entender o estado interno de um sistema só pelas suas saídas externas. Os "três pilares" — logs, métricas e traces — viraram vocabulário padrão a partir de ~2017. Para um projeto solo em 2026, observabilidade começa exatamente onde este capítulo começa: sabendo ler o journal.
Observabilidade costuma ser descrita por três tipos de sinal. Vale conhecer os três para saber onde os logs se encaixam — e onde param.
Para um servidor único rodando uma aplicação, logs respondem 80% das perguntas operacionais: o que quebrou, quando, com qual erro, precedido de quê. Métricas entram quando você quer tendências ("a memória vem subindo há uma semana?"). Traces só fazem sentido com múltiplos serviços. Comece pelos logs, domine-os, e adicione o resto quando a dor justificar — não antes.
Em um Ubuntu/Debian moderno, há essencialmente dois mundos coexistindo. Saber qual olhar economiza muito tempo.
| Fonte | Onde | O que tem |
|---|---|---|
| journald | journalctl (binário em /var/log/journal ou /run/log) | Tudo que os serviços systemd emitem: app, ssh, cron, kernel |
| rsyslog (texto) | /var/log/syslog, /var/log/auth.log | Cópia em texto, por compatibilidade e ferramentas antigas |
| Logs próprios da app | /var/log/nginx/, /var/log/caddy/, ou onde a app escrever | Access logs, error logs específicos do serviço |
| Kernel | dmesg / journalctl -k | OOM killer, problemas de hardware/disco, rede |
/var/log/journal não existe, o journald escreve em /run — que é memória, apagada a cada reboot. Você investiga uma queda, reinicia o servidor "pra ver se resolve", e perde justamente os logs de antes. Garanta persistência criando o diretório (mostrado na seção 10.4) ou definindo Storage=persistent.
O journalctl é a ferramenta que você vai viver dentro. Vale memorizar um punhado de invocações — elas cobrem quase tudo.
# Seguir tudo ao vivo (o "tail -f" do journal): $ journalctl -f # Logs de UM serviço, ao vivo: $ journalctl -u nginx -f $ journalctl -u meuapp.service -f # Desde o último boot (perde ruído de boots antigos): $ journalctl -b # Janela de tempo — investigando "caiu às 3h14": $ journalctl --since "03:00" --until "03:30" $ journalctl --since "2026-06-15 02:00" --since "1 hour ago" # Só erros e acima (prioridade): $ journalctl -p err -b # Kernel (OOM killer, disco, rede): $ journalctl -k -b # Mais recentes primeiro, sem paginador: $ journalctl -u meuapp -n 100 --no-pager -r
journalctl -u meuapp -p err --since "1 hour ago" = "erros da minha app na última hora". Uma pergunta operacional, um comando, resposta em segundos. Compare com grep num arquivo de texto gigante de múltiplos serviços misturados — onde você primeiro precisa achar as linhas da sua app, depois filtrar erro, depois recortar tempo.
# O journal está persistente ou volátil? $ journalctl --header | grep -i "File path" # /run/log/... → volátil (some no reboot) # /var/log/journal/... → persistente (sobrevive) # Tornar persistente: $ sudo mkdir -p /var/log/journal $ sudo systemd-tmpfiles --create --prefix /var/log/journal $ sudo systemctl restart systemd-journald # Verificar uso de disco do journal: $ journalctl --disk-usage
A maneira mais limpa de a sua app aparecer no journalctl -u meuapp é rodá-la como um serviço systemd e deixar que ela escreva em stdout/stderr. O systemd captura essas saídas e as roteia para o journal automaticamente — sem você gerenciar arquivo de log nenhum.
[Unit] Description=Minha aplicação After=network.target [Service] User=appuser WorkingDirectory=/opt/meuapp ExecStart=/opt/meuapp/.venv/bin/gunicorn app:app -b 127.0.0.1:8000 Restart=on-failure # Saída vai pro journal; identificador facilita o filtro: StandardOutput=journal StandardError=journal SyslogIdentifier=meuapp [Install] WantedBy=multi-user.target
Log que cresce sem limite é uma bomba-relógio: o disco enche, a aplicação que tenta escrever falha, o banco para de aceitar gravação, e o servidor vira um tijolo. Há dois sistemas de rotação rodando em paralelo, um para cada mundo de logs.
[Journal] Storage=persistent # Teto de espaço total do journal: SystemMaxUse=1G # Apagar entradas mais antigas que isto: MaxRetentionSec=30day # Deixar pelo menos isto livre no disco: SystemKeepFree=2G
# Reduzir o journal a 500MB agora: $ sudo journalctl --vacuum-size=500M # Apagar tudo mais antigo que 7 dias: $ sudo journalctl --vacuum-time=7d
Logs próprios (Nginx, Caddy, e qualquer coisa que escreva em /var/log/*.log) são geridos pelo logrotate, que roda diariamente via cron/timer. Ele renomeia, comprime e descarta logs antigos.
/var/log/meuapp/*.log {
daily # rotacionar todo dia
rotate 14 # manter 14 arquivos (≈14 dias)
compress # gzip dos antigos
delaycompress # não comprimir o mais recente
missingok # ok se o arquivo não existir
notifempty # não rotacionar arquivo vazio
create 0640 appuser appuser
sharedscripts
postrotate # avisar a app pra reabrir o arquivo
systemctl reload meuapp.service > /dev/null 2>&1 || true
endscript
}
$ sudo logrotate --debug /etc/logrotate.d/meuapp # dry-run $ sudo logrotate --force /etc/logrotate.d/meuapp # forçar agora
Se a app mantém o arquivo de log aberto e o logrotate só renomeia, a app continua escrevendo no arquivo antigo (agora invisível) — e o novo fica vazio. Por isso o postrotate com reload (ou a diretiva copytruncate para apps que não recarregam). Apps que logam em stdout via journald não têm esse problema — mais um ponto a favor de logar no journal.
Dois hábitos de aplicação melhoram drasticamente a utilidade dos logs — e ambos são decisões de quem escreve a app, não de quem opera o servidor.
A regra prática: se tudo é ERROR, nada é ERROR. Logar com nível certo é o que permite journalctl -p err retornar exatamente os problemas reais, e não 10 mil linhas de ruído de INFO.
User joao falhou login do IP 1.2.3.4 às 3h14
Legível por humano, péssimo para máquina. Filtrar "todos os logins falhos do usuário joao" exige regex frágil. Cada mensagem tem formato diferente.
{"level":"warning",
"event":"login_failed",
"user":"joao",
"ip":"1.2.3.4"}
Campos consultáveis. "Todos os login_failed do joao" vira filtro exato. Pronto para qualquer ferramenta de log futura, sem reescrever parser.
structlog (Python), pino (Node) ou o slog (Go) fazem o trabalho. E o benefício aparece exatamente no pior momento: 3h da manhã, produção fora, você precisando recortar "todos os erros do checkout do cliente X na última hora". Texto livre transforma isso em arqueologia; JSON transforma em uma consulta.
O salto de maturidade do capítulo é este: parar de "olhar logs" e começar a interrogá-los. Toda investigação operacional é uma pergunta. Veja como traduzir perguntas reais em comandos.
| Pergunta operacional | Como responder |
|---|---|
| "A app caiu — por quê e quando?" | journalctl -u meuapp -p err -b --no-pager |
| "O servidor matou meu processo?" (OOM) | journalctl -k -b | grep -i "out of memory" |
| "Quem tentou entrar por SSH?" | journalctl -u ssh --since today | grep -i fail |
| "O que aconteceu no boot que travou?" | journalctl -b -1 -p warning (boot anterior) |
| "Picos de erro 5xx no Nginx?" | grep '" 5' /var/log/nginx/access.log | tail |
| "A app reiniciou sozinha? Quantas vezes?" | journalctl -u meuapp | grep -c "Started" |
# Sintoma: "o site ficou lento/fora por volta das 3h" # 1. O que a app reportou naquela janela? $ journalctl -u meuapp --since "03:00" --until "03:30" -p warning # 2. O kernel matou algo por falta de memória? $ journalctl -k --since "03:00" | grep -i "oom\|killed process" # 3. A app reiniciou? (systemd registra Started/Stopped) $ journalctl -u meuapp --since "03:00" | grep -E "Started|Stopped|Failed" # 4. O disco encheu naquele momento? (correlacionar) $ journalctl --since "03:00" | grep -i "no space\|disk full" # Conclusão típica: OOM killer derrubou gunicorn → systemd # reiniciou → 90s de indisponibilidade. Causa raiz: memória.
Em algum momento alguém vai sugerir mandar todos os logs para um serviço externo — Grafana Loki, a stack ELK, Datadog, Better Stack. É uma boa ideia… eventualmente. O ponto é não fazer cedo demais nem tarde demais.
Um servidor, uma app, projeto solo: o journalctl na própria máquina responde tudo. Montar Loki/ELK para um servidor é complexidade sem retorno — mais uma coisa para manter, mais consumo de recurso, mais superfície de falha. A regra: centralize quando o atrito de não ter superar o custo de manter. Sinais de que chegou a hora estão logo abaixo.
| Opção | Perfil | Custo de operar |
|---|---|---|
| journalctl local | 1 servidor, projeto solo | Zero. Já está lá. |
| Grafana Loki (self-host) | Poucos servidores, controle e custo baixo | Médio — você mantém |
| Better Stack / Logtail | Quer simplicidade, tier grátis generoso | Baixo — gerenciado |
| ELK (Elasticsearch) | Volume alto, busca poderosa | Alto — pesado de manter |
| Datadog / similar | Time, orçamento, tudo num lugar | $$ — pode ficar caro rápido |
Você acorda com uma mensagem de um cliente: "o sistema ficou fora por uns minutos às 3h da manhã". O monitor de uptime confirma um blip de ~2 minutos às 03h12. A app voltou sozinha. Um servidor único, Ubuntu, app Python em gunicorn via systemd, Postgres local. Nenhum deploy foi feito à noite. Você precisa da causa raiz antes que aconteça de novo.
# Journal sobreviveu à madrugada? $ journalctl --header | grep -i "file path" # /var/log/journal = bom # Ver tudo na janela do incidente: $ journalctl --since "03:05" --until "03:20" -p warning --no-pager
$ journalctl -u meuapp --since "03:05" | grep -E "Stopped|Started|Failed|killed" # Achado: "Main process exited, code=killed, signal=KILL" # seguido de "Started Minha aplicação" 8s depois. # → algo MATOU o processo, e o systemd o reergueu.
$ journalctl -k --since "03:05" | grep -i "out of memory\|oom" # Achado: "Out of memory: Killed process 1234 (gunicorn)" # → o kernel matou o gunicorn por falta de memória. Causa raiz.
# Havia um job pesado às 3h? cron, backup, relatório? $ journalctl --since "03:00" --until "03:15" | grep -iE "cron|backup|report" # Achado: backup do Postgres roda às 03h10 e consome RAM. # Pico de backup + carga da app estourou a memória da VPS.
Causa raiz: o backup noturno do banco competiu por memória com a aplicação numa VPS justa de RAM; o OOM killer escolheu o gunicorn; o systemd reiniciou em 8s — daí o blip de ~2min. Ações: (1) adicionar swap como rede de segurança (Cap 6), (2) escalonar o backup para uma janela mais calma ou limitar seu uso de memória, (3) considerar um tier de RAM maior se recorrer. E — fechando o ciclo do livro — configurar um alerta para "OOM" para não depender de cliente avisar.
Resultado: uma queda misteriosa "que voltou sozinha" virou uma causa raiz precisa em poucos minutos, usando só journalctl — porque o journal era persistente e as perguntas certas foram feitas na ordem certa. Isto é observabilidade no nível fundação: nenhuma ferramenta externa, só saber interrogar o que o servidor já registra. A Parte III vai construir sobre esta base — Sentry para capturar a exceção no instante em que acontece, uptime para detectar o blip antes do cliente.
Servidor com journal em /run (volátil). Você dá reboot "pra ver se resolve" e apaga exatamente os logs do incidente. Sempre garanta /var/log/journal antes de precisar. É a primeira coisa a configurar num servidor novo.
Log cresce sem rotação, /var chega a 100%, a app não consegue mais escrever, o Postgres entra em modo read-only ou para. Pior: às vezes o próprio servidor fica inacessível. Configure SystemMaxUse no journald e logrotate nos arquivos desde o início.
App que loga toda requisição como ERROR, ou que nunca usa nível acima de INFO. Nos dois casos journalctl -p err vira inútil — ou cheio de ruído, ou vazio quando há problema real. Níveis têm que significar algo.
Token, senha, número de cartão ou CPF caindo no log. Agora o segredo está em disco, em backup, e — se você centralizar — num serviço de terceiro. Logs também são superfície de vazamento (e questão de LGPD, Cap 20). Mascare campos sensíveis na origem.
Rotaciona o arquivo mas a app continua escrevendo no inode antigo (renomeado). O log "novo" fica vazio e você jura que a app parou de logar. Use postrotate com reload ou copytruncate — ou logue no journal e esqueça o problema.
Montar ELK ou Loki para um único servidor de hobby. Você passa a manter um sistema de logs mais complexo que a aplicação que ele observa. Comece com journalctl; centralize quando tiver mais de um servidor ou logs efêmeros — não antes.
Ausência de log de erro não é prova de saúde — pode ser que a app travou antes de logar, ou que o nível de log esconde o problema, ou que o disco de log encheu. Logs respondem "o que aconteceu"; para "está saudável agora?" você precisa de métricas e uptime (Parte III).
journalctl -b. Qual a explicação mais provável para os logs do incidente terem sumido?Numa VPS sua, descubra se o journal é persistente ou volátil. Se for volátil, torne-o persistente. Depois, descubra quanto espaço ele ocupa e qual é o evento de erro mais recente.
# Persistente ou volátil? $ journalctl --header | grep -i "file path" # Se aparecer /run/log → volátil. Corrigir: $ sudo mkdir -p /var/log/journal $ sudo systemd-tmpfiles --create --prefix /var/log/journal $ sudo systemctl restart systemd-journald # Espaço ocupado: $ journalctl --disk-usage # Erro mais recente: $ journalctl -p err -n 1 --no-pager -r
O que observar: depois de tornar persistente, o caminho passa a ser /var/log/journal/<machine-id>/. Se --disk-usage mostrar um número grande (vários GB), vale configurar SystemMaxUse no journald.conf — coberto no próximo exercício.
Crie uma unit systemd para uma aplicação (real ou um script de exemplo que imprime no stdout). Faça-a logar no journal com um SyslogIdentifier próprio, confirme que journalctl -u a encontra, e configure o journald para nunca passar de 500MB.
[Unit]
Description=App de exemplo
After=network.target
[Service]
ExecStart=/usr/bin/bash -c 'while true; do echo "tick $(date)"; sleep 5; done'
StandardOutput=journal
StandardError=journal
SyslogIdentifier=exemplo
Restart=on-failure
[Install]
WantedBy=multi-user.target
$ sudo systemctl daemon-reload $ sudo systemctl enable --now exemplo $ journalctl -u exemplo -f # deve ver os "tick" # Limitar o journal em /etc/systemd/journald.conf: [Journal] Storage=persistent SystemMaxUse=500M $ sudo systemctl restart systemd-journald
Critério de sucesso: journalctl -u exemplo retorna as linhas do script, journalctl -t exemplo (por identificador) também, e journalctl --disk-usage respeita o teto após algum tempo. Lembre de systemctl disable --now exemplo para limpar depois.
Sem rodar nada, escreva o comando journalctl (ou grep) que responde cada pergunta: (a) erros da minha app na última hora; (b) tentativas de login SSH falhas hoje; (c) o kernel matou algum processo por falta de memória neste boot; (d) minha app reiniciou quantas vezes desde o boot; (e) o que houve entre 02h00 e 02h05 ontem.
# (a) erros da app na última hora $ journalctl -u meuapp -p err --since "1 hour ago" # (b) logins SSH falhos hoje $ journalctl -u ssh --since today | grep -i "failed\|invalid" # (c) OOM killer neste boot $ journalctl -k -b | grep -i "out of memory\|killed process" # (d) quantas vezes a app iniciou desde o boot $ journalctl -u meuapp -b | grep -c "Started" # (e) janela específica de ontem $ journalctl --since "yesterday 02:00" --until "yesterday 02:05"
O insight: cada pergunta operacional vira uma composição de três eixos — fonte (-u, -k), severidade (-p) e tempo (--since/--until, -b). Internalizar esses três eixos é o que transforma "vasculhar logs" em "consultar logs".
Você entrou numa startup com um único servidor de produção e nenhuma observabilidade além de "tem logs em algum lugar". O CTO pergunta: "qual é o plano para os próximos seis meses, e quando a gente para de usar só journalctl?" Estruture uma resposta cobrindo: o que fazer no dia 1, o que adicionar conforme cresce, e os gatilhos concretos para cada passo.
Filosofia da resposta: observabilidade se adiciona por dor, não por moda. Cada camada nova precisa de um gatilho concreto, senão é complexidade prematura. A progressão típica:
Dia 1 · Fundação (custo ~zero):
SystemMaxUse definido. Logrotate nos logs em arquivo. Isso sozinho já cobre a maioria das investigações.Semanas seguintes · Sinais ativos (Parte III):
Quando parar de usar só journalctl — gatilhos concretos:
O que um bom candidato enfatiza: nunca pular a fundação correndo para ferramentas caras; structured logging adotado cedo (barato agora, salva depois); e que journalctl local não é "atraso técnico" — é a escolha certa enquanto há um servidor só. A frase de fechamento: "centralizo quando o atrito de não ter superar o custo de manter, e o gatilho número um é o segundo servidor."
Da porta interna ao mundo. Reverse proxy, plataformas de deploy, edge e a observabilidade ativa que avisa antes do cliente. Cinco capítulos para tirar a aplicação do "roda na minha máquina" e colocá-la de pé, visível e monitorada.
Sua aplicação escuta em 127.0.0.1:8000. O mundo espera em 443. Entre os dois mora o reverse proxy — a peça que recebe o tráfego da internet, encerra o TLS, e entrega para a aplicação certa. É a porta de entrada de quase tudo que você vai colocar no ar, e a primeira coisa a configurar quando a app sai do "roda na minha máquina".
Este capítulo abre a Parte III conectando o servidor (Parte II) à aplicação. Vamos entender por que praticamente nunca se expõe a aplicação direto na porta 443, comparar honestamente as três ferramentas dominantes — Caddy, Nginx e Traefik — e configurar cada uma para o caso real: HTTPS automático, proxy reverso para a app, e os headers sem os quais a aplicação enxerga o mundo errado. A recomendação será pragmática (Caddy para a maioria dos projetos solo), mas você vai sair sabendo operar Nginx, que ainda domina o que já existe lá fora.
No começo, web server era literal: um programa que lia um arquivo de disco e o devolvia por HTTP. O Apache httpd (1995) reinou por uma década, modular e onipresente. Mas seu modelo de um processo/thread por conexão sofria sob muitas conexões simultâneas — o famoso problema C10k (dez mil conexões), articulado por Dan Kegel em 1999.
Em 2004, Igor Sysoev lançou o Nginx, desenhado de raiz com arquitetura orientada a eventos (event-driven, assíncrona) para resolver o C10k. Ele segurava milhares de conexões com pouquíssima memória. Rapidamente virou o reverse proxy e load balancer padrão da web — não tanto para servir arquivos, mas para ficar na frente das aplicações, distribuindo carga e encerrando TLS.
Em 2015 surgiu o Caddy, com uma ideia radical para a época: HTTPS automático por padrão. Ele obtinha e renovava certificados Let's Encrypt sozinho, sem você configurar nada. O que no Nginx exigia certbot, cron e blocos de config virou, no Caddy, duas linhas. No mesmo período, com a ascensão de contêineres, nasceu o Traefik (2016) — um proxy que se autoconfigura descobrindo serviços via labels do Docker/Kubernetes.
Em 2026, os três coexistem por razões diferentes: Nginx pela base instalada gigantesca e performance comprovada, Caddy pela simplicidade que elimina classes inteiras de erro, e Traefik pelo encaixe natural em ambientes dinâmicos de contêiner. Escolher entre eles é menos "qual é melhor" e mais "qual se encaixa no seu mundo".
A pergunta honesta de quem está começando: minha app já consegue escutar na 443 e falar HTTPS — por que botar um proxy na frente? Porque o proxy resolve, num só lugar, uma lista de problemas que você não quer que cada aplicação resolva sozinha:
api.seudominio.com e app.seudominio.com na mesma máquina, na mesma 443, roteados por nome (virtual hosts). Sem proxy, eles brigariam pela porta.127.0.0.1 (localhost), nunca em 0.0.0.0. Só o proxy escuta nas interfaces públicas (80/443). Assim, mesmo que o firewall falhe, a app não está diretamente acessível — o único caminho até ela passa pelo proxy. Isso amarra de volta no Cap 8: ss -tlnp deve mostrar sua app em 127.0.0.1:8000 e só o proxy em 0.0.0.0:443.
Os dois "ficam no meio", mas em pontas opostas. Um forward proxy fica na frente dos clientes (uma VPN corporativa que filtra a saída dos funcionários). Um reverse proxy fica na frente dos servidores: o cliente acha que fala com um servidor só, mas por trás há um proxy distribuindo para uma ou mais aplicações. É deste segundo que o capítulo trata.
Cliente Reverse proxy Aplicações (upstream) │ │ │ │ GET / HTTPS :443 │ │ ├────────────────────────►│ encerra TLS │ │ │ roteia por hostname │ │ │ GET / HTTP :8000 │ │ ├─────────────────────────►│ app principal │ │ │ │ │ (api.dominio → :9000) │ │ ├─────────────────────────►│ API │ resposta HTTPS │ │ │◄────────────────────────┤◄─────────────────────────┤ │ │ │ internet (público) 127.0.0.1 só local só localhost
127.0.0.1:8000.Antes da prática, a decisão. Os três fazem o trabalho; o que muda é o atrito e o encaixe.
| Critério | Caddy | Nginx | Traefik |
|---|---|---|---|
| HTTPS automático | Nativo, padrão | Via certbot (manual) | Nativo |
| Config | Caddyfile (mínima) | Verbosa, poderosa | Labels / dinâmica |
| Curva de aprendizado | Suave | Íngreme | Média-íngreme |
| Encaixe com Docker | Bom | Manual | Excelente (autodescoberta) |
| Base instalada / exemplos | Crescendo | Gigantesca | Média |
| Performance bruta | Ótima | Referência | Ótima |
O argumento do Caddy se prova na primeira config. Aqui está um proxy reverso completo, com HTTPS automático, para uma aplicação rodando em 127.0.0.1:8000:
# É isto. Caddy obtém e renova o certificado sozinho. meuapp.com.br { reverse_proxy 127.0.0.1:8000 }
Duas linhas úteis. O Caddy resolve o ACME (Cap 4) automaticamente: pede o certificado, valida, instala, renova antes de expirar, sem cron, sem certbot, sem você lembrar. Um exemplo mais realista, com estáticos, compressão, headers e múltiplos sites:
meuapp.com.br {
encode gzip zstd # compressão automática
# Servir estáticos direto do disco, app só pro resto:
handle_path /static/* {
root * /opt/meuapp/static
file_server
}
reverse_proxy 127.0.0.1:8000
# Headers de segurança básicos:
header {
Strict-Transport-Security "max-age=31536000"
X-Content-Type-Options "nosniff"
-Server # esconde o header Server
}
log {
output file /var/log/caddy/meuapp.log
}
}
# Segundo serviço, mesma máquina, outro hostname:
api.meuapp.com.br {
reverse_proxy 127.0.0.1:9000
}
# Redirect do www pro apex:
www.meuapp.com.br {
redir https://meuapp.com.br{uri} permanent
}
$ sudo apt install caddy # repo oficial do Caddy # Validar a sintaxe antes de aplicar: $ caddy validate --config /etc/caddy/Caddyfile # Recarregar sem downtime (graceful): $ sudo systemctl reload caddy # Ver o que está acontecendo: $ journalctl -u caddy -f # amarra no Cap 10
Você vai encontrar Nginx em todo lugar, então precisa saber lê-lo e operá-lo mesmo que escolha Caddy para o novo. A mesma tarefa — proxy reverso com HTTPS — exige mais peças, mas cada uma é explícita e ajustável.
# Redireciona HTTP → HTTPS: server { listen 80; listen [::]:80; server_name meuapp.com.br; return 301 https://$host$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name meuapp.com.br; # Certificado (gerado pelo certbot — Cap 4): ssl_certificate /etc/letsencrypt/live/meuapp.com.br/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/meuapp.com.br/privkey.pem; # Estáticos direto do disco: location /static/ { alias /opt/meuapp/static/; expires 30d; } # Tudo o mais vai para a app: location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
# Habilitar o site (symlink para sites-enabled): $ sudo ln -s /etc/nginx/sites-available/meuapp \ /etc/nginx/sites-enabled/ # SEMPRE testar a config antes de recarregar: $ sudo nginx -t # nginx: configuration file ... test is successful # Recarregar sem derrubar conexões: $ sudo systemctl reload nginx # HTTPS automático no Nginx? Use certbot: $ sudo certbot --nginx -d meuapp.com.br # Cap 4
HTTPS é problema seu: certbot, renovação, recarga após renovar. Os quatro proxy_set_header são obrigatórios e fáceis de esquecer — sem eles a app vê o IP do proxy como o de todo cliente. Config verbosa multiplica chance de erro.
Controle total e granular: cada timeout, buffer, cipher, regra de cache é ajustável. Vinte anos de tutoriais, Stack Overflow e módulos. Performance de referência sob carga extrema. Em time grande, o "já conhecemos" tem valor real.
O Traefik brilha num cenário específico: você roda tudo em Docker e quer que novos serviços apareçam no proxy sem editar config. Em vez de você declarar rotas, o Traefik observa o Docker e se configura sozinho a partir de labels nos contêineres.
services:
meuapp:
image: meuapp:latest
labels:
# O Traefik lê isto e cria a rota sozinho:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`meuapp.com.br`)"
- "traefik.http.routers.app.tls.certresolver=le"
- "traefik.http.services.app.loadbalancer.server.port=8000"
Suba um novo contêiner com as labels certas e ele já está roteado e com HTTPS — sem tocar no proxy. Em ambiente estático (um servidor, dois ou três serviços fixos), essa dinâmica é complexidade desnecessária; Caddy é mais simples. Mas quando serviços nascem e morrem o tempo todo, o Traefik economiza um trabalho manual constante. É também o motor por trás de várias plataformas self-hosted que veremos no Cap 12.
Quando há um proxy no meio, a aplicação para de ver o cliente diretamente — ela vê o proxy. Sem repassar certas informações, a app toma decisões erradas. Estes são os headers críticos:
| Header | Para quê | Se faltar |
|---|---|---|
X-Forwarded-For | IP real do cliente | App vê todo mundo com o IP do proxy (127.0.0.1). Rate limit e logs inúteis. |
X-Forwarded-Proto | Cliente veio por HTTPS? | App acha que é HTTP, gera links http://, entra em loop de redirect. |
Host | Qual hostname foi pedido | App com múltiplos domínios roteia errado; links absolutos quebram. |
X-Forwarded-Host | Host original (atrás de várias camadas) | Relevante com Cloudflare + proxy: host pode se perder. |
SECURE_PROXY_SSL_HEADER e USE_X_FORWARDED_HOST; em Express, app.set('trust proxy', ...); em Rails, config.action_dispatch.trusted_proxies. Sem isso, ou a app ignora o header (vê tudo como HTTP), ou confia cegamente em headers forjados (cliente mente o IP). Configure a confiança apontando só para o IP do seu proxy. O Caddy já envia esses headers por padrão no reverse_proxy; no Nginx, são os proxy_set_header que você não pode esquecer.
Você tem uma VPS (Cap 5–10 já feitos: endurecida, com firewall e logs) e precisa publicar: (1) o app web em meuapp.com.br, rodando gunicorn em 127.0.0.1:8000; (2) a API em api.meuapp.com.br, em 127.0.0.1:9000; (3) arquivos estáticos servidos direto, sem passar pela app. Tudo com HTTPS e atrás da Cloudflare. Escolha: Caddy, pela simplicidade.
$ sudo ss -tlnp | grep -E ":8000|:9000" # Devem aparecer em 127.0.0.1, NÃO em 0.0.0.0. # Se estiverem em 0.0.0.0, corrija o bind da app (Cap 8).
meuapp.com.br {
encode gzip zstd
handle_path /static/* {
root * /opt/meuapp/static
file_server
}
reverse_proxy 127.0.0.1:8000
log { output file /var/log/caddy/app.log }
}
api.meuapp.com.br {
reverse_proxy 127.0.0.1:9000
log { output file /var/log/caddy/api.log }
}
# 80/443 abertos só para os ranges Cloudflare (Cap 8): $ sudo ufw status | grep -E "80|443" # DNS dos dois hostnames com proxy laranja ON (Cap 2). # Modo SSL/TLS: Full (strict) (Cap 4).
$ caddy validate --config /etc/caddy/Caddyfile $ sudo systemctl reload caddy $ journalctl -u caddy -f # ver o ACME obter os certs # Do seu lado, testar cada superfície: $ curl -I https://meuapp.com.br $ curl -I https://api.meuapp.com.br $ curl -I https://meuapp.com.br/static/logo.png
Resultado: três superfícies públicas (app, API, estáticos) atendidas por um único proxy, cada uma com HTTPS automático e logs próprios, com as apps invisíveis fora do localhost e o tráfego entrando só pela Cloudflare. O que no Nginx seriam ~60 linhas de config e um certbot a manter, aqui são ~15 linhas que se renovam sozinhas. Próximo capítulo: subir um nível de abstração com plataformas self-hosted (Coolify, Dokploy) que orquestram proxy, deploy e TLS por você — úteis quando os serviços começam a se multiplicar.
Você põe o proxy, mas a app continua em 0.0.0.0:8000. Agora ela é acessível direto pela porta 8000, contornando TLS, headers e qualquer proteção do proxy. Sempre 127.0.0.1 na app; confirme com ss -tlnp (Cap 8).
A app, sem saber que veio HTTPS, redireciona para HTTPS; o proxy entrega; a app redireciona de novo. ERR_TOO_MANY_REDIRECTS. Repasse X-Forwarded-Proto e configure a app para confiar nele.
systemctl restart nginx com erro de sintaxe derruba todos os sites da máquina, não só o que você mexeu. Sempre nginx -t (ou caddy validate) antes, e prefira reload a restart.
Chat, notificações ao vivo, hot-reload — tudo via WebSocket — falham porque o proxy não foi configurado para o upgrade da conexão. No Nginx faltam os headers Upgrade/Connection; o Caddy faz automático. Sintoma: HTTP funciona, "tempo real" não.
Upload de arquivo retorna 413 Request Entity Too Large. O proxy tem um teto de tamanho de corpo (no Nginx, client_max_body_size default de 1MB). Ajuste no proxy e na app — os dois precisam concordar.
Instala Caddy mas deixa o Nginx/certbot antigo rodando. Os dois querem a porta 80 para o ACME; um falha. Escolha um proxy e desative o outro completamente (systemctl disable --now nginx) antes de subir o novo.
A app lê X-Forwarded-For sem validar a origem. Um cliente malicioso forja o header e burla rate limit ou bloqueio por IP. Configure a app para confiar no header só quando vem do IP do seu proxy — não de qualquer um.
ERR_TOO_MANY_REDIRECTS — o navegador diz que houve redirects demais. A aplicação está configurada para forçar HTTPS. Qual a causa mais provável?Suba qualquer app simples escutando em 127.0.0.1:8000 (um python -m http.server 8000 --bind 127.0.0.1 serve). Configure o Caddy para servi-la com HTTPS automático num subdomínio seu. Confirme que o certificado foi emitido e que a app NÃO é acessível direto pela porta 8000 de fora.
teste.meudominio.com.br {
reverse_proxy 127.0.0.1:8000
}
$ caddy validate --config /etc/caddy/Caddyfile $ sudo systemctl reload caddy $ journalctl -u caddy -f # ver o certificado ser emitido $ curl -I https://teste.meudominio.com.br # 200 + cadeado # De OUTRA máquina, a porta 8000 deve estar inacessível: $ curl --max-time 5 http://SEU-IP:8000 # deve dar timeout
O que confirma sucesso: HTTPS abre pelo hostname (Caddy emitiu o cert via ACME), mas a porta 8000 timeouta de fora porque a app está em 127.0.0.1 e o firewall (Cap 8) não a expõe. As duas coisas juntas são o objetivo: público só pelo proxy, app invisível direto.
Dado o Caddyfile abaixo (app + estáticos + redirect de www), escreva a config Nginx equivalente, incluindo o redirect HTTP→HTTPS e todos os proxy_set_header necessários. Assuma que o certbot já gerou os certificados.
loja.com.br {
handle_path /static/* {
root * /opt/loja/static
file_server
}
reverse_proxy 127.0.0.1:8000
}
www.loja.com.br {
redir https://loja.com.br{uri} permanent
}
# HTTP → HTTPS para os dois hostnames: server { listen 80; server_name loja.com.br www.loja.com.br; return 301 https://loja.com.br$request_uri; } # Redirect do www (em HTTPS): server { listen 443 ssl http2; server_name www.loja.com.br; ssl_certificate /etc/letsencrypt/live/loja.com.br/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/loja.com.br/privkey.pem; return 301 https://loja.com.br$request_uri; } # Site principal: server { listen 443 ssl http2; server_name loja.com.br; ssl_certificate /etc/letsencrypt/live/loja.com.br/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/loja.com.br/privkey.pem; location /static/ { alias /opt/loja/static/; expires 30d; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
O que observar: o que eram ~10 linhas no Caddy viram ~35 no Nginx, e há três armadilhas: (1) o redirect HTTP precisa cobrir os dois nomes; (2) o www precisa de seu próprio bloco SSL (não dá pra redirecionar em HTTPS sem certificado); (3) os quatro proxy_set_header são manuais e silenciosamente quebram a app se faltarem. É exatamente essa diferença de superfície de erro que motiva a recomendação por Caddy.
A app registra todos os acessos vindos de 127.0.0.1 — o IP real dos clientes sumiu depois que você pôs o proxy. O rate limit por IP parou de funcionar (bloqueia todo mundo ou ninguém). Liste as causas possíveis e como corrigir, tanto no proxy quanto na app.
Diagnóstico em duas camadas:
1. O proxy está repassando o IP?
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; e X-Real-IP $remote_addr; no bloco location. Se faltam, a app nunca recebe o IP do cliente.2. A app está lendo o header certo?
X-Forwarded-For em vez do IP da conexão TCP (que é sempre o proxy, 127.0.0.1).USE_X_FORWARDED_HOST + middleware; Express app.set('trust proxy', 'loopback'); Rails trusted_proxies.X-Forwarded-For e dribla o rate limit (Erro 7).Há Cloudflare na frente? Então o IP real vem em CF-Connecting-IP, e o X-Forwarded-For conterá o IP da Cloudflare. Use o header da Cloudflare ou restaure o IP real com o módulo/realip apontando para os ranges da Cloudflare. É uma camada a mais de "quem é o cliente de verdade".
Para cada cenário, escolha entre Caddy, Nginx e Traefik e justifique em 2-3 frases, citando o fator decisivo. (a) MVP solo, um servidor, um app Django, quer subir rápido. (b) Empresa com Nginx em 12 servidores, time experiente, precisa adicionar mais um site. (c) Plataforma que sobe dezenas de contêineres efêmeros por dia via CI, cada um precisando de subdomínio e HTTPS. (d) App que precisa de regras de cache muito específicas, rewrite complexo e tuning fino de buffers.
(a) MVP solo, um app, subir rápido → Caddy. Fator decisivo: HTTPS automático elimina toda a manutenção de certificado (Cap 4) e a config é trivial. Não há nada no cenário que justifique a verbosidade do Nginx nem a dinâmica do Traefik. É o caso-alvo do Caddy.
(b) 12 servidores Nginx, time experiente, +1 site → Nginx. Fator decisivo: consistência operacional e conhecimento do time valem mais que elegância. Introduzir um segundo proxy fragmenta a operação (dois jeitos de fazer tudo, dois conjuntos de runbooks). "Já temos e dominamos" é uma razão técnica legítima, não preguiça.
(c) Dezenas de contêineres efêmeros/dia via CI → Traefik. Fator decisivo: autodescoberta por labels. Editar config a cada contêiner que nasce/morre seria inviável; o Traefik se reconfigura sozinho observando o Docker. É exatamente o cenário para o qual ele foi feito. (Caddy com plugins também consegue, mas Traefik é o encaixe natural.)
(d) Cache específico, rewrite complexo, tuning de buffer → Nginx. Fator decisivo: controle granular. Nginx expõe cada timeout, buffer, zona de cache e regra de rewrite com 20 anos de documentação e exemplos para casos exóticos. Quando a exigência é "controle fino", a verbosidade vira vantagem.
O insight da decisão: não existe "melhor proxy", existe encaixe. Caddy ganha em simplicidade (default para novos projetos solo), Nginx em controle e base instalada, Traefik em dinamismo de contêiner. Um bom arquiteto nomeia o fator decisivo de cada caso em vez de aplicar uma preferência fixa a tudo.
No capítulo anterior você configurou o proxy na mão. Funciona, e ensina muito. Mas quando os serviços começam a se multiplicar — app, API, banco, fila, três projetos de cliente — editar Caddyfile e escrever units systemd vira trabalho repetitivo. As plataformas self-hosted dão o "git push e tá no ar" do Heroku, rodando na sua própria VPS. O Heroku que é seu.
Este capítulo é sobre subir um nível de abstração sem perder o controle. Vamos entender o que Coolify e Dokploy resolvem (deploy via git, TLS automático, gestão de proxy e contêineres por uma interface), o trade-off honesto que essa conveniência cobra, e como escolher entre os dois. Você vai instalar uma plataforma e subir o primeiro app. Mas o ponto mais importante — e o que diferencia este livro de um tutorial — é entender o que roda por baixo: tudo que a plataforma faz, você já sabe fazer na mão (Caps 8, 11). Isso é o que te salva quando a abstração vaza.
Em 2007, o Heroku definiu uma categoria: você fazia git push heroku main e sua aplicação estava no ar, com banco, SSL e escala — sem tocar em servidor nenhum. Era mágica. O modelo PaaS (Platform as a Service) virou o jeito padrão de startups subirem produto rápido. Vercel, Netlify, Railway e Render seguiram a fórmula para nichos diferentes.
O problema apareceu com a conta. Conforme o tráfego crescia, o preço do PaaS escalava de forma desproporcional ao custo real da infraestrutura. Pior: em 2022 o Heroku encerrou seu tier gratuito, empurrando uma geração inteira de hobbyistas e projetos pequenos para fora. A pergunta ficou óbvia: e se eu pudesse ter a experiência do Heroku, mas rodando num VPS de 6 dólares que eu controlo?
A resposta veio do open source. O Dokku (2013) foi o pioneiro — "o menor PaaS que você já viu", construído sobre Docker. O CapRover (2017) adicionou interface web. Mas a virada de maturidade veio com o Coolify (2022, de Andras Bacsai) e o Dokploy (2024, de Mauricio Siu), que trouxeram interfaces polidas, marketplaces de serviços de um clique e uma experiência que finalmente rivalizava com o PaaS comercial.
Em 2026, a tendência tem nome: empresas e desenvolvedores abandonando Vercel, Railway e Render de volta para o próprio VPS, em busca de controle, previsibilidade de custo e fim do vendor lock-in. As plataformas amadureceram a ponto de "não restar traço da antiga complexidade de self-hosting". É território adjacente ao código que todo dev de produto precisa conhecer.
Tudo que uma plataforma self-hosted faz, você poderia montar à mão com as peças dos capítulos anteriores. O valor está em ter tudo integrado numa interface, sem cola manual entre as partes:
app.dominio.com" numa caixa de texto; a plataforma escreve a config do proxy. O Cap 11 virou um formulário.Toda abstração cobra um preço. A plataforma esconde a complexidade, mas a complexidade não some — ela passa a ser problema da plataforma, que agora você também precisa entender e manter.
Velocidade de deploy (push e pronto). Menos config manual e menos erro bobo. Interface para tarefas que seriam comandos. Onboarding de outras pessoas mais fácil. Rollback e logs sem SSH. Serviços de um clique.
Mais uma peça crítica para manter e atualizar (e que tem CVEs — ver 12.7). Overhead de recurso (o painel consome CPU/RAM). Uma camada a mais para debugar quando algo quebra. Risco de "mágica que vaza" sem você entender o porquê.
As duas são open source, rodam num VPS, fazem deploy via git, TLS automático por Traefik e Docker por baixo. São mais parecidas do que diferentes — a escolha é fit-para-o-caso, não estilo de vida. Onde divergem:
| Critério | Coolify | Dokploy |
|---|---|---|
| Lançamento / maturidade | 2022 · mais maduro | 2024 · mais novo, cresceu rápido |
| Comunidade | Enorme (~40–50k stars, Discord ativo) | Grande e crescendo (~24k stars) |
| Filosofia | Feature-rich, UI-driven, "canivete suíço" | Enxuto, transparente, Docker-first |
| Catálogo de serviços | 280+ de um clique | Menor, foco no essencial |
| Orquestração | Docker (multi-server via SSH) | Docker Swarm nativo (multi-node) |
| Consumo em repouso | Maior (~5–7% CPU) | Menor (~1% CPU) |
| Licença | Apache 2.0 (permissiva) | Source-available (restrições) |
A leitura prática: Coolify se você quer o ecossistema mais maduro, o maior catálogo de serviços de um clique e mais comunidade/tutoriais — ao custo de mais consumo de recurso. Dokploy se você quer algo leve, com UI limpa, e está confortável perto do Docker/Swarm — especialmente num VPS pequeno onde o overhead importa.
O fluxo é parecido nas duas. Usarei Coolify como exemplo por ser o mais comum, mas os conceitos transferem direto para o Dokploy. Pré-requisito honesto: faça os Caps 6–8 primeiro. A plataforma assume um servidor minimamente são; instalá-la num servidor cru e exposto é convidar problema.
# O instalador detecta o SO, instala Docker se faltar, # baixa as imagens e configura o serviço. Leia o script antes: $ curl -fsSL https://cdn.coollabs.io/coolify/install.sh -o install.sh $ less install.sh # NUNCA rode curl|bash às cegas $ sudo bash install.sh # Ao fim, ele indica a URL do painel, tipicamente: # http://SEU-IP:8000 # Crie a conta admin IMEDIATAMENTE (antes que outro crie).
Com o painel no ar, o fluxo para qualquer app é o mesmo, e mapeia exatamente nos capítulos anteriores:
app.seudominio.com; a plataforma configura o Traefik e emite o TLS (Caps 4 e 11, num campo de texto).# A plataforma criou contêineres — veja-os: $ docker ps # Deve aparecer o Traefik (proxy), o painel, e seu app. # O DNS do app aponta pra Cloudflare → VPS? (Caps 1, 2) # O firewall libera 80/443 só pra Cloudflare? (Cap 8) # O TLS emitiu? (Cap 4) $ curl -I https://app.seudominio.com
Aqui está o coração do capítulo. Uma plataforma self-hosted não é mágica — é uma orquestração das peças que você já conhece. Mapear o painel de volta para os fundamentos é o que te dá poder de debug:
| O que a plataforma faz | O que é, por baixo | Capítulo |
|---|---|---|
| "Adicionar domínio" num campo | Config de reverse proxy (Traefik) por hostname | Cap 11 |
| "SSL automático" ligado | ACME / Let's Encrypt via Traefik | Cap 4 |
| "Deploy" do app | Build de imagem Docker + troca de contêiner | Cap 9 (supply chain) |
| "Serviço Postgres de 1 clique" | Contêiner com volume; bind interno | Caps 8, 19 |
| "Logs" na tela | docker logs do contêiner | Cap 10 |
| "Variáveis de ambiente" | Secrets injetados no contêiner | Cap 17 |
docker ps (Cap 11). Um serviço expõe porta que não devia? Você sabe ler ss -tlnp (Cap 8). Cada problema da plataforma é um problema dos fundamentos com uma camada de UI por cima. É por isso que este livro ensinou a mão antes do automático.
Instalar uma plataforma de deploy adiciona uma superfície de ataque poderosa: um painel que pode subir contêineres, ler secrets e executar comandos no servidor. Tratá-lo com o mesmo rigor do SSH (Cap 7) não é exagero.
Comprometer o painel é comprometer tudo que ele gerencia — todos os apps, todos os secrets, o servidor inteiro. Diferente de uma app específica vazada, aqui o estrago é total. Por isso o painel merece tratamento de joia da coroa: acesso restrito, atualizado, com 2FA. É a peça mais perigosa que você instalou no servidor.
Um único app simples, num servidor só: Caddy + uma unit systemd (Cap 11) já entregam deploy, TLS e proxy com menos peças para manter e menos superfície de ataque. A plataforma adiciona um painel para gerenciar uma coisa só — overhead sem retorno. Restrição séria de recurso: num VPS de 1 vCPU/1GB, o overhead do painel (sobretudo Coolify) compete com a sua app. Você já tem um fluxo que funciona: se Docker Compose + reverse proxy já roda liso, trocar por uma plataforma é refazer o que está pronto. A regra: adote a plataforma quando o número de serviços e a frequência de deploy tornarem a gestão manual um fardo — não antes.
Você é freelancer com três sites de cliente numa VPS (Hetzner, 4GB). Hoje cada um é uma unit systemd com Caddy na frente, configurados à mão (Cap 11). Funciona, mas cada novo cliente é meia hora de setup repetitivo, cada deploy é um git pull + systemctl restart por SSH, e você quer dar a um cliente acesso para ver os próprios logs sem te ligar. Hora de avaliar uma plataforma.
Três serviços, deploys frequentes, necessidade de delegar acesso a logs: sim, passou do ponto em que a gestão manual compensa. Escolha: Coolify, pela maturidade e pelo controle de permissões por projeto (cada cliente vê só o seu). O VPS de 4GB suporta o overhead.
# O servidor já passou pelos Caps 6-8? Confirmar: $ sudo ufw status # firewall ativo $ sudo systemctl status fail2ban # SSH protegido (Cap 7) # Snapshot do provedor ANTES de instalar nada novo.
$ curl -fsSL https://cdn.coollabs.io/coolify/install.sh -o i.sh $ less i.sh && sudo bash i.sh # Criar admin na hora. Depois, fechar o painel ao mundo: $ sudo ufw allow from SEU-IP-CASA to any port 8000 proto tcp # (ou pôr atrás de domínio + 2FA). Habilitar 2FA no admin.
Para cada site: criar o "project" no Coolify, conectar o repo, definir o domínio (a plataforma configura Traefik + TLS), colar as env vars, e fazer o primeiro deploy. Validar que responde em HTTPS antes de apontar o DNS de produção para a nova config. Só então desativar a unit systemd antiga (systemctl disable --now site-antigo). Migrar um, confirmar, próximo — nunca os três de uma vez.
Criar um usuário por cliente com permissão só ao próprio projeto. Agora o cliente vê os próprios logs e o status do deploy pela interface, sem SSH e sem te ligar. O ganho operacional que justificou a migração.
Resultado: três sites geridos por uma interface, cada novo cliente agora é "conectar repo + digitar domínio" em vez de meia hora de config, deploys disparam por git push, e os clientes têm acesso isolado aos próprios logs. O custo: um painel a mais para manter atualizado e blindar — uma troca que vale para três serviços, mas que não valeria para um só. E o que torna tudo debugável quando algo quebra é que, por baixo, é Traefik + Docker + ACME, que você já domina. Próximo capítulo: levar o desempenho adiante com CDN e edge — Cloudflare além do proxy básico.
Instala, cria o admin, e deixa o dashboard acessível na internet "porque é prático". É acesso root indireto à mão de qualquer um que ache o IP. Restrinja por firewall ao seu IP, VPN, ou no mínimo domínio + 2FA — sempre.
curl | bash sem ler o scriptRodar o instalador direto da internet no bash com sudo, sem olhar o que ele faz. É executar código de terceiro como root às cegas (Cap 9, supply chain). Baixe, leia, depois rode.
Instala e esquece. A plataforma acumula CVEs (todas têm), e o painel — a peça mais poderosa do servidor — fica vulnerável. Atualize religiosamente; aplique o pensamento do Cap 9 a ela também.
Subir a plataforma numa VPS recém-criada, sem firewall, SSH endurecido ou usuário não-root (Caps 6–8). A plataforma assume um servidor são; instalá-la num exposto só multiplica a superfície de ataque. Endureça primeiro.
"Cliquei e não funcionou, vou perguntar no Discord." Sem entender Traefik/ACME/Docker por baixo (Caps 4, 8, 11), cada problema vira uma espera. Quem entende os fundamentos debuga em minutos o que para os outros é mistério.
A plataforma facilita criar um Postgres de um clique — e dá a falsa sensação de que ele está "cuidado". Os dados vivem num volume Docker que ninguém faz backup. Quando o disco falha, sumiu tudo. Backup é seu (Cap 16), a plataforma não te isenta.
Um app simples num VPS pequeno não precisa de um painel inteiro com seu overhead e superfície de ataque. Caddy + systemd (Cap 11) faz o mesmo com menos peças. Plataforma é para quando os serviços se multiplicam, não para um só.
app.cliente.com na interface, mas o HTTPS não emite — o cadeado não aparece e o navegador reclama de certificado. O DNS já aponta para o servidor. Qual é a abordagem de quem entende o que roda por baixo?Sem instalar nada, liste seis funções de uma plataforma self-hosted (ex.: "adicionar domínio", "SSL automático") e, para cada uma, escreva o que ela faz por baixo e em qual capítulo deste livro você aprenderia a fazer na mão.
| Função do painel | Por baixo | Cap |
|---|---|---|
| Adicionar domínio | Reverse proxy (Traefik) por hostname | 11 |
| SSL automático | ACME / Let's Encrypt | 4 |
| Deploy via git | Webhook → build Docker → troca de contêiner | 9 |
| Postgres 1 clique | Contêiner + volume, bind em localhost | 8, 19 |
| Ver logs | docker logs / journald | 10 |
| Env vars / secrets | Secrets injetados no contêiner | 17 |
O ponto do exercício: nenhuma função da plataforma é nova — todas são peças que o livro ensina à mão. Internalizar esse mapa é o que transforma a plataforma de caixa-preta em ferramenta debugável.
Numa VPS descartável (já com Caps 6–8 feitos), instale Coolify ou Dokploy, restrinja o painel ao seu IP via firewall, habilite 2FA, e suba um app de teste (um "hello world" qualquer) com domínio e HTTPS. Confirme do lado do servidor o que foi criado.
# 1. Instalar (lendo o script antes): $ curl -fsSL https://cdn.coollabs.io/coolify/install.sh -o i.sh $ less i.sh && sudo bash i.sh # 2. Fechar o painel ao mundo — só seu IP: $ sudo ufw allow from SEU-IP to any port 8000 proto tcp $ sudo ufw reload # 3. No painel: criar admin, habilitar 2FA, conectar git, # criar app, definir domínio, deploy. # 4. Conferir do lado do servidor: $ docker ps # Traefik + painel + app $ curl -I https://app-teste.seudominio.com # 200 + TLS $ sudo ss -tlnp # app NÃO deve estar exposta direto
Critério de sucesso: o app responde em HTTPS pelo domínio, o painel só abre do seu IP (de outro IP, timeout), e docker ps mostra a stack esperada. Se o TLS não emitir, debugue pelos fundamentos (porta 80, DNS, Traefik) — é o exercício do quiz na prática.
Para cada cenário, decida entre "plataforma self-hosted" e "Caddy + systemd (Cap 11)", justificando: (a) um blog pessoal estático num VPS de 1GB; (b) cinco microsserviços com deploys diários e dois desenvolvedores; (c) um SaaS único, mas que você redeploya 10× por dia e quer rollback fácil; (d) dez sites de clientes de uma agência, cada um precisando de acesso isolado.
(a) Blog estático, VPS 1GB → Caddy. Um serviço, recurso escasso. O overhead de um painel competiria com a app numa máquina pequena, para gerenciar uma coisa só. Caddy serve estático com duas linhas. Plataforma seria peso morto.
(b) Cinco serviços, deploy diário, dois devs → Plataforma. Múltiplos serviços + frequência de deploy + mais de uma pessoa = exatamente o ponto em que a gestão manual vira fardo. Deploy via git e interface compartilhada pagam o overhead.
(c) Um SaaS, 10 deploys/dia, quer rollback → Plataforma (ou Caddy + CI bom). É um serviço só, o que pesa contra; mas a frequência de deploy e a necessidade de rollback fácil pesam a favor. Aceitável dos dois jeitos: a plataforma dá rollback de botão; um pipeline de CI com Caddy também resolve. Decisão de preferência, com leve vantagem à plataforma pela conveniência do rollback.
(d) Dez sites de clientes, acesso isolado → Plataforma, claramente. Volume alto + necessidade de delegar acesso por projeto = o caso de uso onde a plataforma mais brilha. Fazer isso com Caddy + systemd seria reinventar, mal, o controle de permissões que a plataforma dá pronto.
O insight: a decisão é função de três eixos — número de serviços, frequência de deploy e número de pessoas/necessidade de delegar. Um serviço, deploy raro, uma pessoa → Caddy. Conforme qualquer eixo cresce, a balança pende para a plataforma.
Você instalou uma plataforma self-hosted que agora gerencia toda a produção de uma empresa. O CTO pergunta: "se esse painel é tão poderoso, como a gente garante que ele não vira nosso ponto único de falha — nem de segurança, nem de disponibilidade?" Estruture a resposta cobrindo segurança, atualização, backup e o cenário de "o painel caiu".
Enquadramento: o painel concentra poder (sobe contêineres, lê secrets, executa no host), então merece tratamento de joia da coroa em quatro frentes.
1. Segurança do acesso:
2. Atualização (a plataforma tem CVEs):
3. Backup (o painel não te isenta):
4. O cenário "o painel caiu":
O que um bom candidato enfatiza: que a produção não depende do painel para continuar rodando (só para mudar), e que o domínio dos fundamentos (Caps 8–11) é o que permite operar mesmo com a plataforma fora. A frase de fechamento: "o painel é conveniência, não dependência — e a prova disso é eu conseguir fazer tudo na mão se precisar."
No Capítulo 2 você botou a Cloudflare na frente do domínio e ganhou o IP escondido e proteção de borda quase de graça. Mas aquilo foi a ponta do iceberg. Por baixo do "proxy laranja ligado" há uma rede global que pode servir seu conteúdo a partir de um datacenter a 20ms do usuário em vez de 200ms — e rodar código no edge antes de a requisição chegar ao seu servidor. Este capítulo é sobre usar isso de verdade.
Vamos entender o que é uma CDN e o que significa "edge", como o cache da Cloudflare realmente decide o que guardar e por quanto tempo, e como você controla isso com Cache-Control e Cache Rules — a ferramenta moderna que substituiu as antigas Page Rules. Veremos invalidação (o "segundo problema mais difícil da computação"), uma introdução prática a Workers (código serverless rodando na borda) e, importante para o nosso contexto, o que muda quando seu público é majoritariamente brasileiro. O objetivo: tirar do edge a performance percebida que o usuário sente, sem servir dados velhos nem quebrar conteúdo dinâmico.
Nos anos 1990, todo request ia até o servidor de origem, onde quer que ele estivesse. Um usuário em São Paulo acessando um site hospedado na Virgínia pagava o preço da distância física: a luz não viaja mais rápido que a luz, e cada ida e volta custava centenas de milissegundos. Sites globais eram lentos para quem estava longe da origem — e não havia o que fazer no servidor para mudar a geografia.
Em 1998, a Akamai — fundada por pesquisadores do MIT — comercializou a solução: uma rede de servidores espalhados pelo mundo que guardavam cópias do conteúdo perto dos usuários. Nascia a CDN (Content Delivery Network). Em vez de todo mundo buscar na origem distante, buscavam numa cópia local. A Akamai ganhou fama servindo eventos de tráfego massivo que derrubariam qualquer servidor único.
Por uma década, CDN foi luxo de empresa grande — cara, complexa, contratada por vendedor. A Cloudflare (2010) democratizou: CDN, DNS e segurança num tier gratuito, com setup de minutos. De repente, o blog de hobby tinha a mesma rede de entrega que uma multinacional.
A fronteira seguinte foi rodar código no edge, não só servir arquivos estáticos. A AWS lançou Lambda@Edge (2017); a Cloudflare lançou Workers (2017), levando computação serverless para os mesmos 300+ pontos de presença que já faziam cache. Em 2026, o "edge" virou uma plataforma: cache, computação (Workers), armazenamento (R2, KV, D1) — tudo rodando perto do usuário. O servidor de origem deixou de ser o único lugar onde as coisas acontecem.
Usuário SP PoP São Paulo (edge) Origem (sua VPS) │ │ │ │ GET /logo.png │ │ ├─────────────────────►│ tem cópia? (HIT) │ │ ~15ms │◄── serve direto │ │◄─────────────────────┤ │ │ │ │ │ GET /pagina (MISS) │ busca na origem │ ├─────────────────────►├─────────────────────────►│ ~180ms │ │◄─────────────────────────┤ │◄─────────────────────┤ guarda + serve │ │ │ │ perto (rápido) centenas de PoPs longe (lento), 1 lugar
Com o proxy laranja ligado (Cap 2), a Cloudflare já cacheia por padrão certos tipos de arquivo estático — imagens, CSS, JS, fontes — pela extensão. O que ela não cacheia por padrão é o HTML e qualquer coisa que pareça dinâmica, justamente para não servir página personalizada de um usuário para outro.
Você verifica o que está acontecendo olhando um header de resposta:
$ curl -I https://meusite.com.br/logo.png # O header que importa: cf-cache-status: HIT # servido do edge, rápido # Outros valores possíveis: # MISS → não tinha; buscou na origem e guardou # EXPIRED → tinha, mas o TTL venceu; rebuscou # DYNAMIC → não é cacheável (HTML, etc.) → sempre origem # BYPASS → uma regra mandou pular o cache
| cf-cache-status | Significa | O que fazer |
|---|---|---|
| HIT | Servido do edge, sem tocar a origem | Ótimo, é o objetivo |
| MISS | Não tinha; buscou e guardou para a próxima | Normal na 1ª vez; suspeito se sempre |
| EXPIRED | TTL venceu; rebuscou na origem | Normal; ajuste o TTL se frequente |
| DYNAMIC | Não cacheável por padrão (HTML) | Use Cache Rule se quiser cachear |
| BYPASS | Regra forçou pular o cache | Verifique se é intencional |
Há dois lugares que decidem por quanto tempo algo fica em cache, e entender a interação evita muita confusão.
Seu servidor (ou proxy do Cap 11) envia o header Cache-Control dizendo como o conteúdo deve ser cacheado. É a forma idiomática e a que você deve preferir, porque mantém a política junto do conteúdo:
# Estático versionado (app.a3f9.js) — cacheia forte e longo: Cache-Control: public, max-age=31536000, immutable # Página que pode cachear no edge mas revalidar no browser: Cache-Control: public, s-maxage=300, max-age=0 # Nunca cachear (página de conta, checkout, dados pessoais): Cache-Control: private, no-store # max-age → tempo para o BROWSER # s-maxage → tempo para a CDN (tem prioridade na CDN) # immutable → o browser nem revalida; o arquivo nunca muda # private → só o browser pode guardar, CDN não # no-store → ninguém guarda, em lugar nenhum
app.a3f9c2.js). Como o nome muda quando o conteúdo muda, você pode cachear immutable por um ano sem medo — uma nova versão é um arquivo de nome diferente, que o browser busca naturalmente. Todo bundler moderno (Vite, webpack, esbuild) faz esse fingerprinting automático. É a diferença entre brigar com cache e deixá-lo trabalhar a seu favor.
Quando você não controla os headers da origem, ou quer sobrepor a política dela na borda, usa as Cache Rules no painel da Cloudflare. Elas são a ferramenta moderna que substituiu as Page Rules (legadas, limitadas a 3 no plano grátis). Cache Rules são mais expressivas e é onde a Cloudflare está investindo.
Cache Rules funcionam por expressão de match (quando aplicar) e ação (o que fazer). Alguns padrões que você vai montar com frequência:
| Objetivo | Match (quando) | Ação |
|---|---|---|
| Cachear imagens agressivamente | URI termina em .jpg .png .webp | Eligible for cache; Edge TTL alto |
| Nunca cachear a API | Hostname = api.meusite.com.br | Bypass cache |
| Cachear o HTML da home | URI path = / | Eligible for cache; Edge TTL curto (ex.: 5 min) |
| Pular cache de área logada | Cookie de sessão presente | Bypass cache |
Há uma piada clássica: "só existem dois problemas difíceis em computação: invalidação de cache, nomear coisas, e erros de off-by-one." O cache é rápido justamente porque guarda cópias — mas e quando o conteúdo muda e a cópia fica velha? Você precisa purgar (invalidar) o cache.
| Tipo de purge | Quando usar |
|---|---|
| Purge by URL | Mudou um arquivo específico; o mais cirúrgico |
| Purge by tag / prefix | Mudou uma seção (ex.: todos os posts do blog) |
| Purge everything | Deploy grande ou emergência. Brusco — derruba todo o cache de uma vez, sobrecarregando a origem com MISSes até reencher |
# Purgar um arquivo específico (cirúrgico): $ curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" \ -H "Authorization: Bearer $CF_TOKEN" \ -H "Content-Type: application/json" \ --data '{"files":["https://meusite.com.br/style.css"]}' # A melhor estratégia, no entanto, é NÃO precisar purgar: # use nomes versionados (style.a3f9.css) e o problema some.
Workers são funções que rodam nos PoPs da Cloudflare — perto do usuário, antes de a requisição chegar (ou não) à sua origem. É computação serverless na borda. O modelo gratuito é generoso (100 mil requisições/dia, ~poucos ms de CPU por request), o que cobre muito projeto pequeno inteiro.
// Roda no edge, em centenas de PoPs, sem servidor seu: export default { async fetch(request) { const url = new URL(request.url); // Redirect de país, header de segurança, A/B test, // tudo na borda — sem tocar a origem: if (url.pathname === "/healthz") { return new Response("ok", { status: 200 }); } // Repassa pra origem, adicionando um header: const resp = await fetch(request); const novo = new Response(resp.body, resp); novo.headers.set("X-Served-By", "edge"); return novo; } }
Casos de uso reais e proporcionais a um projeto solo: redirects inteligentes (por país, device, idioma), adicionar/reescrever headers, autenticação leve no edge, servir páginas de erro customizadas, pequenas APIs sem servidor. Em volta dos Workers há um ecossistema — KV (chave-valor no edge), R2 (object storage sem taxa de egresso, alternativa ao S3), D1 (SQLite no edge) — que dá para construir aplicações inteiras sem origem tradicional. Mas isso é um livro à parte.
Workers são poderosos e sedutores, mas adicionam uma camada de lógica num lugar a mais para debugar e versionar. Para a maioria dos projetos, cache bem configurado resolve o problema de performance sem escrever uma linha de Worker. Não mova lógica para o edge por modismo: faça-o quando há um ganho claro (latência de um redirect global, tirar carga de auth da origem) que o cache sozinho não dá. Comece pelo cache; vá para Workers quando o cache não bastar.
Para um público majoritariamente brasileiro, a geografia muda o cálculo, e vale pensar nisso explicitamente.
Seu site (loja de conteúdo + blog) roda numa VPS barata em Ashburn, EUA. O público é 90% brasileiro e reclama de lentidão. Medindo de São Paulo, o TTFB (time to first byte) é ~200ms porque cada request cruza o Atlântico. Cloudflare já está na frente (Cap 2), mas tudo aparece como DYNAMIC — nada está sendo cacheado de verdade.
$ curl -I https://loja.com.br/ # HTML da home $ curl -I https://loja.com.br/img/banner.webp $ curl -I https://loja.com.br/app.js # Olhar cf-cache-status em cada um. # Achado: estáticos dão MISS toda vez (origem não manda # Cache-Control), HTML é DYNAMIC. Nada acelera.
loja.com.br {
# Build gera nomes com hash (app.a3f9.js) via Vite.
# Servir estáticos com cache longo e immutable:
@static path /assets/*
header @static Cache-Control "public, max-age=31536000, immutable"
reverse_proxy 127.0.0.1:8000
}
Match: hostname eq "loja.com.br" and URI path in {"/" "/blog"} and not cookie contains "session" Ação: Eligible for cache Edge TTL: 5 minutos # Páginas públicas cacheiam; quem está logado (cookie de # sessão) sempre vê a origem — sem vazar página personalizada.
$ curl -I https://loja.com.br/assets/app.a3f9.js # cf-cache-status: HIT → servido do PoP de São Paulo $ curl -w "%{time_total}\n" -o /dev/null -s https://loja.com.br/ # Segunda visita: TTFB cai de ~200ms para ~20-30ms. # A origem na Virgínia só é tocada a cada 5 min (HTML) ou # quando muda um asset (nunca, pois é versionado).
Resultado: os estáticos passaram a ser servidos do PoP brasileiro com cache de um ano (versionados, sem risco de ficarem velhos), o HTML público cacheia por 5 minutos respeitando usuários logados, e o TTFB para o visitante de São Paulo despencou de ~200ms para ~20–30ms na maioria dos acessos. A origem barata na Virgínia, antes o gargalo, agora é tocada raramente — ela respira e a conta não muda. Nenhuma linha de Worker foi necessária: só cache bem configurado. Próximo capítulo: quando algo der errado apesar de tudo isso, você precisa saber no instante — Sentry e monitoramento de erros.
Cachear HTML que mostra o nome/carrinho/saldo do usuário, e a CDN serve a página de um cliente para outro. É vazamento de dados, não só bug. HTML personalizado nunca deve ser cacheado publicamente; use private/no-store ou bypass por cookie de sessão.
Mandar max-age de um ano num style.css de nome fixo. Você atualiza o CSS, mas os usuários veem o antigo por um ano (ou até purgar manualmente). Versione o nome (style.a3f9.css) e o problema desaparece.
Esperar que o HTML cacheie sozinho e estranhar o cf-cache-status: DYNAMIC. HTML não é cacheado por padrão, de propósito. Para cachear, é preciso uma Cache Rule explícita — e com o cuidado do Erro 1.
Limpar todo o cache a cada deploy "para garantir". Cada purge total joga uma enxurrada de MISSes na origem, que pode não aguentar o pico. Purgue cirurgicamente (por URL/tag) ou, melhor, versione e não purgue.
Servir vídeos ou downloads pesados pelo CDN grátis. Os termos de uso proíbem (o CDN é para sites, não streaming), e há limite de tamanho por arquivo em cache (512MB nos planos não-Enterprise). Para vídeo/arquivos grandes, use object storage adequado (R2, S3) ou serviço de vídeo.
Cachear uma resposta que varia por Accept-Encoding ou idioma sem o header Vary correto, e servir a versão gzip para quem não aceita, ou o idioma errado. Quando a resposta muda por header, o cache precisa saber disso via Vary.
Escrever um Worker para resolver lentidão que uma Cache Rule resolveria. Mais código, mais camada para debugar, sem ganho extra. Esgote o cache antes de mover lógica para o edge.
Pegue um site seu (ou qualquer site atrás de Cloudflare) e inspecione o cf-cache-status de três recursos diferentes: o HTML da home, uma imagem e um arquivo CSS/JS. Classifique cada um e explique por que está naquele estado.
$ curl -sI https://SITE/ | grep -i cf-cache-status
$ curl -sI https://SITE/img/alguma.png | grep -i cf-cache-status
$ curl -sI https://SITE/app.js | grep -i cf-cache-status
# Repita o comando da imagem/JS uma 2ª vez:
# 1ª vez costuma dar MISS, 2ª vez HIT (já guardou).
Interpretação típica: o HTML quase sempre será DYNAMIC (não cacheado por padrão, de propósito). A imagem e o JS darão MISS na primeira vez e HIT na segunda — a Cloudflare guardou a cópia no PoP. Se um estático der MISS toda vez, a origem provavelmente está mandando um Cache-Control que impede o cache (ou nenhum), e vale corrigir.
Para um site com estas superfícies, escreva a estratégia de cache de cada uma (cachear ou não, qual Cache-Control ou Cache Rule, qual TTL): (a) bundle JS versionado app.8fa2.js; (b) logo logo.png de nome fixo; (c) home pública /; (d) painel do usuário /dashboard; (e) endpoint /api/precos que muda a cada hora.
(a) app.8fa2.js (versionado): Cache-Control: public, max-age=31536000, immutable. Cache eterno e seguro — se mudar o conteúdo, muda o hash, vira outro arquivo. O caso ideal.
(b) logo.png (nome fixo): cachear, mas com TTL moderado (ex.: max-age=86400, 1 dia) ou renomear ao trocar. Como o nome não muda, um TTL eterno te prenderia ao logo antigo. Melhor: versionar também.
(c) home / (pública): cacheável com TTL curto via Cache Rule (ex.: 5 min), desde que seja idêntica para todos os visitantes não logados. Bypass se houver cookie de sessão.
(d) /dashboard (personalizado): nunca cachear. Cache-Control: private, no-store. É o caso clássico de vazamento se cacheado (quiz/Erro 1).
(e) /api/precos (muda por hora): cacheável com s-maxage=3600 (1h na CDN), se a resposta for igual para todos. Tira carga repetida da origem. Se variar por usuário, não cachear.
O princípio: a pergunta decisiva para cada superfície é "esta resposta é idêntica para todos os visitantes?". Se sim, cacheável (TTL conforme a frequência de mudança). Se varia por usuário, não cacheável publicamente. Versionar transforma "não posso cachear muito" em "posso cachear para sempre".
Um cliente reclama que o site está lento mesmo com Cloudflare. Você inspeciona e vê que um arquivo de imagem grande dá cf-cache-status: MISS em toda requisição, nunca HIT. Liste as causas possíveis e como investigar cada uma.
Causas prováveis, da mais comum à menos:
Cache-Control: no-store ou private: a CDN obedece e não guarda. Cheque curl -I direto na origem (ou veja o header na resposta). Correção: ajustar o header na origem ou sobrepor com Cache Rule.Set-Cookie não são cacheadas por padrão (parecem personalizadas). Imagem não deveria setar cookie — investigar por que a origem está mandando.?v=randomico), cada uma é uma chave de cache nova → sempre MISS. Cheque se há cache-busting acidental.Vary que fragmenta o cache em variações demais. Raro, mas possível.Como investigar: compare curl -I passando pela Cloudflare vs direto na origem (resolvendo o IP real). A diferença nos headers Cache-Control/Set-Cookie entre os dois mostra o que a origem está pedindo e por que a CDN não guarda.
Você precisa desenhar a entrega de uma plataforma de conteúdo (artigos públicos + área logada + uploads de imagem dos usuários + API) cujo público é 95% brasileiro, com orçamento apertado. Decida: onde fica a origem, o que cacheia e como, onde guardar as imagens, e quando (se) usar Workers. Justifique cada escolha pelo trade-off latência × custo × risco.
Origem: com orçamento apertado e público BR, há duas escolhas defensáveis. (1) Origem barata fora do Brasil (EUA/Europa) + cache agressivo no PoP brasileiro — funciona muito bem para o conteúdo público (a maioria do tráfego), que nem toca a origem. (2) Origem em São Paulo, mais cara, mas reduz a latência até dos MISSes e da área logada. Decisão pragmática: origem barata fora + Cloudflare agressivo, porque o grosso do tráfego (artigos públicos) é cacheável e o usuário não sentirá a distância. Reavaliar para origem BR se a área logada (que não cacheia) crescer e a latência dela incomodar.
O que cacheia e como:
immutable, cache de 1 ano.private, no-store, nunca cacheada. Bypass por cookie de sessão. Risco de vazamento é inaceitável.s-maxage curto; endpoints por usuário, nunca.Imagens dos usuários: não no servidor de origem (enche disco, não escala, e é tráfego pesado). Object storage com a CDN na frente — R2 é especialmente atraente aqui por não cobrar egresso (custo previsível), e a Cloudflare serve do edge brasileiro. Uploads grandes nunca devem trafegar pelo CDN como hospedagem improvisada (Erro 5).
Workers — quando: não na largada. O cache resolve a performance do conteúdo público sem Workers. Considerar depois para casos específicos: servir/transformar imagens no edge, autenticação leve antes da origem, ou roteamento por geografia. Esgotar o cache primeiro (princípio do "quando não usar Workers").
O insight da entrevista: o candidato forte separa o tráfego por "é igual para todos?" — e percebe que o grosso (conteúdo público) é cacheável e resolve a latência BR sem origem cara nem Workers. Reconhece que área logada e dados de usuário nunca cacheiam (risco de vazamento) e que imagens vão para object storage com egresso barato, não para o disco da origem. A frase de fechamento: "cacheio o que é público e igual para todos no edge brasileiro, isolo o que é pessoal, e só pago por origem cara/Workers quando o cache não cobrir."
Um usuário clica em "finalizar compra", a página dá tela branca, ele desiste e some. Você só fica sabendo se ele se der ao trabalho de reclamar — e a maioria não dá. Sem monitoramento de erros, sua visão de produção é a dos clientes que tiveram paciência de avisar. O monitoramento de erros inverte isso: a exceção chega até você, com stack trace e contexto, no segundo em que acontece.
Este capítulo é sobre observabilidade ativa de erros — saber que algo quebrou antes do cliente avisar, e com informação suficiente para corrigir sem adivinhação. Vamos entender por que os logs do Capítulo 10 não bastam para isso, instrumentar uma aplicação com o Sentry (o padrão de fato), ler a anatomia de um erro capturado, e — crucial — domar o ruído para que os alertas continuem significando algo. Terminamos com o modelo de preço (que tem armadilhas reais) e as alternativas, incluindo opções compatíveis e self-hosted. O Sentry será o exemplo concreto, mas os conceitos valem para qualquer ferramenta da categoria.
O monitoramento de erros começou primitivo. Nos anos 1990 e 2000, "saber que quebrou" era ler arquivos de log à mão, ou pior, esperar o usuário relatar. Aplicações web maduras mandavam um email a cada exceção — solução que desmoronava no primeiro pico de erros, quando a caixa de entrada do dev recebia dez mil mensagens idênticas em minutos.
Em 2008, dentro da Disqus, David Cramer escreveu uma ferramenta para agrupar exceções do Django em vez de mandar um email por ocorrência. Ela agrupava erros idênticos num único "issue" com contagem, mostrava o stack trace completo e o contexto da requisição. Virou open source com o nome Sentry e, depois, uma empresa. A ideia central — agrupar, contextualizar, priorizar — definiu a categoria.
Ao longo dos anos 2010, o Sentry e concorrentes (Rollbar, Bugsnag, Airbrake) amadureceram: SDKs para dezenas de linguagens, integração com deploys e controle de versão, breadcrumbs (a trilha de eventos antes do erro), source maps (para desmontar JavaScript minificado de volta ao código original). Monitoramento de erros virou item básico de qualquer produção séria.
Em 2026, a categoria tem duas tensões. Uma é de escopo: ferramentas de erro puro competem com plataformas de observabilidade completa (logs + métricas + traces + erros num lugar só). A outra é de preço e controle: o modelo por evento do Sentry gera surpresas de fatura, e cresce um ecossistema de alternativas compatíveis com seu SDK (troca-se uma URL e pronto) e de opções self-hosted. A função, porém, continua a mesma desde a Disqus: transformar "algo quebrou em algum lugar" em "este erro, aqui, nesta linha, com este contexto".
Você já tem logs (Cap 10). Por que outra ferramenta? Porque log e monitoramento de erros respondem perguntas diferentes, e tentar fazer um com o outro dói:
Você precisa saber onde procurar e quando. O mesmo erro aparece mil vezes misturado no fluxo. Não há agrupamento, contagem nem "é novo ou já existia?". Sem alerta ativo — você só vê se for olhar. O contexto (qual usuário, qual request, qual versão) está espalhado ou ausente.
A exceção chega até você (push), agrupada por tipo, com contagem e tendência. Stack trace completo, breadcrumbs do que o usuário fez antes, qual release introduziu, quantos usuários afetou. "Erro novo desde o último deploy" é uma notificação, não uma caça.
NullPointerException na mesma linha viram um issue com contador "1.000". É o que salva sua sanidade.O setup é deliberadamente simples — esse é parte do valor. Instala-se o SDK da linguagem, inicializa-se com o DSN, e a partir daí exceções não tratadas são capturadas automaticamente. Exemplos nas duas stacks mais comuns:
# pip install sentry-sdk import sentry_sdk sentry_sdk.init( dsn=os.environ["SENTRY_DSN"], # nunca hardcode — env var environment=os.environ.get("ENV", "production"), release=os.environ.get("GIT_SHA"), # qual versão # Amostragem de performance (não 100% em prod movimentada): traces_sample_rate=0.1, # NÃO enviar dados pessoais sem pensar (LGPD, Cap 20): send_default_pii=False, ) # Pronto. Exceções não tratadas vão para o Sentry sozinhas.
// npm install @sentry/node import * as Sentry from "@sentry/node"; Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV, release: process.env.GIT_SHA, tracesSampleRate: 0.1, sendDefaultPii: false, }); // Capturar algo manualmente, com contexto extra: try { processarPagamento(pedido); } catch (err) { Sentry.captureException(err, { tags: { etapa: "pagamento" } }); throw err; }
send_default_pii=False evita capturar automaticamente dados pessoais (emails, IPs, corpo de requisição) que cairiam no Sentry e virariam questão de LGPD (Cap 20). Configure scrubbing de campos sensíveis (senhas, tokens, CPF) antes do envio — vale tanto para Sentry quanto para logs (Cap 10, Erro 4).
O valor aparece na tela do issue. Diferente de uma linha de log, um erro do Sentry traz uma investigação quase pronta:
| O que o issue mostra | Para que serve |
|---|---|
| Stack trace com a linha exata | Onde, no código, quebrou — sem adivinhar |
| Contagem e tendência (gráfico) | Acontece muito? Está piorando? Começou quando? |
| Breadcrumbs | O que o usuário fez nos segundos anteriores |
| Release / commit | Qual deploy introduziu (regressão?) |
| Usuários afetados | 1 pessoa ou 5.000? Prioriza o impacto |
| Tags (browser, OS, env, custom) | Só num navegador? Só em produção? Filtra o padrão |
| Ambiente e variáveis de contexto | Reproduzir as condições do erro |
O fracasso mais comum do monitoramento de erros não é técnico, é humano: alertas demais viram alerta nenhum. Se o Sentry te notifica 200 vezes por dia, você silencia o canal, e quando o erro que importa chega, ninguém vê. Domar o ruído é o que mantém a ferramenta útil — e é exatamente o que a maioria negligencia.
ignore/beforeSend para descartá-los na origem.Liga tudo no máximo → enxurrada de notificações → você cria uma regra de email que arquiva o canal → o Sentry vira uma aba que ninguém abre → um erro grave passa semanas despercebido → "pra que a gente paga por isso mesmo?". A ferramenta não falhou; o ruído a tornou invisível. Curar os alertas é trabalho contínuo, não setup único.
O Sentry cobra por evento, e em categorias separadas — erros, spans de performance, session replays, anexos — cada uma com sua cota. Entender isso evita sustos reais.
| Plano Sentry (2026) | Inclui |
|---|---|
| Developer (grátis) | ~5.000 erros/mês, 1 usuário, sem session replay |
| Developer pago (~$26/mês) | ~50.000 erros/mês |
| Team (~$31/usuário/mês) | ~100.000 erros/mês, mais recursos de equipe |
sample_rate e deduplicação no cliente para um loop de erro não inundar; limites de cota (spike protection) configurados; e descartar na origem o que não é acionável (14.6). A noite do incidente não é hora de descobrir que a cota acabou.
O Sentry é o padrão, mas não é a única escolha — e a portabilidade do SDK muda o jogo. Como ferramentas compatíveis falam o mesmo protocolo, migrar costuma ser trocar o DSN, sem reescrever instrumentação.
| Opção | Perfil | Observação |
|---|---|---|
| Sentry (SaaS) | Padrão, SDKs para tudo, UI polida | Preço por evento; cuidado com picos |
| GlitchTip | Compatível com SDK do Sentry, leve | Self-host grátis e ilimitado; ~4 contêineres vs ~40 do Sentry; troca só o DSN |
| PostHog | Free tier generoso (~100k erros/mês) | Erros + analytics + replay no mesmo lugar |
| Honeybadger | Preço por projeto, não por evento | Sem susto de fatura em pico de erro |
| Sentry self-hosted | Controle total, dados na sua casa | Pesado: Kafka, Redis, Postgres, ClickHouse, dezenas de contêineres, ~8GB RAM mínimo |
Seu SaaS faz deploys quase diários. Numa terça às 14h32 você sobe uma feature. Às 15h, a receita do dia está estranhamente baixa. Sem monitoramento de erros, você descobriria isso talvez amanhã, olhando o relatório de vendas. Com Sentry instrumentado (e alerta de "novo issue" ligado), a história é outra.
Às 14h41, uma notificação: novo issue — TypeError: cannot read 'total' of undefined em checkout.js:212. Marcado com a release das 14h32. 23 usuários afetados nos últimos 9 minutos, curva subindo. Você não esperou ninguém reclamar.
Issue: TypeError: cannot read 'total' of undefined Local: checkout.js linha 212 (stack trace completo) Release: v2.4.0 ← exatamente o deploy das 14h32 Breadcrumb: usuário → carrinho → aplica cupom → ERRO Usuários afetados: 23 e subindo Tag: só ocorre quando cupom aplicado
A correlação com a release v2.4.0 e o breadcrumb "aplica cupom → erro" já entregam o diagnóstico: a feature nova quebrou o fluxo de cupom. Não houve caça nos logs.
# A decisão certa em incidente: reverter primeiro, # investigar com calma depois (Cap 12, rollback de botão): # → reverter para v2.3.x na plataforma de deploy # Os 23 usuários voltam a fechar pedido em minutos. # Confirmar no Sentry que parou: # a curva do issue achata após o rollback → resolvido.
Marcar o issue como resolved na próxima release. Corrigir o bug do cupom com calma, com o stack trace exato em mãos. No deploy do fix, o Sentry confirma que o issue não reaparece — e alerta se reaparecer (regressão). Ciclo fechado.
Resultado: um bug que custaria um dia inteiro de receita (e seria descoberto tarde, por relatório de vendas) virou um incidente de 10 minutos: alerta automático às 14h41, diagnóstico instantâneo pela correlação erro↔release↔breadcrumb, rollback, e fix tranquilo depois. A diferença entre "souber pelo cliente amanhã" e "saber pela ferramenta em 9 minutos" é literalmente a diferença que paga o monitoramento de erros. Próximo capítulo: fechar a observabilidade ativa com uptime e métricas básicas — saber não só quando a app dá erro, mas quando ela some inteira, e como ela respira no dia a dia.
Liga o Sentry, recebe enxurrada, silencia o canal. A ferramenta vira uma aba morta e o erro grave passa despercebido. Curar alertas (14.6) é trabalho contínuo, não setup único — sem isso, o monitoramento não monitora nada.
Deixar send_default_pii ligado ou não fazer scrubbing, e mandar emails, IPs, CPFs, corpo de requisição para um terceiro. Vira questão de LGPD (Cap 20) e de segurança. Configure o que é enviado, mascare o sensível na origem.
Esquecer de mandar a versão/commit (release). Perde-se a pergunta de ouro "qual deploy introduziu?", e cada incidente vira investigação do zero em vez de "reverte a v2.4.0". Configure release no init e no pipeline de deploy.
Um endpoint em loop gera milhares de eventos idênticos e estoura a cota mensal em horas — no pior momento, durante um incidente. Use sample rate, deduplicação e spike protection. O modelo por evento pune o silêncio: barulho custa dinheiro.
Erro de JS em produção mostra a.min.js:1:48211 — ilegível, porque o bundle está minificado. Sem upload dos source maps, o stack trace é inútil. Configure o upload de source maps no build para ver o código original.
Achar que com Sentry não precisa de logs (Cap 10), ou vice-versa. São camadas diferentes: log para a sequência ampla de tudo, Sentry para o holofote nas exceções. Quem larga um fica cego de um lado.
Não instrumentar staging, ou não separar ambientes pela tag environment. Erros de staging poluem produção (ou vice-versa), e você perde a chance de pegar bugs antes do cliente. Separe ambientes; use staging como rede antes da produção.
Crie um projeto no Sentry (ou GlitchTip self-hosted), instrumente uma app de teste com o DSN via variável de ambiente, e provoque uma exceção de propósito. Confirme que o erro chegou com stack trace, e identifique três informações úteis no issue que um log simples não te daria.
import sentry_sdk, os sentry_sdk.init(dsn=os.environ["SENTRY_DSN"]) # Provocar uma exceção não tratada de propósito: def quebrar(): dados = {"a": 1} return dados["total"] # KeyError → vai pro Sentry quebrar()
Três coisas que um log não daria: (1) o agrupamento — se você rodar 50 vezes, vira 1 issue com contador "50", não 50 linhas; (2) o stack trace navegável com a linha exata e variáveis locais; (3) tags automáticas (ambiente, versão do runtime, OS) que contextualizam sem você logar nada. Bônus: a tendência no tempo, que mostra "começou agora" vs "sempre existiu".
Numa app instrumentada, faça três coisas: (a) associe a release ao commit atual; (b) adicione contexto custom (tag) a uma captura manual num ponto crítico (ex.: pagamento); (c) configure o SDK para descartar um tipo de erro não-acionável (ex.: uma exceção de cliente cancelando request) antes do envio.
Sentry.init({
dsn: process.env.SENTRY_DSN,
release: process.env.GIT_SHA, // (a) release
// (c) descartar o que não é acionável, na origem:
beforeSend(event, hint) {
const err = hint?.originalException;
if (err?.name === "AbortError") return null; // não envia
return event;
},
});
// (b) contexto custom num ponto crítico:
try { cobrarCartao(p); }
catch (e) {
Sentry.captureException(e, {
tags: { etapa: "pagamento", gateway: "stripe" },
extra: { pedidoId: p.id },
});
throw e;
}
O que cada peça compra: a release te dá "qual deploy introduziu" em todo issue; as tags permitem filtrar "só erros de pagamento via Stripe" no painel; o beforeSend retornando null descarta o ruído antes de virar evento — economiza cota e mantém o sinal limpo. Os três juntos são a diferença entre um Sentry útil e um barulhento.
Para cada cenário, escolha entre "Sentry SaaS grátis", "Sentry SaaS pago", "GlitchTip self-hosted" e "alternativa com free tier generoso (ex.: PostHog)", justificando: (a) projeto solo, baixo volume de erros, quer zero manutenção; (b) app movimentada gerando ~80k erros/mês, orçamento apertado, já tem VPS sobrando; (c) empresa com requisito legal de manter todos os dados em servidores próprios; (d) startup que quer erros + analytics de produto no mesmo lugar.
(a) Solo, baixo volume, zero manutenção → Sentry SaaS grátis. ~5.000 erros/mês cobrem um projeto pequeno, e não há nada para manter. Self-host aqui seria trabalho sem retorno. O tier grátis é exatamente para este caso.
(b) ~80k erros/mês, orçamento apertado, VPS sobrando → GlitchTip self-hosted. 80k/mês passaria do grátis e o plano pago do Sentry pesaria no orçamento; como já há VPS e o volume não é absurdo, o GlitchTip (compatível com o SDK, ~4 contêineres, ilimitado e grátis) é o ponto ideal. Bônus: começar com GlitchTip e migrar para o Sentry SaaS depois é só trocar o DSN.
(c) Requisito legal de dados em casa → self-hosted. Aqui o controle de dados não é preferência, é obrigação. GlitchTip self-hosted resolve com pouco overhead; Sentry self-hosted completo se houver necessidade de todos os recursos (e equipe para manter o stack pesado). A nuvem está fora de questão pela exigência.
(d) Erros + analytics no mesmo lugar → PostHog (ou similar). Quando a necessidade é mais ampla que erro puro (analytics de produto, session replay, feature flags), uma plataforma que junta tudo evita manter várias ferramentas. O free tier generoso (~100k erros/mês) ainda por cima cobre a fase inicial.
O insight: a decisão é função de volume (cabe no grátis?), quem mantém (tolera self-host?), requisito de dados (precisa ser em casa?) e escopo (só erro ou observabilidade ampla?). E a portabilidade do SDK (troca de DSN) reduz o custo de errar a escolha inicial — você não fica preso.
Você assumiu a operação de um SaaS com Sentry instalado mas inútil: 1.200 issues abertos, ninguém olha, alertas silenciados. O CTO pergunta: "como a gente faz isso voltar a significar alguma coisa, sem virar barulho de novo?" Estruture um plano cobrindo limpeza, política de alertas, prevenção de custo e como medir que está funcionando.
Diagnóstico: o problema não é a ferramenta, é a falta de processo. 1.200 issues e alertas silenciados são o sintoma clássico de ruído não curado (14.6). O plano tem quatro frentes.
1. Limpeza (recuperar o sinal):
beforeSend/inbound para que esses não-acionáveis nem entrem mais.2. Política de alertas (alertar por significância):
release em todo evento, para todo alerta vir com "qual deploy".3. Prevenção de custo:
4. Como medir que funciona:
O que um bom candidato enfatiza: que a cura é de processo, não de ferramenta; que "menos alertas, mais significativos" é o objetivo (alerta que sempre exige ação); e que isso é manutenção contínua, não um mutirão único. A frase de fechamento: "um alerta do Sentry tem que valer uma interrupção — se não vale, ele não devia ter disparado."
O Sentry (Cap 14) te avisa quando a aplicação dá erro. Mas e quando ela não dá erro nenhum — porque está completamente fora do ar? Uma app que caiu não manda exceção; ela simplesmente para de responder. Para esse caso, e para a pergunta diária "ela está saudável agora?", você precisa de duas coisas que erro não cobre: monitoramento de uptime e métricas do servidor.
Este capítulo fecha a Parte III completando a observabilidade ativa. Vamos ver como um monitor externo detecta que a app sumiu e te avisa em minutos, o que "99,9% de uptime" realmente significa em horas de queda permitida, como evitar o pesadelo do alerta falso (e o paradoxo de monitorar de dentro da própria máquina), como vigiar cron jobs que falham em silêncio com heartbeats, e quais métricas de servidor (CPU, memória, disco, latência) de fato importam para um projeto solo. No fim, a status page — a peça que transforma "está fora?" numa página que responde sozinha aos seus usuários. Junto com logs (Cap 10) e erros (Cap 14), isto completa o tripé de "saber o que está acontecendo".
No começo, "monitorar" era um humano rodando ping de vez em quando, ou recarregando o site para ver se abria. Quando caía de madrugada, ninguém sabia até alguém acordar. Nos anos 1990, ferramentas como o Nagios (1999) automatizaram a verificação — checavam serviços em intervalos e alertavam — mas eram pesadas, complexas de configurar e pensadas para datacenters internos.
A virada de mentalidade veio do Google. Em meados dos anos 2000, a empresa formalizou a disciplina de SRE (Site Reliability Engineering) e popularizou o vocabulário que usamos até hoje: SLI (o que você mede — disponibilidade, latência), SLO (a meta — "99,9% no mês") e error budget (quanta queda você pode gastar antes de estourar a meta). Confiabilidade virou algo a medir e orçar, não torcer para acontecer.
Em paralelo, surgiram serviços de monitoramento externo simples e baratos. O Pingdom (2007) e o UptimeRobot (2010) checavam seu site de fora, de vários pontos do mundo, e avisavam por email/SMS — sem você manter servidor de monitoramento. Democratizaram o que o Nagios tornava difícil.
Em 2026, o leque é amplo: SaaS de setup instantâneo com tiers grátis generosos (UptimeRobot, Better Stack), opções self-hosted leves (o Uptime Kuma, um único contêiner Docker, virou queridinho), e stacks de métricas que vão do simples (Netdata) ao poderoso (Prometheus + Grafana). A função essencial, porém, é a mesma de sempre — só que agora é fácil e barato fazer certo: saber que caiu antes do cliente, e enxergar como o servidor respira.
Vale fixar como os três capítulos da observabilidade ativa se encaixam — porque a confusão entre eles é a causa de muito "monitorei a coisa errada":
Um monitor de uptime é conceitualmente trivial: de fora, em intervalos regulares, ele faz uma requisição ao seu serviço e verifica a resposta. Se falhar (timeout, status errado, conteúdo inesperado), alerta. A sofisticação está nos detalhes — tipo de checagem, intervalo, de onde checa, e como evita falso alarme.
| Tipo de checagem | O que valida | Limitação |
|---|---|---|
| Ping (ICMP) | O servidor está alcançável na rede | "Liga" não é "funciona": a app pode estar morta com o servidor de pé |
| HTTP(S) status | O site responde 200 (não 500/502) | 200 não garante que a página está correta |
| HTTP keyword | A resposta contém um texto esperado | Pega "respondeu 200 mas com página de erro" |
| Endpoint de health | Um /healthz que checa banco, deps | O melhor: você define o que "saudável" significa |
| TCP port / SSL | Porta aberta; certificado válido/não expirado | Complementar (SSL amarra no Cap 4) |
/healthz que a sua app responde verificando o que importa: consigo falar com o banco? a fila responde? as dependências críticas estão de pé? Aí o monitor bate nesse endpoint. Assim "up" significa "realmente funcional", não só "o processo está vivo". Um 200 OK em /healthz que internamente testou o Postgres vale muito mais que um ping. É a diferença entre monitorar a fachada e monitorar a saúde de verdade.
Setup em segundos, checa de vários pontos do mundo, zero manutenção. Tiers grátis cobrem projeto solo (dezenas de monitores). Independente da sua infra — se seu servidor cai, o monitor está noutro lugar e te avisa.
Controle total, monitores ilimitados, ótimo para serviços internos/privados, um contêiner Docker. Mas checa de um lugar (seu) e — o problema — se hospedado na mesma infra que cai, ele cai junto e não te avisa.
"Três noves", "quatro noves" — o jargão de disponibilidade vira concreto quando você traduz em tempo de queda permitido. É instrutivo ver os números:
| Disponibilidade | Apelido | Queda permitida / mês | / ano |
|---|---|---|---|
| 99% | "dois noves" | ~7,2 horas | ~3,65 dias |
| 99,9% | "três noves" | ~43 minutos | ~8,8 horas |
| 99,95% | ~22 minutos | ~4,4 horas | |
| 99,99% | "quatro noves" | ~4,3 minutos | ~52 minutos |
O inimigo do monitoramento de uptime não é a queda — é o alerta falso. Assim como no Sentry (Cap 14), se o monitor te acorda às 3h por um soluço de rede que se resolveu sozinho em 10 segundos, você logo silencia os alertas, e aí o monitor para de servir. Boas práticas para manter o sinal confiável:
Você instala o Uptime Kuma na mesma VPS que ele deve monitorar. O servidor cai — e leva o Uptime Kuma junto. Resultado: a coisa caiu e o seu monitor, que caiu também, não tem como te avisar. Um monitor tem que viver fora do que ele observa. Se for self-host, hospede-o em outra máquina/provedor. Para o caso "minha única VPS caiu", um SaaS externo (que está noutro continente) é justamente o que funciona. Combinar os dois é comum: SaaS externo para o "está no ar?", Kuma para serviços internos.
Há uma classe de falha que nenhum monitor de "está no ar?" pega: o trabalho que deveria acontecer e não aconteceu. O backup noturno que não rodou. O cron de cobrança que travou. A fila que parou de processar. Eles não derrubam o site — falham caladamente, e você só descobre quando precisa do backup que não existe.
A solução é o heartbeat (ou "dead man's switch", interruptor de homem morto): em vez de o monitor checar você, você avisa o monitor que o job rodou. Se o aviso não chegar no prazo, o monitor alerta. A lógica se inverte: o silêncio é que dispara o alarme.
#!/bin/bash # O backup roda normalmente... pg_dump minhabase | gzip > /backups/db-$(date +%F).sql.gz # ...e SÓ se chegou até aqui (sucesso), avisa o monitor. # Se o backup falhar antes, o ping não acontece, e o # monitor — não recebendo o heartbeat — te alerta. if [ $? -eq 0 ]; then curl -fsS -m 10 https://hc-ping.com/SEU-UUID-AQUI fi
Uptime é binário (no ar / fora). Métricas são contínuas e respondem a pergunta que erro e uptime não respondem: está saudável agora, e para onde está indo? Memória subindo há uma semana sugere um vazamento antes de virar um OOM (Cap 10). Disco enchendo devagar avisa antes de travar tudo. As métricas que de fato importam para um projeto solo são poucas:
| Métrica | O que indica | Quando preocupar |
|---|---|---|
| CPU | Carga de processamento | Sustentadamente alta (não picos): subdimensionado ou loop |
| Memória (RAM) | Uso e tendência | Crescimento contínuo = vazamento; cheia = risco de OOM |
| Disco | Espaço livre | Acima de ~80%: aja antes de 100% travar tudo |
| Latência (p95/p99) | Tempo de resposta dos 5%/1% piores | Subindo = experiência degradando antes de "cair" |
| Taxa de erro | % de respostas 5xx | Pico = algo quebrou (cruza com o Cap 14) |
Para coletar e visualizar, há um espectro de esforço. Comece pelo simples:
htop, df -h, free -h por SSH respondem "como está agora?" num aperto. Não guardam histórico, mas tiram dúvida imediata.Para um único servidor de projeto solo, montar a stack completa Prometheus + Grafana costuma ser complexidade prematura — é mais infraestrutura para manter (e que também pode cair) do que o problema exige. O Netdata num comando, ou até checagens manuais somadas aos alertas básicos do SaaS de uptime, cobrem a necessidade real. Suba para Prometheus quando tiver vários servidores, precisar de histórico longo para análise de tendência, ou quiser alertas baseados em métrica que o simples não dá. Antes disso, é peso morto — o mesmo princípio dos Caps 10 e 12.
Quando algo cai, seus usuários querem saber se é com eles ou com você. Sem uma resposta, eles te inundam de mensagens "o site tá fora?" exatamente no momento em que você está ocupado consertando. A status page é uma página pública, hospedada fora da sua infra, que mostra o estado dos seus serviços — e responde a pergunta por você.
Você tem um SaaS rodando numa VPS única (toda a Parte II e III feitas até aqui), com Sentry já capturando erros (Cap 14). Falta o resto da observabilidade ativa: saber se a app some, vigiar o backup noturno, e ter olho na saúde do servidor. Orçamento: o mínimo. Quer cobertura real, não um NOC de empresa grande.
# Um endpoint que testa o que importa, não só "estou vivo": GET /healthz → - consigo conectar no Postgres? (SELECT 1) - consigo escrever no storage? → tudo ok: 200 {"status":"ok"} → algo falhou: 503 + qual dependência caiu
Monitor: HTTP keyword
URL: https://meuapp.com.br/healthz
Espera: status 200 E contém "ok"
Confirma de 2ª localização antes de alertar
Alerta após 2 falhas consecutivas → email + Telegram
# Externo de propósito: se a VPS cair, o monitor (noutro
# continente) sobrevive e te avisa.
# Ao fim do script de backup, se deu certo: curl -fsS -m 10 https://hc-ping.com/SEU-UUID # Monitor configurado para esperar 1 ping/dia. # Sem ping até as 4h → alerta "backup não rodou".
# Um comando, dashboard em tempo real + alertas embutidos: $ wget -O /tmp/nd.sh https://my-netdata.io/kickstart.sh $ less /tmp/nd.sh && sudo sh /tmp/nd.sh # ler antes # Restringir o acesso ao dashboard (porta 19999) ao seu IP, # como o painel do Cap 12 — não deixar aberto ao mundo. # Alertas padrão já cobrem disco >80%, OOM iminente, etc.
Ligar a status page do próprio SaaS de uptime, apontando para os monitores do /healthz, num subdomínio (status.meuapp.com.br) hospedado fora da VPS. Pronto: quando cair, os usuários têm onde olhar, e você não recebe vinte "tá fora?" no meio do incidente.
Resultado: com gasto perto de zero, o tripé está completo. Erros (Sentry) avisam exceções com contexto; uptime (monitor externo no /healthz) avisa se a app some, sobrevivendo à própria queda da VPS; o heartbeat garante que o backup silencioso seja vigiado; as métricas (Netdata) mostram a saúde e antecipam disco/memória antes do colapso; e a status page responde aos usuários sozinha. Os três pontos cegos — "quebrou", "sumiu", "está doente" — agora têm sentinela. Isto fecha a Parte III: a aplicação não só roda, ela é observável. A Parte IV cuida do que não pode se perder: os dados.
Hospedar o monitor de uptime na mesma VPS que ele vigia. Quando o servidor cai, o monitor cai junto e o silêncio é total — justo na hora que importa. Monitor de "está no ar?" tem que viver fora do que observa; para uma VPS única, use um SaaS externo.
Configurar um ping ICMP e dormir tranquilo. O servidor responde ping com a aplicação completamente morta — "a máquina liga" não é "o serviço funciona". Use HTTP keyword ou, melhor, um /healthz que testa as dependências reais.
Alertar a cada blip de uma única checagem. Soluços de rede te acordam à toa, você silencia o canal, e o alerta real passa despercebido (a espiral do Cap 14, de novo). Exija confirmação de 2ª localização e 2+ falhas consecutivas antes de disparar.
Monitorar o site mas não os cron jobs. O backup parou há meses e ninguém soube, porque ele não derruba nada — falha calado. Todo job agendado crítico precisa de heartbeat; o silêncio tem que disparar alarme.
Montar redundância e failover caros para "garantir 99,99%" num projeto que toleraria 99,9%. Cada nove extra multiplica custo e complexidade. Escolha a meta pela tolerância real do negócio; mais noves é decisão do Cap 24, não da estreia.
Confiar na latência média e jurar que "está rápido", enquanto 5% dos usuários esperam 8 segundos. A média esconde a cauda. Meça p95/p99 — é lá que mora a experiência ruim que você não está vendo.
Subir Netdata/Grafana e deixar o dashboard aberto na internet. Ele revela detalhes da sua infra a qualquer um (reconhecimento para um atacante) e às vezes tem suas próprias falhas. Restrinja por firewall/IP ou VPN, como o painel do Cap 12.
Crie um monitor num SaaS externo (UptimeRobot ou Better Stack) apontando para um endpoint do seu site. Use checagem por keyword (não só status). Depois, derrube o serviço de propósito por um minuto e confirme que o alerta chegou. Identifique quanto tempo levou entre a queda e o alerta.
Passos: criar monitor tipo "HTTP keyword", URL do seu site (idealmente um /healthz), keyword esperada (ex.: "ok"), canal de alerta (email/Telegram). Parar o serviço (systemctl stop meuapp ou o contêiner), esperar, observar o alerta, religar.
O que observar: o tempo até o alerta ≈ intervalo de checagem (5 min no grátis) + tempo de confirmação. Esse é o seu "tempo cego": quanto a app pode ficar fora antes de você saber. Se 5 min é demais para o seu caso, é o argumento concreto para um intervalo menor (plano pago). E note: o monitor pegou a queda justamente por estar fora da máquina parada.
Pegue um cron job real ou de teste (um backup, uma limpeza). Adicione um heartbeat que só pinga em caso de sucesso. Configure o monitor para esperar o ping no intervalo certo. Depois, faça o job falhar de propósito e confirme que o monitor alertou pela ausência do ping.
#!/bin/bash set -e # aborta no primeiro erro # O trabalho de verdade: pg_dump minhabase | gzip > /backups/db-$(date +%F).sql.gz # Só chega aqui se NADA acima falhou (set -e): curl -fsS -m 10 https://hc-ping.com/SEU-UUID
O teste de falha: quebre o job de propósito (renomeie a base, tire a permissão da pasta). Com set -e, o script aborta antes do curl — o ping não acontece. O monitor, esperando 1 ping no período e não recebendo, dispara "job não rodou". O insight: o alarme nasce do silêncio, não de um erro reportado — é assim que você pega falhas que não derrubam nada. Cuidado para o curl ficar depois do trabalho e protegido pelo set -e; pingar no começo, ou sem checar sucesso, anula o propósito.
Para cada serviço, defina uma meta de uptime razoável e diga o que ela implica em (a) tempo de queda mensal tolerado e (b) o que você precisaria fazer para alcançá-la: (i) blog pessoal; (ii) SaaS B2B cujo cliente trabalha em horário comercial; (iii) gateway de pagamento de um e-commerce; (iv) cron de relatório que roda 1×/dia.
(i) Blog pessoal → 99% basta (~7h/mês). Ninguém perde dinheiro se cair um pouco. Monitoramento externo simples + alerta. Não vale gastar em redundância. Cache na Cloudflare (Cap 13) já dá resiliência grátis.
(ii) SaaS B2B horário comercial → 99,9% no horário de trabalho (~43min/mês), relaxado fora dele. O que importa é estar de pé quando o cliente usa. Monitoramento + deploys cuidadosos + recuperação rápida. Janelas de manutenção podem ir para a madrugada. Não precisa de quatro noves.
(iii) Gateway de pagamento → 99,95%+ (~22min/mês ou menos). Cada minuto fora é receita perdida direta e confiança abalada. Aqui justifica redundância, failover, monitoramento agressivo de baixa latência — é o caso que paga os noves extras (e provavelmente já o Cap 24).
(iv) Cron 1×/dia → "uptime" não é a métrica certa. Não importa estar "no ar" 24h; importa que ele rode com sucesso na hora marcada. A meta é "executou e terminou ok todo dia", vigiada por heartbeat — não por monitor de disponibilidade contínua.
O insight: a meta de uptime é função do custo do downtime para aquele serviço específico, não um número universal. E para trabalho agendado, "uptime" sequer é a pergunta — "rodou com sucesso?" é, e a ferramenta é o heartbeat.
Você entrou numa startup com um único servidor de produção, Sentry já instalado, e nada mais de monitoramento. O CTO pergunta: "qual é o plano completo de observabilidade ativa, do dia 1 ao ponto em que a gente vira uma empresa séria, e como você garante que não vira ruído nem custo desnecessário?" Estruture cobrindo os três sinais, falsos positivos, jobs silenciosos, e quando subir de nível.
Enquadramento: observabilidade ativa são três sinais complementares (erros, uptime, métricas), e o sucesso depende tanto de cobrir os três quanto de não afogar em ruído. Plano em fases.
Dia 1 — cobrir os três pontos cegos (custo ~zero):
/healthz que testa dependências reais. Externo de propósito — sobrevive à queda da VPS. Confirmação de 2ª localização + 2 falhas antes de alertar (anti-falso-positivo).Definir metas (SLO) por serviço:
Quando subir de nível (gatilhos concretos):
Garantir que não vira ruído nem custo: alertas por significância e com confirmação; descartar não-acionável na origem; não montar Prometheus antes do segundo servidor; não perseguir noves antes que o downtime doa de verdade. Frase de fechamento: "cubro os três pontos cegos no dia 1 com ferramentas externas e baratas, calibro para que todo alerta valha uma interrupção, e só adiciono peso (Prometheus, redundância, on-call) quando um gatilho real — segundo servidor, downtime caro, equipe — justificar."
Tudo que, se você perder, não volta. Backup que restaura de verdade, secrets fora do código, storage de arquivos, a escolha de banco gerenciado, e LGPD na prática. Cinco capítulos sobre a parte da operação em que não há segunda chance.
Existem dois tipos de pessoa: quem faz backup e quem ainda vai começar a fazer backup. A frase é velha porque é verdadeira — quase todo mundo só leva backup a sério depois de perder algo que não volta. Este capítulo é a tentativa de te poupar dessa aula. Mas há uma armadilha mais cruel que não ter backup: achar que tem, e descobrir no desastre que ele nunca funcionou.
Backup é a parte da operação em que não existe segunda chance. Vamos separar backup de redundância (uma confusão que custa caro), aprender a regra 3-2-1 que organiza tudo, decidir o que salvar e como, e — o ponto central do capítulo — entender que o produto do backup não é o backup, é o restore. Um backup que você nunca testou restaurar é uma suposição, não uma proteção. Cobriremos criptografia, retenção, ferramentas concretas, e o que muda quando há dados de brasileiros (amarrando no Cap 20, LGPD). No fim, você terá um backup que de fato te devolve os dados — porque você já provou que devolve.
Por décadas, backup foi sinônimo de fita magnética. Desde os anos 1950, e dominando até os 2000, fitas guardavam cópias que rodavam de noite e eram levadas para um cofre — às vezes literalmente para outro prédio. Era lento, manual e fácil de negligenciar, mas estabeleceu o princípio fundamental que sobrevive até hoje: a cópia tem que estar longe do original.
A regra 3-2-1 — três cópias, em dois tipos de mídia, uma fora do local — foi cristalizada pelo fotógrafo Peter Krogh nos anos 2000 ao pensar em como não perder seu acervo digital. Virou o mantra da indústria por ser simples e capturar o essencial: redundância de cópias, diversidade de mídia, e separação geográfica contra um desastre que atinja um lugar só.
A nuvem mudou a economia. Armazenamento de objetos barato (S3, 2006, e compatíveis) tornou trivial e barato manter cópias "fora do local" — não mais um cofre físico, mas um bucket noutra região. Snapshots de disco dos provedores (DigitalOcean, Hetzner, AWS) deram um botão para congelar o estado de uma máquina inteira.
Em 2026, fazer backup nunca foi tão fácil ou barato — e, paradoxalmente, a perda de dados continua comum. Não por falta de ferramenta, mas por excesso de confiança: backups configurados e nunca testados, snapshots que não cobrem o banco direito, cópias "fora" que estavam na mesma conta comprometida por ransomware. A tecnologia resolveu o "como copiar"; o que continua falhando é a disciplina do "será que restaura?".
Muita gente acha que tem backup quando na verdade tem redundância — e são coisas que protegem contra perigos diferentes. Confundir as duas é como achar que o cinto de segurança apaga incêndio.
Protege contra falha de hardware: um disco morre, o espelho assume. Mas a réplica copia tudo na hora — inclusive o DROP TABLE errado, o ransomware, o arquivo que você apagou. O erro se propaga instantaneamente para a cópia. Disponibilidade, não recuperação.
Protege contra erro e corrupção: é uma foto do passado, isolada, que você pode restaurar. O DROP TABLE de hoje não afeta o backup de ontem. Te deixa voltar no tempo para antes do estrago. Recuperação, não disponibilidade.
A 3-2-1 é o esqueleto de qualquer estratégia de backup sã:
Backup não é uma coisa só; são camadas com naturezas diferentes. Tratar todas igual leva a salvar o que não importa e esquecer o que importa.
| O que | Como | Prioridade |
|---|---|---|
| Banco de dados | Dump lógico (pg_dump) e/ou snapshot consistente | Crítica — é o coração |
| Arquivos de usuário (uploads) | Sync para object storage com versionamento | Crítica — não se regenera |
| Secrets / configuração | Cofre separado e cifrado (Cap 17) | Alta — sem eles não reconstrói |
| Código | Já está no git (origem externa) | Baixa — versionado por natureza |
| O servidor inteiro (SO) | Snapshot do provedor | Conveniência — reprovisionável |
# Dump lógico, comprimido, com data no nome: $ pg_dump -Fc minhabase > /backups/db-$(date +%F-%H%M).dump # -Fc = formato custom (comprimido, restore seletivo). # Copiar o arquivo NÃO substitui o pg_dump: copiar os # arquivos do Postgres com ele rodando dá backup corrompido. # Use pg_dump (consistente) ou snapshot com o banco ciente. # Restaurar (o que de fato importa): $ pg_restore -d minhabase_nova /backups/db-2026-06-22-0300.dump
Aqui está a tese central do capítulo, e a que mais gente ignora: ninguém precisa de backup; todo mundo precisa de restore. O backup é só o meio. Um backup que você nunca restaurou é uma hipótese não testada — e hipóteses sobre dados costumam falhar exatamente quando você mais depende delas.
As formas de um backup "existir" mas não restaurar são muitas e silenciosas:
O espectro vai do snapshot de um clique ao backup incremental cifrado. Para um projeto solo, a combinação certa costuma ser duas camadas: snapshots do provedor (conveniência, recuperação rápida da máquina) mais um backup lógico dos dados críticos para object storage (a cópia que sobrevive a tudo).
| Ferramenta | Perfil | Cobre o quê |
|---|---|---|
| Snapshot do provedor | Um clique/agendado; máquina inteira | SO + disco; recuperação rápida. Mas fica na mesma conta |
pg_dump + cron + rclone | Simples, explícito, controlável | Banco → object storage. O básico bem feito |
| restic / BorgBackup | Incremental, deduplicado, cifrado | Arquivos e dados; eficiente em espaço e banda |
| WAL archiving / PITR | Point-in-time recovery do Postgres | Restaurar a qualquer segundo; mais avançado |
| Backup gerenciado (banco) | O provedor cuida (Cap 19) | Transfere a responsabilidade — com seus trade-offs |
#!/bin/bash set -euo pipefail # falha cedo e ruidosamente STAMP=$(date +%F-%H%M) FILE=/tmp/db-$STAMP.dump.gpg # 1. Dump consistente + 2. cifrar antes de sair da máquina: pg_dump -Fc minhabase | gpg --encrypt -r backup@meuapp > $FILE # 3. Enviar para object storage FORA da conta do servidor: rclone copy $FILE backup-remoto:meuapp-backups/ # 4. Limpar o temporário local: rm -f $FILE # 5. Heartbeat — só pinga se chegou aqui (Cap 15): curl -fsS -m 10 https://hc-ping.com/SEU-UUID
Seu backup contém tudo de mais sensível: dados de usuários, talvez secrets. Se ele vai para object storage (um terceiro), deve estar cifrado antes de sair — assim nem o provedor de storage, nem quem vazar o bucket, lê o conteúdo. Cifrar no cliente (como no script acima) é a postura correta; confiar só na "criptografia em repouso" do provedor deixa a chave nas mãos dele.
Guardar todo backup para sempre é caro e, sob LGPD, problemático (Cap 20: dado pessoal tem que ter prazo). Um esquema comum e barato é o escalonado (GFS — grandfather-father-son):
| Frequência | Retenção típica | Para quê |
|---|---|---|
| Diário | Últimos 7–14 dias | O "errei ontem, volta pra ontem" |
| Semanal | Últimas 4–8 semanas | Erro descoberto com atraso |
| Mensal | Últimos 6–12 meses | Conformidade, histórico, auditoria |
Backup de dados de brasileiros tem considerações específicas que amarram com o Cap 20 (LGPD):
Você acorda com o monitor (Cap 15) disparando: o site está fora. Ao acessar a VPS, encontra os arquivos cifrados e um bilhete de resgate. Um atacante entrou, criptografou o disco — incluindo o banco e os uploads — e exige pagamento. O servidor está perdido. A pergunta não é "como recupero esta máquina?", é "meus backups me salvam?".
A primeira decisão é não pagar (sem garantia, e financia o crime) e não tentar "consertar" a máquina comprometida. Provisione um servidor novo e limpo (Cap 6). A pergunta crítica: o backup mais recente está intacto e fora do alcance do atacante? Aqui o "+1" da regra 3-2-1-1-0 decide seu destino.
# O backup foi para um bucket em conta SEPARADA, com # versionamento/object-lock — o atacante na VPS não o alcançou. $ rclone copy backup-remoto:meuapp-backups/db-2026-06-22-0300.dump.gpg . # Decifrar (a chave NÃO estava na VPS comprometida): $ gpg --decrypt db-2026-06-22-0300.dump.gpg > db.dump # Restaurar no servidor novo: $ pg_restore -d minhabase db.dump
Restaurar os arquivos de usuário do object storage versionado (Cap 18) e os secrets do cofre separado (Cap 17). É aqui que muita gente descobre, tarde, que salvava o banco mas não os uploads, ou que os secrets só existiam na máquina perdida. Como neste caso as três camadas (banco, arquivos, secrets) tinham backup separado e fora, a app sobe completa.
Antes de pôr o servidor novo no ar, entender como o atacante entrou (Cap 21, resposta a incidentes) — SSH fraco? CVE não aplicada (Cap 9)? — e fechar a brecha, senão o servidor novo é invadido de novo. Restaurar dados sem fechar a porta é repetir o desastre.
Resultado: o que poderia ter sido o fim do negócio (todos os dados perdidos ou um resgate pago a criminosos) foi um incidente de algumas horas: servidor novo, restore do banco e dos uploads a partir de cópias cifradas e fora do alcance do atacante, secrets de um cofre separado, e a brecha fechada antes de reabrir. A diferença entre tragédia e contratempo foi uma decisão tomada meses antes: manter uma cópia imutável, cifrada, em conta separada — e ter testado que ela restaurava. Backup não te salva no dia do desastre; te salva no dia em que você o configurou direito. Próximo capítulo: secrets em produção — onde guardar as chaves que, neste caso, salvaram a restauração.
O pecado capital. O backup roda fielmente todo dia, mas ninguém nunca tentou restaurá-lo. No desastre, descobre-se que estava corrompido / incompleto / não documentado. Backup não testado é fé. Restaure periodicamente num ambiente limpo e confirme que a app sobe.
"Tenho o banco replicado, estou protegido." A réplica copia o DROP TABLE e o ransomware na mesma hora. RAID e réplica são disponibilidade, não recuperação. Você precisa de uma cópia no tempo, isolada, além da redundância.
Salvar o dump num outro diretório do mesmo disco, ou num bucket da mesma conta. O incêndio, o disco que morre ou o ransomware que criptografa o servidor levam o backup junto. A regra do "fora do local" existe exatamente para isto — pelo menos uma cópia que o desastre local não alcança.
Fazer cp ou snapshot dos arquivos do Postgres/MySQL enquanto o banco está ativo, sem cuidado de consistência. O resultado é um backup corrompido que parece ok até a hora de restaurar. Use pg_dump (consistente) ou snapshot com o banco ciente / em quiesce.
Salvar só o banco e descobrir, no restore, que os arquivos enviados pelos usuários sumiram e que os secrets para subir a app só existiam na máquina perdida. Backup é todas as camadas que não se regeneram — banco, arquivos e configuração — não só a mais óbvia.
O cron quebrou, a credencial expirou, o disco encheu — e o backup parou silenciosamente. Você só descobre no desastre, e o último arquivo bom é de três meses atrás. Ponha um heartbeat (Cap 15): o silêncio do backup tem que te alertar na hora.
Mandar dumps com dados pessoais, em texto puro, para object storage. Um bucket mal configurado ou vazado entrega tudo, e vira incidente de LGPD (Cap 20). Cifre no cliente, antes de o backup sair da máquina, com a chave guardada separada do backup.
DELETE sem WHERE e apaga metade dos registros de clientes. O erro só é percebido 20 minutos depois. O que de fato te devolve os dados?Para uma aplicação sua (real ou hipotética), liste tudo que precisaria existir para reconstruí-la do zero após perder o servidor. Classifique cada item em "se regenera" (código, SO, pacotes) ou "não volta" (estado único). Para os que não voltam, diga onde está o backup hoje — e se há algum sem backup.
Inventário típico:
| Item | Categoria | Backup? |
|---|---|---|
| Código-fonte | Regenera (git) | Sim, no GitHub/GitLab |
| Banco de dados | NÃO VOLTA | Tem que ter — dump cifrado fora |
| Uploads de usuários | NÃO VOLTA | Tem que ter — object storage versionado |
| Secrets / .env | NÃO VOLTA (na prática) | Cofre separado (Cap 17) |
| SO + pacotes | Regenera (provisionamento) | Snapshot é conveniência |
| Config de proxy/DNS | Regenera (documentado/IaC) | Versionar a config |
O insight: o exercício quase sempre revela pelo menos um item "não volta" sem backup adequado — tipicamente os uploads ou os secrets, esquecidos porque o foco vai todo para o banco. Achar esse buraco agora, no papel, é mais barato que achá-lo no desastre.
Escreva (e, se puder, rode) um script que faça dump consistente de um banco, cifre o resultado, envie para um object storage em conta/local separado, limpe o temporário, e pingue um heartbeat só em caso de sucesso. Identifique os pontos onde ele "falha cedo e ruidosamente".
#!/bin/bash set -euo pipefail # -e aborta em erro, -u em var indefinida, # -o pipefail propaga erro no pipe STAMP=$(date +%F-%H%M) FILE=/tmp/db-$STAMP.dump.gpg # Dump | cifra — pipefail garante que falha do pg_dump aborta: pg_dump -Fc minhabase | gpg --encrypt -r backup@meuapp > "$FILE" # Envia para conta/local SEPARADO: rclone copy "$FILE" backup-remoto:meuapp-backups/ rm -f "$FILE" # Heartbeat só aqui, no fim do caminho feliz: curl -fsS -m 10 https://hc-ping.com/SEU-UUID
Onde falha cedo e ruidosamente: o set -euo pipefail é o coração. Sem -o pipefail, um pg_dump que falha no meio do pipe | gpg passaria despercebido (o gpg "teve sucesso" cifrando lixo). Com ele, qualquer falha aborta antes do heartbeat — então o ping não acontece e o monitor te alerta (Cap 15). A ordem importa: cifrar antes de sair, heartbeat por último. Cuidado: a chave privada do gpg não deve viver na mesma máquina/backup, senão de nada adianta cifrar.
Descreva o procedimento completo para testar que seu backup restaura — do nada a uma aplicação funcionando — e o que verificar em cada etapa. Suponha que perdeu o servidor inteiro e só tem os backups. Liste o que daria errado se você nunca tivesse feito esse teste antes.
Procedimento de drill de restore:
pg_restore. Verifica: completa sem erro? contagem de registros bate?O que o teste revela (e o desastre não perdoaria): credencial de storage expirada, chave de cripto perdida junto com a máquina, uploads/secrets que ninguém salvava, dump que não abre, passos que você não sabia, e um RTO muito maior do que imaginava. Cada um desses, descoberto num drill tranquilo, é um desastre evitado. Faça o drill virar rotina — é o que converte "tenho backup" em "sei que recupero".
Você assumiu um SaaS B2B brasileiro (dados pessoais de clientes) com um único servidor e nenhum backup confiável — só "achamos que o provedor faz snapshot". O CTO pergunta: "desenhe nossa estratégia de backup do zero, que sobreviva a hardware falho, erro humano, ransomware e uma auditoria de LGPD." Estruture cobrindo as camadas, a regra que aplica, retenção, teste e conformidade.
Enquadramento: a estratégia tem que cobrir quatro ameaças distintas (hardware, erro humano, ransomware, conformidade), e nenhuma ferramenta sozinha cobre todas. Aplico a 3-2-1-1-0 como esqueleto.
1. O que salvar (camadas que não voltam):
2. Como, por ameaça:
3. Aplicando a 3-2-1-1-0: 3 cópias (produção + snapshot + object storage); 2 mídias (disco do provedor + object storage); 1 fora do local (bucket em outra região/conta); +1 imutável/offline (object-lock); +0 verificado (drill de restore).
4. Retenção (GFS): diário 7–14 dias, semanal 4–8 semanas, mensal 6–12 meses — equilibra "errei ontem", "corrupção descoberta tarde" e conformidade, sem guardar tudo para sempre (caro e problemático sob LGPD).
5. Teste e vigilância: heartbeat no job (Cap 15) para saber na hora se parar de rodar; drill de restore mensal num ambiente limpo, cronometrado para conhecer o RTO real. Sem isso, é o "achamos que o provedor faz snapshot" de novo, só que mais elaborado.
O que um bom candidato enfatiza: que redundância ≠ backup (a réplica não salva do erro humano nem do ransomware); que o produto é o restore, então teste é parte da estratégia, não um extra; e que a cópia imutável em conta separada é o que transforma ransomware de catástrofe em contratempo. Frase de fechamento: "uma estratégia de backup só está pronta quando eu já restaurei dela — antes disso, é uma hipótese sobre os dados mais importantes da empresa."
Toda aplicação tem chaves: a senha do banco, o token da API de pagamento, a chave da Cloudflare, o segredo que assina as sessões. Cada uma é uma chave do reino — quem a tem, entra. E a história recente é constrangedora: em 2025, mais de dez milhões de credenciais vazaram no GitHub, quase sempre pela mesma causa boba: uma chave que foi parar onde não devia. Este capítulo é sobre nunca ser essa estatística.
No capítulo de backup, foram os secrets guardados num cofre separado que permitiram restaurar a aplicação após o ransomware. Agora vamos cuidar deles diretamente: o que conta como secret, por que o onipresente arquivo .env virou um problema (especialmente na era das ferramentas de IA que leem seu disco), qual é o padrão correto — guardar num cofre, injetar em runtime, nunca escrever em texto puro no disco — e os níveis de maturidade do .env committado por acidente até o cofre dedicado. Veremos rotação, o que fazer quando um secret vaza, o curioso "paradoxo do secret zero", e como escolher entre as ferramentas sem cair em complexidade prematura. O objetivo: as chaves do seu reino guardadas como chaves, não espalhadas como bilhetes.
No começo, secrets viviam dentro do código. A senha do banco era uma string no fonte, commitada junto com tudo. Funcionava até o código ser compartilhado, virar open source, ou simplesmente vazar — e aí a senha ia junto. A primeira lição da segurança de secrets foi: tire-os do código.
Por volta de 2011, o manifesto Twelve-Factor App (da Heroku) cristalizou a prática que dominaria a década: guardar configuração e secrets em variáveis de ambiente, separadas do código. Bibliotecas como o dotenv (Node, ~2013) e o python-dotenv popularizaram o arquivo .env — um arquivinho local com as chaves, lido pela app na inicialização, e (idealmente) no .gitignore. Foi um avanço enorme em relação ao hardcode, e por isso pegou: o .env tem hoje dezenas de milhões de downloads semanais.
Mas o .env resolveu o problema de 2013, não o de 2026. Ao longo dos anos 2010, ficou claro que ele apenas mudou o risco de lugar: o secret saiu do código e foi para um arquivo de texto puro no disco — committado por acidente, compartilhado por Slack, copiado entre máquinas, esquecido em laptops. Surgiram os secrets managers: cofres dedicados (HashiCorp Vault, 2015; AWS Secrets Manager; depois Doppler e Infisical, focados em experiência de desenvolvedor) que armazenam cifrado, controlam acesso, auditam quem leu o quê, e injetam o secret na aplicação em tempo de execução.
Em 2026, há um agravante novo: ferramentas de IA de codificação (Claude Code, Cursor, Copilot) leem o sistema de arquivos do projeto para ajudar — e leem o .env junto. O .gitignore impede o commit, mas não impede a leitura. O arquivo de texto puro no disco, que sempre foi um risco latente, virou um risco ativo. A direção do estado da arte é clara e é a mesma para todos: o secret não deve tocar o disco em texto puro; deve ser buscado de um cofre e injetado em memória, em runtime.
Antes de proteger, é preciso saber o que proteger. Um secret é qualquer valor que, nas mãos erradas, dá acesso ou causa dano. Confundi-lo com configuração comum leva a proteger demais o trivial e de menos o perigoso.
| É secret (proteja) | É config (pode ser aberto) |
|---|---|
| Senha do banco de dados | Nome do banco, host interno |
| Chave de API (Stripe, Cloudflare, email) | URL pública da API |
| Chave secreta de assinatura de sessão/JWT | Tempo de expiração da sessão |
| Chave privada TLS/SSH | Certificado público |
| Token de acesso a object storage | Nome do bucket, região |
| Credencial do cofre de secrets | Nível de log, feature flags |
O .env não é o demônio — foi um avanço e ainda serve para desenvolvimento local simples. O problema é confiar nele como mecanismo de produção, porque ele carrega riscos estruturais:
git add . distraído, um .gitignore que faltou, e a chave está no histórico público para sempre (remover do último commit não basta — fica no histórico)..env da máquina que você perdeu, a restauração trava — você tem o backup do banco mas não a chave para a app subir.Você commitou o .env com a chave da Stripe por engano e percebeu cinco minutos depois. Apagar o arquivo e commitar de novo não resolve: a chave continua no histórico do git, recuperável por qualquer um que clone o repo. A única resposta correta é tratar a chave como vazada e rotacioná-la imediatamente (gerar uma nova, invalidar a antiga). Limpar o histórico (BFG, filter-repo) ajuda, mas a premissa é: o que foi commitado, considere comprometido.
Toda a disciplina de secrets cabe em três princípios. O resto são detalhes de implementação.
.env/config estático (ruim). O padrão moderno mantém a conveniência da env var como canal de injeção, mas troca o .env em disco por um cofre que injeta o valor em memória em runtime. Você continua lendo os.environ["DB_PASSWORD"] na app — só muda de onde aquele valor veio.
Não se salta do caos ao cofre perfeito de uma vez. Há uma escada, e o importante é saber em que degrau você está e qual o próximo passo proporcional ao seu contexto:
| Nível | Prática | Veredito |
|---|---|---|
| 0 | Secret hardcoded no código | Inaceitável. Vaza com o código. |
| 1 | .env committado no repo | Perigoso. O caso das credenciais vazadas. |
| 2 | .env no .gitignore, só local | Mínimo aceitável para dev. Frágil em produção. |
| 3 | Secrets na config da plataforma/CI | Bom para muitos casos. Injetados, não em disco. |
| 4 | Cofre cifrado em git (SOPS) ou referências | Sólido. Versionado e cifrado, ou só referências no repo. |
| 5 | Secrets manager dedicado (Doppler, Infisical, Vault) | O alvo. Cofre, injeção, auditoria, rotação. |
op:// que apontam para um cofre sem expor o valor) é um meio-termo elegante. Nível 5 (cofre dedicado) compensa quando há equipe, múltiplos ambientes/serviços, exigência de auditoria, ou rotação frequente. Subir de nível por modismo, antes de o anterior doer, é a complexidade prematura dos Caps 10, 12 e 15 de novo.
O mercado amadureceu e o leque é largo. O importante: a escolha da ferramenta importa menos que adotar o padrão (cofre, injeção, runtime). Um panorama honesto:
| Ferramenta | Perfil | Observação |
|---|---|---|
| Config da plataforma (Coolify, CI) | Já está ali; injeta em runtime | Suficiente para projeto solo (nível 3) |
| SOPS | Cifra o arquivo e commita no git | Sem serviço externo; chave via KMS/age. Bom self-host leve |
1Password (referências op://) | Você já paga; injeta sem tocar disco | Sweet spot para solo/time pequeno que já usa |
| Doppler | Cloud-first, DX excelente, doppler run | Onboarding simples; gerenciado (não self-host) |
| Infisical | Open-source (MIT), self-host ou cloud | Alternativa ao Vault sem o peso; bom para data residency |
| HashiCorp Vault | Enterprise, dynamic secrets, PKI | Poderoso e pesado; overkill para solo. Licença BSL |
| Cloud-native (AWS/GCP/Azure) | Integra com a infra do provedor | Ótimo se você é all-in num provedor; lock-in |
# Em vez de a app ler um .env do disco, um wrapper busca # os secrets do cofre e os injeta na memória do processo. # Exemplos do mesmo padrão em ferramentas diferentes: $ doppler run -- gunicorn app:app # Doppler $ infisical run -- npm start # Infisical $ op run --env-file=.env.1password -- ./app # 1Password # Em todos: o secret é buscado em runtime, vive só na # memória do processo, e NÃO fica num arquivo de texto. # A app continua lendo de os.environ — só a origem mudou.
# Nenhum valor real aqui — só ponteiros para o cofre. # Pode ir para o git sem medo: DATABASE_URL=op://Producao/meuapp/database-url STRIPE_SECRET_KEY=op://Producao/meuapp/stripe-key SESSION_SECRET=op://Producao/meuapp/session-secret # Cada dev resolve contra o PRÓPRIO cofre. Fim do # "me manda o .env por Slack".
Um secret que nunca muda é um secret que, se vazar, dá acesso para sempre. Rotação é trocar o secret periodicamente (e sempre que houver suspeita de vazamento), invalidando o anterior. Rotacionar manualmente é chato, e por isso raramente feito — daí o valor de um secrets manager que automatiza ou ao menos facilita. O princípio: quanto mais sensível o secret e mais gente teve acesso, mais curto o ciclo de vida.
# Suspeitou ou confirmou que um secret vazou:
1. ROTACIONE JÁ → gere um novo, invalide o antigo.
Velocidade > investigação. Faça primeiro.
2. AVALIE O DANO → o que esse secret acessava? Houve uso
indevido? (logs de acesso da API/cofre)
3. LIMPE O RASTRO → remova do histórico do git, dos logs,
de onde mais vazou. Mas assuma o pior:
o que vazou, vazou.
4. ENTENDA A CAUSA → como vazou? .env commitado? log? E
feche a porta para não repetir.
Há um problema filosófico divertido e prático no fundo de tudo isto. Para a app buscar os secrets do cofre, ela precisa de uma credencial para autenticar no cofre. Mas essa credencial é, ela mesma, um secret — o "secret zero". Onde guardá-lo? Se você o coloca num .env, voltou à estaca zero; só empurrou o problema um nível acima.
As respostas práticas, em ordem de sofisticação:
.env. Mesmo a solução "imperfeita" — um secret zero guardado no cofre da plataforma, protegendo dezenas de outros secrets — é muito melhor que dezenas de secrets soltos em arquivos. Você reduziu a superfície de um monte de chaves para uma, mais protegida. A perfeição (identidade federada, zero secret estático) é um alvo a perseguir conforme você cresce, não um pré-requisito para começar a fazer melhor que .env.
Sua app roda com um .env na VPS (nível 2). As mesmas chaves estão num .env no seu laptop, num do sócio, e — você descobre com horror — num commit antigo de quando o projeto começou (nível 1, retroativo). Há uma chave de Stripe de produção nesse histórico. Você quer chegar a um cofre injetando em runtime, sem derrubar a app e sem deixar pontas soltas.
# A chave da Stripe está no histórico do git = comprometida. # Rotacionar ANTES de qualquer migração (17.7): # → painel da Stripe: revogar a chave antiga, gerar nova. # → conferir nos logs da Stripe se houve uso indevido. # Só depois mexer no resto.
# Listar todos os secrets reais (a partir do .env atual): $ cat .env # inventário do que existe # Importar para o cofre escolhido (ex.: Infisical/Doppler): $ infisical init $ infisical secrets set DATABASE_URL=... STRIPE_KEY=... ... # Agora o cofre é a fonte única da verdade.
[Service] # Antes: ExecStart=/opt/app/.venv/bin/gunicorn app:app # Depois: o wrapper injeta os secrets do cofre em runtime. ExecStart=infisical run -- /opt/app/.venv/bin/gunicorn app:app # A app continua lendo os.environ["STRIPE_KEY"] — mas agora # o valor vem do cofre, em memória, não de um arquivo.
Apagar o .env da VPS, do seu laptop e do laptop do sócio. Cada um passa a resolver os secrets contra o cofre (com seu próprio acesso). Substituir o "me manda o .env" por "te dou acesso ao projeto no cofre". Limpar o histórico do git da chave antiga (já rotacionada, então é higiene, não emergência).
Fechando o ciclo do Cap 16: os secrets agora estão num cofre, mas o cofre também precisa de backup (export cifrado guardado em separado). No ransomware do capítulo anterior, foi exatamente o cofre separado que permitiu a restauração — garanta que ele sobreviva ao desastre também.
Resultado: a chave vazada foi rotacionada antes de tudo (contendo o dano real e imediato), os secrets passaram a viver num cofre cifrado e auditável como fonte única, a aplicação os recebe injetados em runtime sem nenhum arquivo de texto no disco, os .env espalhados por laptops sumiram, e o cofre entrou no plano de backup. Saímos de "as chaves do reino estão em cinco lugares, um deles público" para "as chaves estão num cofre, e eu sei quem tem acesso". O código não mudou uma linha — só a origem dos valores. Próximo capítulo: storage de arquivos — onde os uploads dos usuários (a outra metade do "estado que não volta" do Cap 16) devem morar.
O erro das dez milhões de credenciais. Um git add . distraído e a chave está no histórico para sempre. Use .gitignore desde o primeiro commit, e melhor ainda: não tenha secrets em arquivo nenhum que possa ser commitado. Se commitou, rotacione — o histórico não se apaga de verdade.
Vazou, você apaga o arquivo e commita por cima, e respira aliviado. A chave continua recuperável no histórico. A única resposta real é rotacionar a chave — considere comprometido tudo que foi commitado, mesmo que por segundos.
Um print(config) ou um log de debug que despeja as variáveis de ambiente, e a senha do banco vai parar nos logs (Cap 10) — que podem ir para um serviço de terceiro (Cap 14). Logs são superfície de vazamento. Mascare secrets antes de logar; nunca logue o objeto de config inteiro.
"Me manda o .env no Slack/WhatsApp/email." Cada envio é uma cópia eterna de credenciais de produção num canal não projetado para segredos, fora de qualquer controle. Dê acesso ao cofre, não cópias da chave.
Configurar os secrets uma vez e nunca mais tocá-los. Um ex-colaborador, um vazamento antigo não detectado, uma cópia esquecida — todos seguem válidos indefinidamente. Rotacione periodicamente e sempre que alguém com acesso sair ou ao menor sinal de vazamento.
O elo com o Cap 16: os secrets vivem apenas no .env da VPS, e quando ela morre (falha, ransomware) você restaura o banco mas não tem as chaves para a app subir. Secrets fazem parte do backup, num cofre separado da máquina que eles servem.
Montar HashiCorp Vault com dynamic secrets e PKI para um projeto solo com cinco chaves. O peso operacional supera o ganho, e a complexidade vira sua própria fonte de erro. Comece no nível proporcional (config da plataforma, SOPS, 1Password) e suba quando doer.
.env com a chave de produção da API de pagamento foi committado por engano no seu repositório (privado, mas com vários colaboradores). Você imediatamente apaga o arquivo, adiciona ao .gitignore e faz um novo commit. Isso é suficiente?Para um projeto seu, faça o inventário: liste cada secret que a aplicação usa e onde ele existe hoje (código? .env local? .env na VPS? config da plataforma? histórico do git?). Marque qual está no nível mais arriscado e qual seria o próximo passo de maturidade proporcional.
# Procurar secrets no histórico do git (não só no estado atual): $ git log -p | grep -iE "api[_-]?key|secret|password|token" | head # Procurar .env já commitados algum dia: $ git log --all --full-history -- "*.env" # Procurar secrets hardcoded no código atual: $ grep -rniE "(secret|api_key|password)\s*=\s*['\"]" --include="*.py" .
O que o inventário costuma revelar: um secret hardcoded esquecido (nível 0), ou um .env que esteve no histórico antes de você adicioná-lo ao .gitignore (nível 1 retroativo). Qualquer coisa que já tocou o git deve ser tratada como comprometida e rotacionada. O próximo passo proporcional para a maioria dos projetos solo: do .env na VPS (nível 2) para os secrets na config da plataforma de deploy (nível 3), que já injeta em runtime.
Pegue uma app que hoje lê um .env do disco. Migre-a para um cofre (config da plataforma, 1Password, Doppler ou Infisical) de forma que (a) a app continue lendo de variáveis de ambiente sem mudança de código, (b) nenhum arquivo de texto com secrets fique no disco, e (c) o que pode ser commitado contenha só referências, não valores.
Estratégia (usando o padrão "run wrapper"):
# 1. Importar os secrets do .env para o cofre, uma vez: $ infisical init && infisical secrets set ... # ou doppler import # 2. Trocar como a app inicia — wrapper injeta em runtime: # Antes: gunicorn app:app (lia .env do disco) $ infisical run -- gunicorn app:app # agora vem do cofre # 3. Apagar o .env do disco: $ rm .env # 4. (a) o código não muda: segue lendo os.environ[...] # (b) nada de secret em disco: o valor vive só em memória # (c) commitável: um .env.example com NOMES, sem valores
Verificação: confirme que a app sobe sem o .env presente (prova que os secrets vêm do cofre), e que git status não mostra nenhum arquivo com valores reais. O .env.example com apenas os nomes das variáveis (sem valores) documenta o que a app precisa, é seguro no repo, e substitui o "me manda o .env".
Simule a descoberta de que uma chave de API de produção vazou (digamos, apareceu num log que foi para um serviço de terceiros). Escreva o procedimento de resposta passo a passo, na ordem correta, e justifique por que essa ordem. Inclua o que fazer mesmo sem saber ainda como vazou.
Procedimento na ordem correta:
print(config)? Falta de masking? Corrigir a origem para não repetir — senão você rotaciona hoje e vaza de novo amanhã.Por que essa ordem: é o princípio "agir antes de investigar" (Caps 14 e 16). A reação instintiva — parar para entender antes de mexer — mantém a janela aberta. Rotacionar primeiro fecha o dano imediato; a investigação tranquila vem depois, com a porta já trancada. O passo 4 é o que diferencia apagar o sintoma de curar a doença.
Você entrou numa startup onde os secrets são um .env compartilhado num gerenciador de senhas, copiado manualmente para staging e produção. Há três desenvolvedores, dois ambientes, e planos de contratar mais. O CTO pergunta: "como organizamos secrets de um jeito que escale com o time e passe numa auditoria, sem virar um monstro para manter?" Estruture a resposta cobrindo escolha de nível/ferramenta, o secret zero, rotação, e o vínculo com backup.
Diagnóstico do estado atual: .env compartilhado em gerenciador de senhas + cópia manual entre ambientes = nível 2-3 com drift garantido (ninguém sabe se a cópia de staging tem a senha rotacionada de produção). Funciona com três pessoas, quebra com seis. O alvo é centralizar a fonte da verdade e injetar em runtime.
1. Nível e ferramenta:
run wrapper ou integração com a plataforma de deploy (Cap 12).2. Secret zero: a credencial de acesso ao cofre fica protegida no ambiente da plataforma/CI (não em .env). Conforme amadurece, migrar para identidade federada (OIDC) — a máquina prova quem é, sem token estático para vazar. Não deixar o paradoxo travar a adoção: um secret zero protegido valendo por dezenas é um ganho enorme já no dia 1.
3. Rotação: política de rotação periódica (e obrigatória quando alguém sai do time — crítico com contratações e saídas previstas) e ao menor sinal de vazamento. O cofre facilita/automatiza isso, o que torna a política realista em vez de um item de wiki que ninguém cumpre.
4. Vínculo com backup (Cap 16): o cofre vira peça crítica — precisa de backup próprio (export cifrado, separado), senão perdê-lo trava toda restauração. Foi o cofre separado que salvou o restore no ransomware do Cap 16.
Como não virar um monstro: escolher a ferramenta pela simplicidade de adoção (DX importa — secrets manager que os devs odeiam volta a virar .env no Slack), e não montar Vault completo se Doppler/Infisical resolve. Frase de fechamento: "centralizo a fonte da verdade num cofre com acesso por pessoa e injeção em runtime — isso mata o drift, passa na auditoria e escala com o time — e protejo o próprio cofre com backup, porque ele virou a chave-mestra."
Seus usuários enviam fotos de perfil, anexos, documentos. Onde isso vai parar? Se a resposta for "numa pasta no disco do servidor", você tem três problemas que ainda não percebeu: o disco vai encher, os arquivos não têm backup, e a hora que você escalar para um segundo servidor, cada um terá metade dos arquivos. Object storage resolve os três — e é o lugar certo para a outra metade do "estado que não volta" do Capítulo 16.
Este capítulo é sobre onde guardar arquivos de usuário usando S3 e seus compatíveis. Vamos ver por que o disco da VPS é o lugar errado, entender o modelo de object storage (bucket, objeto, chave) e o padrão de presigned URLs que tira sua aplicação do caminho do tráfego pesado. Encararemos a armadilha de fatura mais comum do dev solo — as taxas de egresso — e como escolher entre S3, Cloudflare R2, Backblaze B2 e o self-hosted MinIO sem levar susto no fim do mês. Cobriremos o erro perigoso do bucket público acidental, lifecycle e versionamento (que amarram em backup e LGPD), e o que muda com dados de brasileiros. O objetivo: arquivos num lugar que escala, tem backup, e não te surpreende na conta.
Por décadas, "guardar um arquivo" significava o sistema de arquivos: pastas e arquivos num disco, organizados em hierarquia. Funciona perfeitamente para um servidor — até você precisar de dois. Como sincronizar a pasta de uploads entre máquinas? Como fazer backup sem parar o serviço? Como servir um arquivo a milhões de usuários sem o disco virar gargalo? O modelo de arquivos em disco não foi feito para a escala da web.
Em 2006, a Amazon lançou o S3 (Simple Storage Service) e inventou, na prática, o object storage para as massas. A ideia: em vez de uma hierarquia de pastas num disco específico, você guarda objetos (o arquivo mais metadados) em buckets, acessíveis por uma API HTTP de qualquer lugar, com durabilidade altíssima e escala praticamente infinita. Não há "disco" do seu ponto de vista — há um serviço que guarda e devolve objetos.
O S3 foi tão bem-sucedido que sua API virou um padrão de fato. Quando alguém diz "compatível com S3", quer dizer que fala o mesmo protocolo — então o mesmo código, SDK e ferramentas funcionam contra qualquer provedor que o implemente. Isso criou um mercado: MinIO (self-hosted, 2014), Backblaze B2, DigitalOcean Spaces, Wasabi, e muitos outros adotaram a API do S3 para poder receber quem quisesse fugir da AWS.
O ponto de virada recente foi o egresso. A AWS (e Google, Azure) cobram caro para os dados saírem — a famosa surpresa de fatura. Em 2022, a Cloudflare lançou o R2: object storage compatível com S3 e com zero taxa de egresso, um golpe direto no modelo da Amazon (o nome "R2" é uma piada — um degrau abaixo de "S3" na notação química). Em 2026, a escolha de object storage para um projeto solo gira menos em torno de "qual é melhor tecnicamente" e mais em torno de "qual não vai me dar um susto na conta quando o tráfego crescer".
A tentação é óbvia: salvar o upload em /var/uploads e pronto. Funciona no primeiro dia e cobra caro depois. Os problemas:
pg_dump perde todos os uploads no desastre.O modelo é deliberadamente simples, e o vocabulário é o mesmo em todos os provedores compatíveis com S3:
meuapp-uploads). Você cria poucos buckets, organizados por finalidade ou ambiente.users/42/avatar.jpg. Parece pasta, mas é só um nome — não há hierarquia real, só uma convenção de prefixos.import boto3 # O MESMO código serve S3, R2, B2, MinIO — só muda o endpoint: s3 = boto3.client( "s3", endpoint_url=os.environ["S3_ENDPOINT"], # R2/B2/MinIO/... aws_access_key_id=os.environ["S3_KEY"], # secret (Cap 17) aws_secret_access_key=os.environ["S3_SECRET"], ) # Subir, baixar, listar: s3.upload_file("local.jpg", "meuapp-uploads", "users/42/avatar.jpg") s3.download_file("meuapp-uploads", "users/42/avatar.jpg", "out.jpg")
Aqui está o padrão que separa quem usa object storage direito de quem o usa como um disco remoto. A pergunta: quando um usuário faz upload, o arquivo deve passar pela sua aplicação a caminho do storage? E no download, a app deve buscar o arquivo e repassá-lo? A resposta é não — e a ferramenta que evita isso é a presigned URL.
Uma presigned URL é um link temporário e assinado que autoriza uma operação específica (subir este objeto, ou baixar aquele) por um tempo limitado, sem expor suas credenciais. O cliente fala direto com o storage; a app só assina a permissão.
Cliente Sua app (leve) Object storage │ │ │ │ "quero subir foto" │ │ ├──────────────────────►│ gera presigned URL │ │ │ (assina permissão p/ │ │ presigned URL │ ESTE objeto, 5 min) │ │◄──────────────────────┤ │ │ │ │ PUT foto.jpg (direto, sem passar pela app) │ ├──────────────────────────────────────────────────►│ │ upload concluído │ │◄──────────────────────────────────────────────────┤ │ │ o arquivo NUNCA passou pela sua aplicação — ela só assinou.
# Upload: o cliente sobe direto, a app só autoriza: url_subir = s3.generate_presigned_url( "put_object", Params={"Bucket": "meuapp-uploads", "Key": "users/42/foto.jpg"}, ExpiresIn=300, # vale 5 minutos ) # Download de arquivo PRIVADO: link temporário só pra quem pode: url_baixar = s3.generate_presigned_url( "get_object", Params={"Bucket": "meuapp-uploads", "Key": "users/42/nota-fiscal.pdf"}, ExpiresIn=60, # expira em 1 minuto )
Esta é a lição financeira do capítulo, e a que mais pega o dev solo desprevenido. Egresso é o custo de os dados saírem do storage (downloads). A maioria dos provedores cobra pelo armazenamento e pelo egresso — e o egresso, silenciosamente, costuma virar a maior linha da conta.
Você escolhe S3 porque é o padrão. App de fotos cresce. Três meses depois, a fatura chega e a linha de egresso é três vezes o custo de armazenamento. Guardar é barato; deixar os usuários baixarem o que guardaram é que custa — ~$0,09/GB que sai, e isso escala com o sucesso.
R2 não cobra egresso. B2 via Cloudflare (Bandwidth Alliance) também não. Mesma carga que custaria dezenas de dólares no S3 sai por poucos dólares — só o armazenamento. A surpresa de fatura simplesmente não existe nessa categoria.
| Opção | Perfil | Pegadinha |
|---|---|---|
| Cloudflare R2 | Zero egresso, free tier 10GB permanente, S3-compatível | Sem versionamento/object-lock nativo robusto — atenção para backup imutável |
| Backblaze B2 | Armazenamento baratíssimo; egresso grátis via Cloudflare (Bandwidth Alliance) | Limites de API menores; depende do combo com Cloudflare para egresso grátis |
| AWS S3 | O padrão, mais maduro, todos os recursos (versioning, object-lock, tiers) | Egresso caro — a surpresa de fatura. Vale se você já é all-in na AWS |
| MinIO (self-hosted) | Roda na sua infra, controle total, S3-compatível | Você mantém (e faz backup d)o storage; só compensa em volume/controle alto |
| DO Spaces / Wasabi / outros | Meio-termo; preços e mínimos variados | Mínimos mensais e políticas de retenção/egresso variam — leia as letras miúdas |
Cada objeto/bucket pode ser público (qualquer um com a URL acessa) ou privado (só com credencial ou presigned URL). Errar isso é uma das causas mais comuns — e mais constrangedoras — de vazamento de dados.
| Tipo de arquivo | Acesso correto | Como servir |
|---|---|---|
| Asset público (logo, CSS, imagem de blog) | Público (ou via CDN) | URL direta, cacheada na CDN (Cap 13) |
| Foto de perfil (semipública) | Depende do produto | Pública se o perfil é público; senão presigned |
| Documento privado (nota fiscal, contrato) | Privado, sempre | Presigned URL de curta duração, por requisição |
| Backup | Privado + imutável | Nunca público; conta/credencial separada (Cap 16) |
A manchete clássica de vazamento de dados: "empresa expõe X milhões de registros num bucket S3 mal configurado." Quase sempre é um bucket que deveria ser privado, marcado como público por engano (ou com uma política permissiva demais), deixando documentos sensíveis acessíveis a quem adivinhasse a URL. A regra: privado por padrão, público só com decisão consciente para conteúdo que é, de fato, para todos. Documentos de usuário, uploads pessoais, qualquer coisa com dado pessoal (Cap 20) — sempre privados, servidos por presigned URL. Verifique as permissões do bucket explicitamente; não confie no default nem na suposição de que "ninguém vai adivinhar a URL".
Dois recursos do object storage conectam este capítulo diretamente a backup (Cap 16) e custo (Cap 23).
Regras de lifecycle movem ou apagam objetos automaticamente conforme a idade. Mover uploads não acessados há 90 dias para uma classe mais barata ("cold/infrequent"), ou apagar arquivos temporários após 7 dias, sem você tocar em nada. É controle de custo e de retenção no piloto automático — e ajuda a cumprir a retenção finita que a LGPD exige (Cap 20).
Com versionamento ativado, sobrescrever ou apagar um objeto não o destrói — guarda a versão anterior. É o equivalente do "voltar no tempo" do Cap 16 aplicado a arquivos: um usuário apagou algo por engano, um bug sobrescreveu uploads, um ransomware tentou criptografar — a versão boa anterior ainda existe. Combinado com object-lock (que impede a exclusão mesmo por quem tem credencial), vira a cópia imutável anti-ransomware do "+1" da regra 3-2-1-1-0.
Seu SaaS guarda anexos de usuários (alguns públicos, como avatares; outros privados, como documentos) numa pasta no disco da VPS. O disco está em 70%, não há backup dos arquivos, e você quer poder escalar para um segundo servidor um dia. Público brasileiro. Quer resolver os três problemas (disco, backup, escala) sem reescrever a aplicação inteira nem tomar susto na conta.
A app serve downloads frequentes (avatares aparecem em toda página) — egresso importa muito. Escolha: R2 (zero egresso, free tier para começar, S3-compatível). Para o backup imutável dos arquivos críticos, observar que precisará de versionamento/object-lock — pode ser uma cópia num provedor que o ofereça (amarra no Cap 16).
# rclone fala S3-compatível; sincroniza a pasta para o bucket: $ rclone sync /var/uploads r2:meuapp-uploads --progress # Conferir contagem antes de confiar: $ rclone size r2:meuapp-uploads
Upload: cliente pede URL → app assina presigned PUT →
cliente sobe DIRETO no R2 (app não vê o byte).
Download privado: app gera presigned GET de 1 min por
requisição autorizada (documento só pra quem pode).
Avatar público: URL pública via Cloudflare (Cap 13),
cacheada no PoP brasileiro — rápido e egresso zero.
Acesso: privado por padrão. Avatares num prefixo/bucket
público explícito; documentos sempre privados.
Lifecycle: expurgar uploads temporários após 7 dias.
Backup: cópia dos arquivos críticos em provedor com
versionamento/object-lock (Cap 16, o "+1").
Credenciais: no cofre de secrets, não no código (Cap 17).
Resultado: os três problemas resolvidos de uma vez. O disco da VPS esvaziou (uploads agora no R2) e parou de ser risco de queda; os arquivos têm backup e entraram no plano anti-ransomware; e a app está pronta para escalar para múltiplos servidores, porque o estado vive fora deles. De quebra, com presigned URLs a VPS deixou de ser pedágio de tráfego, os avatares são servidos do edge brasileiro via Cloudflare, e a escolha por zero-egresso eliminou a surpresa de fatura. O código mudou só no ponto de upload/download — o resto da app nem percebeu. Próximo capítulo: o banco de dados, o coração do "estado que não volta" — e a grande decisão de gerenciá-lo você mesmo ou deixar o provedor cuidar.
O erro fundamental. Disco enche e derruba tudo, arquivos ficam fora do backup, e a app não escala para dois servidores. Object storage desde o começo — o estado dos usuários vive fora da máquina descartável.
Fazer todo upload e download passar pela aplicação, que vira gargalo de banda e workers. Um download grande prende um worker por minutos. Use presigned URLs: a app só assina, o tráfego vai direto cliente↔storage.
Marcar como público (ou com política permissiva demais) um bucket que deveria ser privado, expondo documentos sensíveis a quem souber a URL. A manchete clássica de vazamento. Privado por padrão; público só por decisão consciente para conteúdo realmente aberto.
Comparar provedores pelo $/GB armazenado e ignorar egresso e operações. O egresso é frequentemente a maior linha da conta e a surpresa que pega o dev solo. Modele os três custos para o seu padrão de uso antes de decidir.
Gerar presigned URLs com expiração longa (horas, dias) ou reaproveitá-las. Um link que vaza dá acesso pelo tempo todo que durar. Use expiração curta (segundos/minutos) e gere por requisição — o link é descartável de propósito.
Deixar o cliente subir qualquer coisa via presigned URL sem limitar tipo e tamanho, nem validar depois. Alguém sobe um arquivo gigante ou um executável malicioso. Restrinja o content-type e o tamanho na assinatura, e valide/escaneie o que chega.
Ligar versionamento (ótimo para desfazer) e esquecer de expirar versões antigas. O espaço e a conta crescem indefinidamente com cada sobrescrita. Combine versionamento com uma regra de lifecycle que descarta versões velhas após um prazo.
Crie um bucket num provedor com free tier (R2 é uma boa escolha). Usando um SDK S3-compatível, suba um arquivo, liste o bucket e baixe o arquivo de volta — tudo via credenciais guardadas como secret (não hardcoded). Confirme que o mesmo código funcionaria contra outro provedor só trocando o endpoint.
import boto3, os s3 = boto3.client("s3", endpoint_url=os.environ["S3_ENDPOINT"], # do cofre (Cap 17) aws_access_key_id=os.environ["S3_KEY"], aws_secret_access_key=os.environ["S3_SECRET"]) s3.upload_file("teste.txt", "meu-bucket", "pasta/teste.txt") print([o["Key"] for o in s3.list_objects_v2( Bucket="meu-bucket").get("Contents", [])]) s3.download_file("meu-bucket", "pasta/teste.txt", "volta.txt")
O insight do exercício: nenhuma credencial no código (vêm do ambiente/cofre, Cap 17), e o endpoint_url sendo uma variável prova a portabilidade — para migrar de R2 para B2 ou MinIO, você troca três variáveis de ambiente e o código continua idêntico. Essa é a vantagem concreta de a API do S3 ser um padrão de fato.
Implemente o padrão de upload por presigned URL: um endpoint na sua app que recebe o nome do arquivo e devolve uma URL assinada de curta duração; e o cliente subindo o arquivo diretamente no storage com essa URL. Garanta que a app nunca recebe o conteúdo do arquivo, e que a URL expira rápido.
# A app só assina; valida quem pode e restringe o destino: @app.post("/upload-url") def upload_url(user, filename): # Chave previsível e isolada por usuário (evita colisão/abuso): key = f"users/{user.id}/{secrets.token_hex(8)}-{filename}" url = s3.generate_presigned_url( "put_object", Params={"Bucket": "uploads", "Key": key, "ContentType": "image/jpeg"}, # restringe tipo ExpiresIn=120) # 2 min return {"url": url, "key": key}
// 1. pede a URL à app; 2. faz PUT direto no storage: const { url, key } = await (await fetch("/upload-url?...")).json(); await fetch(url, { method: "PUT", body: arquivo, headers: { "Content-Type": "image/jpeg" } }); // o byte foi cliente → storage; a app só assinou.
Pontos de atenção: a app valida quem pode subir (autenticação) antes de assinar; a chave inclui um token aleatório para o usuário não sobrescrever/adivinhar arquivos de outros; o ContentType e a expiração curta limitam o abuso (Erros 5 e 6). Confirme que o conteúdo do arquivo nunca aparece no backend — só o nome e a autorização passam por ele.
Para um app com 200 GB armazenados e usuários baixando 2 TB/mês, estime a diferença de custo entre um provedor com egresso a ~$0,09/GB e um com zero egresso. Depois, decida qual escolheria e em que cenário a resposta se inverteria.
A conta (ordens de grandeza):
O que escolheria: zero-egresso (R2, ou B2+Cloudflare), sem dúvida — o app serve muito download. Quando a resposta se inverteria: se fosse um arquivo morto — 200 GB guardados que quase ninguém baixa (egresso perto de zero) — aí o custo de egresso some da conta e o que importa passa a ser o $/GB de armazenamento; um provedor de storage frio baratíssimo (B2 puro, ou tier cold) venceria. O princípio: a escolha segue o padrão de uso. Muito download → otimize egresso; muito guardar e pouco ler → otimize armazenamento. Por isso se modela os três custos para o caso real, não se decide pelo $/GB anunciado.
Você desenha o storage de arquivos de um SaaS brasileiro de gestão de documentos: usuários sobem contratos privados (dados pessoais, LGPD), há também assets públicos do produto, e o negócio precisa de retenção legal e proteção contra ransomware. O CTO pergunta: "como organizamos isso para ser seguro, conforme à LGPD, resistente a desastre e sem susto de custo?" Estruture cobrindo separação público/privado, presigned URLs, egresso, backup imutável e residência de dados.
Enquadramento: documentos com dados pessoais elevam a barra — segurança, conformidade e durabilidade não são opcionais. A arquitetura separa por sensibilidade e cobre cinco frentes.
1. Separação público/privado:
2. Presigned URLs: todo acesso a contrato passa por presigned GET de curta duração (segundos/minutos), gerado só após a app validar que aquele usuário pode ver aquele documento. A app autoriza; o tráfego vai direto. Nenhum documento privado tem URL permanente.
3. Egresso/custo: documentos de gestão são lidos com moderação (não é app de mídia), então egresso é menos crítico que num app de fotos — mas zero-egresso ainda evita surpresa. Modelar os três custos; provavelmente R2 ou B2+Cloudflare. Lifecycle para expurgar temporários e mover documentos antigos para classe mais barata.
4. Backup imutável (anti-ransomware, Cap 16): versionamento + object-lock nos documentos, ou uma cópia em provedor/conta separada com object-lock — o "+1" da 3-2-1-1-0. Crucial: um ransomware que comprometa a app não pode apagar nem criptografar a cópia imutável. Atenção que nem todo provedor zero-egresso tem object-lock robusto — pode justificar provedor distinto para a cópia de backup.
5. Residência de dados (LGPD, Cap 20): dados pessoais de brasileiros pesam a favor de região no Brasil (ou ao menos base legal e DPA para transferência internacional). Retenção finita e documentada (lifecycle), e um plano para pedidos de exclusão de titular que considere os backups (o dado sai quando o backup/versão expira). Credenciais de acesso no cofre (Cap 17).
O que um bom candidato enfatiza: separar por sensibilidade (privado por padrão, público é exceção consciente), que presigned URLs de curta duração são a forma correta de servir privado, que backup imutável é o que transforma ransomware em contratempo (Cap 16), e que LGPD permeia tudo — residência, retenção finita, exclusão. Frase de fechamento: "documentos privados nunca têm URL permanente nem bucket público, vivem cifrados com cópia imutável fora do alcance de um ataque, e com retenção finita em região adequada — segurança, conformidade e durabilidade são a mesma decisão de arquitetura."
O banco de dados é o coração do "estado que não volta". Tudo que importa de verdade na sua aplicação mora ali. E há uma decisão de operação que define quanto do seu tempo, dinheiro e sono ele vai consumir: você cuida do banco, ou paga para o provedor cuidar? Não há resposta universal — há um trade-off entre controle, custo e responsabilidade que muda conforme o seu estágio.
Este capítulo é sobre a maior decisão de operação de dados que você vai tomar. Vamos enquadrar a escolha entre banco gerenciado (RDS, Supabase, Neon, DigitalOcean) e auto-hospedado (o Postgres rodando na sua VPS), entender exatamente o que o gerenciado faz por você — e quanto cobra por isso — e o que você abre mão em troca. Encararemos a conta real (o gerenciado custa de 5 a 20 vezes mais em escala, e o porquê), o mapa das opções modernas, como auto-hospedar com responsabilidade, e — o mais importante — o gatilho concreto para decidir e para mudar de ideia. A tese: para começar, gerenciado quase sempre vale; a pergunta é quando, e se, o auto-hospedado passa a compensar.
Por décadas, ter um banco de dados em produção significava ter um DBA — um administrador de banco de dados, profissional dedicado a instalar, tunar, fazer backup, replicar e socorrer o banco às 3h da manhã. Banco era infraestrutura cara e delicada, e operá-lo bem era um ofício inteiro. Para uma empresa pequena, isso era proibitivo; para um desenvolvedor solo, impensável.
Em 2009, a Amazon lançou o RDS (Relational Database Service) e empacotou o trabalho do DBA num serviço: você clicava, e tinha um banco com backup automático, failover e patches geridos pela AWS. O "banco gerenciado" nasceu e democratizou o acesso a um banco operado com competência — sem contratar ninguém. Google Cloud SQL e Azure Database seguiram o modelo.
Nos anos 2010, surgiu uma segunda onda focada em desenvolvedor. O Heroku Postgres tornou trivial anexar um banco a uma app. Depois vieram plataformas que embrulhavam o Postgres em algo maior: o Supabase (2020, "Firebase open-source" — Postgres com auth, storage e APIs prontos) e o Neon (2023, Postgres serverless, com compute separado do storage, scale-to-zero e branching de banco como se fosse git).
Em 2026, o pêndulo tem um movimento de volta interessante. Conforme o tráfego cresce, a conta do gerenciado escala de forma desproporcional ao custo do hardware — o markup do RDS sobre a máquina crua chega à casa dos 90%, e o mesmo workload que custa dezenas de dólares numa VPS Hetzner pode custar centenas no RDS. Equipes com experiência de operação (justamente o que este livro ensina) têm migrado de volta para auto-hospedado para cortar a conta pela metade ou mais. A decisão deixou de ser "gerenciado é o futuro" para virar "gerenciado quando, auto-hospedado quando" — uma escolha consciente de trade-off, não de moda.
No fundo, a escolha resume-se a uma troca: você prefere gastar dinheiro ou tempo? O gerenciado converte trabalho operacional em fatura mensal; o auto-hospedado converte fatura em trabalho seu. Nenhum é "certo" — depende de qual recurso é mais escasso para você agora.
O provedor opera: backup, failover, patches, monitoramento, réplicas — tudo incluso. Você foca na aplicação. Compra tempo e tranquilidade com dinheiro. Ideal quando seu tempo vale mais que a diferença na conta, ou quando você ainda não domina operação de banco.
Você opera o Postgres na sua VPS: configura, faz backup, aplica patch, monitora, e socorre quando quebra. Compra controle e economia com tempo e responsabilidade. Compensa quando a conta do gerenciado dói e você tem (ou quer ter) a competência de operação.
Para avaliar se o preço vale, é preciso saber exatamente o que está comprando. Um banco gerenciado embute trabalho que, no auto-hospedado, é todo seu:
| O managed faz | No auto-hospedado, você faz | Capítulo |
|---|---|---|
| Backup automático + PITR | Configurar dump/WAL, testar restore, vigiar com heartbeat | Cap 16 |
| Failover / alta disponibilidade | Configurar réplica e promoção (ou aceitar o downtime) | Cap 24 |
| Patches de segurança do banco | Acompanhar CVEs, aplicar, reiniciar | Cap 9 |
| Monitoramento e métricas | Instrumentar CPU/conexões/queries lentas | Cap 15 |
| Connection pooling, tuning básico | Configurar pgbouncer, ajustar parâmetros | — |
| Criptografia, TLS, atualização de versão | Tudo manual e seu | Caps 4, 9 |
A conveniência tem um custo além do financeiro. O gerenciado opera dentro de fronteiras que você não controla:
O número que choca quando você compara: o mesmo hardware custa muito mais quando o provedor o aluga com o banco gerenciado em cima. O markup do RDS sobre o custo cru da máquina passa de 90%; em escala, paga-se de 5 a 20 vezes mais que o equivalente auto-hospedado. Um exemplo de ordem de grandeza: um servidor robusto na Hetzner por ~$60/mês entrega compute que no RDS sairia por centenas.
Banco pequeno (MVP, pouco tráfego): Gerenciado: $0–25/mês (free tier / plano básico) Auto-hospedado: já está na VPS que você paga de qualquer jeito → diferença irrisória. Gerenciado ganha: zero trabalho. Banco médio (produção, milhares de usuários/dia): Gerenciado: $50–250/mês (DO previsível ~ RDS mais caro) Auto-hospedado: o custo da VPS + SEU tempo de operação → começa a doer. Decisão real aparece aqui. Banco grande (escala, HA, réplicas): Gerenciado: centenas a milhares/mês Auto-hospedado: fração disso em hardware + ops dedicada → 5-20× de diferença. Mas exige competência de DBA.
| Opção | Perfil | Quando |
|---|---|---|
| Supabase | Postgres + auth + storage + APIs (BaaS) | MVP que quer backend pronto; economiza semanas de desenvolvimento |
| Neon | Postgres serverless, scale-to-zero, branching | Workloads variáveis/idle, branch por PR; Postgres puro |
| DigitalOcean Managed | Preço fixo previsível, simples | Produção pequena/média sem complexidade enterprise |
| AWS RDS / Cloud SQL | Enterprise, compliance, todos os recursos | Já vive na AWS/GCP; exige certificações (SOC2, HIPAA) |
| Render / Railway | Banco junto da plataforma de deploy | Já deploya ali; tudo num lugar só |
| Auto-hospedado (Hetzner etc.) | Postgres na sua VPS | Conta > ~$150/mês, tem ops, quer performance/$ máximo |
Se você decidir auto-hospedar, faça-o sabendo que assinou para ser o DBA. Isso significa, no mínimo, cobrir cada item da coluna que o gerenciado faria — usando, não por acaso, os capítulos anteriores deste livro:
[ ] Bind só em localhost (127.0.0.1), nunca exposto → Cap 8
O banco NÃO escuta na internet; só a app local fala com ele.
[ ] Backup automático + cifrado + fora + testado → Cap 16
pg_dump/WAL, para object storage, com restore drilado.
[ ] Heartbeat no backup (silêncio = alerta) → Cap 15
[ ] Patches do Postgres acompanhados e aplicados → Cap 9
[ ] Credenciais no cofre, não no código → Cap 17
[ ] Monitorar conexões, CPU, queries lentas, disco → Cap 15
[ ] Connection pooling (pgbouncer) se muitas conexões
[ ] TLS nas conexões se a app não está no mesmo host → Cap 4
[ ] Plano de recuperação ENSAIADO para "o banco caiu"
O erro mais perigoso e mais comum do auto-hospedado: o Postgres escutando em 0.0.0.0, acessível da internet (Cap 8). Bots varrem a internet atrás de bancos abertos com senha fraca o dia inteiro — e bancos comprometidos por isso são manchete recorrente. O banco deve escutar só em 127.0.0.1 e falar apenas com a app local; se app e banco estão em máquinas diferentes, use rede privada e TLS, nunca a internet pública. Confirme com ss -tlnp que o Postgres não está em 0.0.0.0.
Decidir é mais fácil com critérios concretos do que com "depende". A regra prática que sintetiza o capítulo:
Seu SaaS começou no RDS há dois anos — escolha certa na época. Hoje, com tráfego estabilizado, a fatura do RDS é ~$320/mês e virou a maior linha da infraestrutura, maior que toda a parte de compute da aplicação. Você já operou os Caps 5–18, então tem a competência. A pergunta: migrar o banco para auto-hospedado na VPS compensa, e como fazer sem perder dados nem ter downtime longo?
Custo: $320/mês passou de $150 — sim, dói. Competência: você dominou backup (16), patches (9), firewall (8), monitoramento (15) — sim, tem. Tempo: o produto está estável, sobra capacidade para operar. Os três gatilhos alinhados: a migração se justifica. Estimar a economia: VPS robusta na faixa de $60-80/mês contra $320 = ~$240/mês, ~$2.900/ano.
$ sudo apt install postgresql # Bind SÓ em localhost (Cap 8): $ sudo nano /etc/postgresql/*/main/postgresql.conf listen_addresses = 'localhost' $ sudo ss -tlnp | grep 5432 # confirmar: 127.0.0.1, não 0.0.0.0 # Backup cifrado + fora + heartbeat (Caps 16, 15): # → o backup.sh do Cap 16, agendado, apontando pro object storage
# Estratégia de baixo downtime: réplica lógica do RDS para o # novo Postgres, sincroniza ao vivo, e só então o "cutover". # Versão simples (aceita uma janela curta de manutenção): $ pg_dump -Fc -h RDS_HOST -U user minhabase > dump.sql $ pg_restore -d minhabase dump.sql # no novo, local # Validar contagens e integridade ANTES do cutover. # Cutover: apontar a app pro novo banco (env var, Cap 17), # em janela de baixo tráfego. Manter o RDS de pé como # rede de segurança por alguns dias antes de desligar.
Não desligue o RDS no dia da migração. Rode a app contra o banco novo por alguns dias, monitorando (Cap 15) conexões, queries lentas e erros (Cap 14). Confirme que os backups do banco novo estão rodando e restauram (drill, Cap 16). Só quando tudo estiver provado, desligue o RDS — e aí a economia começa de verdade. Manter o caminho de volta aberto até ter certeza é a diferença entre migração e aposta.
Resultado: a conta do banco caiu de ~$320 para ~$70/mês, uma economia anual na casa dos milhares — sem perda de dados e com uma janela de cutover curta, porque o RDS ficou de rede de segurança até o novo banco estar provado. O custo da decisão: você agora é o DBA, com o trabalho de backup, patch e monitoramento que o RDS fazia — mas é trabalho que você já sabia fazer (Caps 8-18), executado com responsabilidade. A migração só foi sã porque os três gatilhos estavam alinhados; tê-la feito no MVP, com tempo escasso e conta de $20, teria sido economia falsa. Próximo capítulo: LGPD na prática — as obrigações legais sobre todos esses dados que você agora guarda e opera, gerenciados ou não. Fecha a Parte IV.
Começar operando o próprio Postgres quando a conta gerenciada seria de poucos dólares, gastando tempo precioso de produto em operação de banco. No começo, seu tempo vale mais que a diferença irrisória na conta. Comece gerenciado; migre quando o gatilho de custo disparar.
O Postgres auto-hospedado escutando em 0.0.0.0, varrido por bots e invadido por senha fraca. O banco deve estar em 127.0.0.1, falando só com a app local (Cap 8). Confirme com ss -tlnp. É a falha de segurança nº 1 do auto-hospedado.
Migrar para auto-hospedado pela economia e esquecer que assinou para fazer backup, patch, monitoramento e socorro. O banco roda lindo até o primeiro incidente, quando se descobre que não havia backup testado nem plano de recuperação. Auto-hospedar é assumir a coluna inteira da seção 19.3.
Decidir pela mensalidade da página de preços e ser surpreendido pelos custos ocultos: egresso, IOPS, backup acima do incluso, taxa por usuário ativo, Extended Support de versão velha. Modele a conta real com os add-ons do seu uso, não o número da manchete.
Construir a app em cima de features específicas de uma plataforma (funções, auth, realtime proprietários) e descobrir, ao tentar sair, que migrar é reescrever metade do backend. Postgres puro migra fácil; quanto mais você adota o que é exclusivo do provedor, mais preso fica. Adote conscientemente.
Desligar o banco antigo no mesmo dia que aponta a app para o novo, sem validar. Se algo der errado no cutover, não há volta. Mantenha o banco antigo de pé até o novo estar provado em produção por alguns dias, com backups testados.
Rejeitar o gerenciado por causa de "sem acesso root" ou "extensões limitadas" sem nunca ter precisado disso. A maioria das apps nunca encosta nessas fronteiras. Escolha pelo que de fato importa para você (custo, tempo, lock-in), não por limitações teóricas.
Liste tudo que um banco gerenciado faz automaticamente (backup, failover, patches, etc.) e, para cada item, escreva o que você precisaria fazer — e em qual capítulo deste livro aprendeu — se auto-hospedasse. Conclua: quantos desses você se sente confortável fazendo hoje?
| O managed faz | Você faria | Cap |
|---|---|---|
| Backup + restore | dump/WAL cifrado, fora, testado | 16 |
| Failover/HA | réplica + promoção, ou aceitar downtime | 24 |
| Patches | acompanhar CVEs, aplicar, reiniciar | 9 |
| Monitoramento | CPU, conexões, queries lentas, disco | 15 |
| Segurança de acesso | bind localhost, firewall, TLS | 4, 8 |
| Credenciais | cofre de secrets | 17 |
O insight: cada linha é um capítulo que você já estudou — o que prova que auto-hospedar é uma opção real ao seu alcance. Mas a coluna inteira somada é trabalho contínuo e responsabilidade de plantão. A pergunta final é honesta: você quer assinar para fazer tudo isso de forma confiável, para sempre? Se sim, ótimo. Se a lista te dá calafrio, o gerenciado vale cada centavo — e não há vergonha nisso.
Para um banco hipotético, estime: o custo mensal gerenciado (incluindo add-ons prováveis), o custo auto-hospedado (VPS) e quantas horas/mês de operação o auto-hospedado consumiria. Atribua um valor à sua hora e calcule a partir de qual conta gerenciada o auto-hospedado passa a compensar financeiramente.
Modelo de cálculo:
(gerenciado) > (VPS) + (horas × valor_hora). Com VPS $40 e 3h a R$100/h (~$60), o auto-hospedado "custa" ~$100/mês equivalente — então só vale se o gerenciado passar disso.O insight: o auto-hospedado nunca é "de graça" — o tempo é um custo real, só que invisível na fatura. Quando você valora suas horas, descobre que o ponto de virada é bem mais alto que o ingênuo "VPS é mais barata que RDS". É por isso que o gatilho de ~$150/mês existe: abaixo dele, o tempo gasto operando vale mais que a economia. Acima, e com tempo sobrando, a balança inverte. O cálculo honesto inclui sua hora.
Para cada caso, escolha entre Supabase, Neon, DigitalOcean Managed, RDS e auto-hospedado, justificando: (a) hackathon/MVP que precisa de banco + auth + APIs ontem; (b) projeto pessoal que fica idle 90% do tempo; (c) SaaS estabelecido, $280/mês de RDS, fundador que domina ops; (d) empresa em setor regulado que precisa de certificação SOC2/HIPAA.
(a) MVP precisando de banco + auth + APIs ontem → Supabase. O valor não é só o Postgres — é o backend pronto (auth, APIs, storage) que economiza semanas. Para velocidade de MVP, esse bundle vence; operar banco ou montar auth na mão seria desperdiçar o recurso mais escasso (tempo).
(b) Projeto idle 90% do tempo → Neon. O scale-to-zero é feito sob medida: o compute desliga quando ocioso e você quase não paga. Um banco gerenciado de preço fixo cobraria o tempo todo por algo quase sempre parado. Serverless ganha para workload intermitente.
(c) SaaS, $280/mês RDS, fundador com ops → auto-hospedado (ou avaliar seriamente). Os três gatilhos alinhados: conta passou de $150, competência existe, e é negócio estabelecido (menos pressão de produto). Migrar pode cortar a conta para uma fração. É exatamente o estudo de caso do capítulo.
(d) Setor regulado, SOC2/HIPAA → RDS (ou Cloud SQL). Aqui o critério decisivo não é custo, é compliance: os hyperscalers têm as certificações, auditorias e controles que um setor regulado exige e que seriam caríssimos de obter e manter auto-hospedando. Pagar pelo gerenciado enterprise é o custo de fazer negócio nesse setor.
O insight: a escolha raramente é só preço. Velocidade de MVP (a), padrão de workload (b), custo em escala com competência (c) e exigência regulatória (d) cada um aponta para uma opção diferente. O bom operador identifica qual eixo domina em cada caso.
Você convenceu a empresa de que migrar o banco do RDS ($350/mês) para auto-hospedado vale a economia. O CTO, nervoso, pergunta: "o banco é nosso ativo mais crítico — como você faz essa migração sem arriscar perder dados, sem downtime longo, e garantindo que a gente não vai se arrepender no primeiro incidente?" Estruture o plano cobrindo decisão, preparação, cutover, validação e a operação contínua depois.
Enquadramento: o banco é o "estado que não volta" — a migração precisa de rede de segurança em cada passo e de um plano de operação para depois, porque o risco real não é o cutover, é os meses seguintes sendo o DBA.
1. Confirmar a decisão (os três gatilhos): $350 > $150 (custo dói), a equipe tem ops (competência), e o negócio está estável (tempo). Quantificar a economia anual para justificar o esforço. Se algum gatilho falhasse, recuar — a economia não vale o risco sem competência.
2. Preparação (antes de tocar em produção):
3. Cutover com downtime mínimo:
4. Validação (não desligar o RDS!): manter o RDS de pé como rede de segurança por dias/semanas. Rodar a app contra o novo banco monitorando conexões, queries lentas, erros (Caps 14, 15). Só desligar o RDS quando o novo estiver provado em produção real e com backups testados. Esse período de paralelo é o seguro contra "se arrepender no primeiro incidente".
5. Operação contínua (o risco real): documentar o runbook de DBA — como restaurar, como aplicar patch, o que monitorar, quem é acionado quando o banco cai. A migração é um evento; ser DBA é para sempre. O plano só está completo quando a operação do dia a dia está definida, não só o cutover.
O que um bom candidato enfatiza: backup testado do banco novo antes de migrar, RDS mantido como rede de segurança até validação, rollback de um comando, e — o ponto que separa os bons — que o desafio não é o cutover, é assumir a operação contínua de forma confiável. Frase de fechamento: "migro com o banco antigo de pé como seguro e backup novo já testado, viro a chave em minutos com rollback pronto, e só desligo o RDS quando provei que opero o novo com a mesma confiabilidade que ele tinha — porque agora o plantão é meu."
Você passou a Parte IV inteira aprendendo a guardar e operar dados — backup, secrets, storage, banco. Mas se esses dados são de pessoas (e quase sempre são), existe uma camada que não é técnica: a lei. No Brasil, a LGPD define obrigações concretas sobre como você coleta, guarda, usa e protege dados pessoais — e, desde 2024-2025, a ANPD saiu do papel e entrou em modo fiscal, com multas reais. Operar dados sem entender isso virou um risco de negócio, não só de consciência.
Este capítulo fecha a Parte IV traduzindo a LGPD para a prática de quem opera — não para advogados, mas para desenvolvedores que precisam saber o que fazer com o servidor, o banco e os backups. Primeiro um aviso honesto: não sou advogado, e este capítulo não é aconselhamento jurídico — é um mapa do terreno para você conversar melhor com quem entende, e tomar decisões técnicas conscientes. Vamos cobrir o vocabulário mínimo (titular, controlador, base legal), os direitos que os usuários podem exercer contra você, como a lei se conecta com cada capítulo técnico anterior, o protocolo de quando há um vazamento (com prazo legal apertado), o mínimo viável para um projeto solo, e como as sanções funcionam — incluindo o que reduz a multa. O objetivo: você operar dados de brasileiros sem ignorância que vira passivo.
Por muito tempo, dados pessoais no Brasil foram terra de ninguém. Empresas coletavam, vendiam e vazavam dados sem consequência clara. A proteção existia de forma fragmentada (Código de Defesa do Consumidor, Marco Civil da Internet de 2014), mas não havia uma lei geral dedicada. A virada veio do exterior: a Europa aprovou o GDPR em 2016 (em vigor 2018), o regulamento de proteção de dados mais influente do mundo, que inspirou legislações em dezenas de países.
O Brasil seguiu. A LGPD (Lei Geral de Proteção de Dados, Lei 13.709) foi sancionada em 2018 e entrou em vigor em 2020, fortemente inspirada no GDPR. Criou a ANPD (Autoridade Nacional de Proteção de Dados) como o órgão fiscalizador, e estabeleceu princípios, bases legais, direitos dos titulares e sanções. Mas, nos primeiros anos, faltava dente: a ANPD ainda se estruturava, e a regulamentação para aplicar multas não estava pronta.
Isso mudou. Em 2023, a ANPD publicou o Regulamento de Dosimetria (Resolução 4/2023), definindo como calcular multas — o "dente" que faltava. Virou autarquia (agência reguladora de fato). Em 2024 vieram regulamentos sobre comunicação de incidentes (prazo de 3 dias úteis) e sobre o encarregado de dados. E em 2025-2026 a Autoridade entrou claramente em modo fiscal, publicando um Mapa de Temas Prioritários e aplicando as primeiras multas — inclusive em microempresas, deixando claro que porte não isenta.
Em 2026, a LGPD deixou de ser "aquela lei que ninguém aplica" para ser um risco concreto. A boa notícia para quem opera com cuidado: a lei premia a boa-fé. Um programa de conformidade documentado, transparência e a resposta correta a um incidente são fatores que reduzem sanções — e, às vezes, encerram o processo sem multa. Operar direito (o que este livro ensina) não é só ética; é a melhor defesa jurídica.
A LGPD tem jargão próprio. Cinco termos respondem por 90% das conversas:
O princípio central da LGPD: você não pode tratar dado pessoal "porque quer". Todo tratamento precisa de uma base legal — uma das hipóteses que a lei autoriza. As mais comuns para quem opera uma aplicação:
| Base legal | Quando se aplica | Exemplo |
|---|---|---|
| Execução de contrato | O dado é necessário para entregar o serviço pedido | Email e endereço para entregar o pedido |
| Consentimento | O titular concordou, livre e informado | Newsletter de marketing opt-in |
| Legítimo interesse | Interesse legítimo seu que não fere direitos do titular | Prevenção a fraude, segurança |
| Obrigação legal | A lei te obriga a guardar | Notas fiscais, dados fiscais/trabalhistas |
| Proteção ao crédito | Análise de risco de crédito | Consulta a histórico |
A LGPD dá aos titulares direitos que eles podem exercer contra você, e você é obrigado a atender. Os que mais aparecem na operação:
Aqui está o que torna este capítulo parte de um livro técnico: quase toda obrigação da LGPD se traduz em algo que você já aprendeu a fazer. A lei exige segurança e boas práticas — e segurança, neste livro, tem nome e capítulo.
| A LGPD exige / pune | O que você já fez | Capítulo |
|---|---|---|
| Segurança dos dados (criptografia, controle de acesso) | TLS, firewall, SSH endurecido, secrets em cofre | 4, 7, 8, 17 |
| Backup contra perda de dados | Backup cifrado, fora, testado, com retenção | 16 |
| Retenção finita (não guardar para sempre) | Lifecycle no storage, GFS no backup | 16, 18 |
| Resposta a incidente / comunicação | Logs, monitoramento, resposta a incidentes | 10, 15, 21 |
| Não vazar dados pessoais nos logs/erros | Mascarar PII antes de logar; send_default_pii=False | 10, 14 |
| Transferência internacional consciente | Pensar onde o bucket/banco fica (região BR) | 16, 18, 19 |
O caso técnico mais espinhoso: um titular pede exclusão, mas seus dados estão num backup de três meses atrás. Reescrever backups antigos para remover uma pessoa é impraticável (e os corromperia). A prática consagrada e aceita: documentar que backups têm ciclo de retenção finito, e que o dado do titular será eliminado dos sistemas de produção imediatamente e dos backups quando aquele backup expirar naturalmente, dentro do prazo de retenção. Você não viola a lei por não reescrever o backup; você cumpre tendo um prazo de retenção definido e documentado. É por isso que a retenção finita do Cap 16 não é só boa prática técnica — é requisito de conformidade.
Se houver um incidente de segurança que possa causar risco ou dano relevante aos titulares (um vazamento, um ransomware como o do Cap 16), a LGPD impõe obrigações com prazo. Diferente dos protocolos técnicos dos capítulos anteriores, aqui há um relógio jurídico correndo.
Descoberta do incidente
│
▼
1. CONTER e investigar → resposta técnica (Caps 16, 21):
fechar a brecha, avaliar o escopo.
2. AVALIAR a relevância → houve risco/dano aos titulares?
Que dados? Quantas pessoas?
3. NOTIFICAR a ANPD → prazo de 3 DIAS ÚTEIS a contar do
conhecimento, se houver risco/dano
relevante (Resolução 15/2024).
4. COMUNICAR os titulares→ informar as pessoas afetadas.
5. DOCUMENTAR tudo → registro do incidente, guardado
(obrigação de manter por anos).
Não notificar = agravante pesado.
Conformidade total com a LGPD é um projeto contínuo, e a partir de certo porte envolve advogado e encarregado de dados (DPO). Mas há um patamar mínimo, proporcional, que um projeto solo consegue e deve atingir — e que, não por acaso, é quase todo técnico (você já sabe fazer) mais um pouco de documentação:
A LGPD prevê sanções que vão de advertência a multa, e a ANPD agora as aplica. Conhecer a régua ajuda a dimensionar o risco — e a entender por que operar direito compensa juridicamente.
| Sanção | O que é |
|---|---|
| Advertência | Aviso com prazo para corrigir — a mais branda |
| Multa simples | Até 2% do faturamento no Brasil, limitada a R$ 50 milhões por infração |
| Multa diária | Por dia de descumprimento continuado, com o mesmo teto total |
| Publicização da infração | Tornar pública a violação — dano reputacional |
| Bloqueio / eliminação dos dados | Forçar a parada ou apagamento do tratamento irregular |
| Suspensão / proibição da atividade | Até interromper o tratamento de dados — a mais grave |
Você tem um SaaS B2B brasileiro rodando há um ano, com dados pessoais de clientes (nomes, emails, CNPJs, e alguns documentos enviados). Tecnicamente está sólido (fez toda a Parte II e IV). Mas em LGPD nunca pensou — não há política de privacidade, base legal definida, nem plano de incidente. Um cliente pergunta sobre conformidade, e você percebe que precisa se adequar ao mínimo. Sem orçamento para um time jurídico ainda.
Dado Onde mora Base legal Retenção ───────────────────────────────────────────────────────────────── nome, email banco, backup execução contrato enquanto cliente CNPJ banco obrigação legal 5 anos (fiscal) documentos object storage (Cap18) execução contrato enquanto cliente logs de acesso journald (Cap 10) legítimo interesse 90 dias email marketing lista CONSENTIMENTO até revogar
Este mapa — saber qual dado mora onde, sob qual base e por quanto tempo — é a fundação de tudo. Sem ele, não dá para responder a um titular nem demonstrar conformidade.
Escrever (com ajuda de um modelo confiável ou orientação jurídica pontual) uma política clara: que dados coleta, por quê, por quanto tempo, com quais operadores compartilha (o provedor de cloud, o de email, o Sentry), e como o titular exerce direitos. Honesta e legível, não um juridiquês copiado que não reflete a realidade — a ANPD valoriza a prática real, não o documento decorativo.
Canal: privacidade@meuapp.com.br (monitorado)
Acesso: query que reúne todos os dados de um titular
(banco + storage + logs) → exportar.
Exclusão: apagar de produção imediatamente; documentar que
backups expiram em 30d (retenção, Cap 16) e o dado
sai naturalmente. Não reescrever backup.
Escrever o protocolo: ao descobrir um vazamento, conter (Caps 16, 21), avaliar relevância, e — se houver risco relevante — notificar a ANPD em 3 dias úteis e comunicar os titulares, documentando tudo. Esse plano escrito, pronto antes do incidente, é o que torna o prazo cumprível e é, ele próprio, um atenuante (programa de conformidade).
Conferir que a base técnica já feita atende a LGPD: PII não vaza em logs nem no Sentry (send_default_pii=False, Caps 10/14); backups cifrados com retenção finita (Cap 16); documentos privados, nunca em bucket público (Cap 18); dados de brasileiros com atenção à região (Caps 18, 19). A maior parte já estava certa — agora está certa e documentada.
Resultado: em poucos dias de trabalho — sem time jurídico, aproveitando que a base técnica já era sólida — o SaaS saiu de "nunca pensei nisso" para um mínimo defensável: dados mapeados, política de privacidade honesta, base legal por tipo de dado, canal do titular que funciona, plano de incidente com o prazo legal, e a confirmação de que a operação técnica já respeitava a lei. Não é conformidade total (isso virá com advogado e DPO conforme crescer), mas é a postura de boa-fé documentada que a própria lei premia como atenuante — e que responde ao cliente que perguntou. O trabalho técnico da Parte IV inteira virou, de quebra, a maior parte da conformidade. Isto fecha a Parte IV: os dados estão guardados, operados e agora tratados dentro da lei. Próxima parte: a maturidade — o que fazer quando, apesar de tudo, algo dá muito errado, e como o negócio cresce de forma sustentável.
Achar que porte isenta. A primeira multa da ANPD foi numa microempresa, e a fiscalização chega por denúncia, setor prioritário ou vazamento viralizado. O dever de cumprir independe do tamanho; o que muda é a proporcionalidade da exigência, não a obrigação.
Tratar "aceito os termos" como base legal universal. Consentimento é só uma das bases, revogável, e nem sempre a adequada — o necessário ao serviço costuma ser execução de contrato. Aplicar a base errada é tão problemático quanto não ter base. Saiba qual base sustenta cada tratamento.
Logar emails, CPFs, corpo de requisição, ou deixar o Sentry capturar dados pessoais (Caps 10, 14). Isso espalha dado pessoal para lugares e terceiros sem controle, e é a própria infração que a lei pune. Mascare PII na origem; send_default_pii=False.
Descobrir, quando o pedido de exclusão chega, que não há como localizar tudo de um titular espalhado por banco, logs, storage e backups. Projete essa capacidade antes (privacy by design). O desespero de procurar dado de uma pessoa sob prazo é evitável.
Reter dados pessoais indefinidamente "porque pode ser útil". A LGPD exige retenção finita e proporcional à finalidade. Guardar para sempre é tanto risco de segurança quanto infração. Defina e documente prazos (Caps 16, 18).
Descobrir um incidente e tentar abafar, ou demorar para investigar antes de comunicar. O prazo de 3 dias úteis corre do conhecimento, e a comunicação tardia é agravante expresso. Esconder vira dolo presumido. Notifique no prazo — é obrigação e atenuante.
Copiar uma política genérica que não reflete o que você realmente faz com os dados. A ANPD valoriza a prática real e o programa funcional, não o documento de fachada. Política que mente sobre a operação é pior que não ter — é prova contra você. Que ela descreva a verdade.
Para uma aplicação sua, faça o mapeamento básico: liste cada tipo de dado pessoal que você coleta, onde ele mora (banco, logs, storage, backups, terceiros), qual a base legal provável, e por quanto tempo você guarda. Identifique algum dado que você coleta mas não usa (candidato a parar de coletar).
Modelo do mapa (uma linha por tipo de dado):
Dado | Onde mora | Base legal | Retenção | Compartilhado com
─────────────────────────────────────────────────────────────
email | banco, backup, provedor de email | contrato | enquanto
cliente | operador de email
...
O que o exercício revela: quase sempre aparecem (1) um dado coletado "por via das dúvidas" que nunca é usado — candidato à minimização (parar de coletar reduz risco e trabalho); (2) dados em lugares esquecidos, como logs ou Sentry, que você não tinha contabilizado como "guardar dado pessoal"; e (3) um prazo de retenção que era "para sempre" por inércia. O mapa é a base de toda conformidade: sem saber o que você tem e onde, não dá para proteger, entregar ao titular, nem apagar.
Para cada tratamento, diga a base legal mais adequada e por quê (e onde consentimento seria a escolha errada): (a) email do cliente para criar a conta; (b) endereço para entregar um produto físico; (c) CNPJ guardado por exigência fiscal; (d) envio de newsletter promocional; (e) coleta de IP para prevenir fraude.
(a) Email para criar a conta → execução de contrato. É necessário para prestar o serviço pedido. Consentimento seria errado aqui: se o usuário pudesse "revogar" o email, a conta não funcionaria — o dado é essencial, não opcional.
(b) Endereço para entrega → execução de contrato. Mesma lógica: sem endereço não há entrega. É necessário ao serviço contratado.
(c) CNPJ por exigência fiscal → obrigação legal. A lei te obriga a guardar para fins fiscais — base própria, e define inclusive a retenção (prazo legal), independente da vontade do titular.
(d) Newsletter promocional → consentimento. Aqui sim: é opcional, adicional ao serviço, e o titular deve poder dar e revogar o "sim" livremente (opt-in real, com descadastro fácil). É o caso clássico de consentimento.
(e) IP para prevenir fraude → legítimo interesse. Há um interesse legítimo (segurança) que normalmente não fere os direitos do titular. Consentimento seria impraticável (o fraudador não consentiria) e desnecessário.
O insight: consentimento é a base certa só para (d), o que é opcional. Para tudo que é necessário ao serviço ou exigido por lei, há bases mais adequadas e estáveis. Forçar consentimento onde não cabe cria a armadilha de um "não" do titular quebrar algo essencial — e sinaliza que você não entendeu as bases.
Escreva o plano de resposta a um vazamento de dados para um projeto solo, na ordem correta, respeitando o prazo legal. Defina quem faz o quê, o que decide se a ANPD precisa ser notificada, e o que precisa ser documentado. Indique onde isso se conecta com os capítulos técnicos.
Plano de resposta a incidente (projeto solo):
Conexões técnicas: detecção (Cap 15), contenção/restore (Cap 16), entender a entrada (Cap 21). O ponto crítico: esse plano tem que existir escrito antes do incidente — no meio do caos, com 3 dias úteis correndo, não há tempo de inventar o processo. Ter o plano pronto é o que torna o prazo cumprível e é, em si, um atenuante na dosimetria.
Você é o único dev técnico de uma startup brasileira de saúde digital (dados sensíveis de pacientes). O fundador, sem orçamento jurídico grande, pergunta: "qual é o mínimo que a gente precisa fazer em LGPD para não estar correndo risco sério, e o que justifica investir mais?" Estruture a resposta cobrindo o que é não-negociável, o que pode esperar, e por que o setor muda o cálculo.
Enquadramento honesto: primeiro, deixar claro que não sou advogado e que saúde é setor sensível — aqui orientação jurídica não é luxo, é parte do não-negociável. Dito isso, o operador técnico tem muito a fazer, e o setor muda o cálculo de risco.
Por que o setor muda tudo: dados de saúde são dados pessoais sensíveis — proteção reforçada na LGPD. E saúde está no Mapa de Temas Prioritários da ANPD para fiscalização. Ou seja: maior probabilidade de fiscalização e maior gravidade de qualquer infração. O cálculo de risco de uma startup de saúde não é o de um blog — investir em conformidade aqui é proteger a viabilidade do negócio.
Não-negociável (fazer já):
Pode amadurecer com o tempo: RIPD formal completo, auditoria externa (que para PME de setor sensível custa na casa de poucas dezenas de milhares — fração do risco de multa setorial), certificações, governança elaborada. Importante, mas escalonável conforme cresce.
O argumento de negócio: a lei premia boa-fé — programa documentado, DPO ativo, medidas corretivas reduzem multa e podem encerrar processo via termo de compromisso. Em saúde, onde a fiscalização é prioritária e o dano de um vazamento é enorme, investir em conformidade não é custo, é seguro — e diferencial comercial (clientes B2B de saúde exigem isso do fornecedor).
O que um bom candidato enfatiza: reconhecer o limite ("não sou advogado, e saúde exige jurídico"), separar o não-negociável (segurança técnica, base legal de dado sensível, plano de incidente) do que escala depois, e enquadrar conformidade como decisão de negócio — risco proporcional ao setor, e boa-fé como defesa. Frase de fechamento: "em saúde, conformidade é parte do produto, não um anexo — faço a segurança técnica impecável que já sei fazer, acerto a base legal de dado sensível com apoio jurídico, e tenho o plano de incidente pronto, porque aqui um erro não é multa, é o fim do negócio."
O que separa um projeto que sobrevive de um que prospera. Responder a incidentes com método, receber pagamentos sem quebrar, entender e cortar custo, e escalar na hora certa — nem antes, nem depois. Quatro capítulos sobre operar como gente grande, mesmo sozinho.
Mais cedo ou mais tarde, algo vai dar muito errado. O site cai na Black Friday, um deploy quebra o checkout, um atacante entra, o banco corrompe. Não é questão de se, é de quando. O que separa o operador maduro do amador não é nunca ter incidentes — é o que faz quando eles chegam. Pânico improvisado versus método ensaiado é a diferença entre dez minutos de transtorno e um dia de catástrofe.
Este capítulo abre a Parte V — a maturidade — ensinando a responder quando a operação falha. Tudo que você construiu nas partes anteriores (monitoramento que avisa, backup que restaura, logs que respondem, secrets que rotacionam) são as ferramentas; este capítulo é o método de usá-las sob pressão. Vamos definir o que conta como incidente, as fases da resposta (detectar, conter, erradicar, recuperar, aprender), como manter a cabeça fria quando tudo está pegando fogo, o valor dos runbooks e do post-mortem sem culpa, como comunicar durante o caos, e o que muda quando você é o único de plantão. O objetivo: transformar o incidente de um momento de pânico num procedimento que você executa — porque pensou nele antes.
Por muito tempo, responder a incidentes foi pura adrenalina improvisada. Algo quebrava, alguém gritava, e os engenheiros mais experientes mergulhavam no problema na base do instinto — herói exausto salvando o dia às 4h. Funcionava às vezes, falhava espetacularmente outras, e não deixava nada aprendido: o mesmo incêndio voltava semanas depois.
A virada veio de duas fontes que pensavam em sistemas críticos havia décadas: a aviação e a medicina de emergência. Ambas descobriram que, sob estresse extremo, humanos cometem erros previsíveis — e que checklists e procedimentos salvam mais vidas que heróis. O piloto não improvisa quando um motor falha; ele executa uma lista que decorou. Atul Gawande popularizou isso em software com "The Checklist Manifesto" (2009).
O Google formalizou a disciplina para sistemas digitais no movimento SRE (meados dos anos 2000), e seu livro de SRE (2016) cristalizou as práticas modernas: o incident commander que coordena, o runbook que documenta a resposta, o post-mortem blameless (sem culpa) que extrai aprendizado em vez de punir. A ideia central: incidentes são inevitáveis, então a competência está em respondê-los com método e aprender com eles, não em fingir que não acontecem.
Em 2026, mesmo o desenvolvedor solo herda essa cultura. Você não tem um time de plantão nem um incident commander dedicado — mas os princípios escalam para baixo: ter o procedimento pensado antes, agir com método em vez de pânico, comunicar com transparência, e fazer o post-mortem honesto consigo mesmo. Maturidade operacional não é tamanho de equipe; é a diferença entre sofrer o incidente e conduzi-lo.
Nem todo erro é um incidente, e tratar tudo como emergência esgota você tão rápido quanto ignorar o que importa. Um incidente é um evento que degrada ou ameaça o serviço de forma que exige resposta — não a falha rotineira que o sistema absorve sozinho.
| É incidente | Não é incidente (rotina) |
|---|---|
| Site fora do ar (Cap 15 alertou) | Um erro 500 isolado que o Sentry agrupou |
| Checkout quebrado após deploy | Um bug cosmético sem impacto |
| Vazamento de dados / invasão | Uma tentativa de login bloqueada (Cap 7) |
| Banco corrompido / perda de dados | Pico de tráfego que o sistema aguentou |
| Degradação grave de performance | Latência levemente acima do normal |
A resposta madura segue uma sequência que veio da segurança e da operação. Conhecê-la na ordem é o que substitui o pânico por método quando o relógio está correndo.
1. DETECTAR → saber que há um incidente (monitoramento, Cap 15; erros, Cap 14; ou um cliente) 2. CONTER → estancar o sangramento AGORA (reverter o deploy, isolar o sistema, bloquear o acesso) Objetivo: parar de piorar, mesmo sem entender a causa. 3. ERRADICAR → remover a causa raiz (corrigir o bug, fechar a brecha, expulsar o invasor) 4. RECUPERAR → voltar ao normal (restaurar de backup, religar, validar que funciona) 5. APRENDER → post-mortem sem culpa (por que aconteceu? como evitar a repetição?)
O maior inimigo durante um incidente não é o problema técnico — é o seu próprio estado mental. Pânico leva a decisões precipitadas que pioram tudo: o famoso rm errado, o "restaura o backup por cima sem checar" que destrói dados, o deploy às pressas que adiciona um segundo bug ao primeiro. As práticas que mantêm a cabeça fria:
O padrão que arruína operadores: o incidente original era recuperável, mas a resposta em pânico criou um dano pior e irreversível. Restaurou o backup por cima do banco bom "para garantir" e perdeu o que ainda tinha. Rodou o DROP no banco errado porque tinha dois terminais abertos. Fez deploy às pressas que derrubou o que ainda funcionava. A regra: sob pânico, a ação destrutiva merece o dobro de cuidado, não a metade. Quando a mão treme, pare e respire antes do enter — o problema já é ruim; não o torne pior.
Um runbook é um procedimento escrito para um cenário específico, preparado antes do incidente, para você executar durante. É o equivalente da lista do piloto: quando o motor falha, você não improvisa, você executa o que já pensou com a cabeça fria. O valor está exatamente em ter decidido os passos quando você não estava em pânico.
# Runbook: o banco de dados está fora / não responde ## Sintomas - App retorna erro de conexão; /healthz falha (Cap 15) - Alerta de uptime disparou ## Diagnóstico rápido (em ordem) 1. O processo do Postgres está vivo? → systemctl status 2. O disco encheu? → df -h (causa comum, Cap 10) 3. Estourou o limite de conexões? → ver logs (Cap 10) 4. O servidor inteiro caiu? → o monitor externo sabe ## Contenção - Disco cheio → liberar espaço (limpar logs/journal, Cap 10) - Conexões esgotadas → reiniciar a app; checar pool - Processo morto → systemctl restart postgresql ## Se nada disso → escalar para recuperação de backup → ver runbook-restore.md (Cap 16) ## Contatos / acessos - Provedor de VPS: [painel] Object storage: [backup]
Depois que o fogo apaga, vem a fase mais negligenciada e mais valiosa: entender o que aconteceu para não repetir. O post-mortem (ou retrospectiva de incidente) é o documento que registra o ocorrido, a causa raiz e as ações para prevenir a recorrência. A palavra-chave é blameless — sem culpa.
"O fulano derrubou o banco." Procura um culpado, gera medo, e ensina as pessoas a esconder erros. O foco vira defesa pessoal, não aprendizado. Resultado: o sistema continua frágil, e o próximo erro é abafado em vez de prevenido.
"Foi possível rodar um DROP em produção sem confirmação." Pergunta por que o sistema permitiu o erro, não quem errou. Foco em corrigir o processo (faltava um guard-rail). Resultado: o sistema fica mais robusto, e erros são reportados, não escondidos.
Durante um incidente, há uma segunda batalha além da técnica: a comunicação. Usuários ansiosos, talvez um cliente importante, talvez você mesmo precisando avisar alguém. A comunicação ruim transforma um problema técnico num problema de confiança.
Os manuais de SRE pressupõem um time: um incident commander, um comunicador, gente para investigar. Você é tudo isso ao mesmo tempo, provavelmente cansado, provavelmente sozinho. Os princípios escalam para baixo, mas com adaptações honestas:
Sexta, 22h. Seu celular alerta: o monitor externo (Cap 15) detectou que o site está fora. Você é solo. O Sentry não mostra exceção nova — sinal de que a app não está "errando", está ausente. Há clientes usando o produto à noite. Hora de conduzir, não de entrar em pânico.
Confirmar o incidente (não é falso positivo — testa de outro lugar, Cap 15). Em segundos, atualizar a status page: "Estamos cientes de uma indisponibilidade e investigando." Isso compra silêncio dos usuários enquanto você trabalha. Começar a anotar a linha do tempo: "22h03 — alerta; 22h05 — confirmado, status page atualizada."
# Seguir o runbook, não improvisar. Em ordem: $ systemctl status meuapp postgresql # processos vivos? $ df -h # disco cheio? (Cap 10) $ journalctl -u meuapp --since "22:00" -p err # o que houve? # Achado: df mostra /var em 100%. O disco encheu, o # Postgres parou de escrever, a app caiu junto.
# Causa imediata: disco cheio. Conter = liberar espaço. # Uma ação, observar, próxima. Sem rm às cegas. $ journalctl --vacuum-size=200M # logs antigos (Cap 10) $ df -h # observar: liberou? $ sudo systemctl restart postgresql meuapp $ curl -I https://meuapp.com.br/healthz # voltou? (Cap 15) # Serviço de volta às 22h18. Status page: "Resolvido."
Liberar espaço fez o site voltar, mas por que o disco encheu? Investigar com calma agora que o sangramento parou. Achado: os logs do journal não tinham limite (SystemMaxUse não configurado, Cap 10) e cresceram até lotar. O sintoma era disco cheio; a causa raiz é falta de rotação de log. Corrigir de verdade: configurar o limite, para não repetir.
O QUE: site fora 22h03-22h18 (15 min). Disco /var em 100%
parou o Postgres.
IMPACTO: ~15 min de indisponibilidade, horário de baixo uso.
CAUSA RAIZ: journal sem SystemMaxUse cresceu até lotar /var.
(5 porquês: site caiu→pg parou→disco cheio→
journal sem limite→nunca configurei rotação)
PREVENÇÃO:
[x] SystemMaxUse=1G configurado (Cap 10)
[x] alerta de disco >80% no Netdata (Cap 15)
[ ] runbook atualizado com "disco cheio" no topo
SEM CULPA: o sistema permitia log crescer sem limite.
O guard-rail (limite + alerta) impede a repetição.
Resultado: um incidente de 15 minutos, conduzido com método em vez de pânico. O monitoramento avisou (Cap 15), a status page comunicou, o runbook guiou o diagnóstico, a contenção estancou antes de entender tudo, a erradicação atacou a causa raiz (não só o sintoma), e o post-mortem sem culpa fechou o ciclo com um guard-rail (limite de log + alerta de disco) que impede a repetição. Nada foi improvisado: cada peça já existia porque foi construída antes. Compare com o cenário alternativo — sem monitor, você só saberia de manhã; sem runbook, cavaria às cegas; sem post-mortem, o disco encheria de novo em três meses. A maturidade não está em não ter o incidente; está em conduzi-lo. Próximo capítulo: pagamentos — porque a partir do momento em que entra dinheiro, os incidentes ganham um peso novo, e há um conjunto de cuidados específicos.
Ficar entendendo a causa enquanto o prejuízo corre. Se um deploy quebrou produção, reverta primeiro (Cap 12) e investigue depois. Conter para de piorar; investigar pode esperar o sangramento estancar. Cada minuto investigando sob fogo é dano evitável.
A resposta apressada cria dano pior que o original: restaura backup por cima do bom, roda comando no terminal errado, faz deploy às pressas que quebra mais. Sob pânico, a ação destrutiva merece o dobro de cuidado. Respire, releia, snapshot antes de destruir.
Reiniciar o serviço, o site volta, e você considera resolvido — sem perguntar por que caiu. Três meses depois, o mesmo incidente. Erradicar a causa raiz (os 5 porquês) é o que impede a repetição; só recuperar é adiar o próximo.
Sumir enquanto conserta, deixando usuários no escuro. O silêncio gera pânico, enxurrada de mensagens e perda de confiança. Comunique cedo, mesmo sem solução ("estamos cientes, investigando"); a status page faz o trabalho pesado.
Apagar o fogo, sentir alívio, e seguir em frente sem registrar nada. O aprendizado evapora, e o mesmo incidente volta. O post-mortem é a fase que transforma dor em robustez — pulá-lo é desperdiçar o preço que você acabou de pagar.
Focar em quem errou em vez de por que o sistema permitiu o erro. Isso ensina a esconder erros e mantém o sistema frágil. Mesmo solo, pergunte "o que tornou esse erro possível?" e crie o guard-rail — não se puna, conserte o sistema.
Confiar que vai lembrar o que fazer no momento — justamente quando está em pânico e exausto, a pior hora para improvisar. Escreva os runbooks dos cenários prováveis e catastróficos com a cabeça fria. É um presente do seu eu tranquilo para o seu eu em crise.
Para cada situação, decida se é um incidente (exige resposta imediata) ou rotina (entra na fila), e justifique: (a) um erro 500 isolado que o Sentry agrupou; (b) o site inteiro fora do ar; (c) latência 10% acima do normal; (d) suspeita de acesso não autorizado ao banco; (e) um bug visual numa página secundária.
Aplicando o teste "está causando dano agora que piora se eu esperar?":
O insight: o que separa incidente de rotina é o dano em curso, não a "estranheza" do evento. (b) e (d) doem agora e pioram esperando; o resto não. Saber distinguir poupa você do pânico de tratar tudo como SEV1 e do perigo de ignorar o que importa.
Escolha um cenário de incidente provável para uma app sua (ex.: "o banco está fora", "preciso restaurar o backup", "suspeito de invasão") e escreva o runbook completo: sintomas, diagnóstico em ordem, contenção, e quando escalar. Conecte cada passo aos capítulos/ferramentas relevantes.
# Runbook: suspeita de invasão / acesso não autorizado ## Sintomas - Logins SSH estranhos (journalctl -u ssh, Cap 10) - Processos/conexões desconhecidos (ss -tlnp, Cap 8) - Arquivos alterados, uso de CPU inexplicado ## CONTER (primeiro, sem investigar a fundo) 1. Isolar: bloquear acessos suspeitos no firewall (Cap 8) 2. Rotacionar TODOS os secrets já (Cap 17) — assuma vazados 3. Se ransomware: NÃO pagar, provisionar servidor limpo (Cap 16) ## ERRADICAR / RECUPERAR 4. Entender a entrada: SSH? CVE não aplicada (Cap 9)? 5. Restaurar de backup limpo e fora (Cap 16) 6. Fechar a brecha ANTES de reabrir (senão reinvadem) ## DEVER LEGAL (se há dados pessoais) 7. Avaliar relevância → notificar ANPD em 3 dias úteis (Cap 20) 8. Comunicar titulares; documentar tudo ## Contatos: provedor [x] | backup [x] | jurídico [x]
O que faz um bom runbook: ordem clara (conter antes de investigar), passos acionáveis (não "verifique a segurança", mas "rotacione os secrets"), conexão com as ferramentas que você já tem, e o dever legal embutido para não esquecer no caos. Escrito agora, com calma, ele é executável às 4h por um você exausto — que é exatamente o ponto.
Pegue um incidente real (ou este: "o site caiu porque o disco encheu") e faça a análise dos 5 porquês até chegar à causa raiz. Depois, proponha o guard-rail que impede a repetição, e formule a lição de forma sem culpa (focando no sistema, não na pessoa).
Os 5 porquês (exemplo do disco):
SystemMaxUse nunca foi configurado (Cap 10).Causa raiz: não é "o disco encheu" (sintoma) nem "eu esqueci" (culpa) — é a ausência de um guard-rail: nada limitava o log nem alertava antes do disco lotar.
Guard-rails (prevenção): (1) configurar SystemMaxUse (limita o log); (2) alerta de disco >80% no monitoramento (avisa antes de lotar, Cap 15); (3) adicionar ambos ao checklist de provisionamento (Cap 6), para todo servidor futuro já nascer protegido.
Lição sem culpa: "O sistema permitia que um log crescesse até derrubar o serviço, sem aviso prévio. O problema não foi esquecimento individual, foi a falta de um limite e de um alerta — guard-rails que agora existem e impedem a repetição." Note como isso é acionável e construtivo, enquanto "fui descuidado" não conserta nada e só gera medo de errar de novo.
Você é o único responsável técnico de um SaaS com clientes pagantes. O fundador pergunta: "se algo grave acontecer — o site cair, um vazamento, o banco corromper — como a gente garante que você consegue responder bem mesmo sozinho, de madrugada, cansado, sem entrar em pânico?" Estruture o plano de prontidão cobrindo preparação, execução e aprendizado.
Enquadramento: a resposta a incidentes solo se ganha antes do incidente. A prontidão é 80% preparação e 20% execução — porque sozinho e cansado, você só consegue executar o que já está pronto e automatizado.
1. Preparação (o trabalho que realmente importa):
2. Execução (quando acontece):
3. Aprendizado: post-mortem sem culpa em todo incidente sério — causa raiz pelos 5 porquês, guard-rail concreto que impede a repetição. Mesmo solo, escrevo: meu eu futuro é o colega que vai ler. Cada incidente deixa o sistema mais robusto, não só uma cicatriz.
O que um bom candidato enfatiza: que a prontidão solo é construída antes (monitoramento, backup testado, runbooks, automação), que conter vem antes de entender, que a comunicação e o dever legal são parte da resposta, e que o post-mortem sem culpa é o que converte dor em robustez. Frase de fechamento: "eu não respondo bem a incidentes por ser corajoso às 4h — respondo bem porque, com a cabeça fria, deixei o monitor me avisando, o backup testado, os runbooks escritos e os guard-rails no lugar. No incidente, eu só executo o que o meu eu tranquilo preparou."
No momento em que sua aplicação começa a receber dinheiro, tudo muda. Um bug que antes era um inconveniente agora cobra duas vezes do cliente, ou libera o produto sem pagamento, ou perde uma venda confirmada. Pagamento é a área onde os incidentes do capítulo anterior ganham peso financeiro e jurídico — e onde alguns erros técnicos sutis viram prejuízo direto. A boa notícia: as armadilhas são conhecidas, e evitá-las é questão de método.
Este capítulo é sobre operar pagamentos com segurança — não um tutorial de integração de cada gateway (a documentação deles faz isso melhor e muda toda hora), mas os princípios que valem para todos e que separam quem move dinheiro com tranquilidade de quem descobre um rombo no fim do mês. Vamos ver a regra de ouro (nunca seja você o responsável pelos dados de cartão), por que webhooks — não o navegador do cliente — são a fonte da verdade, o conceito de idempotência que impede a cobrança dupla, por que nunca se confia no preço que vem do cliente, e como os gateways brasileiros (Stripe, Pagar.me, Asaas, Mercado Pago) se diferenciam. O objetivo: receber dinheiro sem que um detalhe técnico vire um problema com o cliente, com o banco ou com a lei.
No começo do e-commerce, aceitar cartão era um pesadelo. Você precisava de uma conta de adquirente no banco, integrar com sistemas bancários arcaicos, e — o pior — lidar você mesmo com os dados sensíveis do cartão, sujeito a um padrão de segurança rígido (o PCI-DSS) cujo descumprimento gerava multas pesadas. Aceitar pagamento online era um projeto de meses e um passivo de segurança permanente. Poucos negócios pequenos conseguiam.
Em 2010, o Stripe mudou o jogo. Sua proposta: algumas linhas de código e você aceita cartão, sem tocar nos dados sensíveis, sem lidar com bancos, sem o fardo do PCI recaindo sobre você. O gateway virou infraestrutura como serviço — e democratizou os pagamentos online da mesma forma que a Cloudflare democratizou CDN e o RDS democratizou banco. Receber dinheiro deixou de ser projeto e virou integração.
No Brasil, o ecossistema floresceu com particularidades locais. Surgiram boleto, e depois o Pix (2020, do Banco Central) — pagamento instantâneo que virou o método dominante, usado em mais de 40% das transações online. Players nacionais se firmaram: o Pagar.me (grupo Stone) com infraestrutura robusta para a realidade brasileira, o Mercado Pago com a menor barreira de entrada, o Asaas forte em cobrança e recorrência. O Stripe entrou, mas com limitações locais (Pix restrito, custos internacionais e IOF).
Em 2026, aceitar pagamento é tecnicamente trivial — e é justamente essa facilidade que esconde a armadilha. A integração "funciona" em minutos, mas operar pagamentos direito — sem cobrança dupla, sem liberar produto sem pagar, sem vazar dado de cartão, reconciliando o que entrou — exige entender um punhado de princípios que o "copie e cole o snippet" não ensina. Este capítulo é sobre esses princípios, porque é neles que mora o dinheiro de verdade.
O princípio que organiza todo o resto: delegue ao gateway tudo que for sensível e difícil. Você não quer ser o responsável por guardar números de cartão, por cumprir PCI-DSS, por lidar com fraude e bancos. O gateway existe para carregar esse peso — e a sua parte é integrá-lo de forma que o dinheiro entre corretamente e o sistema saiba disso de maneira confiável. Quase todo erro grave de pagamento vem de assumir, por descuido, uma responsabilidade que era do gateway.
A regra mais importante de segurança em pagamentos, e a mais fácil de violar por ingenuidade: o número do cartão nunca deve passar pelo seu servidor. No instante em que dados de cartão tocam sua infraestrutura, você entra no escopo pesado do PCI-DSS e vira responsável por protegê-los — um fardo que afunda projetos pequenos. A solução que todos os gateways oferecem: o dado vai do navegador do cliente direto para o gateway, e você recebe de volta apenas um token inofensivo que representa aquele cartão.
Cliente (navegador) Gateway (Stripe/Pagar.me) Seu servidor │ │ │ │ digita o cartão num campo │ │ │ do GATEWAY (Elements/ │ │ │ checkout transparente) │ │ ├───────────────────────────►│ recebe o cartão │ │ │ devolve um TOKEN │ │◄───────────────────────────┤ (tok_abc123) │ │ │ │ envia só o TOKEN (não o cartão) ─────────────────────►│ │ │ cobra usando │ │ o token + valor │ │ (do servidor!) o número do cartão NUNCA passou pelo seu servidor → fora do PCI pesado.
<input> de número de cartão e mande para seu backend — isso joga você no PCI completo e cria um passivo de segurança enorme. Se em algum momento o número do cartão está numa variável do seu código, você errou o desenho. O cartão é do cliente e do gateway; você só vê o token.
Aqui está o erro conceitual que mais causa "liberei o produto e o pagamento não entrou" (ou o contrário). A pergunta: como você sabe que um pagamento foi confirmado? A resposta ingênua — "o cliente foi redirecionado para a página de sucesso" — está errada e é perigosa.
O redirect do navegador não é confiável: o cliente pode fechar a aba antes de voltar, perder a conexão, ou — maliciosamente — acessar a URL de "sucesso" sem ter pago. A confirmação real vem por webhook: o gateway faz uma chamada HTTP direto ao seu servidor, de servidor para servidor, dizendo "o pagamento X foi confirmado". Essa é a fonte da verdade.
Liberar o produto porque o navegador chegou na página de "obrigado pela compra". Frágil (cliente fecha a aba = pagou mas não recebeu) e inseguro (acessar a URL de sucesso sem pagar = recebeu sem pagar). O cliente controla o navegador; ele não é fonte da verdade.
Liberar o produto quando o gateway, de servidor para servidor, confirma o pagamento. Confiável (independe do navegador do cliente) e seguro (vem do gateway, com assinatura verificável). O webhook é a verdade; o redirect é só UX.
@app.post("/webhook/pagamento") def webhook(): # 1. VERIFICAR A ASSINATURA — senão qualquer um forja um # "pagamento confirmado" e ganha o produto de graça: payload = request.body assinatura = request.headers["X-Signature"] evento = gateway.verificar(payload, assinatura, WEBHOOK_SECRET) # WEBHOOK_SECRET vem do cofre (Cap 17), nunca do código. # 2. Agir só em eventos de pagamento confirmado: if evento.tipo == "payment.confirmed": liberar_produto(evento.pedido_id) # idempotente (22.5) return "ok", 200 # responder 200 rápido; processar pode ser async
Um fato incômodo sobre webhooks: eles podem chegar mais de uma vez. O gateway, para garantir entrega, reenvia o webhook se não receber sua confirmação (200) rápido — por timeout, por uma falha de rede momentânea. Então o mesmo evento "pagamento confirmado" pode bater no seu endpoint duas, três vezes. Se cada chegada libera o produto ou credita saldo, você acabou de entregar três vezes, ou creditar em triplicado. A defesa é a idempotência: processar o mesmo evento múltiplas vezes tem o mesmo efeito que processá-lo uma vez.
def liberar_produto(pedido_id, evento_id): # Já processei ESTE evento antes? (chave única do evento) if ja_processado(evento_id): return # não faz nada — webhook duplicado, ignora # Processa UMA vez, e registra que processou, # idealmente na MESMA transação de banco (atômico): with db.transaction(): marcar_pedido_pago(pedido_id) registrar_evento_processado(evento_id) # trava futura conceder_acesso(pedido_id) # Se o webhook chegar de novo, ja_processado() retorna True # e a função não faz nada. Efeito: liberou exatamente 1 vez.
Uma armadilha clássica que vira fraude: deixar o valor da cobrança ser definido por algo que o cliente controla. Se o seu frontend manda "cobrar R$ 100" e o backend obedece, um atacante intercepta a requisição e troca para "cobrar R$ 1" — e compra seu produto por um centavo. Tudo que vem do navegador do cliente é manipulável.
# ERRADO — o cliente manda o preço: POST /comprar { produto: "X", preco: 100 } # → atacante troca preco para 1. Compra por R$ 1. # CERTO — o cliente manda só O QUE quer; o servidor decide # QUANTO custa, consultando a fonte da verdade (banco): POST /comprar { produto_id: "X" } # servidor: preco = SELECT preco FROM produtos WHERE id='X' # o cliente nunca toca no valor cobrado.
Os princípios acima valem para todos; o que muda é o encaixe de cada gateway com o seu caso. Um panorama do mercado brasileiro em 2026:
| Gateway | Força | Quando |
|---|---|---|
| Stripe | API e DX de referência mundial, ótimo para SaaS e cobrança internacional | Vende para fora, quer a melhor API; mas Pix limitado e custos internacionais/IOF no BR |
| Pagar.me (Stone) | Robusto para a realidade BR, Pix/boleto nativos, taxas negociáveis em volume | Operação brasileira séria, alto volume, quer customização e checkout transparente |
| Mercado Pago | Menor barreira de entrada, ativação simples, ecossistema Mercado Livre | Começando, quer simplicidade; aceita modelo custodial e taxa atrelada ao prazo |
| Asaas | Forte em cobrança recorrente e gestão de assinaturas para PME | SaaS/serviços com mensalidade, cobrança por boleto/Pix recorrente, conta digital |
Três realidades operacionais que aparecem assim que o dinheiro flui de verdade:
Cobrança recorrente (assinatura) tem um problema próprio: cartões falham — expiram, ficam sem limite, são recusados. O dunning é o processo de retentar a cobrança recusada (D+3, D+7, D+15) e avisar o cliente para atualizar o cartão antes de cancelar o acesso. Os gateways voltados a assinatura (Asaas, Stripe Billing, Pagar.me Subscriptions) gerenciam isso. Deixar o dunning bem configurado é a diferença entre perder receita por cartão expirado e recuperá-la — e cobrar até quando não devia (cancelou mas continuou cobrando) é um problema de confiança e de lei (CDC).
O chargeback é quando o cliente contesta a cobrança com o banco e o valor é estornado — por fraude real, ou por "não reconheço", ou por insatisfação. Diferente do Pix (irreversível) e do boleto, o cartão tem esse risco. Você não controla o chargeback, mas reduz a exposição com boa descrição na fatura (o cliente reconhece a compra), antifraude do gateway, e guardando evidências da entrega. Chargeback em excesso te penaliza com as bandeiras — é um custo a monitorar, não ignorar.
Reconciliar é conferir periodicamente que o que o gateway diz que processou bate com o que o seu sistema registrou e com o que caiu na conta. Webhooks se perdem, eventos falham, estados divergem. Sem reconciliação, pequenas divergências viram um rombo invisível. Uma rotina que compara os pagamentos do gateway com os pedidos do seu banco — e alerta nas diferenças — é a contabilidade básica de quem move dinheiro.
Seu SaaS brasileiro vai começar a cobrar: um plano mensal, pago por cartão ou Pix. Você precisa que o cliente pague, que o acesso seja liberado de forma confiável, que ninguém pague duas vezes nem ganhe acesso de graça, e que você consiga conferir no fim do mês que tudo bate. Escolha do gateway: Asaas ou Pagar.me (recorrência BR forte); aqui, os princípios valem para ambos.
Frontend: usa o checkout transparente do gateway para o cartão
→ o número do cartão NUNCA toca seu backend (22.3).
Cliente envia só: { plano_id: "mensal" }.
Backend: preco = consulta o preço do plano no BANCO (22.6),
nunca confia em valor vindo do cliente.
Cria a cobrança no gateway com chave de idempotência.
POST /webhook/asaas
1. verificar assinatura (22.4) — secret do cofre (Cap 17)
2. se evento == "PAYMENT_CONFIRMED":
liberar_acesso(assinatura_id) ← idempotente (22.5)
3. responder 200 rápido
# A página de "obrigado" que o cliente vê é só UX.
# Quem libera o acesso é o webhook, server-to-server.
O liberar_acesso registra o ID do evento numa tabela e checa antes de agir (22.5). Se o Asaas reenviar o webhook (timeout, retry), a segunda chegada vê que o evento já foi processado e não faz nada. O cliente ganha acesso exatamente uma vez, não importa quantas vezes o webhook chegue. Tudo numa transação de banco, para ser atômico.
Configurar a assinatura recorrente no gateway com dunning: se a cobrança mensal falhar (cartão expirado), retentar em D+3/D+7 e avisar o cliente para atualizar o cartão, suspendendo o acesso só após as tentativas. E garantir o cancelamento limpo: quando o cliente cancela, parar de cobrar imediatamente (cobrar depois do cancelamento é problema de CDC e de confiança).
Uma rotina que, no fim do mês, compara os pagamentos confirmados no painel do gateway com os acessos liberados no seu banco e com o valor que caiu na conta. Qualquer divergência (um pagamento sem acesso liberado, um acesso sem pagamento, um valor que não bate) vira alerta para investigar. É a contabilidade que pega o webhook que se perdeu antes de virar reclamação ou rombo.
Resultado: um fluxo de pagamento sem os buracos clássicos. O cartão nunca tocou seu servidor (fora do PCI pesado), o preço veio do servidor (sem fraude de valor), o acesso é liberado pelo webhook verificado (confiável e seguro, não pelo navegador), a idempotência impede a entrega/cobrança dupla, o dunning recupera cartões que falham, e a reconciliação garante que o que entrou bate com o que foi liberado. Cada uma dessas decisões fecha um buraco por onde escaparia dinheiro, acesso ou confiança. E se algo der errado, é um incidente financeiro — tratado com o método do Cap 21, agora com o peso extra de envolver o dinheiro e os dados dos clientes (Cap 20). Próximo capítulo: FinOps — entender e cortar o custo de tudo que você construiu, agora que há receita entrando e contas para pagar.
Liberar o produto porque o navegador chegou na página de sucesso. O cliente pode fechar a aba (pagou, não recebeu) ou acessar a URL de sucesso sem pagar (recebeu sem pagar). A fonte da verdade é o webhook server-to-server, verificado — nunca o navegador, que o cliente controla.
Aceitar qualquer POST no endpoint de webhook como pagamento confirmado. É uma porta aberta: qualquer um forja "pagamento confirmado" e ganha o produto. Sempre verifique a assinatura com o secret do gateway (do cofre, Cap 17) antes de confiar no evento.
Cada chegada do webhook libera o produto ou credita saldo, e o gateway reenvia o webhook por retry — resultado: entregou três vezes, creditou em triplicado. Processe por ID de evento, registrando o que já foi feito. Webhooks repetem; seu código tem que aguentar.
Deixar o frontend mandar o valor da cobrança. O atacante troca R$ 100 por R$ 1 e compra por um centavo. O preço sempre é calculado no servidor, a partir de dados que o cliente não controla. O cliente manda intenção; o servidor decide o valor.
Criar seu próprio input de cartão e mandar para o backend, jogando-se no PCI-DSS completo e criando um passivo de segurança enorme. Use os componentes do gateway (Elements, checkout transparente); o cartão vai direto para o gateway, você só recebe o token.
Pegar o gateway de menor MDR e ignorar prazo de repasse, métodos (Pix!), custódia, qualidade de API e conversão do checkout. Um checkout ruim com taxa baixa perde mais em vendas abandonadas do que economiza. Modele o custo total e o encaixe, não o percentual.
Confiar que tudo que o gateway processou virou acesso liberado, sem nunca conferir. Webhooks se perdem, estados divergem, e pequenas diferenças viram um rombo invisível. Uma reconciliação periódica (gateway vs seu banco vs conta) pega a divergência antes de virar prejuízo ou reclamação.
/sucesso após pagar. Funciona nos testes. Mas em produção começam a aparecer dois problemas: alguns clientes pagam e não recebem acesso, e você descobre que uma URL como seusite.com/sucesso?pedido=123 libera acesso sem pagamento. Qual é a causa raiz e a correção?Para um fluxo de pagamento, liste o que é responsabilidade do gateway e o que é responsabilidade do seu servidor. Em seguida, identifique em qual lado mora o dado do número do cartão — e por que ele nunca deve cruzar para o seu lado.
| Gateway | Seu servidor |
|---|---|
| Capturar/tokenizar o cartão | Definir o preço (do banco) |
| Processar com os bancos | Pedir a cobrança (com idempotência) |
| Conformidade PCI | Receber o webhook (verificado) |
| Antifraude | Liberar o produto (idempotente) |
| Avisar o resultado (webhook) | Reconciliar |
O número do cartão mora no lado do gateway — capturado pelo componente dele (Elements/checkout transparente), direto do navegador do cliente. Ele nunca deve cruzar para o seu servidor porque, no instante em que cruza, você entra no escopo pesado do PCI-DSS e vira responsável por proteger um dos dados mais sensíveis que existem. Sua parte é toda lógica de servidor sobre tokens e valores — nunca sobre o cartão em si. Manter essa fronteira é a base da segurança de pagamentos.
Escreva o pseudo-código de um endpoint de webhook de pagamento que faça as três coisas essenciais: verificar a assinatura, ser idempotente (não processar o mesmo evento duas vezes), e responder rápido. Explique o que aconteceria de errado se cada uma das três faltasse.
@app.post("/webhook")
def webhook():
# 1. VERIFICAR ASSINATURA (secret do cofre, Cap 17):
evento = gateway.verificar(request.body,
request.headers["X-Sig"], WEBHOOK_SECRET)
# se inválida → 400 e para aqui.
# 2. IDEMPOTÊNCIA: já processei este evento?
if ja_processado(evento.id):
return "ok", 200 # duplicado, ignora
if evento.tipo == "payment.confirmed":
with db.transaction(): # atômico
liberar(evento.pedido_id)
marcar_processado(evento.id)
# 3. RESPONDER RÁPIDO (200) — trabalho pesado vai async,
# senão o gateway acha que falhou e reenvia.
return "ok", 200
O que quebra se cada um faltar:
Um e-commerce recebe a requisição POST /checkout { itens: [{id: 1, preco: 50, qtd: 2}], total: 100 } e cobra o total enviado. Descreva como um atacante explora isso, e reescreva o fluxo para ser seguro. Liste todos os campos que o servidor deve calcular em vez de aceitar.
Como o atacante explora: intercepta a requisição (ferramentas de dev do navegador, ou um proxy) e altera os valores antes de enviar. Troca preco: 50 por preco: 1 e total: 100 por total: 2 — e compra dois itens por R$ 2. Tudo que vem do cliente é manipulável; o servidor obedeceu cegamente a um número de dinheiro vindo de fora.
# Cliente envia só INTENÇÃO (o que quer), não valores: POST /checkout { itens: [{id: 1, qtd: 2}], cupom: "X" } # Servidor CALCULA tudo a partir da fonte da verdade: for item in itens: preco = SELECT preco FROM produtos WHERE id = item.id # do banco subtotal += preco * item.qtd # qtd: validar > 0 e razoável desconto = validar_e_aplicar_cupom("X") # cupom existe? aplica? total = subtotal - desconto # servidor decide o total
Campos que o servidor DEVE calcular (nunca aceitar do cliente): preço unitário (do banco), subtotal, desconto/cupom (validar existência e aplicabilidade), frete, impostos, e o total. O cliente só pode enviar: quais produtos (id), quantidade (a validar), e qual cupom tentar (a validar). A regra: o cliente manda o que quer, o servidor decide quanto custa.
Você desenha o sistema de pagamentos de um SaaS brasileiro com assinatura mensal (cartão e Pix), vendendo também para alguns clientes internacionais. O CTO pergunta: "como garantimos que recebemos certo, ninguém é cobrado errado, não viramos um problema de PCI nem de fraude, e conseguimos confiar nos nossos números?" Estruture cobrindo escolha de gateway, segurança, confiabilidade do recebimento e reconciliação.
Enquadramento: pagamentos têm quatro frentes que não podem falhar — segurança (PCI/fraude), confiabilidade do recebimento (webhook/idempotência), corretude do valor (preço do servidor) e confiança nos números (reconciliação). A arquitetura cobre as quatro.
1. Escolha de gateway (BR + internacional):
2. Segurança (PCI + fraude):
3. Confiabilidade do recebimento:
4. Confiança nos números (reconciliação): rotina periódica comparando pagamentos do gateway × acessos liberados no banco × valor na conta, alertando divergências. É a contabilidade que pega o webhook perdido antes de virar rombo ou reclamação. Monitorar também chargebacks (Cap 21: é incidente financeiro) e a taxa deles.
Tratamento de erro como incidente: pagamento é onde os incidentes do Cap 21 ganham peso financeiro — runbook para "cobrança dupla", "acesso não liberado após pagamento", "webhook caiu". E dados de pagamento são dados pessoais sensíveis sob LGPD (Cap 20).
O que um bom candidato enfatiza: a fronteira de responsabilidade (gateway carrega o pesado, cartão nunca toca o servidor), webhook+idempotência como o par que garante recebimento confiável, preço do servidor contra fraude, e reconciliação como a única forma de confiar nos números. Frase de fechamento: "o gateway carrega o cartão e o PCI; eu garanto que o valor é do servidor, que o acesso é liberado pelo webhook verificado e idempotente, e que no fim do mês os números do gateway, do meu banco e da minha conta batem — porque em pagamento, 'parece que funcionou' não basta."
A essa altura você acumulou uma pilha de serviços: servidor, storage, banco, CDN, gateway de pagamento, monitoramento, backup. Cada um tem uma conta, e juntas elas somam de formas que surpreendem. A maioria dos desenvolvedores olha a fatura mensal com uma mistura de confusão e resignação — o número sobe, e ninguém sabe explicar exatamente por quê. FinOps é a disciplina que troca essa resignação por controle.
Este capítulo é sobre entender para onde o dinheiro vai e cortar o desperdício sem quebrar nada. Vamos ver o ciclo do FinOps (enxergar, otimizar, manter), por que a visibilidade vem antes de tudo, os cinco vilões que respondem pela maior parte do desperdício (incluindo o egresso que você já encontrou no Cap 18 e o monitoramento que custa mais que o que monitora), como cortar com segurança sem virar o "pão-duro" que economiza centavos e arrisca produção, o conceito de unit economics que conecta custo a valor, e como manter o gasto sob controle de forma contínua em vez de tomar sustos. O objetivo: você olhar sua fatura e entender cada linha — e cortar o que é desperdício de verdade, deixando intacto o que importa.
Na era dos data centers próprios, o custo de infraestrutura era CapEx — despesa de capital. Você comprava os servidores de uma vez, um investimento grande e planejado, e eles eram seus. O custo era previsível porque era fixo: comprou, acabou. O desperdício existia (servidores subutilizados), mas a conta não te surpreendia todo mês.
A nuvem inverteu isso. Trocou CapEx por OpEx — despesa operacional, paga conforme o uso. Foi libertador: sem investimento inicial, escala sob demanda, pague só o que usar. Mas trouxe um problema novo e insidioso: o custo virou variável e opaco. A conta podia dobrar de um mês para o outro por motivos difíceis de rastrear, e o modelo "pague pelo uso" significava que cada recurso esquecido continuava cobrando, silenciosamente, para sempre.
Por volta de 2019, a indústria deu nome à resposta: FinOps (Financial Operations) — a disciplina de trazer responsabilidade financeira ao gasto com nuvem. Não é sobre ser pão-duro; é sobre entender o gasto, alinhá-lo ao valor que gera, e tomar decisões conscientes em vez de pagar faturas que ninguém explica. A FinOps Foundation formalizou o ciclo: informar (visibilidade), otimizar (cortar desperdício) e operar (manter contínuo).
Em 2026, com a pressão de custos crescendo mais rápido que a receita em muitos negócios — e a conta de IA somando à de infraestrutura —, FinOps deixou de ser preocupação de grande empresa para virar habilidade de sobrevivência até do desenvolvedor solo. A diferença de escala é enorme (você não tem multi-cloud nem time de FinOps), mas os princípios são os mesmos, e escalam para baixo: enxergar antes de cortar, cortar o desperdício real sem arriscar produção, e ligar cada custo ao valor que ele entrega. Sua fatura de cem dólares merece o mesmo entendimento que a de um milhão.
O FinOps organiza-se num ciclo de três fases que se repetem. Pular a primeira — a mais comum das pressas — sabota as outras duas.
A causa raiz do custo descontrolado quase nunca é desperdício deliberado — é falta de visibilidade. Ninguém otimiza o que não vê. Antes de cortar qualquer coisa, você precisa de um retrato honesto de para onde o dinheiro vai. Para um projeto solo, isso é mais simples do que parece:
Liste TODA fatura recorrente, com valor e o que entrega: Serviço Valor/mês Entrega o quê Vale? ────────────────────────────────────────────────────────── VPS (servidor) $XX roda a app sim Object storage $XX uploads + backup sim Banco gerenciado $XX o banco avaliar (Cap 19) CDN/Cloudflare $XX (ou $0) cache, proteção sim Monitoramento $XX observabilidade avaliar volume Sentry $XX erros avaliar plano Gateway (taxas) % pagamentos custo de fazer negócio Domínio, email... $XX diversos revisar TOTAL: $___ — e agora você SABE, em vez de adivinhar.
O desperdício de infraestrutura é previsível: quase sempre é uma combinação dos mesmos cinco padrões. Conhecê-los é saber onde procurar.
| Vilão | O que é | Onde procurar |
|---|---|---|
| Superdimensionamento | Recurso maior que o necessário "por segurança" | VPS/banco rodando a 10-20% de uso (Cap 15) |
| Egresso | Custo de dados saindo, que não aparece como linha óbvia | Storage sem zero-egresso (Cap 18); muito download |
| Recursos órfãos | Coisas criadas e esquecidas, cobrando para sempre | Droplets de teste, snapshots antigos, volumes soltos |
| Monitoramento/logs | Observabilidade que custa mais que o que observa | Volume de logs/métricas ingeridos; planos por evento |
| Ambientes ociosos | Dev/staging rodando 24/7 sem ninguém usando | Servidores de teste ligados de madrugada e fim de semana |
Identificado o desperdício, o corte tem uma ordem que maximiza ganho e minimiza risco. A regra de ouro: comece pelo que dá mais economia com menos risco de produção.
A maturidade final do FinOps não é olhar o custo total, mas o custo por unidade de valor — quanto custa servir um cliente, processar uma transação, entregar um pedido. Esse é o número que conecta a infraestrutura ao negócio, e que muda completamente a interpretação da fatura.
"A conta subiu de $100 para $200, que ruim!" Sem contexto, todo aumento parece problema. Você corta às cegas e pode estrangular o que está crescendo. O número total, sozinho, mente sobre a saúde do negócio.
"A conta dobrou, mas os clientes triplicaram — o custo por cliente caiu." Agora o aumento é ótima notícia: você escala com eficiência. O custo unitário revela se você está ganhando ou perdendo eficiência conforme cresce.
Cortar custo mal feito causa mais prejuízo do que o desperdício que elimina. As armadilhas de quem otimiza com a mão pesada:
Há custos que cortar é falsa economia, porque o que eles previnem custa muito mais que eles. Backup (Cap 16): economizar nele é apostar contra o desastre que ele evita. Monitoramento básico (Cap 15): cortar a observabilidade te cega para o incidente que vai custar caro. Segurança (Caps 7, 8, 17): o "economizei no firewall/secrets" vira o vazamento de LGPD (Cap 20). Redundância crítica de quem realmente precisa dela. A pergunta antes de cortar não é só "quanto economizo?", é "o que isto previne, e quanto custaria se acontecer?". Cortar o seguro para economizar o prêmio é o erro clássico — até o dia do sinistro.
Uma armadilha sutil de "rate optimization": você compra um plano anual com 30% de desconto, ou um compromisso de longo prazo com tarifa melhor — para um recurso que você nem deveria ter, ou maior do que precisa. Um desconto de 30% sobre algo desnecessário ainda é 100% de desperdício. Otimize primeiro o uso (você precisa disto? deste tamanho?), e só depois a tarifa (estou pagando o melhor preço pelo que de fato preciso?). A ordem invertida — travar contrato barato antes de validar a necessidade — prende você ao desperdício com desconto.
O FinOps falha quando vira projeto único — uma faxina de custos feita uma vez e esquecida, com o desperdício voltando a se acumular em silêncio. A terceira fase, manter, é o que torna o controle contínuo:
Seu SaaS cresceu, e a fatura de infraestrutura passou de ~$200 para ~$450/mês em alguns meses. Parte é crescimento legítimo, mas você suspeita de desperdício — só não sabe onde. Sem entrar em pânico nem cortar às cegas, você quer entender e enxugar o que for desperdício real, mantendo intacto o que sustenta o produto.
VPS produção $80 roda a app ok VPS "staging-old" $40 ??? ninguém usa ← órfão! Object storage S3 $90 uploads egresso alto Snapshots (vários) $25 backups antigos acumulou Banco gerenciado $120 o banco avaliar Monitoramento $60 logs volume sem controle Sentry $30 erros ok Domínio/email $5 diversos ok ───────────────────────────────────────────────── TOTAL: ~$450 — e agora os problemas ficaram VISÍVEIS.
# O "staging-old" não é usado há meses → apagar (-$40) # Snapshots redundantes além da retenção → limpar (-$15) # (Confirmar que não são o backup ativo! Cap 16) # Nenhum desses toca produção. -$55/mês sem risco.
Storage: migrar S3 → R2 (zero egresso, Cap 18). A linha de
$90 (muito egresso) cai para ~$30. -$60/mês.
Monitoramento: o volume de logs disparou. Reduzir retenção,
amostrar, filtrar ruído (Cap 14). $60 → ~$25. -$35.
(sem perder a observabilidade essencial)
O banco gerenciado ($120) é o maior item depois dos cortes. Aplicar os gatilhos do Cap 19: a conta passou de $150 somando tudo? Tenho competência de ops? Vale a migração para auto-hospedado? Decisão consciente, não corte automático — talvez sim, talvez o gerenciado valha a tranquilidade. O ponto é decidir com critério, não cortar por reflexo.
Billing alarm: alerta se a fatura passar de $300/mês. Revisão mensal: 15 min lendo cada linha. Unit economics: custo total ÷ clientes. Antes: $450 / 100 clientes = $4,50/cliente Depois dos cortes: ~$300 / 100 = $3,00/cliente → e cai mais conforme cresce. A pergunta certa.
Resultado: a fatura caiu de ~$450 para ~$300 (sem ainda mexer no banco) — cerca de 33% — atacando o desperdício real e deixando intacto tudo que sustenta o produto. O segredo foi a ordem: enxergar primeiro (o inventário revelou o staging órfão e o egresso), cortar o risco-zero antes (recursos órfãos), depois os tiers (egresso, monitoramento) com medição, e avaliar o grande (banco) com critério em vez de reflexo. Nada quebrou porque nada essencial foi cortado — backup, segurança e observabilidade básica ficaram de pé. E o número que de fato importa, o custo por cliente, caiu e continua caindo conforme o negócio cresce. Compare com o cenário do pânico: cortar o backup para economizar, descobrir o desastre, e gastar dez vezes mais. FinOps é entender antes de cortar, e cortar o desperdício, não o seguro. Próximo e último capítulo: quando escalar — a decisão de crescer a infraestrutura, que é o outro lado desta moeda, e que fecha o livro.
Sair cortando custos no susto, sem o inventário do que se gasta e por quê. Você corta o que dá menos economia, ou o que era essencial, e deixa o desperdício real intacto. Visibilidade vem sempre primeiro: liste cada custo antes de tocar em qualquer um.
Economizar em backup, monitoramento básico ou segurança — os custos que previnem desastres muito mais caros que eles. É falsa economia até o dia do sinistro. A pergunta antes de cortar: "o que isto previne, e quanto custaria se acontecesse?".
Gastar energia cortando o serviço de $3 enquanto a VPS de $80 roda superdimensionada. 10% de desperdício no item grande vale mais que 50% no pequeno. Ordene do maior para o menor e ataque o topo — é onde mora o dinheiro.
Comparar só o custo de armazenamento/computação e ser surpreendido pelo egresso, que não aparece como linha óbvia e compõe savagely em escala (Cap 18). Modele o custo de dados saindo, especialmente para apps com muito download; zero-egresso costuma ser a maior economia fácil.
Investir em observabilidade e acabar com uma conta de logs/métricas que supera a de computação, por ingerir volume sem disciplina. Amostre, filtre o não-acionável, defina retenção, escolha o plano certo. Observabilidade essencial, sim; desperdício de ingestão, não.
Travar um plano anual/compromisso barato para um recurso desnecessário ou superdimensionado. 30% de desconto sobre desperdício ainda é desperdício. Otimize o uso primeiro (preciso disto, deste tamanho?), a tarifa depois (pago o melhor preço pelo que preciso?).
Fazer uma grande otimização uma vez e nunca mais olhar — e o desperdício volta a se acumular em silêncio (órfãos novos, tiers que escalam). Sem billing alarm e revisão mensal, o controle evapora. FinOps é hábito contínuo, não evento.
Faça o inventário completo da sua infraestrutura (real ou de um projeto hipotético): liste cada custo recorrente, o valor, o que entrega, e classifique como "essencial", "avaliar" ou "candidato a corte". Some o total e identifique os três maiores itens.
O que o inventário deve conter (uma linha por custo): serviço, valor/mês, o que entrega, e classificação. Tipicamente: VPS, object storage, banco, CDN, monitoramento, Sentry, taxas de gateway, domínio/email, e quaisquer serviços menores.
O que o exercício revela: quase todo inventário expõe pelo menos um recurso órfão (um droplet/serviço de teste esquecido), um tier que escalou e nunca desceu, ou um custo que você nem lembrava de ter. Identificar os três maiores itens é o passo de priorização: é neles que qualquer otimização rende de verdade (foque no grande, não no centavo). E o simples ato de ver tudo junto — em vez de faturas espalhadas — já é metade do trabalho de FinOps. Visibilidade antes de corte.
Para uma infraestrutura (sua ou hipotética), procure cada um dos cinco vilões do desperdício: superdimensionamento, egresso, recursos órfãos, monitoramento sem controle, e ambientes ociosos. Para cada um que encontrar, estime a economia e classifique o risco do corte (zero / controlado / arquitetural).
Como caçar cada vilão:
Ordem de ataque: comece pelos de risco zero (órfãos, ociosos) — economia imediata sem tocar produção —, depois os controlados (egresso, tier, monitoramento) com medição. O resultado típico: a soma das economias seguras já é significativa, antes mesmo de qualquer mudança arriscada.
Para um cenário, calcule o custo por cliente e interprete: a infra custa $300/mês com 100 clientes; seis meses depois, custa $500/mês com 400 clientes. O custo total subiu — mas o que aconteceu com a eficiência? E como esse número se conecta com a receita por cliente (Cap 22)?
O cálculo:
A interpretação correta: olhar só o total ("subiu $200, ruim!") levaria a cortar custos e talvez estrangular o crescimento. O custo unitário conta a história verdadeira: o negócio está ficando mais eficiente conforme cresce — o sinal de uma infraestrutura saudável. O aumento da fatura é uma boa notícia disfarçada.
Conexão com receita (Cap 22): o número que fecha a conta é custo por cliente vs receita por cliente. Se cada cliente paga, digamos, $20/mês e custa $1,25 para servir, a margem de infraestrutura é enorme e melhora com escala. É essa relação — não o total da fatura — que diz se o negócio funciona. FinOps maduro pensa em margem unitária, não em valor absoluto.
Você é o responsável técnico de uma startup cuja fatura de infraestrutura cresce mais rápido que a receita, e o investidor cobrou eficiência. O CEO pergunta: "como a gente controla esse custo sem parar de crescer nem quebrar nada — e como eu sei se estamos melhorando?" Estruture o plano cobrindo visibilidade, onde cortar e onde não, unit economics, e como manter.
Enquadramento: "custo crescendo mais rápido que receita" é o sintoma exato que FinOps existe para resolver — mas a resposta não é cortar no susto, é entender, otimizar com critério, e medir pelo número certo (unit economics, não total). O plano segue o ciclo enxergar→cortar→manter.
1. Enxergar (antes de qualquer corte):
2. Cortar na ordem certa (sem quebrar):
3. Unit economics (a métrica que o investidor quer): reportar custo por cliente, não custo total. Se ele cai com a escala, estamos escalando com eficiência — exatamente o que prova que o crescimento é saudável. Cruzar com receita por cliente: a margem unitária é o número que diz se o negócio funciona. "A fatura subiu" é ruído; "o custo por cliente caiu de $4 para $2" é a história.
4. Manter (para não voltar): billing alarm (aviso de gasto anômalo no meio do mês), revisão mensal da fatura (15 min), estimar custo antes de adicionar serviço, e limpar ao desligar projetos. FinOps é hábito contínuo, não faxina única.
O que um bom candidato enfatiza: enxergar antes de cortar, não cortar o seguro (backup/segurança/observabilidade), reportar unit economics em vez de total, e tornar o controle contínuo. Frase de fechamento: "eu não corto no susto — enxergo o inventário, elimino o desperdício real começando pelo risco zero, deixo o seguro intacto, e provo o progresso pelo custo por cliente caindo com a escala. A meta não é a menor fatura, é a fatura que cada linha justifica e que cresce mais devagar que a receita."
"Mas isso escala?" é a pergunta que assombra todo desenvolvedor, geralmente cedo demais. A cultura de engenharia premia quem constrói para milhões de usuários — e pune, com vergonha, quem "só" tem um servidor. O resultado é uma epidemia de escala prematura: gente construindo infraestrutura para um tráfego que nunca vai chegar, pagando em complexidade e dinheiro por uma escala imaginária. Este capítulo, o último, é sobre a decisão oposta à do anterior — crescer, não cortar — e sobre a virtude mais subestimada da operação: saber quando não escalar.
Este é o capítulo final, e ele fecha duas coisas: o tema da maturidade e o livro inteiro. Vamos enquadrar a decisão de escalar pela pergunta que quase ninguém faz primeiro — "eu preciso?" —, entender escala vertical (para cima) e horizontal (para os lados) e os pré-requisitos de cada uma, ver quão longe um único servidor bem operado realmente vai, como escalar o banco (o gargalo mais comum), a ordem certa de escalar, e o custo real da escala prematura. No fim, uma síntese de tudo: porque cada capítulo deste livro foi, no fundo, sobre a mesma coisa — operar com intenção, fazendo o necessário com excelência e resistindo ao desnecessário com disciplina. Escalar bem é o teste final dessa filosofia.
Nos anos 2000, os gigantes da web — Google, Amazon, Facebook — resolveram problemas de escala reais e sem precedentes: bilhões de usuários, petabytes de dados. Para isso, inventaram tecnologias extraordinárias: bancos distribuídos, sharding, sistemas que rodavam em milhares de máquinas. Publicaram artigos, abriram código, e o mundo aprendeu. O problema: o mundo aprendeu a copiar, não a contextualizar.
A virada cultural foi o termo "web scale", que nos anos 2010 virou tanto aspiração quanto piada. Startups com cem usuários adotavam arquiteturas projetadas para cem milhões, porque era o que os gigantes faziam e o que soava impressionante. O auge foi a febre de microsserviços e Kubernetes: equipes minúsculas fragmentando aplicações simples em dezenas de serviços distribuídos, herdando toda a complexidade de sistemas distribuídos sem nenhum dos problemas que a justificariam.
A reação veio na forma de uma redescoberta do óbvio. Engenheiros experientes começaram a defender publicamente o que sempre souberam: o monólito num servidor único, bem operado, vai muito mais longe do que a cultura admite. Stack Overflow rodando em pouquíssimos servidores, empresas servindo milhões com arquiteturas "chatas", o movimento de volta do microsserviço para o "monólito modular". A lição amadureceu: escala é uma ferramenta para um problema, não um troféu.
Em 2026, com o custo de cloud sob escrutínio (Cap 23) e a complexidade cobrando seu preço, o pêndulo está num lugar saudável. A pergunta madura deixou de ser "como escalo para milhões?" e passou a ser "qual é o problema real que tenho agora, e qual é a coisa mais simples que o resolve?". Escalar continua sendo essencial quando o problema é real — mas a maturidade está em distinguir o problema real da escala imaginária. Este capítulo é sobre essa distinção.
Antes de como escalar, a pergunta que deveria vir sempre primeiro e quase nunca vem: você precisa escalar? Escalar resolve um problema específico — capacidade insuficiente para a demanda real. Se você não tem esse problema, escalar não é progresso, é complexidade e custo sem retorno.
A forma mais simples de escalar, e por isso a primeira a considerar: escala vertical, ou "scale up" — dar mais recursos (CPU, RAM, disco) à máquina que você já tem. Trocar a VPS de 2 vCPU/4GB por uma de 8 vCPU/16GB. O código não muda, a arquitetura não muda, o deploy não muda — você só tem mais cavalos de potência.
Simplicíssima: redimensionar a instância e pronto, sem refatorar nada. Sem o problema de sistemas distribuídos. Bancos relacionais (Postgres, MySQL) escalam assim há décadas. Para a maioria dos projetos, resolve por muito tempo — é a primeira e mais subestimada resposta.
Tem teto: existe um limite de quão grande uma máquina fica, e ele fica caro nas pontas. Continua sendo um servidor — um ponto único de falha (se cai, tudo cai) e downtime no upgrade. Não te dá alta disponibilidade. Em algum momento, "uma máquina maior" não basta.
Aqui está a verdade que a cultura de engenharia esconde: uma aplicação monolítica bem operada, num único servidor robusto, atende uma quantidade enorme de usuários. Não é "legado", não é gambiarra — é, para a vasta maioria dos negócios, a arquitetura certa por muito mais tempo do que se admite.
E o ponto crucial: você já fez quase todo o trabalho que faz um servidor único ir longe. Cada capítulo deste livro contribuiu para isso:
| O que estica a capacidade de um servidor | Capítulo |
|---|---|
| CDN/cache tirando tráfego de estáticos das costas dele | Cap 13 |
| Object storage tirando uploads e downloads do servidor | Cap 18 |
| Reverse proxy servindo estáticos e bufferizando conexões | Cap 11 |
| Monitoramento mostrando onde está o gargalo real | Cap 15 |
| Banco bem indexado e dimensionado | Cap 19 |
| FinOps mantendo o tier certo para a carga | Cap 23 |
Quando a escala vertical esbarra no teto, ou quando você precisa de alta disponibilidade (não pode ter um ponto único de falha), vem a escala horizontal, ou "scale out" — múltiplas instâncias da aplicação atrás de um load balancer, dividindo a carga. Mais poderosa e mais resiliente — e muito mais complexa.
Para rodar várias instâncias, a aplicação precisa ser stateless — sem estado guardado na própria máquina. Se o servidor A guarda a sessão do usuário na sua RAM, ou o upload no seu disco, o servidor B não tem acesso, e o usuário quebra ao ser roteado para B. Tornar a app stateless é o trabalho que viabiliza o horizontal — e, repare, é trabalho que você já fez ao longo do livro:
| Estado que precisa sair da máquina | Para onde vai | Capítulo |
|---|---|---|
| Sessões de usuário (na RAM) | Store externo (Redis) ou tokens | — |
| Uploads de arquivo (no disco) | Object storage (S3/R2) | Cap 18 |
| Secrets e config (no .env local) | Cofre injetado em runtime | Cap 17 |
| Os dados (no banco local) | Banco compartilhado | Cap 19 |
Na prática, o primeiro gargalo de verdade quase nunca é a aplicação — é o banco de dados. A app é fácil de multiplicar (se stateless); o banco, que tem o estado, é o difícil. As estratégias, em ordem de complexidade crescente:
Juntando tudo, há uma sequência natural — do mais simples e barato ao mais complexo e caro. Subir um degrau só quando o anterior se esgotou:
0. MEDIR → onde está o gargalo? (Cap 15) Sem isto, pare.
1. OTIMIZAR → índice, query, N+1, cache simples.
Quase sempre resolve. Grátis ou quase.
2. OFFLOAD → CDN (Cap 13), object storage (Cap 18):
tirar carga das costas do servidor.
3. ESCALAR ↑ → mais recursos na máquina (vertical).
Simples, sem refatorar. Vai longe.
4. CACHE / REPLICA → Redis, read replica para o banco.
Quando leitura é o gargalo.
5. ESCALAR ↔ → múltiplas instâncias + load balancer
(horizontal). Precisa statelessness.
6. DISTRIBUIR → sharding, microsserviços. Escala real.
99% dos projetos nunca chegam aqui.
Regra: nunca pule degraus. Cada um resolve mais barato
o que o próximo resolveria mais caro.
O erro oposto a não escalar quando precisa — e muito mais comum — é escalar quando não precisa. A escala prematura é cara de formas que não aparecem na hora:
Um cluster Kubernetes rodando 40 pods para um serviço que precisaria de 12 não é um sucesso de escala — é um ônibus de 40 lugares carregando uma família de quatro, com o taxímetro ligado. A escala prematura cobra em três moedas: dinheiro (você paga por capacidade ociosa e por serviços que a complexidade exige), complexidade (cada peça distribuída é mais uma coisa para operar, debugar e que pode falhar — você herda os problemas de sistemas distribuídos sem ter o problema que os justifica), e tempo (cada hora montando infraestrutura para uma escala imaginária é uma hora não gasta no produto que ainda precisa de você). O custo mais alto da escala errada não é a fatura — é a atenção desviada do que de fato faria o negócio crescer.
Seu SaaS cresceu, e nos horários de pico o site fica lento — clientes reclamam. Seu instinto (e o do investidor nervoso) grita "precisamos escalar, adicionar servidores, montar um cluster!". Antes de gastar com infraestrutura e complexidade, você decide aplicar a disciplina do livro: medir antes de escalar.
# Durante o pico, olhar as métricas (Cap 15): - CPU do servidor de app: 35% ← NÃO é o gargalo - Memória: 50% ← folga - CPU do banco: 98% ← AQUI. O banco está afogado. - Query mais lenta: uma busca sem índice, 4s # Diagnóstico: não faltam servidores de app. O banco é o # gargalo, e por uma query específica, não por falta de hardware.
# A query de 4s varria a tabela inteira. Faltava um índice: CREATE INDEX idx_pedidos_cliente ON pedidos(cliente_id); # Mesma query: 4s → 12ms. A CPU do banco no pico: 98% → 30%. # O site voltou a ser rápido. Custo: zero. Servidores # adicionais comprados: zero.
Se você tivesse "escalado" comprando mais servidores de app, teria gasto dinheiro e adicionado complexidade — e o site continuaria lento, porque o gargalo era o banco, não a app. Pior: um cluster horizontal teria mais instâncias batendo no mesmo banco afogado, talvez piorando. A escala prematura não só custaria caro como nem resolveria o problema. Medir revelou que a "necessidade de escala" era, na verdade, um índice faltando.
Meses depois, com 10× mais tráfego, o banco volta a apertar — agora por carga legítima, não por query ruim. Aí a ordem: primeiro escalar o banco verticalmente (mais RAM/CPU); depois, como a leitura domina, uma read replica e um cache Redis (Cap 24.6); e a app, já stateless (uploads no R2, secrets no cofre, dados no banco — Caps 17-19), escala horizontalmente com facilidade quando preciso. Cada degrau no momento certo, medido, sem pular etapas.
Resultado: o problema de "precisamos escalar" se revelou um índice de banco faltando — resolvido por R$ 0 em vez de uma conta mensal maior e um cluster para manter. A disciplina de medir antes de escalar economizou dinheiro, complexidade e tempo, e ainda resolveu o problema de verdade (o que a escala cega não faria). E quando a escala se tornou real, ela veio na ordem certa, sobre uma fundação stateless já construída ao longo do livro. Este é o capítulo inteiro em um caso: escalar é uma ferramenta para um gargalo medido, não uma resposta reflexa ao medo — e a maturidade está em saber a diferença.
Adicionar infraestrutura "para estar preparado" ou "porque os grandes fazem", sem um gargalo medido. Paga-se hoje, em dinheiro e complexidade, por um problema hipotético. Meça o gargalo (Cap 15) antes; sem ele, a resposta para "devo escalar?" é não.
Montar múltiplos servidores e um cluster quando uma máquina maior resolveria, herdando os problemas de sistemas distribuídos sem necessidade. Escale verticalmente primeiro — é mais simples, sem refatoração, e vai longe. Horizontal só quando bater no teto ou precisar de HA.
O banco está lento, você escala o banco — mas a causa era um índice faltando ou uma query N+1. Escalar hardware para resolver um problema de código é jogar dinheiro no lugar errado. Otimize a query primeiro; quase sempre é isso.
Rodar várias instâncias de uma app que guarda sessão na RAM ou upload no disco local — e ver usuários quebrarem ao trocar de servidor. Statelessness é pré-requisito: estado fora da máquina (storage, cofre, banco). Sem isso, horizontal não funciona.
Adicionar réplicas de leitura para um gargalo de escrita (não ajudam), ou achar que a réplica é seu backup (não é — replica o erro, Cap 16). Diagnostique leitura vs escrita, e mantenha o backup no tempo separado da replicação.
Fragmentar uma aplicação simples em dezenas de serviços distribuídos por modismo, herdando toda a complexidade operacional sem nenhum dos problemas que a justificariam. O monólito num servidor vai longe; microsserviços são para problemas de escala e de organização que você quase certamente não tem.
Gastar o tempo (escasso) construindo para uma escala imaginária enquanto o produto ainda não encontrou clientes. A maioria dos projetos morre por falta de gente usando, não por excesso. Resolva o problema que você tem — quase sempre é o produto, não a escala.
Este é o último capítulo, então vale fechar o livro com a ideia que sempre esteve por baixo de todos eles. Se você reler o sumário, parece um catálogo de assuntos díspares — DNS, TLS, firewall, backup, pagamentos, escala. Mas há um único princípio costurando tudo: operar com intenção. Fazer o necessário com excelência, e resistir ao desnecessário com disciplina.
Repare como a mesma decisão de maturidade reaparece em cada parte, com roupas diferentes:
Para cada situação, decida se a resposta inicial é escala vertical, horizontal, ou nenhuma das duas (otimizar antes), e justifique: (a) o banco está a 95% de CPU no pico; (b) você precisa de alta disponibilidade (não pode ter o serviço fora se um servidor cair); (c) uma página específica demora 5 segundos; (d) a RAM do servidor vive estourando.
(a) Banco a 95% no pico → medir primeiro, depois vertical. Antes de escalar, ver por que: se é uma query sem índice, otimizar resolve de graça. Se a carga é legítima, escala vertical do banco (mais RAM/CPU) é o passo natural — bancos escalam para cima muito bem.
(b) Precisa de alta disponibilidade → horizontal. Aqui o motivo não é capacidade, é resiliência: um único servidor é ponto único de falha. Só múltiplas instâncias (com a app stateless) eliminam o SPOF. É o caso em que o horizontal se justifica mesmo sem gargalo de carga.
(c) Página de 5s → nenhuma das duas, otimizar. Lentidão de uma página específica é quase sempre uma query lenta, um N+1, ou falta de cache — não falta de hardware. Escalar não conserta código ruim. Medir e otimizar a página é a resposta.
(d) RAM estourando → vertical (depois de checar vazamento). Primeiro descartar um vazamento de memória (Cap 10 — crescimento contínuo sugere bug, não falta de RAM). Se o uso é legítimo, mais RAM na máquina (vertical) é a resposta simples e direta.
O padrão: em quase todos, "medir/otimizar" ou "vertical" vem antes de "horizontal". O horizontal é a resposta para teto da vertical ou para HA — não o primeiro reflexo.
Suponha que você precise escalar uma app horizontalmente amanhã. Faça a auditoria de statelessness: onde a aplicação guarda estado que quebraria com múltiplas instâncias (sessão, uploads, cache local, arquivos temporários, secrets)? Para cada um, diga onde o estado deveria morar — e note quais você já resolveu seguindo o livro.
| Estado | Problema com N instâncias | Onde deve morar |
|---|---|---|
| Sessão de usuário | Servidor A tem, B não → logout ao trocar | Redis ou tokens (stateless) |
| Uploads | No disco de A, invisível para B | Object storage (Cap 18) ✓ |
| Secrets/config | .env de cada máquina diverge | Cofre em runtime (Cap 17) ✓ |
| Dados | Banco local não compartilha | Banco compartilhado (Cap 19) ✓ |
| Cache local (em RAM) | Cada instância tem o seu, inconsistente | Cache compartilhado (Redis) |
| Arquivos temporários | Em A, sumiu em B | Storage ou repensar o fluxo |
O insight: se você seguiu o livro, três dos itens críticos (uploads, secrets, dados) já estão externalizados — você fez a parte mais difícil da prontidão para o horizontal por outros motivos. O que normalmente resta é a sessão (mover para Redis ou usar tokens stateless tipo JWT) e o cache local. A lição: boas decisões compõem — externalizar estado por organização, segurança e backup deixou você, de brinde, quase pronto para escalar quando precisar.
Um app tem um gargalo de leitura no banco (muitas consultas repetidas a dados que mudam pouco). Liste as opções de solução da mais simples/barata à mais complexa/cara, e diga qual você tentaria primeiro e por quê. Explique por que pular direto para a opção complexa seria um erro.
As opções, do simples ao complexo:
O que eu tentaria primeiro: dado o cenário (leitura repetida de dados que mudam pouco), o cache é quase sob medida — depois de garantir que a query tem índice. Resolve o gargalo de leitura com baixíssima complexidade.
Por que pular para o complexo seria erro: montar read replicas ou sharding de cara adiciona uma camada distribuída (e seus modos de falha, sua consistência eventual, sua manutenção) para um problema que um cache resolveria em uma tarde. É escala prematura: pagar em complexidade por uma solução superdimensionada para o gargalo real. A regra da escada — nunca pular degraus — existe porque cada degrau resolve mais barato o que o próximo resolveria mais caro.
Você é o responsável técnico de um SaaS em crescimento. O CEO, animado com a tração e nervoso com a estabilidade, diz: "estamos crescendo rápido, precisamos garantir que a infraestrutura aguente — quero que a gente esteja pronto para 100× o tráfego atual. Monta um cluster, microsserviços, o que for preciso." Como você responde, equilibrando preparar para o crescimento e não cair na escala prematura? Estruture cobrindo o que fazer agora, o que adiar, e como saber a hora.
Enquadramento da resposta (diplomática, mas firme): validar a preocupação legítima do CEO (estabilidade no crescimento importa) e redirecionar do "preparar para 100×" — que é escala prematura — para "preparar a fundação que torna escalar fácil quando precisar, e escalar de fato só quando o gargalo for real". Construir hoje para 100× é pagar agora, em dinheiro e complexidade, por um tráfego que talvez nunca venha — e desviar atenção do produto que sustenta o crescimento.
O que fazer AGORA (fundação, barato, alto retorno):
O que ADIAR (escala real, sob demanda): cluster horizontal, read replicas, sharding, microsserviços. Cada um entra quando um gargalo medido o justificar — e, graças à fundação stateless, entram com facilidade, não em pânico. Subir a escada um degrau por vez (medir→otimizar→offload→vertical→cache/replica→horizontal), nunca pular.
Como saber a hora: os gatilhos são as métricas, não o calendário nem a ansiedade. Vertical quando a máquina aperta; cache/replica quando a leitura do banco vira gargalo; horizontal quando bater o teto da vertical ou precisar de HA. Cada degrau tem um sinal medido que o dispara.
A mensagem para o CEO: "Não vou montar um cluster para 100× hoje — isso custaria caro, adicionaria complexidade que nos atrasaria, e provavelmente nem resolveria o gargalo certo. Vou fazer melhor: deixar a fundação pronta (medição, estado externalizado, banco eficiente, cache) para que, quando o tráfego crescer de verdade, a gente escale em horas, na ordem certa, gastando só onde o gargalo real estiver. Assim ficamos prontos para crescer sem pagar pela escala antes de precisar dela — e mantemos o foco no produto, que é o que de fato traz os 100× usuários."
O que um bom candidato demonstra: equilíbrio entre não negar a preocupação e não ceder à escala prematura, a distinção entre preparar a fundação (fazer já) e escalar de fato (sob gatilho medido), e a maturidade de dizer "não" a um pedido bem-intencionado mas equivocado — propondo algo melhor. É a síntese do livro inteiro: fazer o necessário com excelência, recusar o desnecessário com disciplina.
Cinco partes, vinte e quatro capítulos — da Fundação (DNS, Cloudflare, email, TLS) ao Servidor, à Aplicação rodando, aos Dados e à Maturidade. 96+ exercícios graduados, 24 quizzes interativos, do primeiro pacote de DNS à decisão de quando escalar. Toda a operação ao redor do código: a parte que ninguém ensina e todo mundo precisa. Operar bem é fazer o necessário com capricho e o desnecessário nunca.