Technology Sep 01, 2026 · 5 min read

Setting up nginx as a reverse proxy

If you run more than one service on a server, you need a single entry point in front of them. Instead of setting up a web server and SSL inside every service, it gets handled once, in one place. We will use jwilder/nginx-proxy. It watches the Docker socket and builds nginx config automatically, bas...

DE
DEV Community
by Yaroslav
Setting up nginx as a reverse proxy

If you run more than one service on a server, you need a single entry point in front of them. Instead of setting up a web server and SSL inside every service, it gets handled once, in one place.

We will use jwilder/nginx-proxy. It watches the Docker socket and builds nginx config automatically, based on labels and env vars on your containers. No manual nginx.conf editing every time you add a service.

Prerequisites:

  • A server with Docker and the Docker Compose plugin installed.
  • A domain name pointed at the server (needed later for SSL).
  • Basic comfort with docker compose files.

Base setup: nginx-proxy

The core piece is the nginx-proxy container itself. It exposes ports 80 and 443, and reads the Docker socket to see which containers are running. When a container starts or stops, it updates the routing on its own.

services:
  nginx-proxy:
    image: nginxproxy/nginx-proxy
    container_name: nginx_proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - html:/usr/share/nginx/html
      - certs_volume:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - /srv/nginx-vhosts:/etc/nginx/vhost.d:ro
    networks:
      - webproxy

The docker socket mount lets nginx-proxy see new containers as they appear. The vhost.d mount is a folder for custom nginx snippets per domain, we will use this later to lock down some endpoints from public access.

Adding HTTPS with acme-companion

Plain HTTP is not enough for anything public, so we add acme-companion, which pairs with nginx-proxy and handles Let's Encrypt certificates.

  acme-companion:
    image: nginxproxy/acme-companion
    container_name: acme_companion
    environment:
      - DEFAULT_EMAIL=${EMAIL}
    volumes:
      - certs_volume:/etc/nginx/certs:rw
      - acme:/etc/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock:ro
    volumes_from:
      - nginx-proxy
    networks:
      - webproxy
    restart: unless-stopped

It shares the certs volume with nginx-proxy through volumes_from, so both containers see the same certificate files.

Full docker-compose.yml

Let's put both containers, volumes, and networks together:

version: '3.8'

services:
  nginx-proxy:
    image: nginxproxy/nginx-proxy
    container_name: nginx_proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - html:/usr/share/nginx/html
      - certs_volume:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - /srv/nginx-vhosts:/etc/nginx/vhost.d:ro
    networks:
      - webproxy

  acme-companion:
    image: nginxproxy/acme-companion
    container_name: acme_companion
    environment:
      - DEFAULT_EMAIL=${EMAIL}
    volumes:
      - certs_volume:/etc/nginx/certs:rw
      - acme:/etc/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock:ro
    volumes_from:
      - nginx-proxy
    networks:
      - webproxy
    restart: unless-stopped

volumes:
  certs_volume:
  acme:
  html:

networks:
  webproxy:
    external: true

The webproxy network is marked external, meaning it is not created by this compose file. We create it once by hand, and every service that wants to sit behind the proxy joins this same network.

Environment variables

.env:

EMAIL=admin@example.com

EMAIL is used by acme-companion as the default contact address for Let's Encrypt. Fill it in with a real address, because Let's Encrypt sends expiry warnings there.

Creating the network

Since the network is external, create it before the first deploy:

docker network create webproxy

Run this once per server. Every stack that needs to be reachable through the proxy connects to this network.

Creating the vhost directory

nginx-proxy mounts /srv/nginx-vhosts as a read only folder for per-domain nginx snippets. Services publish their own config fragments here, e.g. to restrict access to internal routes. Create the folder and give your user ownership:

sudo mkdir -p /srv/nginx-vhosts
sudo chown $USER:$USER /srv/nginx-vhosts

Connecting a service to the proxy

Any container can join the proxy without touching nginx directly. Add a few env vars and put the container on the webproxy network.

version: '3.8'

services:
  myapp:
    build: .
    container_name: myapp
    restart: unless-stopped
    environment:
      - VIRTUAL_HOST=${DOMAIN}
      - VIRTUAL_PORT=8000
      - LETSENCRYPT_HOST=${DOMAIN}
      - LETSENCRYPT_EMAIL=${EMAIL}
    networks:
      - webproxy

networks:
  webproxy:
    external: true

VIRTUAL_HOST tells nginx-proxy which domain routes to this container. VIRTUAL_PORT is the port your app listens on inside the container. LETSENCRYPT_HOST and LETSENCRYPT_EMAIL tell acme-companion to request a certificate for that domain.

.env for the service:

DOMAIN=localhost
EMAIL=admin@example.com

Replace localhost with your real domain before deploying.

Custom nginx config for a service

Sometimes the default routing is not enough. A route needs an IP allow list, custom headers, or something else nginx-proxy does not expose through env vars. For this, place a config snippet into /srv/nginx-vhosts/${DOMAIN}, and nginx-proxy loads it automatically for that domain.

A generic template, filled in by a deploy script using envsubst:

location /internal/ {
    allow 10.0.0.0/8;
    deny all;

    proxy_pass         http://${DOMAIN};
    proxy_set_header   Host              $http_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;
    proxy_set_header   X-Forwarded-Port  $server_port;
    proxy_http_version 1.1;
}

Now, only requests from the allowed range reach the route, everything else gets a 403. For a full example of a deploy script that fills in a template like this, see this article.

Conclusion

We now have one nginx-proxy container in front of everything, and it picks up new services on its own, no manual nginx edits needed. Certificates renew automatically through acme-companion, and any route that needs special rules gets its own config. From here, any service just joins the webproxy network and gets a domain and SSL for free.

DE
Source

This article was originally published by DEV Community and written by Yaroslav.

Read original article on DEV Community
Back to Discover

Reading List