Overview
Static websites — portfolios, documentation, landing pages, built React or Vue apps — are the simplest thing to containerise, and a great place to learn production habits. In this project you will build a self-contained Nginx image for Priya’s portfolio that is fast, cache-friendly and easy to update and roll back.
- A portfolio site with clean URLs (/about instead of /about.html) and a custom 404 page.
- An Nginx configuration with gzip compression and browser caching for stylesheets.
- A versioned image with a HEALTHCHECK, so Docker knows whether the site is working.
- A release routine: build a new version, swap the container, and roll back in seconds.
Prerequisites
- Running containers and publishing ports — lessons 3 and 7.
- Writing a Dockerfile — lesson 5.
- Basic HTML. (Nginx basics help; the Linux course’s Nginx project covers them.)
Step 1: The Website
Create a folder called portfolio-site with this layout. The website lives in site/; the Nginx configuration and Dockerfile sit next to it:
tree -aClick Run to see what this code prints.
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Priya Sharma - Developer</title> <link rel="stylesheet" href="/css/style.css"></head><body> <h1>Hi, I'm Priya</h1> <p>I build web apps and ship them in containers.</p> <a href="/about">About me</a></body></html><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>About - Priya Sharma</title> <link rel="stylesheet" href="/css/style.css"></head><body> <h1>About me</h1> <p>Python, Docker and Linux enthusiast.</p> <a href="/">Home</a></body></html><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Page not found</title></head><body><h1>404 - This page does not exist</h1><a href="/">Go home</a></body></html>body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; max-width: 40rem; margin: 4rem auto; padding: 0 1rem; line-height: 1.6; color: #1f2933; background: #f8fafc;}
h1 { color: #2563eb; font-size: 2.25rem; margin-bottom: 0.5rem;}
a { color: #2563eb; font-weight: 600; text-decoration: none;}
a:hover { text-decoration: underline;}Step 2: Configure Nginx
The official image serves /usr/share/nginx/html with a default configuration. Replace that configuration with your own:
server { listen 80; server_name _; root /usr/share/nginx/html; index index.html;
# Compress text files before sending them gzip on; gzip_types text/css application/javascript image/svg+xml; gzip_min_length 256;
# Clean URLs: /about serves about.html location / { try_files $uri $uri.html $uri/ =404; }
# Let browsers cache stylesheets for a week location /css/ { expires 7d; }
error_page 404 /404.html;}try_files $uri $uri.html $uri/ =404tries the exact file, then the same name with .html added, then a folder — so /about serves about.html.gzipcompresses text files on the fly;gzip_min_lengthskips tiny files where compression does not help.expires 7dtells browsers they may reuse stylesheets for a week without asking again.error_page 404 /404.htmlshows your own page instead of Nginx’s plain one.
Step 3: The Dockerfile
FROM nginx:1.31-alpine
COPY nginx.conf /etc/nginx/conf.d/default.confCOPY site/ /usr/share/nginx/html/
HEALTHCHECK \ CMD wget -qO /dev/null http://127.0.0.1/ || exit 1.git*.mdnginx:1.31-alpinepins the Nginx version line, so a rebuild next month does not silently jump to a new major version.- Your config replaces the default one in
/etc/nginx/conf.d/. - The site is copied into the image, so the image is complete on its own — no bind mounts needed in production.
HEALTHCHECKfetches the home page every 10 seconds. It uses 127.0.0.1 rather than localhost, because this config only listens on IPv4 and localhost can resolve to the IPv6 address.
Step 4: Build and Run
docker build -t portfolio-site:1.0 .Click Run to see what this code prints.
docker images portfolio-siteClick Run to see what this code prints.
Run it with a restart policy, so it comes back automatically after a crash or a server reboot:
docker run -d --name portfolio -p 8080:80 --restart unless-stopped portfolio-site:1.0Click Run to see what this code prints.
Step 5: Test Every Feature
Open http://localhost:8080 in a browser, or test from the terminal. The home page and the clean /about URL:
curl -s http://localhost:8080/ | grep "<h1>"curl -s http://localhost:8080/about | grep "<h1>"Click Run to see what this code prints.
A page that does not exist returns status 404 with the custom page:
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/blogcurl -s http://localhost:8080/blog | grep "<h1>"Click Run to see what this code prints.
The stylesheet carries caching headers — the Expires date is exactly seven days after the request:
curl -sI http://localhost:8080/css/style.css | grep -iE "^(HTTP|Content-Type|Expires|Cache-Control)"Click Run to see what this code prints.
Compression works when the browser asks for it (all browsers do). Even this small stylesheet shrinks by about a third; larger files typically shrink by 70% or more:
curl -s -H "Accept-Encoding: gzip" -o /dev/null -w "%{size_download} bytes compressed\n" http://localhost:8080/css/style.csscurl -s -o /dev/null -w "%{size_download} bytes uncompressed\n" http://localhost:8080/css/style.cssClick Run to see what this code prints.
After the first health check has passed, the container reports itself healthy:
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"Click Run to see what this code prints.
You can also check the configuration inside the running container at any time:
docker exec portfolio nginx -tClick Run to see what this code prints.
Step 6: Update and Roll Back
Priya edits the paragraph in site/index.html, builds the result as a new version, and swaps the container. The old image, 1.0, stays on her machine:
docker build -q -t portfolio-site:1.1 . > /dev/nulldocker rm -f portfoliodocker run -d --name portfolio -p 8080:80 --restart unless-stopped portfolio-site:1.1 > /dev/nullsleep 1curl -s http://localhost:8080/ | grep "<p>"Click Run to see what this code prints.
If something is wrong with 1.1, rolling back is the same swap with the previous tag — no rebuild, no editing files on a server:
docker rm -f portfolio > /dev/nulldocker run -d --name portfolio -p 8080:80 --restart unless-stopped portfolio-site:1.0 > /dev/nullsleep 1curl -s http://localhost:8080/ | grep "<p>"Click Run to see what this code prints.
Put Priya’s release routine in order.
- Build a new version: docker build -t portfolio-site:1.1 .
- Run a container from the new version
- Check the site — and if it is broken, run the previous version again
- Edit the site files
- Remove the old container
Extend This Project
- Push the image to a registry (lesson 13) and run it on a cloud server.
- Add security headers such as X-Content-Type-Options and Content-Security-Policy with add_header.
- Build a React or Vue app in a Node build stage and copy its dist folder into this image (lesson 11).
- Put the site behind Caddy for automatic HTTPS — see the reverse-proxy project.
Summary
You packaged a static website into a small, self-contained Nginx image with clean URLs, a custom 404 page, compression, caching and a health check — and practised the versioned release and rollback routine used for real deployments.