Nginx in Docker is one of the easiest containers to get running and one of the most useful — local dev server, static site host, reverse proxy in front of an app, TLS terminator. The image is small (~25 MB on Alpine), the config story is straightforward, and most tutorials skip exactly the parts that matter for real use.
How do I run Nginx in Docker?
Default static-file server on port 8080:
docker run -d --name nginx -p 8080:80 nginx:alpineVisit http://localhost:8080 and you see Nginx's default welcome page.
Serve your own static files by bind-mounting a directory:
docker run -d --name nginx \
-p 8080:80 \
-v "$(pwd)/site:/usr/share/nginx/html:ro" \
nginx:alpineDrop any files into ./site on the host and Nginx serves them.
Configure version, port, and the directory you want served.
Pick a version
nginx:alpine— Alpine-based, ~25 MB. The default for most dev and production use.nginx:stable-alpine— pinned to Nginx's "stable" branch (slower-moving, security fixes only).nginx:latest— the regular Debian-based image, ~150 MB. Use only if you specifically need a tool that Alpine lacks.
Tag with a major: nginx:1.27-alpine for predictable upgrades.
Mount a custom nginx.conf
For anything beyond default static serving, you need your own config. The image's main config lives at /etc/nginx/nginx.conf, and the default site is /etc/nginx/conf.d/default.conf. Replace either with a bind mount:
docker run -d --name :container_name \
-p :host_port:80 \
-v "$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf:ro" \
-v ":site_dir:/usr/share/nginx/html:ro" \
nginx::nginx_versionA minimal nginx.conf for a single-page-app fallback:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html;
}
}Reload after config changes without restarting:
docker exec :container_name nginx -s reloadRun as non-root
Nginx official images run as root by default and Nginx itself drops to the nginx user for its worker processes. For tighter setups, the upstream image also publishes an nginxinc/nginx-unprivileged variant that runs entirely as non-root:
docker run -d --name :container_name -p :host_port:8080 nginxinc/nginx-unprivileged:alpineThe unprivileged variant listens on 8080 (not 80) by default — root is required to bind ports below 1024. That's fine for most dev use; in production behind a load balancer, the LB handles the privileged port.
Reverse proxy in front of an app
The common production-shaped pattern: a Node/Python/PHP app on its own container, Nginx in front handling TLS, gzip, and routing.
upstream app {
server app:3000;
}
server {
listen 80;
server_name _;
location / {
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}In a Compose file:
services:
web:
image: nginx::nginx_version
ports:
- "::host_port:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
app:
image: my-app:latest
expose:
- "3000"Inside the Compose network, app:3000 resolves to the app container. No need to publish port 3000.
Practical usage: dev server for a static site build
A common React/Vue/Astro/Next-export pattern: build static files locally, serve them with Nginx in a container, get HTTP/2 and gzip "for free":
# Build
npm run build
# Serve the dist/ directory
docker run --rm -d --name preview \
-p 8080:80 \
-v "$(pwd)/dist:/usr/share/nginx/html:ro" \
nginx:alpine
open http://localhost:8080--rm cleans up automatically on stop. Stop with docker stop preview when you're done.
TLS
Nginx in Docker can terminate TLS, but you have to mount your certs:
docker run -d --name :container_name \
-p 443:443 -p 80:80 \
-v "$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf:ro" \
-v "$(pwd)/certs:/etc/nginx/certs:ro" \
nginx::nginx_versionFor local dev, use mkcert to issue a trusted localhost cert into ./certs. For production, an Nginx-in-Docker setup is often paired with a separate ACME container (Caddy, Traefik, or nginx-proxy-acme) that handles Let's Encrypt issuance.
Common pitfalls
- 404 on a single-page app refresh. SPA routes don't exist on disk. Add the
try_files $uri $uri/ /index.html;fallback shown above. - Permission errors on bind-mounted directories. The
nginxuser inside the container has UID 101. If the host directory's permissions don't allow it to read, files 404 or 403. Use:roon the mount (read-only) and ensure the host dir is readable by all. - Forgetting to reload after editing nginx.conf. Bind mounts pick up changes on the host, but Nginx caches the parsed config in memory.
docker exec :container_name nginx -s reloadto re-read. - Port 80 already in use. Pick a different host port:
-p 8080:80. Inside the container, Nginx still listens on 80. unprivilegedvariant listening on 8080, not 80. Adjust your-paccordingly.
FAQ
Sources
Authoritative references this article was fact-checked against.
- Official Nginx image — Docker Hubhub.docker.com
- Nginx documentationnginx.org


