Mapa da trilha
Conteúdo detalhado
🖥️ Servidor e Docker
Escolher VPS, hardening básico do Ubuntu, Docker + Compose e o arquivo docker-compose.prod.yml que vai pra produção.
Servidor virtual privado (VPS) com IP público fixo, onde a stack inteira do MkBlogs vai rodar 24/7. Hetzner CX22 (2 vCPU, 4GB RAM, 40GB SSD) na Alemanha custa ~€4,51/mês e cobre tranquilamente Postiz + Postgres + Redis + uma app NestJS.
Render, Railway e Fly.io ficam caros rápido quando você precisa de banco persistente, fila e worker. Uma VPS de €4 entrega 5x mais recurso pela mesma faixa de preço — e você aprende infra de verdade. Hetzner, OVH, Contabo e DigitalOcean são opções sérias; AWS Lightsail funciona mas é o mais caro.
vCPU vs CPU dedicada; RAM mínima 2GB pra Postgres + Redis + Node; localização do data center (latência pra Brasil: Ashburn/EUA ~120ms, Hetzner Falkenstein ~200ms); IPv4 dedicado; snapshot/imagem de backup.
Hardening básico do Ubuntu 24.04 LTS recém-provisionado: usuário não-root com sudo, login SSH só por chave (PasswordAuthentication no), ufw liberando apenas 22/80/443 e fail2ban banindo IPs que tentam força bruta no SSH.
Uma VPS sem hardening recebe milhares de tentativas de login por dia em 1h após subir. Sem fail2ban e firewall, é questão de tempo até alguém entrar. Esses 4 passos básicos eliminam 99% dos ataques automatizados.
Princípio do menor privilégio; chave SSH ed25519 (não RSA); ufw default deny incoming; jaula de fail2ban (maxretry, bantime, findtime); unattended-upgrades pra patches de segurança automáticos.
Instalação do Docker Engine + plugin Compose v2 via repositório oficial do Docker (download.docker.com), não pelo apt do Ubuntu que entrega versão velha. Compose v2 vem como subcomando: docker compose up, sem hífen.
O docker.io do apt pode ter 1-2 anos de atraso e bugs já corrigidos. O repositório oficial garante a versão atual, suporte a buildx, networking moderno e o Compose v2 nativo. Adicionar seu usuário ao grupo docker evita ter que usar sudo a cada comando.
Docker Engine vs Docker Desktop (desktop é só pra dev); grupo docker = root equivalente (cuidado); docker compose vs docker-compose; daemon.json pra log driver e storage driver; docker system prune pra limpar lixo.
Arquivo Compose dedicado à produção: imagens fixadas por tag (não :latest), variáveis vindas de .env, sem bind mounts de código, sem portas expostas direto (só via Caddy), restart: unless-stopped e limites de recursos (mem_limit, cpus).
Compose de dev e prod são animais diferentes: dev quer hot reload e ports expostas; prod quer reprodutibilidade, segurança e estabilidade. Misturar os dois leva a "funciona no meu PC" e portas 5432 do Postgres expostas na internet — receita pra invasão.
Override files (-f compose.yml -f compose.prod.yml); networks internas vs externas; expose vs ports; secrets via .env com permissão 600; pin de versão de imagem (ghcr.io/postiz/postiz-app:v1.30.0).
Named volumes do Docker (postgres_data, redis_data, postiz_uploads) que vivem em /var/lib/docker/volumes/ e sobrevivem a docker compose down. Ou bind mounts pra /srv/mkblogs/data quando você quer ver os arquivos direto no host.
Container sem volume = dado evaporado no primeiro docker compose down -v. Volume mal configurado = backup quebrado porque você está fazendo dump de pasta vazia. Saber exatamente onde cada dado mora é pré-requisito pra dormir tranquilo.
Named volume vs bind mount vs tmpfs; docker volume ls, inspect, prune; nunca usar volume pra banco em prod sem backup; UID/GID consistente entre host e container; cuidado com NFS pra Postgres (lock issues).
restart: unless-stopped faz o Docker subir o container de volta se ele crashar ou se a VPS reiniciar. healthcheck define como o Docker sabe que o container está saudável (HTTP /health, pg_isready, redis-cli ping) — e depends_on: condition: service_healthy faz o app só subir depois do banco estar pronto.
Sem restart policy, um OOM-kill no meio da madrugada deixa o site fora do ar até alguém perceber. Sem healthcheck, o Caddy roteia requisições pra um app que ainda está bootando e devolve 502. Esses dois ajustes resolvem 80% dos "caiu durante a noite".
Policies: no, on-failure, always, unless-stopped (esse é o certo); interval/timeout/retries/start_period do healthcheck; loop de restart (CrashLoopBackOff); diferença entre liveness e readiness.
🔒 Domínio, HTTPS, reverse proxy
Apontar DNS, escolher reverse proxy, configurar Caddyfile com auto-HTTPS, subdomínios e headers de segurança.
Criar registro A em mkblogs.com apontando pro IPv4 da VPS, AAAA pro IPv6, e CNAMEs pros subdomínios (api., app.) apontando pro domínio raiz. Cloudflare grátis na frente (modo DNS-only, nuvem cinza) facilita gerenciar.
Sem DNS apontando certo, Caddy não consegue validar Let's Encrypt e fica sem HTTPS. TTL alto (24h) atrapalha quando você precisa mudar IP. Subdomínios facilitam separar API, painel e status sem precisar de novos certificados.
A vs AAAA vs CNAME; TTL (use 300s durante mudanças, 3600s depois); propagação (dig +trace); Cloudflare proxy desligado pra Let's Encrypt HTTP-01 funcionar; wildcard *.mkblogs.com exige validação DNS-01.
Reverse proxy é a porta de entrada HTTPS que termina TLS, roteia por hostname e fala HTTP pros containers internos. Caddy (Go) tem auto-HTTPS no Caddyfile com 3 linhas. Traefik é mais flexível mas YAML/labels complexas. nginx-proxy + acme-companion é o jeito antigo, ainda funciona.
Pra MkBlogs (1-3 serviços HTTP), Caddy é a escolha óbvia: Caddyfile cabe em 10 linhas, certificado automático, renovação automática, HTTP/3 ligado por padrão. Traefik ganha se você tem 10+ serviços e quer descobrir por labels do Docker.
Termination TLS; SNI; upstream HTTP interno; service discovery (labels Docker, etcd, file); ACME provider; admin API do Caddy na porta 2019 (não expor!); reload sem downtime.
Arquivo Caddyfile no formato: app.mkblogs.com { reverse_proxy postiz:3000 }. Caddy sozinho resolve ACME challenge HTTP-01, pega certificado Let's Encrypt, instala, configura HSTS e redireciona 80 → 443.
É literalmente a configuração mais simples de HTTPS em produção que existe hoje. Comparado ao nginx + certbot + cronjob de renovação + reload, são 3 linhas vs 50. Erro humano cai drasticamente.
Site block; matcher (@api host api.mkblogs.com); reverse_proxy com health check; encode gzip zstd; volume caddy_data persistente (guarda os certificados!); tls internal pra dev local.
Separar superfícies por subdomínio: app.mkblogs.com (painel Postiz), api.mkblogs.com (sua API NestJS), status.mkblogs.com (status page), grafana.mkblogs.com (métricas). Cada um vira um site block no Caddyfile.
Manter tudo em /api, /app, /status no mesmo domínio gera conflito de cookies, CORS confuso e dificulta limites de rate por surface. Subdomínios isolam cookies de sessão e simplificam regras.
Cookie scope por domínio; CORS cross-subdomain (Access-Control-Allow-Origin); rate-limit por host no Caddy; CSP connect-src; um certificado por subdomínio (Let's Encrypt grátis até 50/semana por domínio raiz).
Conjunto de headers HTTP que o reverse proxy adiciona automaticamente: Strict-Transport-Security (HSTS, força HTTPS), X-Frame-Options: DENY (anti-clickjacking), X-Content-Type-Options: nosniff, Referrer-Policy e CSP básica.
Pega de graça nota A no securityheaders.com e fecha vetores comuns (clickjacking, MIME sniffing, downgrade HTTPS→HTTP). Custa 6 linhas no Caddyfile e protege contra ataques que existem desde 2010.
HSTS preload (cuidado, é difícil reverter); CSP report-only antes de enforce; nonce e hash em scripts inline; Permissions-Policy (substitui Feature-Policy); diretiva header do Caddy.
Certificados Let's Encrypt duram 90 dias. Caddy renova automaticamente quando faltam ~30 dias, usando o mesmo desafio HTTP-01 (ou DNS-01 se você configurou). Tudo persiste no volume caddy_data em /data.
A maioria dos "site fora do ar com cadeado vermelho" é certificado expirado. Com Caddy, isso é problema resolvido — desde que (1) o volume persista, (2) o DNS continue apontando certo e (3) a porta 80 continue acessível pra desafio HTTP-01.
ACME v2; rate limit de 5 certificados/semana por domínio (cuidado em testes — usar staging); acme_ca https://acme-staging-v02.api.letsencrypt.org/directory; ZeroSSL como fallback; alerta em UptimeRobot pra expiração (cinto e suspensório).
📊 Monitoramento e logs
Centralizar logs (Loki/Better Stack), uptime, métricas no Grafana, alertas, Sentry pra erros de app e status page pública.
Coletar logs de todos os containers num lugar pesquisável. Opção self-hosted: Grafana Loki + Promtail/Alloy lendo do Docker socket. Opção SaaS: Better Stack (ex-Logtail) com plano grátis de 1GB/mês.
docker logs -f só funciona quando você sabe qual container investigar. Quando o usuário diz "deu erro às 14h32 ontem", você precisa de busca textual com filtros por serviço, tempo e nível — exatamente o que Loki/Better Stack entregam.
Structured logging (JSON > texto solto); log driver do Docker (json-file padrão, com rotação max-size); LogQL do Loki (similar a PromQL); retention vs custo; PII em logs (não logar token/email cru).
Monitor externo (fora da sua VPS!) que faz HTTP GET no /health de cada serviço a cada 5 minutos e alerta quando responde !=200 ou demora demais. UptimeRobot tem 50 monitors grátis; BetterStack Uptime e Healthchecks.io são alternativas.
Monitor que roda dentro da própria VPS é inútil — se a VPS cai, o monitor cai junto e silencia. Monitor externo é a primeira linha de defesa contra "tô fora do ar e nem sei". Healthchecks.io ainda monitora cron jobs (pings de heartbeat).
HTTP check vs ping vs keyword check; intervalo (5min grátis, 1min pago); endpoint /health que valida banco + Redis, não só "200 OK"; dead-man's-switch pra cron (Healthchecks.io); SSL expiration check de brinde.
Stack Prometheus + Grafana rodando ao lado da app. node_exporter coleta CPU/RAM/disco da VPS, cadvisor coleta métricas por container, e a app expõe /metrics (latência, fila BullMQ, falhas de OAuth). Grafana desenha gráficos.
Sem métricas, você descobre que o disco encheu quando o Postgres trava. Com gráfico de disco e CPU dos últimos 7 dias, você vê a tendência e age antes. Pra MkBlogs, métricas críticas são: profundidade da fila, latência das APIs (X, LinkedIn), erros 5xx, uso de disco em volumes.
Pull-based (Prometheus) vs push-based; counter/gauge/histogram; labels (cuidado com cardinalidade alta); retention de 15 dias é suficiente; dashboards do Grafana com grafana.com/dashboards (ID 1860 = Node Exporter Full, 893 = cadvisor).
Regras no Alertmanager (parte do Prometheus) que disparam quando uma métrica passa do limite (disco > 85%, fila > 1000 jobs, taxa de erro > 5%). Saída pra email SMTP, Telegram (via bot), Discord webhook ou ntfy.sh.
Dashboard que ninguém olha não serve. Alerta no celular acorda você. Telegram via @BotFather é grátis, instantâneo e chega no celular sem precisar de SMS pago. Cuidado com alert fatigue: poucos alertas e acionáveis.
Severity (warning vs critical); for: 5m evita flapping; agrupamento (group_by); silenciamento durante manutenção; runbook URL em cada alerta; ntfy.sh self-hosted como alternativa zero-friction.
Sentry captura exceptions não tratadas do Node, agrupa por fingerprint, mostra stack trace, breadcrumbs, request body, user ID e dispara alerta. Tem SaaS com free tier de 5k eventos/mês e versão self-hosted (mais pesada).
Logs mostram que deu erro; Sentry mostra por que. Pra app real do MkBlogs, você quer saber: qual user, qual rede social, qual job da fila, qual versão do código (release tag), e ver a tendência (esse erro está crescendo?).
DSN; tracesSampleRate (performance) vs sampleRate (erros); beforeSend pra remover PII; release tracking + sourcemaps; integração com BullMQ (capturar falhas de job); GlitchTip como alternativa open-source.
Página em status.mkblogs.com mostrando estado de cada componente (API, painel, agendamento, OAuth) e histórico de incidentes. Open-source: Uptime Kuma, Statping-ng, Cachet. SaaS: Better Stack, Instatus.
Quando algo cai, status page reduz suporte em ~70%: usuário vê "X API está fora" e entende. Também serve de histórico público de SLA — clientes B2B pedem isso. Uptime Kuma sobe em 2 minutos com docker-compose e tem visual decente.
Componente vs incidente; postmortem público; comunicação durante incidente (4 estágios: investigando → identificado → monitorando → resolvido); status page fora da mesma VPS (senão cai junto); subscribe por email/RSS pra updates.
💾 Backup e segurança
Dump diário do Postgres, off-site em S3/R2 com restic, teste de restore mensal, rotação de tokens e plano de disaster recovery.
Cron job diário (03:00 UTC) que roda docker compose exec -T postgres pg_dump -Fc -U mkblogs mkblogs > /srv/backups/mkblogs-$(date +%F).dump. Formato -Fc (custom) é binário e comprimido, restaurável com pg_restore.
Backup é a única coisa que separa "incidente chato" de "fim do negócio". Snapshot da VPS no provider ajuda, mas dump lógico é o que permite restaurar uma tabela específica, migrar de versão de Postgres ou ir pra outro provedor.
Dump lógico (pg_dump) vs físico (pg_basebackup, snapshot LVM); -Fc custom vs -Fp plain SQL; rotação local (manter últimos 7 dias); flock pra evitar overlap; cron com MAILTO ou Healthchecks.io ping.
Mandar o dump pra fora da VPS. restic faz dedupe + criptografia + snapshots e fala com S3/R2/B2 nativamente. rclone é mais simples (rsync pra cloud). Cloudflare R2 tem 10GB grátis e zero egress fee — perfeito pra backup pequeno.
Backup que mora só na própria VPS é backup que some junto com a VPS (ransomware, datacenter pegando fogo, conta suspensa). Regra 3-2-1: 3 cópias, 2 mídias diferentes, 1 off-site. R2 + restic resolve isso por ~US$0/mês.
restic snapshots + forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12; chave de criptografia (guardada FORA da VPS!); S3-compatible (R2, B2, Wasabi, MinIO); egress fee como armadilha (AWS S3 cobra, R2/B2 não); restic check pra validar integridade.
Script que mensalmente baixa o último snapshot do R2, sobe um Postgres descartável em container, roda pg_restore, faz SELECT count(*) FROM users e compara com um número esperado. Falhou? Alerta no Telegram.
Backup não testado é Schrödinger backup — você não sabe se funciona até precisar. Histórias de empresas que perderam tudo porque o dump estava corrompido há 6 meses são abundantes. Restore mensal automatizado dá confiança real.
RTO (Recovery Time Objective — quanto tempo até voltar) vs RPO (Recovery Point Objective — quanto dado posso perder); restore em ambiente isolado; smoke test pós-restore (contagens, checksums); pg_restore --jobs pra paralelizar; documentar o procedimento (DR runbook).
Trocar regularmente: senha do banco, JWT secret, encryption key dos OAuth tokens, API keys de Sentry/UptimeRobot, chave SSH do deploy, password do admin do Postiz. Calendário trimestral; emergencial se um dev sai do time ou se houver suspeita.
Token vazado em log, em commit antigo do Git ou no laptop roubado de um dev continua válido até alguém revogar. Rotação periódica limita a janela de exposição. git history não esquece — tem que usar git-secrets, trufflehog e revogar.
Inventário de segredos (lista do que existe); rotação com overlap (chave antiga aceita por 24h); .env com permissão 600 e sem comitar; gerenciadores (Doppler, Infisical, 1Password CLI, sops + age); revogar tokens OAuth nas próprias plataformas (X, LinkedIn).
Cada componente recebe só a permissão mínima necessária: user do Postgres da app não tem SUPERUSER; container não roda como root (USER node no Dockerfile); chave R2 tem escopo de write-only num bucket específico; deploy SSH usa chave dedicada com command= restrito.
Quando (não se) um componente for comprometido, o estrago fica limitado. Container rodando como root + montando /var/run/docker.sock = comprometimento total do host. Chave R2 com s3:* = adversário apaga seus backups.
Defense in depth; CAP_DROP: ALL e read_only: true em containers que aguentam; network_mode: internal pra Postgres/Redis; IAM policy mínima em R2/S3; SSH PermitRootLogin no; sudoers granular.
Documento curto (1-2 páginas) que descreve passo a passo o que fazer quando "a VPS sumiu": (1) provisionar nova VPS, (2) rodar Ansible/script de bootstrap, (3) baixar último snapshot do R2, (4) pg_restore, (5) apontar DNS, (6) revogar tokens da máquina antiga. Tempo-alvo: < 2h.
Durante um desastre você tem adrenalina alta e cabeça ruim. Improvisar leva a erros que multiplicam o dano (rodar restore com --clean no banco errado, por exemplo). Runbook escrito com calma, em tempo seco, vira checklist em tempo de chuva.
Drill anual (fingir que tudo caiu e testar o runbook); contatos de emergência (registrador de domínio, provider, CDN); credenciais de fallback impressas e guardadas físicamente; pre-bootstrap automatizado (Ansible playbook ou Docker image custom da VPS); status page já preparado pra anunciar incidente.
🎉 Fim da jornada MkBlogs
Você passou por 6 trilhas: fundamentos, Postiz pronto, OAuth de redes, blogs, construção do zero e agora deploy em produção. A próxima publicação já pode ser sua — em escala.
Mais cursos em AutomationsAI →