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.
Once a single location / is not enough (matching file extensions, versioned asset paths, or routing only certain URLs to the upstream), the location block accepts regular expressions. Using regex in Nginx location and rewrite covers the ~ and ~* matchers, capture groups feeding rewrite, and the prefix-vs-regex precedence order that decides which block actually wins.
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
Bind-mount the directory at /usr/share/nginx/html and publish port 80: docker run -d -p 8080:80 -v "$(pwd):/usr/share/nginx/html:ro" nginx:alpine. Anything in the host directory is served at http://localhost:8080.
Main config: /etc/nginx/nginx.conf. Default site: /etc/nginx/conf.d/default.conf. For most use cases, mounting your own default.conf over the default is enough; replacing nginx.conf is for advanced cases.
docker exec :container_name nginx -s reload. Picks up bind-mounted config changes without dropping connections.
Yes — mount the cert and key files into the container and reference them in your nginx.conf with ssl_certificate and ssl_certificate_key. For production with Let's Encrypt, pair Nginx with an ACME helper container (Traefik, Caddy, or nginx-proxy-acme) rather than running Certbot inside the Nginx container.
Alpine. Smaller, faster pulls, fewer attack-surface packages. The full image is only worth it if you need a tool the Alpine variant doesn't include (rare).
Sources
Authoritative references this article was fact-checked against.
- Official Nginx image — Docker Hubhub.docker.com
- Nginx documentationnginx.org





