Manual completo – Caddy + Nginx + GoAccess

Este manual consolida, organiza e detalha tudo o que foi feito no meu homelab para hospedagem de sites: criação da VM Debian no Proxmox, configuração de rede, instalação e configuração do Nginx (com vhosts desde o princípio na porta 8080 e logs separados por domínio), Caddy como proxy reverso (TLS automático), GoAccess e cron em /etc/cron.d. Os exemplos usam os domínios genéricos site1.com.br e site2.com.br para facilitar a adaptação.


Índice

  1. Pré-requisitos e topologia
  2. Criar a VM Debian 13 no Proxmox (GUI) — passo a passo
  3. Instalar o Debian 13 (Trixie) — rede estática e QEMU Guest Agent
  4. OPNsense — regras mínimas (ICMP na DMZ) e Port Forward 80/443 (hairpin NAT: observação)
  5. Instalar e preparar o Nginx (porta 8080) — dois vhosts e logs separados desde o início
  6. Instalar o Caddy (repositório oficial do Debian) e configurar proxy reverso + TLS automático
  7. Nginx — IP real do cliente e cabeçalhos de segurança (centralizado em nginx.conf)
  8. Estrutura do /analytics/ por domínio (Basic Auth) e geração inicial com GoAccess
  9. Script /usr/local/bin/update-goaccess.sh
  10. Cron em /etc/cron.d — teste (*/2) e agendamento final (03:17)
  11. Validações úteis e troubleshooting

1) Pré-requisitos e topologia

Meu homelab possui um firewall OPNsense que controla todo o tráfego da rede e separa os diversos dispositivos conectados em VLANs específicas de acordo com cada finalidade. Também possui interfaces de rede dedicadas para cada lan, mas a solução aqui apresentada pode ser implementada em qualquer pc com acesso a internet. Recomendo a configuração de um domínio em algum serviço DDNS, como o Dynu, para direcionar as requisições para o ip fornecido por sua operadora.

Aqui eu tenho a seguinte configuração:

  • Proxmox VE instalado e acessível.
  • Bridge da DMZ (ex.: vmbr4) conectada ao firewall (OPNsense).
  • OPNsense com WAN pública, e possibilidade de criar NAT 80/443 → VM.
  • Domínios públicos (site1.com.br e site2.com.br) apontando para o IP WAN.

2) Criar a VM Debian 13 no Proxmox (GUI)

Objetivo desta seção: criar a VM web_dmz com chipset q35, BIOS OVMF (UEFI) e rede em vmbr4, utilizando a ISO debian-13.1.0-amd64-netinst.iso. Explico rapidamente as escolhas:

  • Debian: Uso Debian há mais 20 anos, é um sistema leve, seguro e fácil de administrar.
  • q35 vs i440fx: q35 emula chipset moderno com PCIe nativo, melhor para SOs atuais e passthrough.
  • OVMF (UEFI) vs SeaBIOS: UEFI permite GPT e secure boot (quando aplicável); é a opção ideal para sistemas atuais.
  1. Proxmox WebUI → clique no nó (ex.: pve) → Create VM.
  2. General
    Node: (o seu)
    VM ID: escolha um ID livre (ex.: 1010)
    Name: web_dmz
    Clique Next.
  3. OS
    Storage: local ou nfs-iso (ou onde estiver sua ISO)
    ISO image: debian-13.1.0-amd64-netinst.iso (ou a iso com o sistema/versão que você tiver disponível)
    Guest OS: Type Linux, Version 6.x/5.x kernel.
    Next.
  4. System
    Machine: q35
    BIOS: OVMF (UEFI) + marque Add EFI Disk
    SCSI Controller: VirtIO SCSI single
    (Graphic card: Default)
    Next.
  5. Hard Disk
    Bus/Device: SCSI
    Storage: local-lvm (ou o seu)
    Disk size: 20 GiB (ou mais, 20 GiB é o mínimo recomendado)
    Discard: habilitado (TRIM)
    SSD emulation: habilitado
    Next.
  6. CPU
    Sockets: 1   Cores: 2
    Type: host
    Next.
  7. Memory
    Memory (MiB): 2048
    Ballooning: opcional (pode manter)
    Next.
  8. Network
    Bridge: No meu caso vmbr4 (DMZ). Escolha a interface de rede que você quiser usar ou tiver disponível
    Model: VirtIO (paravirtualized)
    (Firewall: desmarcado; limites: em branco)
    Next.
  9. Confirm
    (Opcional) marque Start after createdFinish.

