⚡ AutomationsAI|Portal de Cursos →

Verificando acesso...

MÓDULO 6.1

🖥️ Servidor e Docker

Escolher uma VPS barata, endurecer o Ubuntu, instalar Docker e desenhar um docker-compose.prod.yml com volumes, restart policy e healthcheck — a base de qualquer deploy sério.

6
Tópicos
40
Minutos
Avançado
Nível
Prático
Tipo
1

💰 Escolhendo a VPS

Para um stack Postiz + Postgres + Redis + Nginx, o piso confortável é 2 vCPU / 4 GB RAM / 40 GB SSD. Abaixo disso, o build do Next e as filas do BullMQ derrubam o servidor sob carga moderada. A escolha campeã em custo-benefício é a Hetzner Cloud CX22 (Frankfurt/Helsinki) por ~€4/mês — rede de 1 Gbps, snapshots baratos, painel limpo.

Alternativas válidas: Contabo VPS S (mais RAM por euro, mas IO de disco oscila), OVH VPS Starter (DC em Beauharnois/Gravelines, bom para Brasil/Europa) e DigitalOcean Basic (mais caro, mas ecossistema maduro). Evite "VPS de R$ 9/mês" — overcommit cruel.

# Comparativo rápido (Mai/2026, valores aproximados)
# Provider     | CPU  | RAM  | Disco  | Tráfego | Preço/mês
# -------------|------|------|--------|---------|----------
# Hetzner CX22 | 2vC  | 4GB  | 40GB   | 20TB    | €4,15
# Contabo VPS S| 4vC  | 8GB  | 100GB  | 32TB    | €5,99
# OVH Starter  | 2vC  | 4GB  | 80GB   | ilim.   | €6,00
# DO Basic     | 2vC  | 4GB  | 80GB   | 4TB     | $24,00

# Antes de comprar, teste latência do seu lugar:
ping -c 5 nbg1-speed.hetzner.com   # Nuremberg
ping -c 5 ash-speed.contabo.com    # Ashburn
mtr --report --report-cycles 10 fra1-speed.hetzner.com

💡 Dica prática

Para usuário no Brasil, Hetzner Falkenstein/Helsinki dá ~210ms de RTT — ruim para painel, ótimo para cron job. Se latência interativa importa, OVH Beauharnois (~120ms) ou DigitalOcean São Paulo (~10ms, mas caro) ganham. Postiz roda agendamentos em background — latência alta no painel é tolerável.

Conceitos-chave

vCPU

Núcleo virtual; geralmente metade de um core físico.

Overcommit

Provider vende mais recurso do que tem fisicamente.

Snapshot

Imagem do disco para restore rápido.

Egress

Tráfego de saída — cota mensal varia muito.

2

🔒 Setup inicial do Ubuntu

Servidor recém-criado é uma porta aberta. Antes de instalar qualquer coisa, faça os cinco hardenings básicos: usuário não-root, chave SSH (senha desligada), firewall ufw, fail2ban contra brute force, e unattended-upgrades para patches automáticos de segurança.

# 1. Criar usuário deploy não-root
adduser deploy
usermod -aG sudo deploy

# 2. Copiar chave SSH do root para deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

# 3. No SEU laptop, gerar chave e adicionar (se ainda não tem)
ssh-keygen -t ed25519 -C "deploy@mkblogs"
ssh-copy-id deploy@SEU.IP.DO.SERVIDOR

# 4. Desligar senha SSH e login root em /etc/ssh/sshd_config
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl reload ssh

Com SSH endurecido, levante o firewall e os daemons de proteção. A ordem importa: libere SSH antes de habilitar UFW, senão você se tranca para fora.

# 5. Firewall: SSH + HTTP + HTTPS
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp     # SSH (ANTES de enable!)
sudo ufw allow 80/tcp     # HTTP (certbot ACME)
sudo ufw allow 443/tcp    # HTTPS
sudo ufw --force enable
sudo ufw status verbose

# 6. fail2ban contra brute force
sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.local > /dev/null <<EOF
[sshd]
enabled = true
port    = 22
maxretry = 3
bantime  = 1h
findtime = 10m
EOF
sudo systemctl enable --now fail2ban

# 7. Patches automáticos de segurança
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

⚠️ Dica prática

ANTES de desligar PasswordAuthentication, abra uma segunda sessão SSH e confirme que ela está autenticada por chave. Se a chave não funciona e você fecha a primeira sessão, fica sem acesso e precisa do console web do provider para recuperar.

Conceitos-chave

SSH key

Par público/privado; muito mais seguro que senha.

UFW

Frontend amigável do iptables/nftables.

fail2ban

Lê logs e banne IPs com tentativas falhas.

Hardening

