Free Ebook cover Docker for Beginners: Containers Explained with Simple Projects

Docker for Beginners: Containers Explained with Simple Projects

New course

12 pages

Project: Containerizing a Static Website

Capítulo 7

Estimated reading time: 12 minutes

+ Exercise

What you will build

In this project you will containerize a simple static website and run it locally as a portable, repeatable service. A static website is a set of files (typically HTML, CSS, JavaScript, and images) that can be served directly by a web server without any server-side code. Containerizing it means packaging the web server configuration together with your site files so anyone can run the site the same way on any machine that has Docker installed.

The goal is to end up with a container that serves your site on a local port (for example, http://localhost:8080), and a workflow that lets you update the site content quickly during development and then produce a small, production-ready image when you are ready to ship.

Project structure

Create a new folder for the project. Inside it, keep your website files in a dedicated directory so it is obvious what is “site content” versus “container configuration”. A common layout looks like this:

static-site-container/  ├─ site/  │  ├─ index.html  │  ├─ styles.css  │  └─ app.js  ├─ nginx.conf  └─ Dockerfile

If you already have a static site, copy its files into the site/ directory. If you do not, create a minimal site so you can test the container end-to-end.

Example static site files

Create site/index.html:

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

<!doctype html><html lang="en"><head>  <meta charset="utf-8" />  <meta name="viewport" content="width=device-width, initial-scale=1" />  <title>My Static Site</title>  <link rel="stylesheet" href="/styles.css" /></head><body>  <main class="container">    <h1>Hello from a container</h1>    <p>If you can read this, Nginx is serving your static files.</p>    <button id="btn">Click me</button>    <p id="out"></p>  </main>  <script src="/app.js"></script></body></html>

Create site/styles.css:

body {  font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;  margin: 0;  background: #0b1020;  color: #e8ecff;} .container {  max-width: 720px;  margin: 64px auto;  padding: 24px;  background: rgba(255,255,255,0.06);  border: 1px solid rgba(255,255,255,0.12);  border-radius: 12px;} button {  padding: 10px 14px;  border-radius: 10px;  border: 1px solid rgba(255,255,255,0.2);  background: rgba(255,255,255,0.08);  color: #e8ecff;  cursor: pointer;} button:hover {  background: rgba(255,255,255,0.14);}

Create site/app.js:

const btn = document.getElementById('btn'); const out = document.getElementById('out'); btn.addEventListener('click', () => {  const now = new Date().toISOString();  out.textContent = `Button clicked at ${now}`; });

Choosing a web server image for static content

Static sites can be served by many servers, but Nginx is a popular choice because it is fast, stable, and easy to configure for common static-site needs like caching and single-page application routing. In this project you will use an Nginx-based container that includes your site files.

There are two common approaches:

  • Development approach (fast iteration): run a container with Nginx and mount your local site/ folder into the container so changes appear immediately without rebuilding the image.

  • Production approach (portable artifact): build an image that contains the site files inside it. This produces a single image you can run anywhere without needing the source folder.

You will implement both so you can choose the right workflow at different stages.

Step 1: Add an Nginx configuration

Nginx has a default configuration, but it is useful to provide your own so you control the document root, caching headers, and (optionally) single-page application behavior. Create nginx.conf in the project root:

server {  listen 80;  server_name _;  root /usr/share/nginx/html;  index index.html;  location / {    try_files $uri $uri/ =404;  }  location ~* \.(css|js|png|jpg|jpeg|gif|svg|ico|webp)$ {    expires 7d;    add_header Cache-Control "public, max-age=604800";    try_files $uri =404;  }  location = /health {    add_header Content-Type text/plain;    return 200 'ok';  }}

This configuration does a few practical things:

  • Serves files from /usr/share/nginx/html, which is the conventional directory used by the official Nginx image.

  • Uses try_files to return 404 for missing files (good for a basic static site).

  • Adds caching headers for common static assets (CSS/JS/images) to simulate a more realistic deployment.

  • Exposes a simple /health endpoint that you can use to verify the container is responding.

Optional: Single-page application (SPA) routing

If your site is a React/Vue/Angular SPA, direct navigation to routes like /about often needs to return index.html instead of 404. In that case, change the location / block to this:

location / {  try_files $uri $uri/ /index.html;}

Only use this if you actually want SPA behavior; otherwise it can hide real missing-file errors.

Step 2: Create a Dockerfile for the production image

Now create a Dockerfile that copies your static site into an Nginx image and replaces the default Nginx site configuration with yours.

FROM nginx:alpine COPY nginx.conf /etc/nginx/conf.d/default.conf COPY site/ /usr/share/nginx/html/ EXPOSE 80

What this does in practice:

  • Uses a small base image (nginx:alpine) to keep the final image lightweight.

  • Copies your Nginx config into the location Nginx reads by default for site configs.

  • Copies all files from site/ into the container’s web root.

  • Declares that the container listens on port 80 (this is documentation for humans and tooling; you still choose the host port when you run it).

Step 3: Build and run the containerized site

From the project root (where the Dockerfile is), build the image:

docker build -t static-site:1 .

Then run it and map a host port to the container’s port 80:

docker run --rm -p 8080:80 static-site:1

Open your browser to http://localhost:8080. You should see your page. Test the health endpoint at http://localhost:8080/health and confirm it returns ok.

Common port mapping mistakes

  • If you accidentally reverse the mapping (-p 80:8080), you will expose the wrong port and likely see connection errors.

  • If port 8080 is already in use, choose another host port, for example -p 8090:80.

Step 4: Verify what is inside the running container

When debugging a static site container, you often want to confirm that the files are actually present where Nginx expects them and that the configuration file was copied correctly. Start the container in detached mode so you can inspect it:

docker run -d --name static-web -p 8080:80 static-site:1

List the files in the web root:

docker exec static-web ls -la /usr/share/nginx/html

Print the active Nginx config file you copied:

docker exec static-web cat /etc/nginx/conf.d/default.conf

If you need to stop and remove the container:

docker rm -f static-web

Development workflow: live-editing without rebuilding

Rebuilding the image after every HTML/CSS change can slow you down during development. A practical alternative is to run Nginx in a container but serve files directly from your working directory by mounting the site/ folder into the container’s web root. This way, edits on your machine are immediately visible when you refresh the browser.

Run Nginx with a bind mount for the site and a bind mount for the config:

docker run --rm -p 8080:80  -v "$PWD/site:/usr/share/nginx/html:ro"  -v "$PWD/nginx.conf:/etc/nginx/conf.d/default.conf:ro"  nginx:alpine

Notes that matter in practice:

  • :ro makes the mounts read-only inside the container, which is a good habit for static content and reduces the chance of accidental changes from inside the container.

  • If you are on Windows PowerShell, $PWD works, but quoting rules differ. If you run into path issues, use the full path (for example, C:\path\to\project\site) and ensure Docker Desktop has file sharing enabled for that drive.

  • Nginx does not need a restart to serve updated static files; a browser refresh is enough. If you change the Nginx config, you must restart the container to load the new config.

Adding a custom 404 page

Static sites feel more polished with a custom 404 page. Create site/404.html:

<!doctype html><html lang="en"><head>  <meta charset="utf-8" />  <meta name="viewport" content="width=device-width, initial-scale=1" />  <title>Not Found</title>  <link rel="stylesheet" href="/styles.css" /></head><body>  <main class="container">    <h1>404</h1>    <p>That page does not exist.</p>    <p><a href="/">Go back home</a></p>  </main></body></html>

Update nginx.conf to use it:

server {  listen 80;  server_name _;  root /usr/share/nginx/html;  index index.html;  error_page 404 /404.html;  location = /404.html {    internal;  }  location / {    try_files $uri $uri/ =404;  }  location ~* \.(css|js|png|jpg|jpeg|gif|svg|ico|webp)$ {    expires 7d;    add_header Cache-Control "public, max-age=604800";    try_files $uri =404;  }  location = /health {    add_header Content-Type text/plain;    return 200 'ok';  }}

Rebuild and rerun (production workflow) or restart the container (development workflow). Then visit a missing URL like /does-not-exist to confirm your 404 page appears.

Serving under a subpath (base URL) and why it matters

Many static sites are deployed under a subpath such as https://example.com/docs/ instead of the domain root. If your HTML uses absolute paths like /styles.css, it assumes the site is served from the root. Under a subpath, those links can break.

Two practical strategies:

  • Use relative paths in your HTML (for example, styles.css instead of /styles.css) when possible.

  • Configure your build tool (if using one) to emit assets with the correct base path.

For this simple project, switching to relative paths is often the easiest. In index.html, change href="/styles.css" to href="styles.css" and src="/app.js" to src="app.js". This makes the site more portable across different hosting paths.

Making the image smaller and cleaner with a build output folder

If your static site is generated by a tool (for example, it outputs a dist/ folder), you should copy only the final build output into the image, not the entire source tree. Even for a hand-written site, it is useful to keep the container context clean so you do not accidentally include unrelated files.

A practical pattern is:

  • Keep source in site-src/

  • Keep final static output in site/ (or dist/)

  • Copy only the output folder into the image

Even without introducing a build tool here, you can adopt the mindset: the container should contain only what is needed to serve the site.

Troubleshooting checklist

1) You see the default “Welcome to nginx!” page

This usually means your site files were not copied to the expected directory or your bind mount path is wrong.

  • Confirm your files exist in the container: docker exec static-web ls -la /usr/share/nginx/html

  • Confirm your Dockerfile copies site/ to /usr/share/nginx/html/ and that the site/ folder is not empty.

  • If using bind mounts, confirm the host path is correct and readable by Docker.

2) CSS/JS returns 404 but index.html loads

