TechEarl

How to Run Nginx in Docker

Serve static files, mount a custom nginx.conf, run as non-root, and the practical patterns: dev server, reverse proxy in front of an app, and the SSL gotcha most tutorials skip.

Ishan KarunaratneIshan Karunaratne⏱️ 5 min readUpdated
Share thisCopied

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:

bash
docker run -d --name nginx -p 8080:80 nginx:alpine

Visit http://localhost:8080 and you see Nginx's default welcome page.

Serve your own static files by bind-mounting a directory:

bash
docker run -d --name nginx \
  -p 8080:80 \
  -v "$(pwd)/site:/usr/share/nginx/html:ro" \
  nginx:alpine

Drop any files into ./site on the host and Nginx serves them.

Try it with your own values

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:

bash
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_version

A minimal nginx.conf for a single-page-app fallback:

nginx
server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

Reload after config changes without restarting:

bash
docker exec :container_name nginx -s reload

Run 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:

bash
docker run -d --name :container_name -p :host_port:8080 nginxinc/nginx-unprivileged:alpine

The 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.

nginx
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:

yaml
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":

bash
# 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:

bash
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_version

For 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 nginx user inside the container has UID 101. If the host directory's permissions don't allow it to read, files 404 or 403. Use :ro on 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 reload to re-read.
  • Port 80 already in use. Pick a different host port: -p 8080:80. Inside the container, Nginx still listens on 80.
  • unprivileged variant listening on 8080, not 80. Adjust your -p accordingly.

FAQ

Sources

Authoritative references this article was fact-checked against.

TagsDockerNginxWeb ServerReverse ProxyDevOps

Found this useful? Pass it on.

Copied
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

H

How to Run Apache in Docker

Apache httpd in a Docker container: serve static files, mount a custom httpd.conf, enable mod_rewrite for .htaccess, and the patterns that come up most often (PHP, reverse proxy, virtual hosts).

Using regex in Nginx with location blocks and the rewrite directive: location modifier priority, the rewrite directive flags, return-based redirects, and copy-paste config for HTTPS redirects, www normalization, trailing slashes, 301 redirects, clean URLs, and blocking by user-agent or IP.

How to Use Regex in Nginx (location and rewrite)

Use regex in Nginx with location blocks and the rewrite directive: how location modifiers and matching priority work, why return beats rewrite for redirects, and copy-paste config for HTTPS, www, trailing slashes, 301s, clean URLs, and access blocking.