🌐 DNS apontando para a VPS
HTTPS começa no DNS. Sem o domínio resolvendo para o IP da VPS, o Caddy não consegue completar o desafio ACME HTTP-01 do Let's Encrypt e o cadeado nunca aparece. O registro essencial é o A record (IPv4); se a VPS tem IPv6, adicione também um AAAA.
O TTL (time-to-live) controla quanto tempo resolvers fazem cache da resposta. Em mudança de IP, baixe para 300 antes; em produção estável, 3600 ou mais. A propagação global leva de minutos a algumas horas — depende do provider e do TTL anterior.
# Registros no painel do provider (Cloudflare, Registro.br, etc.)
# Tipo Nome Valor TTL
# A postiz.exemplo 203.0.113.42 300
# AAAA postiz.exemplo 2001:db8::1 300
# A api.postiz.exemplo 203.0.113.42 300
# A app.postiz.exemplo 203.0.113.42 300
# Confirmar resolução (IPv4)
dig +short postiz.exemplo.com A
# 203.0.113.42
# IPv6
dig +short postiz.exemplo.com AAAA
# 2001:db8::1
# Resolver específico para ver propagação em outro DNS
dig @1.1.1.1 postiz.exemplo.com +short
dig @8.8.8.8 postiz.exemplo.com +short
💡 Dica prática
Antes de tentar emitir certificado, confirme que o domínio resolve para o IP certo a partir de fora — use dnschecker.org ou rode dig contra múltiplos resolvers. Let's Encrypt valida do datacenter deles, não do seu laptop.
Conceitos-chave
Mapeia hostname → IPv4 da VPS.
Versão IPv6 do A record.
Segundos que resolvers guardam o resultado em cache.
Tempo até a mudança aparecer em todos os resolvers.
🔄 Caddy vs Traefik vs nginx-proxy
Três caminhos para o mesmo objetivo: terminar TLS na borda e fazer reverse proxy para os containers. A escolha depende de quanto automatismo você quer e quão familiar é com cada ecossistema.
Caddy
Auto-HTTPS por padrão. Caddyfile lê como receita. Zero configuração para o caso comum.
Use quando: quer o menor caminho até o cadeado, stack pequeno ou médio.
Traefik
Configuração por labels do Docker — containers se anunciam sozinhos. Dashboard nativo.
Use quando: muitos serviços dinâmicos, Swarm ou Kubernetes na frente.
nginx-proxy
nginx + acme-companion. Máximo controle, sintaxe nginx tradicional, ecossistema enorme.
Use quando: já domina nginx, precisa de tuning fino ou módulos específicos.
Para o Postiz em uma VPS, Caddy é a escolha óbvia: emite e renova certificado sozinho, suporta HTTP/2 e HTTP/3 sem flags, e o arquivo de config cabe em 10 linhas.
# Comparativo rápido (linhas de config para o caso "1 domínio → 1 backend")
# Caddy ~3 linhas
# Traefik ~25 linhas (labels) + traefik.yml estático
# nginx-proxy ~20 linhas + Dockerfile de acme-companion
# Footprint de memória (idle, médio)
# Caddy ~30 MB
# Traefik ~50 MB
# nginx ~10 MB (sem acme-companion)
Conceitos-chave
Recebe na porta 443 e encaminha para o backend interno.
Emite, instala e renova certificado sem intervenção.
Traefik descobre containers por labels.
HTTPS termina na borda; backend fala HTTP no interno.
📝 Caddyfile mínimo + auto-HTTPS
O Caddy adiciona à stack como mais um serviço no docker-compose.yml. Ele assume as portas 80 e 443 do host, e fala com o Postiz pela network interna — sem expor 5000 publicamente.
# ~/postiz/Caddyfile
postiz.exemplo.com {
reverse_proxy postiz:5000
encode gzip zstd
}
Três linhas. O Caddy faz o resto: ACME challenge, certificado, redirect 80→443, HTTP/2, HTTP/3. Adicione o serviço ao compose e remova o ports: ["5000:5000"] do Postiz — ele não precisa mais ser acessível direto.
# ~/postiz/docker-compose.yml (recorte do serviço caddy)
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3 (QUIC)
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data # certificados (NÃO PERDER!)
- caddy-config:/config
networks: [postiz-net]
depends_on:
- postiz
volumes:
caddy-data:
caddy-config:
⚡ Dica prática
O volume caddy-data guarda os certificados emitidos. Apagá-lo força o Caddy a pedir certificado novo — e o Let's Encrypt tem rate limit de 5 emissões por hostname por semana. Trate esse volume como banco de dados.
Conceitos-chave
Sintaxe declarativa, um site por bloco.
Encaminha requests para um backend interno.
Protocolo do Let's Encrypt para emissão automática.
Transporte sobre QUIC/UDP — porta 443 UDP.
🌍 Subdomínios (api., app.)
Frontend e backend em subdomínios separados dá clareza operacional: app.exemplo.com serve a UI, api.exemplo.com a API. Cada subdomínio é um bloco no Caddyfile com seu próprio certificado.
# ~/postiz/Caddyfile com subdomínios
app.exemplo.com {
reverse_proxy postiz:5000
encode gzip zstd
}
api.exemplo.com {
reverse_proxy postiz:3000
encode gzip zstd
# CORS para o frontend
header {
Access-Control-Allow-Origin "https://app.exemplo.com"
Access-Control-Allow-Credentials "true"
}
}
# Redirect raiz → app
exemplo.com {
redir https://app.exemplo.com{uri} permanent
}
Ajuste as variáveis do Postiz para refletir os subdomínios — FRONTEND_URL=https://app.exemplo.com, NEXT_PUBLIC_BACKEND_URL=https://api.exemplo.com. Sem isso o frontend tenta chamar localhost e quebra no browser do usuário.
✓ O que FAZER
- ✓Criar DNS A para CADA subdomínio antes do
caddy reload. - ✓Atualizar
FRONTEND_URLeNEXT_PUBLIC_BACKEND_URLno.env. - ✓Restringir
Access-Control-Allow-Originao domínio do frontend. - ✓Usar redirect 308 do apex para o subdomínio canônico.
✗ O que NÃO fazer
- ✗Liberar
Access-Control-Allow-Origin: *com credenciais. - ✗Misturar HTTP e HTTPS — sempre force HTTPS no canônico.
- ✗Esquecer wildcard quando há mais de 3-4 subdomínios.
- ✗Hardcodar URLs no frontend — sempre via env var.
Conceitos-chave
Hostname filho do domínio principal.
Browser bloqueia origens cruzadas sem header explícito.
Permanente, preserva método HTTP original.
Domínio sem subdomínio (exemplo.com).
🛡️ Headers de segurança
Certificado é só metade do trabalho. Sem headers de segurança, o browser ainda permite ataques que TLS sozinho não cobre: downgrade para HTTP, clickjacking, sniffing de MIME, XSS via scripts injetados.
🧱 Os 4 headers que toda app web deve ter
- Strict-Transport-Security (HSTS): obriga o browser a usar HTTPS pelos próximos N segundos, mesmo se o usuário digitar
http://. - X-Frame-Options: impede que seu site seja carregado dentro de um
<iframe>em outro domínio (clickjacking). - X-Content-Type-Options:
nosniffimpede o browser de adivinhar o tipo MIME e executar scripts disfarçados. - Content-Security-Policy (CSP): whitelist do que pode ser carregado (scripts, styles, imagens, etc.) — defesa em profundidade contra XSS.
# ~/postiz/Caddyfile com headers de segurança
app.exemplo.com {
reverse_proxy postiz:5000
encode gzip zstd
header {
# HTTPS obrigatório por 1 ano, incluindo subdomínios
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Anti-clickjacking
X-Frame-Options "DENY"
# Anti-MIME-sniffing
X-Content-Type-Options "nosniff"
# Referrer só para mesma origem
Referrer-Policy "strict-origin-when-cross-origin"
# CSP básico — ajuste conforme assets externos do app
Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'"
# Remove header que vaza versão do servidor
-Server
}
}
⚠️ Dica prática
O preload do HSTS é irreversível por meses — só ative depois de confirmar que TUDO funciona em HTTPS, inclusive subdomínios futuros. Comece com max-age=300 em teste e suba aos poucos.
Conceitos-chave
Force HTTPS no browser por tempo determinado.
Site embutido em iframe inimigo para roubar cliques.
Whitelist de origens permitidas para cada tipo de asset.
Browser adivinhando tipo do conteúdo — bloqueado por nosniff.
🔁 Renovação automática Let's Encrypt
Certificado do Let's Encrypt dura 90 dias. O Caddy renova automaticamente quando faltam ~30 dias — sem cron, sem script, sem intervenção. O que pode dar errado é o desafio ACME falhar em silêncio; o jeito de saber é olhar os logs.
# Acompanhar logs do Caddy (boot + renovações)
docker compose logs -f caddy
# Procurar especificamente eventos de certificado
docker compose logs caddy | grep -iE "certificate|acme|renew"
# Inspecionar certificado emitido (data de expiração)
echo | openssl s_client -servername app.exemplo.com -connect app.exemplo.com:443 2>/dev/null \
| openssl x509 -noout -dates -issuer
# Forçar reload do Caddyfile sem reiniciar (zero downtime)
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
# Caso renovação falhe: limpar e tentar de novo (cuidado com rate limit!)
docker compose restart caddy
Volume caddy-data persistente
Sem ele, cada restart pede certificado novo e bate no rate limit do Let's Encrypt.
Portas 80 e 443 abertas no firewall
ACME HTTP-01 precisa de 80; TLS precisa de 443. Bloquear qualquer uma quebra renovação.
DNS continuando a apontar para o IP correto
Trocou de VPS? Atualize o A record antes de tentar renovar.
Monitorar logs ao menos 1x/mês
Procure por obtained certificate ou renewed certificate nos últimos 30 dias.
🚨 Dica prática
Configure um alerta externo (UptimeRobot, healthchecks.io) que bate em https://app.exemplo.com a cada 5 min. Se o cert vencer ou a config quebrar, você descobre antes dos usuários — não pelo print no WhatsApp dizendo "site fora do ar".
Conceitos-chave
CA gratuita, certs de 90 dias via protocolo ACME.
Desafio servindo arquivo em /.well-known/acme-challenge/.
5 emissões/semana/hostname no Let's Encrypt prod.
caddy reload troca config sem dropar conexões.
🎯 Resumo do Módulo
dig.caddy-data persistente.app. e api. em blocos próprios, CORS restrito ao frontend.Próximo Módulo:
6.3 — Backup, monitoramento e logs