3 min read · Ingress

Nginx Proxy Manager

One proxy in front of every stack, on a shared external network.

Several stacks need port 80. A shared external network lets NPM own the port and route by hostname, so the same compose files work in development and under Docker Swarm.

Create the shared network

# Development
docker network create gateway

# Swarm
docker network create --driver overlay --attachable gateway

The proxy stack

services:
  app:
    image: jc21/nginx-proxy-manager:latest
    restart: unless-stopped
    ports:
      - '80:80'
      - '443:443'
      - '127.0.0.1:81:81'   # admin UI stays on loopback
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    networks:
      - gateway

networks:
  gateway:
    external: true
docker compose up -d                      # development
docker stack deploy -c docker-compose.yml proxy   # swarm

Binding the admin UI to 127.0.0.1 keeps it off the internet. Reach it over an SSH tunnel: ssh -L 8081:localhost:81 <user>@<host>.

App side

Remove ports: from each app’s nginx service — apps talk to NPM over the gateway, not to the host.

services:
  nginx:
    image: ghcr.io/${REPO}/nginx:latest
    networks:
      - default   # to reach the app's own web service
      - gateway   # to reach NPM

networks:
  gateway:
    external: true

Adding a proxy host

Admin UI → Proxy Hosts → Add Proxy Host. Forward hostname is the service name under Swarm (zaideih_nginx) or the container name under Compose (zaideih-nginx-1); forward port is 80.

SSL

  • HTTP-01 — one click, but needs port 80 reachable from the internet.
  • DNS-01 — works behind a firewall or on a local VM, and needs an API token from the DNS provider. This is the one to use with Cloudflare Tunnel, since no port is open.

Removing it

docker service update --network-rm gateway zaideih_nginx
docker stack rm proxy
docker network rm gateway

docker network inspect gateway    # if "network is in use"