CoursesDocker in depthPractical recipes

Recipe: NGINX web server & reverse proxy

Serve static, proxy an app, terminate TLS.

Intermediate14 min · lesson 24 of 30

NGINX is the workhorse of container setups in three roles: serving static files, reverse-proxying an app, and terminating TLS at the edge. All three are configuration, not code, so the recipe is really “ship the right nginx.conf into the official image” — mounted read-only, with the container running unprivileged. This is also the “build a web server” walkthrough in concrete form.

Dockerfile
# the official image, with your config and static content baked in
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY site/ /usr/share/nginx/html/
# nginx:alpine can run unprivileged; use the -unprivileged variant for non-root
# FROM nginxinc/nginx-unprivileged:1.27-alpine (listens on 8080, runs as uid 101)

Static files and a reverse proxy

A single server block can serve static assets and hand /api to an upstream container by its service name — Docker’s embedded DNS resolves api to the app container. This is the standard front door: one public port, static content served directly, dynamic requests proxied to the backend that is never itself published.

nginx.conf
events {}
http {
server {
listen 8080;
location / {
root /usr/share/nginx/html; # serve static files
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api:3000/; # proxy to the "api" service by name
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}

Terminating TLS

To speak HTTPS, mount the certificate and key (read-only), add a listen 443 ssl block, and redirect HTTP to HTTPS. Keep the key out of the image — mount it as a secret or a read-only bind — and constrain protocols to modern TLS. The app behind the proxy still speaks plain HTTP on the internal network, which never leaves Docker.

compose.yaml
services:
web:
image: nginxinc/nginx-unprivileged:1.27-alpine # non-root by default
ports: ["443:8443", "80:8080"]
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro # cert + key, read-only
depends_on: [api]
api:
build: ./api # never published — only the proxy reaches it
volumes: {}
Config and keys read-only; app not published
Mount nginx.conf and TLS material with :ro so a compromised container cannot rewrite its own config or read-write the private key. And never publish the backend app directly — only NGINX gets a host port; the app is reachable solely through the proxy on the internal network. That is the whole point of the reverse-proxy pattern.