The Goal: Consistent Deployment Anywhere

The objective was straightforward: take a Hugo-generated portfolio website, which was already functional on a local development machine, and package it into a Docker container. The aim is to achieve identical execution environments, eliminating the 'it works on my machine' problem and simplifying deployment across different platforms. This isn't about running a dynamic web application; it's about serving static files reliably.

Understanding Hugo's Build Process

A common misconception when first working with static site generators like Hugo is that commands like hugo server are for production. This command is primarily a development tool. It spins up a local web server that watches for file changes, automatically recompiles the site, and refreshes the browser. It's invaluable for iterating on content and design, but it's not built to handle live traffic or persistent hosting. Its purpose is to provide a real-time preview during the development phase.

The command that actually produces the deployable assets is hugo --minify. This command performs a one-time build. It processes all content, applies themes, and optimises assets (like minifying CSS and JavaScript) to create a set of static files. These files are then placed into a designated output directory, typically named public/. Once the build is complete, the command exits. There's no persistent process running, much like exporting a document from a word processor to a PDF format. You get a finished product, not an ongoing service.

Containerizing Static Assets with Docker

To package these static files for deployment, Docker becomes the tool of choice. The process involves creating a Dockerfile that defines the environment and the steps to serve the generated website. For static sites, a common and efficient approach is to use a lightweight web server within the container. Nginx is a popular and performant choice for this task.

A typical Dockerfile for a Hugo site would involve two stages, a pattern known as a multi-stage build. This optimizes the final image size by separating the build environment from the runtime environment.

A diagram illustrating a multi-stage Docker build for a static site

Stage 1: Building the Hugo Site

The first stage uses a Hugo-specific Docker image (or an image with the Hugo binary installed) to build the static site. This stage will install Hugo, copy the project's source code into the container, and then run the hugo command to generate the public/ directory.

FROM hugo as builder

WORKDIR /app

COPY . .

RUN hugo --minify

Here, FROM hugo as builder specifies that we are using a Hugo image and naming this stage 'builder'. The working directory is set to /app. The project's source files are copied in, and then hugo --minify is executed. The output, the public/ directory, will be available after this stage completes.

Stage 2: Serving the Static Site with Nginx

The second stage uses a minimal Nginx image. It copies the static files generated in the first stage (the contents of the public/ directory) into Nginx's web root directory (usually /usr/share/nginx/html). A custom Nginx configuration file might also be copied to ensure Nginx serves the files correctly, especially if there are any specific routing needs or caching strategies. Finally, the Nginx server is configured to run.

FROM nginx:alpine

COPY --from=builder /app/public/ /usr/share/nginx/html/

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

In this stage, FROM nginx:alpine uses a lightweight Alpine Linux-based Nginx image. The COPY --from=builder command is crucial; it copies only the generated static assets from the 'builder' stage. The site is exposed on port 80, and the CMD instructs Docker to start the Nginx server in the foreground when the container runs.

Local Testing and Deployment

Once the Dockerfile is created, you can build the Docker image locally using docker build -t my-portfolio .. To run the container and test the website locally, you would use docker run -p 8080:80 my-portfolio. This maps port 80 inside the container to port 8080 on your host machine. You can then access your portfolio at http://localhost:8080.

This Docker image can then be deployed to any environment that supports Docker, such as cloud platforms (AWS, Google Cloud, Azure), container orchestration systems (Kubernetes), or even simple virtual private servers. The Docker image encapsulates the entire application and its dependencies, ensuring it runs the same way regardless of the underlying infrastructure. This approach is far more robust than relying on a specific local setup and is the standard for modern web application deployment.

Beyond Basic Serving

While serving static files with Nginx is common, one might consider additional configurations within the Dockerfile or Nginx config. For instance, setting up custom error pages, configuring Gzip compression for faster asset delivery, or implementing browser caching headers can all be managed within the container's Nginx setup. These optimisations are essential for a professional online presence.

The surprising detail here is not the complexity of Docker itself, but how clearly it separates the build process from the serving process. What was perceived as a 'running' website during local development (hugo server) is fundamentally different from the static files ready for a production web server. Docker makes this distinction explicit and manageable.

For anyone managing their own portfolio or a small project website, containerizing with Docker offers a significant step up in deployment reliability and consistency. It transforms a local development artifact into a portable, deployable unit. The initial learning curve for Docker is well worth the long-term benefits of predictable environments.