3) Instalar o Debian 13 — rede estática, SSH e QEMU Guest Agent

  1. Inicie a VM, entre no instalador (graphical install ou install).
  2. Defina linguagem, layout de teclado e hostname web_dmz. Domínio: dmz.home.arpa (exemplo).
  3. Rede manual (estática):
    Address:   10.10.10.10
    Netmask:   255.255.255.0
    Gateway:   10.10.10.1
    DNS:       1.1.1.1 8.8.8.8
    
  4. Particionamento: Guided — use entire disk and set up LVMAll files in one partition → confirme escrever no disco.
  5. Mirror: país Brasil (ou próximo), mirror deb.debian.org; HTTP proxy: vazio.
  6. Pacotes: deixe apenas standard system utilities e SSH server marcados.
  7. GRUB: instale no disco inteiro (ex.: /dev/sda).
  8. Reinicie. Faça login (console) e confirme rede:
ip -4 a
ping -c3 10.10.10.1
ping -c3 1.1.1.1

Habilitar QEMU Guest Agent (host + VM):

# Proxmox GUI → VM → Options → QEMU Guest Agent → Enable
# (opcional) Freeze/thaw on backup → Enable

Reinicie a VM (GUI: VM → Shutdown → Reboot; ou sudo reboot dentro da VM).

# Dentro da VM
sudo apt update
sudo apt install -y qemu-guest-agent
sudo systemctl start qemu-guest-agent
systemctl status qemu-guest-agent --no-pager

Esperado: Active: active (running). No Proxmox (GUI): na aba Summary da VM, verifique se: o campo QEMU Guest Agent aparece como running e o IP 10.10.10.10 é mostrado.


4) OPNsense — regras mínimas e Port Forward 80/443

Por padrão, o OPNsense não permite requisições ICMP, assim tive que criar uma regra específica para verificar se a rede estava funcionando. Se esse não for o seu caso é só pular essa parte.

  1. Permitir ICMP (ping) da DMZ para o firewall:

    Firewall → Rules → DMZ → Add:

    • Action: Pass   Protocol: ICMP
    • Source: DMZ net   Destination: This Firewall
    • ICMP type: Echo request   Description: DMZ → FW: allow ping
  2. Port Forward (WAN → 10.10.10.10):

    Criar duas regras em Firewall → NAT → Port Forward (com “Add associated filter rule”). Crie a regra de port forward que se aplique ao seu caso específico.

    # Regra 1 — HTTP
    WAN TCP  dport:80   →  10.10.10.10:80   (NAT reflection: Disable)
    
    # Regra 2 — HTTPS
    WAN TCP  dport:443  →  10.10.10.10:443  (NAT reflection: Disable)
    

    Observação (hairpin NAT): testes a partir da própria LAN para o FQDN público podem depender de NAT reflection; quando em dúvida, teste na VM com -H "Host: ..." ou de fora da LAN.


5) Configurar Nginx para dois vhosts (porta 8080, logs separados)

Se você tiver apenas um domínio, use as configurações de site1 e ignore as partes relacionadas a site2. Se você tiver mais domínios é só acrescentar os vhosts necessários (site3, site4 etc).

  1. Instale utilitários básicos e Nginx:
sudo apt update
sudo apt -y install curl wget vim htop lsof unzip tar gnupg2 ca-certificates dnsutils net-tools git
sudo apt -y install nginx

