Nginx + Docker cheatsheet.

Quick run

docker run -d -p 80:80 nginx
docker run -d -p 80:80 -v $(pwd)/html:/usr/share/nginx/html:ro nginx
docker run -d -p 80:80 -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro nginx

Custom Dockerfile

FROM nginx:1.27-alpine

# Custom config
COPY nginx.conf /etc/nginx/nginx.conf
COPY conf.d/ /etc/nginx/conf.d/

# Static content
COPY html/ /usr/share/nginx/html/

# Health
HEALTHCHECK --interval=10s --timeout=3s \
    CMD wget --spider -q http://localhost/health || exit 1

Compose

services:
  nginx:
    image: nginx:1.27-alpine
    ports: ["80:80", "443:443"]
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./conf.d:/etc/nginx/conf.d:ro
      - ./html:/usr/share/nginx/html:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - app
    restart: unless-stopped

ENV substitution (templates)

nginx official image: /etc/nginx/templates/*.template are envsubst’d at startup.

# default.conf.template
server {
    listen 80;
    server_name ${SERVER_NAME};
    
    location / {
        proxy_pass http://${APP_HOST}:${APP_PORT};
    }
}
docker run -d \
  -e SERVER_NAME=example.com \
  -e APP_HOST=app \
  -e APP_PORT=8000 \
  -v $(pwd)/template.conf:/etc/nginx/templates/default.conf.template \
  -p 80:80 \
  nginx

reload after config change

docker exec nginx nginx -t
docker exec nginx nginx -s reload

Or use compose to volume-mount config + reload externally.

Watchtower for auto cert renewal

Cert renewals (via certbot in another container) write to a volume; nginx reloads:

services:
  nginx:
    volumes:
      - certs:/etc/letsencrypt
  
  certbot:
    image: certbot/certbot
    volumes:
      - certs:/etc/letsencrypt
    entrypoint: sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h; done'

Add a reloader script that runs docker exec nginx nginx -s reload after renewal.

Custom Dockerfile for static site

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

Caddyfile alternative (TLS auto)

FROM caddy:2-alpine
COPY Caddyfile /etc/caddy/Caddyfile
COPY html /srv

Easier than nginx for auto-HTTPS, smaller config.

Run as non-root

FROM nginxinc/nginx-unprivileged:1.27-alpine

Variant that runs as UID 101, listens on 8080.

services:
  nginx:
    image: nginxinc/nginx-unprivileged:1.27-alpine
    ports: ["8080:8080"]

Forwarding to compose service

upstream app {
    server app:8000;       # docker compose service name
}

Networks for service-name DNS.

hostnames inside container

Docker bridges typically not on host. Use host.docker.internal (Docker Desktop) or 172.17.0.1 (default bridge) to reach host services.

Logging

services:
  nginx:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

By default, nginx official image symlinks access.log and error.log to stdout/stderr. Picked up by docker logs.

Healthcheck endpoint

location = /health {
    access_log off;
    add_header Content-Type text/plain;
    return 200 "ok\n";
}
healthcheck:
  test: ["CMD", "wget", "--spider", "-q", "http://localhost/health"]
  interval: 10s
  timeout: 3s

Reverse proxy compose stack

services:
  nginx:
    image: nginx:1.27-alpine
    ports: ["80:80", "443:443"]
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - certs:/etc/letsencrypt:ro
    depends_on: [api, web]
  
  api:
    build: ./api
    expose: ["8000"]
  
  web:
    build: ./web
    expose: ["3000"]

expose makes ports available within the network without publishing to host.

Performance: bypass docker network for cheap

Use unix sockets across containers:

services:
  nginx:
    volumes: [socket:/run/sock]
  app:
    volumes: [socket:/run/sock]
    command: gunicorn --bind unix:/run/sock/app.sock

volumes:
  socket:
upstream app {
    server unix:/run/sock/app.sock;
}

Common mistakes

  • Mounting whole /etc/nginx over config — wipes templates.
  • Forgetting :ro on config — accidental edits.
  • nginx user UID mismatch with mounted file owner.
  • default.conf overriding everything when you wanted incremental.
  • Logging to file inside container → vanishes on restart.

Read this next

If you want my nginx + docker compose stacks, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .