DevOps  

Hosting a Static Website with Docker and NGINX

Introduction

Welcome to Part 3 of the .NET Docker Series.

So far, we’ve looked at what containers are and how Docker compares to virtual machines. Now, we’re shifting gears a bit, into something just as foundational: hosting static websites using Docker and NGINX.

In this article, you'll learn how to containerize a basic static website and host it with NGINX.

If you're new here, check out the first two parts before jumping in.

What We’ll Cover?

Here’s what’s on the table.

  1. Pulling the NGINX image
  2. Setting up your folder structure
  3. Writing a clean Dockerfile
  4. Creating a simple HTML file
  5. Building a Docker image
    • Common errors (and how to avoid them)
  6. Running the container and accessing your site via a browser
  7. Bonus Bytes
    • Versioning images
    • Layers
    • A few go-to Docker commands

What is NGINX?

Nginx (pronounced "engine-x") is a web server that can also be used as a reverse proxy, load balancer, and HTTP cache. It's known for handling a large number of concurrent connections and is often used to serve static content or to forward requests to backend services like .NET, Node.js, Python, or PHP apps.

Step 1. Pull the NGINX Image.

To serve static content, you’ll need the official NGINX image from Docker Hub.

Option 1. CLI

docker pull nginx

That pulls the latest tag. If you want a specific version.

docker pull nginx:stable-alpine3.20-perl

Option 2. Docker Hub UI

  • Visit Docker Hub NGINX
  • Browse the available tags (stable, alpine, etc.)
  • Click Pull next to the version you want

See the screenshot below from Docker Desktop.

Docker Desktop

Image 1: Pulling NGINX from the Docker Desktop app

Step 2. Setting up your folder structure.

Project structure matters, especially when Docker builds rely on relative paths.

Here’s what we’ll use.

Apps/
├── Dockerfile
└── StaticWebApp/
    └── index.html

This keeps things clean

  • Apps/ is the project root
  • StaticWebApp/ holds your static assets (HTML, CSS, etc.)
  • Dockerfile lives at the root level (same as your build context)

Step 3. What Is a Dockerfile?

A Dockerfile is a plain text file that contains a list of instructions Docker uses to build an image.

Think of it like a recipe for creating your container environment; it defines the base image, sets up configurations, copies files, and more.

FROM nginx
COPY ./StaticWebApp /usr/share/nginx/html

There are two instructions in our Docker file.

  1. FROM nginx
    • Uses the official NGINX base image as your foundation. This base image is essentially the starting point, it's a pre-built container environment that already has certain tools, settings, or services installed.
  2. COPY ./StaticWebApp /usr/share/nginx/html
    • ./StaticWebApp refers to a folder named StaticWebApp located in the same directory where you're building the Docker image in this case, your Apps folder.
    • The ./ simply means "from the current directory".

Tip. Always validate your target path by reviewing the official image docs. For NGINX, Docker Hub explicitly documents /usr/share/nginx/html as the default serving directory. So, before writing your Dockerfile, it's a good habit to check the image's documentation to know where to copy files, which ports to map, or what defaults it comes with.

This applies to other image toolike .NET, Node, Python, etc. each image has its own structure and expectations, so always refer to its Docker Hub page before building.

Step 4. Static HTML File.

Let’s keep it simple.

Your Apps/StaticWebApp/index.html should look like.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My Static Web Page</title>
</head>
<body>
  <h1>Welcome to My Static Web Page</h1>
  <p>This page is served using Docker!</p>
</body>
</html>

Step 5. Build the Docker Image.

Now let’s build the Docker image that will serve our static content.

Navigate to the Apps directory where your Dockerfile is located, and run the following command.

docker build -t static-web-app .

Breakdown

  • docker build: Tells Docker to build an image.
  • -t static-web-app: Tags it with a name so you can refer to it easily.
  • . : Specifies the build context, i.e., the current directory (where the Dockerfile and app content are located).

Tip. Don’t forget the. At the end of the command! Without it, Docker won’t know where to find the Dockerfile and will throw an error like: failed to read Dockerfile: open Dockerfile: no such file or directory

Once successful, you’ll see an output like this in your terminal.

Terminal

Image 2: Successful Docker Build Output

Common Errors During Build

  • Incorrect file name: Must be exactly Dockerfile, no extensions like .txt, and no filename variations (e.g., dockerfile or DockerFile won’t work).
  • Wrong path in COPY: If you use COPY StaticWebApp instead of ./StaticWebApp, it may fail if you're not in the correct working directory.
  • Case Sensitivity: Yes, folder names and paths are case-sensitive, especially on Linux and macOS. For example, StaticWebApp ≠ staticwebapp. Always double-check that your folder names match exactly.
  • Version confusion: Using FROM nginx:latest pulls the latest available version of the image, which could change over time and potentially break your app. A safer approach: pin to a specific version like FROM nginx:1.25-alpine.

Step 6. Running the container and accessing your site via a browser.