Reduzir superfície de ataque do sistema.

3

🐳 Instalando Docker + Compose

O docker.io do apt do Ubuntu vem velho e sem o plugin Compose v2. Use o script oficial de get.docker.com, que adiciona o repositório certo da Docker Inc. e instala Engine + Compose v2 em uma tacada.

# 1. Baixar e rodar script oficial
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# 2. Adicionar usuário deploy ao grupo docker (sem sudo)
sudo usermod -aG docker deploy

# 3. Logout + login para o grupo aplicar (ou: newgrp docker)
exit
ssh deploy@SEU.IP.DO.SERVIDOR

# 4. Verificar instalação
docker --version
# Docker version 27.3.1, build ce12230

docker compose version
# Docker Compose version v2.29.7

# 5. Smoke test
docker run --rm hello-world
# Hello from Docker! ...

# 6. Habilitar auto-start no boot
sudo systemctl enable docker

🧠 Dica prática

Estar no grupo docker = ter root efetivo (qualquer um do grupo pode montar / dentro de um container). Restrinja o grupo a usuários de deploy e nunca adicione contas de serviço de terceiros nele.

Conceitos-chave

Docker Engine

Daemon dockerd + CLI docker.

Compose v2

Plugin oficial em Go, sub-comando docker compose.

Grupo docker

Acesso ao socket /var/run/docker.sock.

systemd

Gerencia o daemon e sobe Docker no boot.

4

📝 docker-compose.prod.yml

O Compose de produção difere do de dev em quatro pontos críticos: sem portas de DB expostas no host, restart: always, redes internas isoladas e limites de recursos. O Postiz fala com o Postgres apenas pela network interna; quem expõe ao mundo é o reverse proxy (próximo módulo).

# /home/deploy/postiz/docker-compose.prod.yml
services:
  postiz:
    image: ghcr.io/gitroomhq/postiz-app:latest
    container_name: postiz
    restart: always
    env_file: .env.prod
    expose:
      - "5000"            # interno, NÃO ports:
    volumes:
      - postiz-uploads:/uploads
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks: [postiz-net]
    deploy:
      resources:
        limits: { cpus: '1.5', memory: 2G }

  postgres:
    image: postgres:16-alpine
    container_name: postiz-postgres
    restart: always
    environment:
      POSTGRES_USER: postiz
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: postiz
    volumes:
      - postgres-data:/var/lib/postgresql/data
    # Sem ports: — Postgres só acessível via postiz-net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postiz -d postiz"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks: [postiz-net]

  redis:
    image: redis:7-alpine
    container_name: postiz-redis
    restart: always
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      retries: 5
    networks: [postiz-net]

volumes:
  postgres-data:
  postiz-uploads:
  redis-data:

networks:
  postiz-net:
    driver: bridge
    internal: false   # postiz precisa de internet p/ APIs

✓ Prod faz assim

  • expose em vez de ports para serviços internos.
  • restart: always para sobreviver a reboots.
  • Imagem com tag fixa (:v1.42.0) em prod, não :latest.
  • Resource limits para evitar OOM derrubar o host.

✗ Dev faz, prod NÃO

  • ports: "5432:5432" no Postgres (DB exposto na internet).
  • Bind mount em ./data com permissão errada.
  • Sem healthcheck — Postiz aborta antes do DB subir.
  • restart: no — primeiro crash derruba o serviço.

Conceitos-chave

expose

Porta visível só para outros containers da network.

ports

Publica porta no host — uso restrito ao proxy.

Resource limits

Teto de CPU/RAM evita um container matar o host.

Tag fixa

Deploy reproduzível — :latest é roleta.

5

💾 Volumes persistentes

Container é efêmero — se você der docker compose down, tudo que estava no FS do container some. Volumes ficam fora desse ciclo: vivem em /var/lib/docker/volumes/ e sobrevivem a destruir e recriar containers.

Há duas opções: named volumes (gerenciados pelo Docker, escolha padrão) e bind mounts (mapeiam um caminho do host). Para dados de DB sempre use named volume — bind mounts dão dor de cabeça com UID/permissão.

📂 Quando usar cada um

  • Named volume (postgres-data:/var/lib/postgresql/data): dados de banco, filas, uploads de usuários. Docker cuida das permissões.
  • Bind mount (./nginx.conf:/etc/nginx/nginx.conf:ro): arquivos de config que VOCÊ edita no host. Use modo :ro sempre que possível.
  • tmpfs (type: tmpfs): dados temporários sensíveis que devem morrer com o container.
  • Nunca: misturar bind mount em diretório de dados que o container escreve com UID diferente do dono do diretório no host.
# Listar volumes do projeto
docker volume ls --filter "name=postiz"

