Notes to my future self, in plain English.
What I Was Trying To Do
I already had my portfolio website working on my computer using Hugo and a template for the design. I wanted to package it up in Docker so I could run it the same way anywhere, instead of relying on my machine's setup.
Lesson 1: Hugo Doesn't Actually "Run" a Website
- When I ran
hugo serveron my computer, it felt like a website was "running." But that command is really just a preview tool for editing. It's not meant to be used for real, live traffic. - The command
hugo --minifydoes something different: it takes all my content and the template and converts it into plain files — HTML, CSS, images — and dumps them in a folder calledpublic/. Then it stops. Nothing is left running. It's like exporting a Word doc to PDF — a one-time conversion, not an app that stays open.
So a real website needs two separate things:
- Something to build the files (Hugo).
- Something to serve those files to visitors (a web server — I used nginx).
That's why my Docker setup has two stages.
Lesson 2: The Two-Stage Dockerfile
FROM hugomods/hugo:exts AS build
WORKDIR /src
COPY . .
RUN hugo --minify
FROM nginx:alpine
COPY --from=build /src/public /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Think of it as two mini-containers glued together:
- First part: a temporary workspace with Hugo installed. It builds my site into plain files.
- Second part: a clean, lightweight container with just nginx (the web server). It grabs the finished files from the first part and throws away everything else — no Hugo, no build tools, just the website files and something to hand them out.
Lesson 3: My Template Needed an Extra Build Step
My first build failed. After digging in, the real problem was simple: my Hugo template also needed some JavaScript/CSS tooling (npm) to build properly, and I wasn't running that step. Without it, the template's styling code came up empty and broke the build.
Fix: install Node.js in the build stage and run the extra setup commands before building the site:
FROM hugomods/hugo:exts AS build
WORKDIR /src
RUN apk add --no-cache nodejs npm
COPY . .
RUN hugo mod npm pack && npm install
RUN hugo --minify
FROM nginx:alpine
COPY --from=build /src/public /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Takeaway: if a template comes with its own JavaScript/CSS setup, I need to run that setup too — Hugo alone won't do it automatically.
Lesson 4: Ports Are Confusing At First
Two separate mistakes here:
Mistake A: I typed -p 80:8080, but Docker's format is -p my_computer_port:container_port. Nginx inside the container listens on port 80, so the second number always has to be 80 — no matter what port I want to use on my own machine.
Mistake B: Even after fixing that, port 80 on my actual computer was already being used by something else. Easiest fix: just pick a different, unused port on my side.
docker run -p 8888:80 portfolio
Takeaway: the EXPOSE line in a Dockerfile is just a note for humans — it doesn't control anything. The real listening port comes from nginx's own settings.
Lesson 5: The /cv Page Redirect Bug
Visiting mysite.com/cv sent me somewhere broken.
Turns out this was expected behavior gone slightly wrong: Hugo saves that page as a folder (/cv/index.html), so nginx automatically redirects /cv to /cv/. That part's normal.
The bug was that nginx was guessing the wrong port when building that redirect, it assumed port 80 (the port inside the container) instead of 8888 (the port I was actually using on my computer). So it sent my browser to the wrong address.
Fix: tell nginx not to guess the full address, just use a relative path:
server {
listen 80;
server_name _;
absolute_redirect off;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
And load that file into the container:
COPY nginx.conf /etc/nginx/conf.d/default.conf
The Final Setup
Dockerfile:
FROM hugomods/hugo:exts AS build
WORKDIR /src
RUN apk add --no-cache nodejs npm
COPY . .
RUN hugo mod npm pack && npm install
RUN hugo --minify
FROM nginx:alpine
COPY --from=build /src/public /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf:
server {
listen 80;
server_name _;
absolute_redirect off;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
.dockerignore:
public/
resources/
.git/
node_modules/
To run it:
docker build -t portfolio .
docker run -p 8888:80 portfolio
The Short Version, In Plain Words
- Hugo builds a website into files. It doesn't keep a website "running" — that's nginx's job.
- A Docker build can use one big toolbox to build something, then hand the finished result to a small, clean container to actually serve it.
- Some templates need an extra JavaScript/CSS setup step — don't skip it.
-
EXPOSEin a Dockerfile is just a label. The real port comes from the app's own settings. - When mapping ports, the right-hand number always has to match what the app inside is really listening on.
- Redirects inside a container can point to the wrong port if you don't tell the web server to keep them relative.
Top comments (0)