Motivación
Quería un sitio personal que reflejara mi filosofía de trabajo: rápido, seguro, reproducible y automatizado. Nada de WordPress, nada de CMS pesados. Un sitio estático generado con Hugo, servido detrás de un reverse proxy hardened, desplegado con Docker Compose y listo para escalar.
Arquitectura
┌──────────────────────────────────────────────────┐
│ Internet │
└──────────────────────┬───────────────────────────┘
│
┌────────▼────────┐
│ Cloudflare DNS │
└────────┬────────┘
│
┌────────▼────────┐
│ Traefik v3 │
│ (Reverse Proxy)│
│ :80 → :443 │
└────────┬────────┘
│
┌────────▼────────┐
│ Hugo Server │
│ (PaperMod) │
│ :1313 │
└─────────────────┘
Componentes
| Componente | Tecnología | Función |
|---|---|---|
| Generador estático | Hugo + PaperMod | Genera el sitio HTML/CSS/JS |
| Reverse proxy | Traefik v3 | TLS termination, security headers, rate limiting |
| Certificados | Let’s Encrypt + DNS Challenge | TLS automático via Cloudflare |
| Contenedores | Docker Compose | Orquestación de servicios |
| DNS | Cloudflare | DNS management y caching |
Hugo + PaperMod
Elegí Hugo por su velocidad extrema y PaperMod por su diseño limpio y minimalista.
Características del sitio
- Bilingüe (ES/EN): Contenido completo en español e inglés con selector de idioma
- Tema oscuro/claro automático: Respeta la preferencia del sistema con toggle manual
- Búsqueda integrada: Índice JSON + Fuse.js para búsqueda client-side sin servidor
- SEO optimizado: OpenGraph, meta tags, sitemap.xml, robots.txt
- Sin JavaScript externo: Cero trackers, cero analytics invasivos
Estructura del contenido
content/
├── about.md / about.en.md # Sobre mí
├── search.md / search.en.md # Búsqueda
├── experience/
│ ├── index.md / index.en.md # Experiencia profesional
├── projects/
│ ├── index.md / index.en.md # Proyectos
└── posts/
├── _index.md / _index.en.md # Índice del blog
├── homelab-hashicorp-stack.md # Posts...
└── ...
Configuración multilingüe
Hugo permite definir idiomas de forma declarativa en hugo.yaml:
defaultContentLanguage: es
languages:
es:
languageName: "🇪🇸 Español"
weight: 1
en:
languageName: "🇬🇧 English"
weight: 2
Cada fichero de contenido tiene su variante .en.md para la versión en inglés. Hugo se encarga del routing automático (/en/about/, /en/posts/, etc.).
Seguridad con Traefik
La seguridad no es opcional. Configuré Traefik con un middleware chain que aplica:
Security Headers (OWASP)
secure-headers:
headers:
stsSeconds: 31536000 # HSTS 1 año
stsIncludeSubdomains: true
stsPreload: true
frameDeny: true # Anti-clickjacking
contentTypeNosniff: true # Anti MIME-sniffing
browserXssFilter: true # XSS protection
referrerPolicy: "strict-origin-when-cross-origin"
contentSecurityPolicy: "default-src 'self'; ..."
Rate Limiting
rate-limit:
rateLimit:
average: 100
burst: 200
period: 1s
TLS Hardened
tls:
options:
default:
minVersion: "VersionTLS12"
cipherSuites:
- "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
- "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
- "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"
- "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"
sniStrict: true
Cadena de Middlewares
Todos los middlewares se encadenan en un único security-chain:
security-chain:
chain:
middlewares:
- secure-headers
- rate-limit
- compress-response
- www-redirect
Despliegue con Docker Compose
El despliegue es un simple docker compose up -d:
services:
traefik:
image: "traefik:v3.7.7"
security_opt:
- no-new-privileges:true
# ... configuración de Traefik
web:
image: hugomods/hugo:exts-non-root-0.154.5
command:
- server
- "--bind=0.0.0.0"
- "--appendPort=false"
- "--baseURL=https://www.gbt55.es"
- "--disableLiveReload"
labels:
- "traefik.http.routers.web.rule=Host(`www.gbt55.es`) || Host(`gbt55.es`)"
- "traefik.http.routers.web.middlewares=security-chain@file"
Decisiones de diseño
- Hugo en modo server en lugar de build estático: permite hot-reload del contenido sin rebuild del contenedor
no-new-privileges: previene escalación de privilegios dentro del contenedor- Imagen
non-root: el proceso Hugo corre como usuario no-root - Red dedicada
proxy: aislamiento de red entre servicios - Let’s Encrypt con DNS Challenge: no necesita exponer puerto 80 para validación
Certificados TLS Automáticos
Los certificados se gestionan automáticamente con Let’s Encrypt usando el DNS challenge de Cloudflare:
- "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
Esto permite renovación automática sin downtime y sin necesidad de abrir puertos adicionales.
Hardening del VPS
El VPS que aloja el portfolio incluye un script de hardening (vps-harden.sh) que configura:
- Firewall (UFW) con reglas restrictivas
- SSH hardening (no root login, key-only auth)
- Fail2ban para protección contra brute force
- Automatic security updates
Resultado
El resultado es un sitio:
- ⚡ Ultra-rápido: Hugo genera páginas en <1ms, servidas con compresión gzip
- 🔒 Seguro: Headers OWASP, TLS 1.2+, rate limiting, CSP restrictivo
- 🐳 Reproducible: Un solo
docker compose up -ddespliega todo - 🌍 Bilingüe: Contenido completo en ES/EN
- 📱 Responsive: PaperMod es totalmente responsive out-of-the-box
- 🎨 Limpio: Sin ads, sin trackers, sin cookies
Stack completo
Hugo (SSG) + PaperMod (tema)
│
Docker Compose (orquestación)
│
Traefik v3 (reverse proxy + TLS + security)
│
Let's Encrypt (certificados automáticos)
│
Cloudflare (DNS + edge caching)
│
VPS hardened (Linux)
El código fuente de este portfolio está disponible en mi GitHub.