Quando terminar a instalação, teste com:

curl -I http://127.0.0.1/

Se aparecer HTTP/1.1 200 OK, o Nginx está funcionando e servindo a página default.

  1. Default server apenas como fallback (8080):
sudo vim /etc/nginx/sites-available/default
server {
  listen 8080 default_server;
  listen [::]:8080 default_server;
  return 404;
}
sudo ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default

⚠️ Importante: sempre teste a configuração antes de recarregar o serviço.

sudo nginx -t && sudo systemctl reload nginx

Na VM:

Criar/atualizar a página padrão

echo 'Hello world!' | sudo tee /var/www/html/index.html

Teste localmente com:

curl -s http://127.0.0.1:8080/ | cat

Resultado esperado: Imprimir “Hello world!”

  1. Criar pastas dos sites (conteúdo estático de exemplo) e logs por vhost:
sudo mkdir -p /var/www/site1 /var/www/site2
echo "<h1>Site 1</h1>" | sudo tee /var/www/site1/index.html >/dev/null
echo "<h1>Site 2</h1>" | sudo tee /var/www/site2/index.html >/dev/null
  1. Dois vhosts (ambos na 8080) com logs separados e bloco /analytics/ já previsto:
sudo vim /etc/nginx/sites-available/site1.com.br
server {
  listen 8080;
  server_name site1.com.br;

  access_log /var/log/nginx/site1.com.br.access.log main_ext;
  error_log  /var/log/nginx/site1.com.br.error.log;

  root /var/www/site1;
  index index.html;

  location / {
    try_files $uri $uri/ =404;
  }

  # reservado para os relatórios deste domínio
  location /analytics/ {
    alias /var/www/analytics/site1/;
    autoindex off;
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/.htpasswd;
    add_header Cache-Control "private, no-store" always;
  }
}
sudo vim /etc/nginx/sites-available/site2.com.br
server {
  listen 8080;
  server_name site2.com.br;

  access_log /var/log/nginx/site2.com.br.access.log main_ext;
  error_log  /var/log/nginx/site2.com.br.error.log;

  root /var/www/site2;
  index index.html;

  location / {
    try_files $uri $uri/ =404;
  }

  # reservado para os relatórios deste domínio
  location /analytics/ {
    alias /var/www/analytics/site2/;
    autoindex off;
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/.htpasswd;
    add_header Cache-Control "private, no-store" always;
  }
}
sudo ln -s /etc/nginx/sites-available/site1.com.br /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/site2.com.br /etc/nginx/sites-enabled/

⚠️ Importante: sempre teste a configuração antes de recarregar o serviço.

sudo nginx -t && sudo systemctl reload nginx

6) Configurar Caddy (TLS automático + proxy reverso)

  1. Instale a partir do repositório oficial do Debian (opção que usei):
sudo apt update
sudo apt -y install caddy
  1. Configure o Caddyfile com dois hostnames apontando para o Nginx na 8080:
sudo vim /etc/caddy/Caddyfile
site1.com.br, site2.com.br {
  # (opcional) email para ACME/Let's Encrypt
  # email usuario@site1.com.br

  reverse_proxy 127.0.0.1:8080 {
    header_up Host {host}
    header_up X-Forwarded-For {remote}
    header_up X-Forwarded-Proto {scheme}
  }
}
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo journalctl -u caddy -f    # acompanhar emissão de certificados

Dica (testes internos sem depender de hairpin):

curl -kI https://127.0.0.1 -H "Host: site1.com.br"
curl -kI https://127.0.0.1 -H "Host: site2.com.br"

7) Centralizar IP real do cliente e cabeçalhos de segurança no nginx.conf

Preferi manter tudo centralizado em /etc/nginx/nginx.conf (evita .conf soltos). Edite o arquivo para que ele fique como o exemplo abaixo.

sudo vim /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
worker_cpu_affinity auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
        # multi_accept on;
}

