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
- Pré-requisitos e topologia
- Criar a VM Debian 13 no Proxmox (GUI) — passo a passo
- Instalar o Debian 13 (Trixie) — rede estática e QEMU Guest Agent
- OPNsense — regras mínimas (ICMP na DMZ) e Port Forward 80/443 (hairpin NAT: observação)
- Instalar e preparar o Nginx (porta 8080) — dois vhosts e logs separados desde o início
- Instalar o Caddy (repositório oficial do Debian) e configurar proxy reverso + TLS automático
- Nginx — IP real do cliente e cabeçalhos de segurança (centralizado em nginx.conf)
- Estrutura do /analytics/ por domínio (Basic Auth) e geração inicial com GoAccess
- Script /usr/local/bin/update-goaccess.sh
- Cron em /etc/cron.d — teste (*/2) e agendamento final (03:17)
- 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.
- Proxmox WebUI → clique no nó (ex.: pve) → Create VM.
- General
Node: (o seu)
VM ID: escolha um ID livre (ex.: 1010)
Name: web_dmz
Clique Next. - 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. - System
Machine: q35
BIOS: OVMF (UEFI) + marque Add EFI Disk
SCSI Controller: VirtIO SCSI single
(Graphic card: Default)
Next. - 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. - CPU
Sockets: 1 Cores: 2
Type: host
Next. - Memory
Memory (MiB): 2048
Ballooning: opcional (pode manter)
Next. - 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. - Confirm
(Opcional) marque Start after created → Finish.
3) Instalar o Debian 13 — rede estática, SSH e QEMU Guest Agent
- Inicie a VM, entre no instalador (graphical install ou install).
- Defina linguagem, layout de teclado e hostname
web_dmz
. Domínio:dmz.home.arpa
(exemplo). - 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
- Particionamento: Guided — use entire disk and set up LVM → All files in one partition → confirme escrever no disco.
- Mirror: país Brasil (ou próximo), mirror
deb.debian.org
; HTTP proxy: vazio. - Pacotes: deixe apenas standard system utilities e SSH server marcados.
- GRUB: instale no disco inteiro (ex.:
/dev/sda
). - 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.
- 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
- 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).
- 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.
- 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!”
- 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
- 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)
- Instale a partir do repositório oficial do Debian (opção que usei):
sudo apt update
sudo apt -y install caddy
- 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
- 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
- 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.