# Inspecionar um volume (onde mora no host)
docker volume inspect postiz_postgres-data | grep Mountpoint
# "Mountpoint": "/var/lib/docker/volumes/postiz_postgres-data/_data"

# Backup ad-hoc de um volume (tar comprimido com data)
docker run --rm \
  -v postiz_postgres-data:/source:ro \
  -v $(pwd)/backups:/backup \
  alpine tar czf /backup/pg-$(date +%F).tar.gz -C /source .

# Restore (CUIDADO: destrói o destino)
docker run --rm \
  -v postiz_postgres-data:/target \
  -v $(pwd)/backups:/backup \
  alpine sh -c "cd /target && tar xzf /backup/pg-2026-05-24.tar.gz"

# Paths críticos para backup em prod
# /var/lib/docker/volumes/postiz_postgres-data    -> banco
# /var/lib/docker/volumes/postiz_postiz-uploads   -> mídia dos posts
# /var/lib/docker/volumes/postiz_redis-data       -> filas pendentes
# /home/deploy/postiz/.env.prod                   -> secrets

🗄️ Dica prática

Backup que não foi restaurado em um dry run não é backup, é torcida. Crie uma VPS pequena uma vez por mês e teste o restore completo. Se em 30 minutos você não tem o Postiz no ar a partir só dos tar.gz, seu plano de DR é fantasia.

Conceitos-chave

Named volume

Docker gerencia, abstrai path e permissões.

Bind mount

Mapeia caminho do host direto no container.

Persistência

Dados que sobrevivem ao ciclo do container.

DR

Disaster recovery — restaurar do zero.

6

❤️ Restart policy e healthcheck

Em produção, dois mecanismos garantem que o stack se recupera sozinho: restart policy (Docker reinicia o container se ele cair) e healthcheck (Docker sabe se o processo dentro está realmente respondendo, não só "rodando").

Use restart: unless-stopped em quase tudo: reinicia em crash e no boot, mas respeita um stop manual seu. restart: always é mais agressivo (ignora stop manual após reboot) e útil só em componentes críticos como o reverse proxy.

# Healthcheck no Postiz com curl interno
services:
  postiz:
    image: ghcr.io/gitroomhq/postiz-app:latest
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
      interval: 30s        # frequência
      timeout: 10s         # cada check tem 10s p/ responder
      retries: 3           # 3 falhas seguidas = unhealthy
      start_period: 60s    # graça inicial: ignora falhas no boot
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

O par depends_on + condition: service_healthy é o segredo de um boot determinístico: o Postiz só inicia depois que pg_isready e redis-cli ping respondem OK. Sem isso, no primeiro up a app crasha tentando rodar migrations em um Postgres que ainda está fazendo initdb.

# Inspecionar saúde de cada serviço
docker compose ps
# NAME              STATUS                   PORTS
# postiz            Up 2h (healthy)          5000/tcp
# postiz-postgres   Up 2h (healthy)
# postiz-redis      Up 2h (healthy)

# Ver últimos resultados do healthcheck (JSON)
docker inspect postiz --format '{{json .State.Health}}' | jq

# Forçar reinício de um serviço
docker compose restart postiz

# Acompanhar reinícios automáticos (quantas vezes caiu)
docker inspect postiz --format '{{.RestartCount}}'

📈 Dica prática

Se RestartCount passar de 5 em uma hora, alguma coisa está crashando em loop — restart policy só esconde o sintoma. Vá nos logs (docker compose logs --tail=200 postiz) e descubra o erro real antes que o disco encha de logs do próprio container reiniciando.

Conceitos-chave

unless-stopped

Reinicia em crash, respeita stop manual.

always

Reinicia sempre, até depois de stop + reboot.

start_period

Graça inicial — evita falso negativo no boot.

service_healthy

Bloqueia boot até dependência estar saudável.

🎯 Resumo do Módulo

VPS escolhida — Hetzner CX22 (€4/mês, 2 vCPU/4 GB/40 GB) como baseline, alternativas Contabo e OVH mapeadas.
Ubuntu endurecido — usuário deploy, SSH só por chave, UFW liberando 22/80/443, fail2ban e unattended-upgrades ativos.
Docker + Compose instalados — via script oficial get.docker.com, usuário no grupo docker, auto-start no boot.
docker-compose.prod.yml pronto — Postgres/Redis sem ports, restart always, network postiz-net isolada e resource limits.
Volumes persistentes definidos — named volumes para DB/uploads/Redis, paths de backup documentados, restore testado.
Restart policy e healthcheckunless-stopped + depends_on: service_healthy garantem boot determinístico e auto-recovery.

Próximo Módulo:

6.2 — Reverse proxy, HTTPS e domínio em produção