http {

        ##
        # Basic Settings
        ##

        sendfile on;
        tcp_nopush on;
        types_hash_max_size 2048;
        server_tokens off; # Recommended practice is to turn this off

        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ##
        # SSL Settings
        ##

        ssl_protocols TLSv1.2 TLSv1.3; # Dropping SSLv3 (POODLE), TLS 1.0, 1.1
        ssl_prefer_server_ciphers off; # Don't force server cipher order.

        ##
        # Confia no IP repassado pelo Caddy (proxy local)
        ##

        real_ip_header X-Forwarded-For;
        set_real_ip_from 127.0.0.1;
        real_ip_recursive on;

        ##
        # Security headers (globais
        ##

        add_header X-Content-Type-Options "nosniff" always;
        add_header X-Frame-Option "SAMEORIGIN" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
        add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

        ##
        # Logging Settings
        ##

        log_format main_ext '$remote_addr - $remote_user [$time_local] '
                   '"$request" $status $body_bytes_sent '
                   '"$http_referer" "$http_user_agent" "$host"';

        access_log /var/log/nginx/access.log main_ext;

        # access_log /var/log/nginx/access.log;

        ##
        # Gzip Settings
        ##

        gzip on;

        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}

⚠️ Importante: sempre teste a configuração antes de recarregar o serviço.

sudo nginx -t && sudo systemctl reload nginx

8) Configurar /analytics/, autenticação e GoAccess inicial

  1. Crie a estrutura de saída dos relatórios e o usuário de Basic Auth:

sudo mkdir -p /var/www/analytics/{site1,site2}/7d /var/www/analytics/{site1,site2}/30d
sudo apt -y install apache2-utils

# Observação: a instalação do GoAccess/htpasswd pode trazer o serviço Apache. # Para evitar conflito de portas (80/443) e garantir que o Caddy ficará na frente, # pare e desabilite o Apache (se estiver presente):


sudo systemctl stop apache2 || true
sudo systemctl disable apache2 || true
sudo systemctl mask apache2 || true

sudo htpasswd -c /etc/nginx/.htpasswd usuario
  1. Instale o GoAccess e gere um relatório inicial para cada domínio (usará os logs separados):
sudo apt -y install goaccess

# (Reforço) garanta que o Apache não está ativo:


sudo systemctl is-active apache2 && echo "Apache ativo — desabilitando..." || true
sudo systemctl stop apache2 || true
sudo systemctl disable apache2 || true
sudo systemctl mask apache2 || true

sudo goaccess /var/log/nginx/site1.com.br.access.log   --log-format=COMBINED -o /var/www/analytics/site1/index.html --html

sudo goaccess /var/log/nginx/site2.com.br.access.log   --log-format=COMBINED -o /var/www/analytics/site2/index.html --html

Abra https://site1.com.br/analytics/ (e https://site2.com.br) para conferir.


Importante: os vhosts devem usar explicitamente o formato main_ext definido no nginx.conf para que o GoAccess consiga interpretar os campos extras (host e forwarded_for).

9) Script /usr/local/bin/update-goaccess.sh

Este script concatena os logs gerados pelo nginx, filtra pelo período de tempo escolhido e deixa tudo pronto para o GoAccess, gerando relatórios com a totalidade dos logs, com os últimos 30 dias e com os últimos 7 dias (script atualizado em 02/10/2025).

Tecle enter após cada bloco, ou, se você preferir, apenas crie um arquivo de texto novo com seu editor favorito, usando, por exemplo, sudo vim /usr/local/bin/update-goaccess.sh, cole o script, salve e saia. Depois não esqueça de tornar o arquivo executável.

sudo tee /usr/local/bin/update-goaccess.sh >/dev/null <<'SH'
#!/usr/bin/env bash

# ==============================================================================
# update-goaccess.sh — Gera relatórios All / 30d / 7d a partir dos logs do Nginx
# Robusto para cron/systemd: lock, locale fixo, filtros tolerantes a “sem dados”.
# ==============================================================================

