Setting Up Hugo with Docker
I rented a cloud machine and put a blog on it: Hugo to build the site, Docker to run it, Caddy in front for HTTPS. Notes from that setup.
-
Container Service Setup: define Hugo in docker-compose with the klakegg/hugo image. Mount the source at
/srcand logs at/tmp/logs/. Expose port 3000 for local access. -
Port Mapping (Host:Container):
127.0.0.1:3000:3000publishes container port 3000 on host loopback. Host port comes first, container port second. Binding to 127.0.0.1 keeps the dev server off the public interface, which matters because Caddy handles everything that faces the internet. -
File Mapping (Volumes):
./hugo:/srcmaps the whole project into the container, so an edit on the host is live in the container with no rebuild../data/hugo/logs/:/tmp/logs/keeps the logs on the host, where they survive the 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:
--debug --log --verboseLog --verbosefor debugging.--buildDrafts --buildFutureto render posts Hugo would otherwise skip. Set base URL, config path, and theme directory by hand, because the defaults assume a layout this one does not have. -
Log Mounting: write logs to
/tmp/logs/hugo.loginside the container, which lands in./data/hugo/logs/on the host and outlives a restart. -
Inspecting Logs: follow them with
docker-compose logs -f hugo, read the history withcat ./data/hugo/logs/hugo.log, and find failures withgrep -i error ./data/hugo/logs/hugo.log. -
Debugging Build Errors: turn on every verbose flag and Hugo names the file it choked on. Most of my breakages came down to a theme missing from
./hugo/themes/or bad TOML, whichhugo configcatches. -
Verifying Functionality: load
http://127.0.0.1:3000, orcurlit from the host. Navigate to/blogand check the post dates, since a wrong--buildFuturehides your newest post. -
Theme Setup: clone the theme into
./hugo/themes/, name it inconfig.toml, and point the container at it with--themesDir=/src/themes/. A theme Hugo cannot find shows up as a warning in the log and a blank page in the browser. -
Restart and Persistence:
restart: unless-stoppedbrings the container back after a reboot. Stop and start it withdocker-compose stop hugoanddocker-compose start hugo. Logs in the mounted volume survive both. -
Production Considerations: drop
--debug --verbose, build once withhugo buildinstead of running the dev server, mount only the directories the container needs, and set the base URL to the HTTPS address.
Caddy Reverse Proxy Setup
-
Reverse Proxy Configuration: Caddy takes requests for the domain and forwards them to the Hugo container on port 3000. The Caddyfile holds the routing, and Caddy gets the certificates from Let’s Encrypt without being asked.
-
Caddy Port Mapping:
0.0.0.0:8080:80and0.0.0.0:8443:443bind all interfaces, so traffic from the internet reaches Caddy on host ports 8080 and 8443 and arrives at container ports 80 and 443. Nothing reaches Hugo except through Caddy. -
Caddy Volume Mounts:
./Caddyfile:/etc/caddy/Caddyfilesupplies the config../caddy_data:/dataholds the certificates and Caddy’s state, which is the mount you least want to lose../caddy_config:/configkeeps the autosaved config across restarts, and./data/caddy/logs/:/data/logs/puts access logs where you can read them.
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: point
reverse_proxyathugo:3000. Docker’s internal DNS resolves the service name, so no IP addresses appear anywhere.
example.com {
reverse_proxy hugo:3000
}
www.example.com {
reverse_proxy hugo:3000
}
-
SSL/TLS Handling: Caddy provisions and renews the certificates itself, and redirects HTTP to HTTPS out of the box.
-
Logging Access Requests: the access log in
./data/caddy/logs/records every request, which is where you look first when a page 404s or a proxy returns 502. -
Service Discovery: Caddy reaches Hugo by container name over the Docker network. Use
hugo:3000as the upstream. Inside a container,localhostis that container, so127.0.0.1:3000would point Caddy at itself.
Let’s Encrypt SSL/TLS Configuration
-
What is Let's Encrypt: a certificate authority that issues SSL/TLS certificates for free over an automated protocol, funded by Mozilla, the EFF, and others.
-
Automatic Certificate Provisioning: Caddy requests the certificate, answers the domain verification, installs the result, and stores it in the
./caddy_data:/datavolume. You buy nothing and install nothing. -
Caddy Caddyfile Configuration: name the domains in the Caddyfile. Caddy reads a bare hostname as a request for HTTPS and goes and gets the certificate.
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: certificates last 90 days, and Caddy renews at 30 days remaining. The renewal happens in the background with no restart and no downtime.
-
HTTPS Redirect: a visitor typing
http://example.comlands onhttps://example.com. Prefix the hostname withhttp://in the Caddyfile if you want to opt out. -
Certificate Storage and Persistence: the
./caddy_data:/datavolume holds them across restarts. Delete that volume and Caddy asks Let’s Encrypt for new certificates, which counts against your rate limit. Back it up. -
Browser Trust and Security: every current browser trusts Let’s Encrypt, so visitors get the padlock and no warning interstitial.
- Rate Limiting Considerations: Let’s Encrypt caps you at 50 certificates per domain per week. Test against their staging endpoint first, because a config you restart twenty times while debugging can burn through that.