This is often caused by incorrect paths in HTML (absolute vs relative) or missing files in the container.

  • Open the browser dev tools Network tab and look at the requested URLs.

  • Check that the referenced filenames match exactly (case-sensitive in Linux containers).

  • Verify the files exist in /usr/share/nginx/html.

3) You get “403 Forbidden”

This can happen if Nginx cannot read the files or if the directory listing is requested without an index file.

  • Ensure index index.html; is set and that index.html exists.

  • If bind mounting from the host, ensure file permissions allow reading. On some setups, permission issues can appear when mounting from certain directories.

4) Changes do not show up during development

If you are using the production image workflow, you must rebuild the image after changes. If you are using bind mounts, changes should show up immediately.

  • Confirm you are running the bind-mount command, not the built image.

  • Hard refresh the browser to bypass cached assets (especially if you enabled caching headers).

  • Temporarily remove caching headers from nginx.conf during development if it gets in the way.

Practical extension: add basic security headers

Even for a static site, it is useful to add a few security-related headers. Add these inside the server block in nginx.conf:

add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;

This does not replace a full security review, but it demonstrates how containerizing a static site also means you can standardize operational settings (headers, caching, health checks) alongside the content.

Practical extension: gzip compression for faster delivery

Compression can significantly reduce the size of HTML, CSS, and JavaScript. Add the following to the server block:

gzip on; gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml; gzip_min_length 1024;

Then rebuild and rerun the image (or restart the container if you are bind-mounting the config). You can verify compression in the browser dev tools by checking the response headers for Content-Encoding: gzip on compressible assets.

Putting it together: two commands you will actually use

Production-style run (portable image)

docker build -t static-site:1 . docker run --rm -p 8080:80 static-site:1

Development-style run (edit files live)

docker run --rm -p 8080:80  -v "$PWD/site:/usr/share/nginx/html:ro"  -v "$PWD/nginx.conf:/etc/nginx/conf.d/default.conf:ro"  nginx:alpine

With these two workflows, you can iterate quickly while building your site and still produce a clean, self-contained image when you want a single artifact that serves the exact same files and configuration everywhere.

Now answer the exercise about the content:

During development of a containerized static website, which workflow best enables quick iteration without rebuilding the image after each change?

You are right! Congratulations, now go to the next page

You missed! Try again.

Bind-mounting the site folder (and config) lets Nginx serve files directly from your working directory, so content updates appear immediately without rebuilding the image.

Next chapter

Project: Simple API Containerization and Configuration

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.