set -euo pipefail
umask 022
export LC_ALL=C.UTF-8
source /etc/profile || true

# ---- Lock anti-concorrência ---------------------------------------------------
LOCK=/run/update-goaccess.lock
exec 9>"$LOCK"
flock -n 9 || { echo "Já existe uma execução em andamento ($(date))."; exit 0; }

# ---- Binário requerido --------------------------------------------------------
command -v goaccess >/dev/null 2>&1 || { echo "goaccess não encontrado no PATH"; exit 1; }

# ---- Formato compatível com seu nginx 'main_ext' ------------------------------
# nginx: '$remote_addr - $remote_user [$time_local]' '"$request" $status $body_bytes_sent' \
#        '"$http_referer" "$http_user_agent" "$host"';
# goaccess:
LOGFMT='%h - %e [%d:%t %^] "%r" %s %b "%R" "%u" "%v"'

# ---- Opções default do GoAccess ----------------------------------------------
GOA_OPTS=(
  --no-global-config
  --log-format="$LOGFMT"
  --date-format='%d/%b/%Y'
  --time-format='%H:%M:%S'
  --ignore-crawlers
  --hl-header
  --unknowns-log=/var/log/goaccess-unknowns.log
)

UNKNOWN=/var/log/goaccess-unknowns.log
# rotação simples do unknowns (50MB)
if [ -f "$UNKNOWN" ] && [ "$(stat -c%s "$UNKNOWN")" -gt $((50*1024*1024)) ]; then
  mv "$UNKNOWN" "${UNKNOWN}.$(date +%F_%H%M%S)" || true
fi

# ---- helpers ------------------------------------------------------------------
generate_date_pattern() {  # n dias -> alternância p/ grep
  local days="$1"
  for i in $(seq 0 $((days - 1))); do
    date -d "$i days ago" '+%d/%b/%Y'
  done | paste -sd '|'
}

write_empty() {  # write_empty caminho título
  local out="$1" title="$2"
  mkdir -p "$(dirname "$out")"
  cat >"$out" <<EOF
<!doctype html><meta charset="utf-8">
<title>$title</title>
<style>body{font:16px/1.4 system-ui,-apple-system,Segoe UI,Roboto} .box{max-width:640px;margin:10vh auto;padding:24px;border:1px solid #ddd;border-radius:12px}</style>
<div class="box">
  <h1>$title</h1>
  <p>Sem dados no período.</p>
  <p><small>Gerado em $(date "+%F %T %z")</small></p>
</div>
EOF
}

# ---- núcleo: gera relatórios para um log -------------------------------------
gen_reports() {
  local log="$1" out="$2"
  echo "==> Iniciando geração de relatórios para $log em $(date)"

  mkdir -p "$out" "$out/30d" "$out/7d"

  local tmpfile; tmpfile=$(mktemp)
  trap 'rm -f "${tmpfile:-}"' RETURN

  local log_dir log_basename
  log_dir=$(dirname "$log")
  log_basename=$(basename "$log")

  echo "--> Coletando dados de log..."
  # rotacionados .gz
  find "$log_dir" -type f -name "$log_basename.*.gz" -print0 \
    | xargs -0 -r zcat -f >> "$tmpfile" 2>/dev/null
  # rotacionados não-gz (.1, .2, …)
  find "$log_dir" -type f -name "$log_basename.[0-9]*" ! -name "*.gz" -exec cat {} + >> "$tmpfile"
  # atual
  [ -f "$log" ] && cat "$log" >> "$tmpfile"

  if [ ! -s "$tmpfile" ]; then
    echo "--> ERRO: nenhum dado coletado; abortando site."
    return 0
  fi
  echo "--> Dados coletados: $(du -h "$tmpfile" | awk '{print $1}')"

  # 1) All
  echo "--> Gerando 'All'..."
  goaccess "$tmpfile" "${GOA_OPTS[@]}" --output="$out/index.html"

  # 2) 30d
  echo "--> Gerando '30d'..."
  local pattern30d rc
  pattern30d=$(generate_date_pattern 30)
  set +e
  grep -E "\[(${pattern30d}):" "$tmpfile" \
    | goaccess "${GOA_OPTS[@]}" --output="$out/30d/index.html"
  rc=$?; set -e
  [ $rc -ne 0 ] && write_empty "$out/30d/index.html" "Relatório 30 dias"

  # 3) 7d
  echo "--> Gerando '7d'..."
  local pattern7d
  pattern7d=$(generate_date_pattern 7)
  set +e
  grep -E "\[(${pattern7d}):" "$tmpfile" \
    | goaccess "${GOA_OPTS[@]}" --output="$out/7d/index.html"
  rc=$?; set -e
  [ $rc -ne 0 ] && write_empty "$out/7d/index.html" "Relatório 7 dias"

  echo "--> Ajustando permissões..."
  chown -R www-data:www-data "$out"

  echo "==> OK: relatórios prontos para $log em $(date)"
  echo "----------------------------------------------------"
}

# ---- sites --------------------------------------------------------------------
main() {
  declare -A SITES=(
    [site1]="/var/www/analytics/site1"
    [site2]="/var/www/analytics/site2"
  )
  declare -A LOGS=(
    [site1]="/var/log/nginx/site1.com.access.log"
    [site2]="/var/log/nginx/site2.com.access.log"
  )

  for site in "${!SITES[@]}"; do
    [ -n "${LOGS[$site]:-}" ] && [ -n "${SITES[$site]:-}" ] || { echo "Config inválida: $site"; continue; }
    gen_reports "${LOGS[$site]}" "${SITES[$site]}"
  done
}

main "$@"
SH

Torna o script executável:

sudo chmod +x /usr/local/bin/update-goaccess.sh

Execute o script

sudo /usr/local/bin/update-goaccess.sh

10) Configurar cron

