Cómo construí un hub minimalista en Hetzner para Docker, Traefik y Nanobot
Quería una infraestructura que pudiera explicar en cinco minutos y operar medio dormido.
Sin magia de plataformas gestionadas. Sin plano de control oculto. Solo:
- un VPS de Hetzner
- DNS y proxy de Cloudflare
- Traefik como edge router
- stacks de Docker Compose
- un sitio público de estado
- Nanobot detrás de HTTPS
- verificaciones de seguridad repetibles
Este es exactamente el playbook que uso ahora.
Qué está ejecutándose
- Host: Hetzner Cloud (
CAX11, Ubuntu24.04) - Edge: Cloudflare (DNS en modo proxied)
- Reverse proxy: Traefik (solo
:80y:443) - Public app:
<PUBLIC_APP_DOMAIN> - App de Nanobot:
<NANOBOT_DOMAIN> - Search sidecar:
ddg-mcp - Base de seguridad: endurecimiento de SSH +
fail2ban+ verificaciones CVE semanales
Arquitectura de un vistazo
flowchart TB
U[Navegador del usuario] --> CF[Proxy de Cloudflare]
CF --> T
subgraph H[VPS en Hetzner]
T[Traefik]
B[Stack público base]
N[Gateway de Nanobot]
D[ddg-mcp]
W[Vigilancia CVE semanal]
end
T --> B
T --> N
N --> D
N --> OAI[API de OpenAI]
W --> N
W --> D
1) Aprovisiona el servidor en Hetzner
Configuración que utilicé:
- Región:
Alemania / NBG1 - Imagen:
Ubuntu 24.04 - Tipo:
CAX11 - IPv4 pública: activada
- Firewall de entrada: TCP
22,80,443 - Firewall de salida: permitir todo
Añade tu clave pública SSH durante la creación.
2) Primer acceso y endurecimiento del host
Haz bootstrap con root una vez y después pasa a un usuario con sudo.
ssh -i <SSH_KEY_PATH> root@<SERVER_IP>
apt-get update
apt-get install -y fail2ban ufw unattended-upgrades ca-certificates curl gnupg
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
sed -i 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?KbdInteractiveAuthentication .*/KbdInteractiveAuthentication no/' /etc/ssh/sshd_config
systemctl restart ssh
systemctl enable --now fail2ban
Después:
- crea
<SSH_USER>con sudo - desactiva el login SSH de root
- usa solo autenticación por clave a partir de ahora
3) Mantén protección SSH con fail2ban
cat >/etc/fail2ban/jail.local <<'EOF2'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
backend = systemd
[sshd]
enabled = true
port = 22
logpath = %(sshd_log)s
EOF2
systemctl restart fail2ban
fail2ban-client status sshd
4) Instala Docker Engine + plugin de Compose
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
. /etc/os-release
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
${VERSION_CODENAME} stable" | tee /etc/apt/sources.list.d/docker.list >/dev/null
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
5) Configura DNS y TLS de Cloudflare
Apunta ambos dominios de apps a la IPv4 del VPS y mantén el proxy activado:
<PUBLIC_APP_DOMAIN>-> IPv4 del VPS (proxied)<NANOBOT_DOMAIN>-> IPv4 del VPS (proxied)
Modo SSL/TLS de Cloudflare: Full (strict).
Traefik gestiona los certificados con ACME DNS challenge usando un token de Cloudflare.
sequenceDiagram
participant Browser as Navegador
participant CF as Cloudflare
participant T as Traefik
participant App as Servicio destino
Browser->>CF: Petición HTTPS (cabecera Host)
CF->>T: HTTPS proxied
T->>T: Coincide router por labels host/path
T->>App: Reenvía al servicio
App-->>T: Respuesta
T-->>CF: Respuesta + cabeceras de seguridad
CF-->>Browser: Respuesta final
6) Despliega el stack público base
Desde el repo local al servidor:
tar -C hetzner/stacks -czf - basic-ok | ssh -i <SSH_KEY_PATH> <SSH_USER>@<SERVER_IP> 'sudo tar xzf - -C /opt/stacks'
ssh -i <SSH_KEY_PATH> <SSH_USER>@<SERVER_IP> 'sudo cp /opt/stacks/basic-ok/.env.example /opt/stacks/basic-ok/.env'
ssh -i <SSH_KEY_PATH> <SSH_USER>@<SERVER_IP> 'cd /opt/stacks/basic-ok && sudo docker compose up -d'
Qué te da esto:
- edge routing con Traefik
- una página pública mínima
- un patrón opcional de status API que puedes reutilizar en stacks posteriores
7) Despliega Nanobot + sidecar DuckDuckGo MCP
Yo despliego desde hetzner/stacks/nanobot-gateway:
nanobot-gateway: app principal (nanobot-ai)ddg-mcp: sidecar proveedor de búsqueda- Ruta Traefik:
<NANOBOT_DOMAIN>
tar -C hetzner/stacks -czf - nanobot-gateway | ssh -i <SSH_KEY_PATH> <SSH_USER>@<SERVER_IP> 'sudo tar xzf - -C /opt/stacks'
ssh -i <SSH_KEY_PATH> <SSH_USER>@<SERVER_IP> 'sudo cp /opt/stacks/nanobot-gateway/.env.example /opt/stacks/nanobot-gateway/.env'
ssh -i <SSH_KEY_PATH> <SSH_USER>@<SERVER_IP> 'cd /opt/stacks/nanobot-gateway && sudo docker compose up -d --build'
Health check rápido:
curl -I https://<NANOBOT_DOMAIN>/health
Si pruebas localmente en el VPS vía 127.0.0.1, incluye Host.
Sin eso, Traefik puede devolver 404 por diseño.
curl -sS -o /dev/null -w "%{http_code}\n" -H "Host: <NANOBOT_DOMAIN>" http://127.0.0.1/
flowchart LR
L[Prueba local desde VPS] --> H{"¿Host header configurada?"}
H -- No --> E[Router por defecto de Traefik: 404]
H -- Sí --> R[Coincide router <NANOBOT_DOMAIN>]
R --> S[Servicio Nanobot]
8) Endurece contenedores con production defaults
Estos patrones hicieron el sistema más seguro sin mucha complejidad:
- sin
ports:en servicios detrás de Traefik read_only: truepara sistemas de archivos en runtime cuando sea posiblecap_drop: [ALL]security_opt: [no-new-privileges:true]tmpfspara rutas temporales con escritura- mantener health endpoints internos fuera del public routing
flowchart TD
A[Baseline de endurecimiento de contenedores] --> B[Sin puertos publicados directamente]
A --> C[Rootfs en solo lectura]
A --> D[Eliminar capacidades Linux]
A --> E[no-new-privileges]
A --> F[tmpfs para /tmp]
A --> G[Rutas públicas solo para paths necesarios]
9) Chequeos de seguridad que sí ejecuto
Parchar y escanear
sudo apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y
sudo trivy image --scanners vuln --severity HIGH,CRITICAL nanobot-gateway:latest
sudo trivy image --scanners vuln --severity HIGH,CRITICAL ddg-mcp:latest
Chequeos de sanidad en runtime
docker ps
sudo fail2ban-client status sshd
10) Vigilancia CVE semanal automatizada con systemd
Añadí un script + timer que hace esto cada semana:
- reconstruir con actualización de base upstream (
--pull) - reiniciar stack
- ejecutar escaneos de Trivy
- registrar si
CVE-2026-0861sigue apareciendo
flowchart TD
T[systemd timer] --> S[nanobot_os_watch.sh]
S --> P[docker compose build --pull]
P --> U[docker compose up -d]
U --> V[Escaneos Trivy]
V --> J[journalctl + logs del servicio]
Instalación:
scp -i <SSH_KEY_PATH> hetzner/scripts/nanobot_os_watch.sh <SSH_USER>@<SERVER_IP>:/tmp/
scp -i <SSH_KEY_PATH> hetzner/systemd/nanobot-os-watch.service <SSH_USER>@<SERVER_IP>:/tmp/
scp -i <SSH_KEY_PATH> hetzner/systemd/nanobot-os-watch.timer <SSH_USER>@<SERVER_IP>:/tmp/
ssh -i <SSH_KEY_PATH> <SSH_USER>@<SERVER_IP> '
sudo install -m 0755 /tmp/nanobot_os_watch.sh /usr/local/bin/nanobot_os_watch.sh
sudo install -m 0644 /tmp/nanobot-os-watch.service /etc/systemd/system/nanobot-os-watch.service
sudo install -m 0644 /tmp/nanobot-os-watch.timer /etc/systemd/system/nanobot-os-watch.timer
sudo systemctl daemon-reload
sudo systemctl enable --now nanobot-os-watch.timer
'
11) Coste mínimo de esto
Si ya tienes dominio, el coste mensual base es sobre todo el VPS. Fecha de referencia de precios: 6 de marzo de 2026.
Base fija (mínimo)
- Hetzner
CAX11: desde 4,49 €/mes (sin IVA) en precios Alemania/Finlandia. - Cloudflare DNS + proxy en plan Free: $0/mes para este setup.
- Traefik, Docker,
fail2ban, Trivy (OSS) yddg-mcp: $0 de coste de licencia.
Así que el suelo práctico es el coste del servidor (más IVA si aplica), asumiendo que:
- ya tienes un dominio
- te mantienes dentro de las features free-tier usadas aquí
Costes variables
- Uso de API de OpenAI: pago por uso según tokens y modelo elegido.
- Renovación de dominio: solo si necesitas comprar o renovar uno.
- Extras opcionales: backups, snapshots, servidor más grande o add-ons de pago de Cloudflare.
pie title Forma del coste mensual mínimo
"VPS Hetzner (fijo)" : 90
"Todo lo demás (puede ser ~0 en baseline)" : 10
12) Checklist listo para publicar
Antes de compartir públicamente, paso esta lista rápida:
- reemplazar placeholders (
<SERVER_IP>,<SSH_USER>, dominios) - confirmar que ambos dominios devuelven
200por HTTPS - verificar estado del jail de
fail2ban - verificar que no haya valores sensibles de entorno en el artículo
- dejar solo comandos probados en una terminal limpia
Cierre
Este setup es intencionalmente aburrido.
Es barato, repetible y fácil de depurar porque cada pieza es visible: Cloudflare, labels de Traefik, Docker Compose y timers de systemd. Para mi caso de uso, ese tradeoff es exactamente el correcto.
También me da un camino limpio para crecer: puedo seguir añadiendo más aplicaciones detrás del mismo borde Traefik + Cloudflare sin rediseñar toda la plataforma.
Y lo mejor: para decisiones de Arquitectura y DevOps, tengo a gpt-5.3-codex como copiloto.