Motivation

I wanted a personal site that reflects my work philosophy: fast, secure, reproducible, and automated. No WordPress, no heavy CMS. A static site generated with Hugo, served behind a hardened reverse proxy, deployed with Docker Compose, and ready to scale.

Architecture

┌──────────────────────────────────────────────────┐
│                   Internet                        │
└──────────────────────┬───────────────────────────┘
                       │
              ┌────────▼────────┐
              │  Cloudflare DNS │
              └────────┬────────┘
                       │
              ┌────────▼────────┐
              │   Traefik v3    │
              │  (Reverse Proxy)│
              │  :80 → :443    │
              └────────┬────────┘
                       │
              ┌────────▼────────┐
              │  Hugo Server    │
              │  (PaperMod)     │
              │    :1313        │
              └─────────────────┘

Components

Component Technology Purpose
Static generator Hugo + PaperMod Generates HTML/CSS/JS site
Reverse proxy Traefik v3 TLS termination, security headers, rate limiting
Certificates Let’s Encrypt + DNS Challenge Automatic TLS via Cloudflare
Containers Docker Compose Service orchestration
DNS Cloudflare DNS management and caching

Hugo + PaperMod

I chose Hugo for its extreme speed and PaperMod for its clean, minimalist design.

Site Features

  • Bilingual (ES/EN): Full content in Spanish and English with language switcher
  • Auto dark/light theme: Respects system preference with manual toggle
  • Built-in search: JSON index + Fuse.js for client-side search without a server
  • SEO optimized: OpenGraph, meta tags, sitemap.xml, robots.txt
  • No external JavaScript: Zero trackers, zero invasive analytics

Content Structure

content/
├── about.md / about.en.md          # About me
├── search.md / search.en.md        # Search
├── experience/
│   ├── index.md / index.en.md      # Professional experience
├── projects/
│   ├── index.md / index.en.md      # Projects
└── posts/
    ├── _index.md / _index.en.md    # Blog index
    ├── homelab-hashicorp-stack.md   # Posts...
    └── ...

Multilingual Configuration

Hugo allows declarative language definition in hugo.yaml:

defaultContentLanguage: es

languages:
  es:
    languageName: "🇪🇸 Español"
    weight: 1
  en:
    languageName: "🇬🇧 English"
    weight: 2

Each content file has its .en.md variant for the English version. Hugo handles automatic routing (/en/about/, /en/posts/, etc.).

Security with Traefik

Security is not optional. I configured Traefik with a middleware chain that enforces:

Security Headers (OWASP)

secure-headers:
  headers:
    stsSeconds: 31536000          # HSTS 1 year
    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

Hardened TLS

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

Middleware Chain

All middlewares are chained into a single security-chain:

security-chain:
  chain:
    middlewares:
      - secure-headers
      - rate-limit
      - compress-response
      - www-redirect

Deployment with Docker Compose

Deployment is a simple docker compose up -d:

services:
  traefik:
    image: "traefik:v3.7.7"
    security_opt:
      - no-new-privileges:true
    # ... Traefik configuration

  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"

Design Decisions

  1. Hugo in server mode instead of static build: allows content hot-reload without container rebuild
  2. no-new-privileges: prevents privilege escalation inside the container
  3. non-root image: Hugo process runs as non-root user
  4. Dedicated proxy network: network isolation between services
  5. Let’s Encrypt with DNS Challenge: no need to expose port 80 for validation

Automatic TLS Certificates

Certificates are managed automatically with Let’s Encrypt using Cloudflare’s DNS challenge:

- "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"

This enables automatic renewal without downtime and without opening additional ports.

VPS Hardening

The VPS hosting the portfolio includes a hardening script (vps-harden.sh) that configures:

  • Firewall (UFW) with restrictive rules
  • SSH hardening (no root login, key-only auth)
  • Fail2ban for brute force protection
  • Automatic security updates

Result

The result is a site that is:

  • ⚡ Ultra-fast: Hugo generates pages in <1ms, served with gzip compression
  • 🔒 Secure: OWASP headers, TLS 1.2+, rate limiting, restrictive CSP
  • 🐳 Reproducible: A single docker compose up -d deploys everything
  • 🌍 Bilingual: Full content in ES/EN
  • 📱 Responsive: PaperMod is fully responsive out-of-the-box
  • 🎨 Clean: No ads, no trackers, no cookies

Full Stack

Hugo (SSG) + PaperMod (theme)
     │
Docker Compose (orchestration)
     │
Traefik v3 (reverse proxy + TLS + security)
     │
Let's Encrypt (automatic certificates)
     │
Cloudflare (DNS + edge caching)
     │
Hardened VPS (Linux)

The source code for this portfolio is available on my GitHub.