Faça primeiro um teste, gerando logs a cada 2 minutos para ver se está tudo funcionando. Depois você pode deixar o cron para gerar os logs uma vez por dia, como eu escolhi fazer, ou gerar a cada hora, a cada semana etc.

Crie o arquivo de cron, primeiro no modo de teste, a cada 2 minutos:

sudo tee /etc/cron.d/update-goaccess >/dev/null <<'CRON'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

*/2 * * * * root /usr/local/bin/update-goaccess.sh >> /var/log/update-goaccess.log 2>&1
CRON
sudo systemctl restart cron
# aguarde alguns minutos e verifique:
tail -n 100 /var/log/update-goaccess.log

Depois de validar, troque para a execução diária às 03:17 (use o horário que você preferir):

sudo tee /etc/cron.d/update-goaccess >/dev/null <<'CRON'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

17 3 * * * root /usr/local/bin/update-goaccess.sh >> /var/log/update-goaccess.log 2>&1
CRON
sudo systemctl restart cron

11) Testes e troubleshooting

  • Caddy na frente: portas 80/443 devem estar livres para o Caddy (Nginx fica apenas na 8080).
  • Logs úteis:
    journalctl -u caddy -e
    sudo tail -f /var/log/nginx/site1.com.br.access.log /var/log/nginx/site1.com.br.error.log
    sudo tail -f /var/log/nginx/site2.com.br.access.log /var/log/nginx/site2.com.br.error.log
    
  • Forçar Host localmente (evita hairpin NAT):
    curl -I http://127.0.0.1:8080 -H "Host: site1.com.br"
    curl -kI https://127.0.0.1       -H "Host: site2.com.br"
    
  • Permissões dos relatórios:
    sudo chown -R www-data:www-data /var/www/analytics
    sudo chmod -R a+rX /var/www/analytics
    

Espero que este guia seja útil de alguma forma.

Deixe um comentário

O seu endereço de email não será publicado Campos obrigatórios são marcados *

Rolar para cima