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.
Top comments (0)