I'm documenting my setup of Hugo and Docker on a cloud machine I recently rented. This guide covers the complete stack: Hugo static site generator, Docker containerization, and Caddy reverse proxy with Let's Encrypt HTTPS. The steps are:

  • Container Service Setup — Define Hugo in docker-compose with klakegg/hugo image. Mount source directory at /src and logs at /tmp/logs/. Expose port 3000 for local access.

  • Port Mapping (Host:Container) — Map 127.0.0.1:3000:3000 exposes container port 3000 on host localhost. First port is host port, second is container port. Restrict to localhost (127.0.0.1) for security. Access at http://127.0.0.1:3000 from host machine.

  • File Mapping (Volumes) — Mount ./hugo:/src maps entire Hugo project from host to container. Container reads source files from /src. Changes on host are immediately visible in container. Mount ./data/hugo/logs/:/tmp/logs/ persists logs on host filesystem. Volumes enable live editing without rebuilding container.

hugo:
  image: klakegg/hugo:latest
  container_name: hugo
  restart: unless-stopped
  ports:
    - "127.0.0.1:3000:3000"
  volumes:
    - ./hugo:/src
    - ./data/hugo/logs/:/tmp/logs/
  command: server --bind="0.0.0.0" --port=3000 --debug --log --verboseLog --verbose --buildDrafts --buildFuture --baseUrl="https://www.example.com" --appendPort=false --logFile=/tmp/logs/hugo.log --config=config.toml --configDir=/src/ --source=/src/ --themesDir=/src/themes/
  environment:
    USER: user
  • Essential Flags — Enable debugging with --debug --log --verboseLog --verbose. Build drafts and future posts with --buildDrafts --buildFuture. Set base URL, config path, and theme directory explicitly.

  • Log Mounting — Store logs on host filesystem at ./data/hugo/logs/. Point container logs to /tmp/logs/hugo.log. Persist logs across restarts for debugging issues.

  • Inspecting Logs — View live logs with docker-compose logs -f hugo. Check historical logs with cat ./data/hugo/logs/hugo.log. Search for errors with grep -i error ./data/hugo/logs/hugo.log.

  • Debugging Build Errors — Enable all verbose flags to see what Hugo processes. Check if theme exists in ./hugo/themes/. Verify config.toml syntax with hugo config. Test locally before pushing changes.

  • Verifying Functionality — Access site at http://127.0.0.1:3000. Check network access with curl http://127.0.0.1:3000. Verify content renders by navigating to /blog and checking post dates.

  • Theme Setup — Clone theme to ./hugo/themes/. Set theme name in config.toml. Mount themes directory in docker-compose with --themesDir=/src/themes/. Verify theme loads in logs without warnings.

  • Restart and Persistence — Use restart: unless-stopped to keep container running. Stop with docker-compose stop hugo. Start with docker-compose start hugo. Logs persist in mounted volume across restarts.

  • Production Considerations — Remove --debug --verbose flags in production. Build site once instead of continuous server with hugo build. Mount only necessary directories for security. Use HTTPS with proper base URL configuration.

Caddy Reverse Proxy Setup

  • Reverse Proxy Configuration — Use Caddy to proxy requests from domain to Hugo container. Define in Caddyfile. Routes HTTP/HTTPS traffic to Hugo on port 3000. Handles SSL certificates automatically with Let's Encrypt.

  • Caddy Port Mapping — Expose 0.0.0.0:8080:80 and 0.0.0.0:8443:443 to accept traffic from all interfaces. Host ports 8080 (HTTP) and 8443 (HTTPS) map to container ports 80/443. Caddy listens on container ports 80/443, accessible via host ports 8080/8443. All traffic flows through Caddy before reaching Hugo.

  • Caddy Volume Mounts — Mount ./Caddyfile:/etc/caddy/Caddyfile provides configuration from host. Mount ./caddy_data:/data stores HTTPS certificates and Caddy state on host. Mount ./caddy_config:/config persists Caddy configuration across restarts. Mount ./data/caddy/logs/:/data/logs/ saves access logs on host filesystem for monitoring and debugging.

caddy:
  image: caddy:latest
  container_name: caddy-reverse-proxy
  restart: unless-stopped
  ports:
    - "8080:80"
    - "8443:443"
  volumes:
    - ./Caddyfile:/etc/caddy/Caddyfile
    - ./caddy_data:/data
    - ./caddy_config:/config
    - ./data/caddy/logs/:/data/logs/
  environment:
    MEMOS_DOMAIN: ${MEMOS_DOMAIN}
  dns:
    - 8.8.8.8
    - 8.8.4.4
  • Caddyfile Example — Map domain to Hugo container. Use reverse_proxy directive pointing to hugo:3000. Caddy auto-discovers container via Docker network.
example.com {
  reverse_proxy hugo:3000
}

www.example.com {
  reverse_proxy hugo:3000
}
  • SSL/TLS Handling — Caddy automatically provisions and renews HTTPS certificates. No manual certificate management needed. Redirects HTTP to HTTPS by default.

  • Logging Access Requests — Mount logs directory at ./data/caddy/logs/. Access logs track all HTTP requests. Monitor traffic and debug connectivity issues through logs.

  • Service Discovery — Caddy and Hugo communicate via Docker network using container names. Use hugo:3000 as upstream instead of localhost. No port exposure needed between containers.

Let's Encrypt SSL/TLS Configuration

  • What is Let's Encrypt — Let's Encrypt is a free, automated certificate authority providing SSL/TLS certificates. Enables HTTPS encryption for websites at no cost. Certificates automatically renew before expiration. Sponsored by Mozilla, EFF, and others to encrypt the web.

  • Automatic Certificate Provisioning — Caddy automatically requests certificates from Let's Encrypt. No manual certificate purchase or installation needed. Caddy handles domain verification and installation seamlessly. Certificates are stored in ./caddy_data:/data volume for persistence.

  • Caddy Caddyfile Configuration — Simply specify domain names in Caddyfile. Caddy automatically detects HTTPS requirement and requests certificate. No explicit certificate configuration needed.

example.com www.example.com {
  reverse_proxy hugo:3000
}

Caddy automatically:

  • Detects domains from Caddyfile
  • Contacts Let's Encrypt ACME server
  • Verifies domain ownership via HTTP-01 challenge
  • Installs certificate
  • Configures HTTPS on ports 8080/8443

  • Certificate Renewal — Let's Encrypt certificates valid for 90 days. Caddy automatically renews 30 days before expiration. No downtime during renewal. Renewal happens silently in background without user intervention.

  • HTTPS Redirect — Caddy automatically redirects HTTP traffic to HTTPS. Visitors to http://example.com are redirected to https://example.com. Enforces secure connections by default. Can be disabled with http:// prefix in Caddyfile if needed.

  • Certificate Storage and Persistence — Certificates stored in ./caddy_data:/data mounted volume. Persists across container restarts. If volume is deleted, certificates are re-requested from Let's Encrypt. Keep backup of ./caddy_data directory for important deployments.

  • Browser Trust and Security — Let's Encrypt certificates trusted by all modern browsers. Browsers display padlock icon for HTTPS sites. Visitors see green lock indicating secure connection. Increases user trust and site credibility.

  • Rate Limiting Considerations — Let's Encrypt has rate limits (50 certificates per domain per week). Caddy respects rate limits and handles throttling gracefully. Test configuration before production to avoid exhausting limits. Staging environment recommended for testing.