Recipe: NGINX web server & reverse proxy
Serve static, proxy an app, terminate TLS.
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.
# the official image, with your config and static content baked inFROM nginx:1.27-alpineCOPY nginx.conf /etc/nginx/nginx.confCOPY 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.
events {}http {server {listen 8080;location / {root /usr/share/nginx/html; # serve static filestry_files $uri $uri/ /index.html;}location /api/ {proxy_pass http://api:3000/; # proxy to the "api" service by nameproxy_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.
services:web:image: nginxinc/nginx-unprivileged:1.27-alpine # non-root by defaultports: ["443:8443", "80:8080"]volumes:- ./nginx.conf:/etc/nginx/nginx.conf:ro- ./certs:/etc/nginx/certs:ro # cert + key, read-onlydepends_on: [api]api:build: ./api # never published — only the proxy reaches itvolumes: {}