Option 1. Run Using Command Line.

Once the image is built, you can launch a container with the following command.

docker run -d -p 9090:80 --name static-container static-web-app
  • -d: Runs the container in detached mode (in the background)
  • -p 9090:80: Maps port 80 inside the container (used by NGINX) to port 9090 on your local machine
  • --name static-container: Assigns a user-friendly name to the container
  • static-web-app: Refers to the Docker image you just built

Tip. If you omit the -p flag, the container will still run, but you won’t be able to access the site from your browser.

If everything works as expected, you’ll see container startup logs in the terminal.

Container startup logs

Image 3: Successful Docker Container Run Output

Output in the Browser

To view your site, visit http://localhost:9090, You should see your HTML page rendered in the browser.

Output in the Browser

Image 4: Hosted static web app showing the contents of index.html

Common Pitfalls

  • Case-sensitive paths: ./StaticWebApp ≠ ./staticwebapp
  • Port already in use: If 9090 is taken, try another port like 3000: -p 3000:80
  • Incorrect working directory: Run docker build from the directory where your Dockerfile resides, unless you specify the path using -f.

Option 2. Run the Container Using Docker Desktop (GUI)

If you prefer clicking over typing or just want to visually manage your containers, Docker Desktop gives you a simple way to run your image directly from the UI.

  1. Open Docker Desktop
  2. Go to the "Images" Tab: You’ll see a list of all locally available images. Look for the one you just built, like staticwebapp, which I have listed below.
    Images
  3. Click "Run": This opens an "options settings" dialog where you can configure.
    Option settings
    • Container Name: (e.g., static-container)
    • Ports: Map container port 80 to host port 9090 (or any port you want)
    • Volume Mounts (optional): Not needed for this static site example
    • Environment Variables (optional): Also not required here
  4. After filling in all the information, click "Run" Again at the bottom of the dialog box.
    Run
  5. Docker will spin up the container, and it’ll appear in the Containers tab.
    Containers tab
  6. Open in Browser: Click the port column "9090:80", or manually go to http://localhost:9090, and you'll see your webapp is now running.

Understanding Versioning in Docker Images

When we run: docker build -t static-web-app.

We're building an image and giving it a name using the -t (or-- tag) flag.

But here's the thing: this image is unnamed when it comes to versioning, because we didn’t specify one. Docker silently adds the: latest tag behind the scenes.

docker build -t static-web-app:latest .

So the full image name becomes.

static-web-app:latest

This works fine for quick testing, but in real-world scenarios, versioning is super useful, especially when you make changes and want to keep track.

How to Add a Version?

You can specify your own version tag like this.

docker build -t static-web-app:v1 .

Now you have a versioned image called static-web-app:v1. You can then build v2, v3, etc., as your app evolves.

Running a Docker Image with a Version Tag

To run it, you simply include the version tag.

docker run -d -p 8080:80 --name static-container static-web-app:v1

Layers

Layers

In Docker Desktop GUI, you’ll see staticwebpage:1.0.0 under the list of images because that’s the tag I used when building the image, and you’ll also see “nginx” listed because your image is based on the official NGINX base image.

My custom image "staticwebapp: 1.0.0" depends on the official NGINX image.

That makes nginx a runtime dependency, just like a NuGet package in .NET or an NPM package in Node.js. Your container won’t work without it, because it’s literally the foundation your image is built on.

So in this case.

  • nginx is the base layer and a required dependency.
  • Your static files are just layered on top of it.
  • Docker pulls nginx from Docker Hub when building, unless it’s already cached.

Think of your final image as a layered stack: base image (dependency) → your custom content → container runtime.

Bonus: Docker commands

1. List All Image Versions

To see all available versions (tags) of your Docker images, run.

docker images

You’ll get an output similar to this.

REPOSITORY        TAG       IMAGE ID       CREATED         SIZE
static-web-app    v1        abc12345        1 minute ago    22MB
static-web-app    latest    abc12345        1 minute ago    22MB

2. Remove image

You can remove a specific image version using.

Syntax: docker rmi <image_id>

Example: docker rmi static-web-app:v1

3. List all containers

To list all containers (running or stopped)

docker ps -a

4. Remove a container

To remove a stopped container.

Syntax: docker rm <container_id>
Example: docker rm 7c3f0a1d23a7

5. Force remove a running container

To stop and remove a running container in one go.

Syntax: docker rm -f <container_id>
docker rm -f 7c3f0a1d23a7

6. Stop a running container

To stop a running container.

Syntax: docker stop <container_id>
Example: docker stop 7c3f0a1d23a7

Conclusion

You just built and hosted your first static website using Docker and NGINX. Even better, you now understand how the pieces fit together: images, containers, port mapping, folder structure, and Dockerfile conventions.

This simple trick is useful across all types of projects, including .NET applications.

Next up, we’ll return to our .NET world and show how to run Blazor apps

Until then, keep it containerized.