Skip to content

Nginx Proxy Manager (NPM) + Docker — Cheat Sheet

A practical reference for wiring up NPM to proxy any Docker container, based on real troubleshooting.


1. The Golden Rule

NPM must be able to reach your backend container over the network, by name, on its internal port.

Not the host IP. Not the published/mapped port. The container name and the port the app listens on inside the container.

Why: if NPM and your backend container are on different Docker networks (which happens by default — each docker-compose.yml project gets its own isolated network), traffic from NPM has to go out to the host and back in, which is fragile and often silently blocked by firewalls (ufw, DOCKER-USER iptables chain, etc). Putting them on the same Docker network avoids that hop entirely.


2. One-Time Setup: Shared Network

Create one Docker network that NPM and every backend service will join:

docker network create proxy-net

Use one consistent name across your whole homelab (proxy-net, web, whatever) — it's the thread that ties every stack to NPM.


3. Compose File Pattern

NPM's docker-compose.yml

services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '80:80'
      - '81:81'
      - '443:443'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    networks:
      - default
      - proxy-net

networks:
  proxy-net:
    external: true

Any backend service's docker-compose.yml

services:
  myapp:
    image: some/image:latest
    restart: unless-stopped
    # no "ports:" needed unless you also want to access it directly on the LAN
    networks:
      - default        # keep this if it talks to its own db/redis/etc
      - proxy-net

networks:
  proxy-net:
    external: true

Apply with:

docker compose up -d

(run in each project's folder — NPM's and every backend's)

Tip: once a service is on proxy-net, you generally don't need to publish its port to the host at all (ports: section) unless something other than NPM needs to reach it directly. Less exposed surface = better.


4. Configuring the Proxy Host in NPM

In NPM's UI → Proxy Hosts → Add Proxy Host:

Field Value
Domain Names yourapp.yourdomain.com
Scheme http — almost always, since most containers don't serve TLS internally
Forward Hostname/IP container name (e.g. vaultwarden), not the host IP
Forward Port the port the app listens on inside the container (check the docs/image, not the host-published port)
Websockets Support enable if the app uses websockets (Vaultwarden, many chat/collab apps, etc.)
Block Common Exploits usually fine to enable
SSL tab request a Let's Encrypt cert, force SSL, enable HTTP/2

5. Finding a Container's Internal Port

Don't guess — check:

docker port <container_name>

Shows internal_port/tcp -> host_ip:host_port. The internal_port (left side) is what you put in NPM's Forward Port field — not the host-side number.

If nothing's published and you're not sure, check the image's documentation/Dockerfile for EXPOSE, or:

docker inspect <container_name> --format '{{json .Config.ExposedPorts}}'

6. Diagnostic Toolkit

When a proxy host isn't working, isolate the problem layer by layer:

# 1. Can NPM even resolve/reach the backend container?
docker exec -it nginx-proxy-manager sh
curl -v http://<container_name>:<internal_port>

# 2. What network is each container actually on?
docker inspect <container_name> --format '{{json .NetworkSettings.Networks}}' | python3 -m json.tool

# 3. Is a Docker network accidentally isolated from everything else?
docker network inspect <network_name>
# look for "Internal": true — that would explain total unreachability

# 4. Check NPM's own error log for the proxy host
docker logs nginx-proxy-manager
# or: NPM UI → Proxy Host → Error Log tab

# 5. Host-level firewall interfering with Docker traffic (if still going host-IP route)
sudo ufw status verbose
sudo iptables -L DOCKER-USER -n -v

Reading the symptoms

Symptom Likely cause
curl from inside NPM just hangs on "Trying..." Packet reaching nowhere — different Docker network, or firewall silently dropping it
curl gives immediate "Connection refused" Reached the right host, but nothing is listening on that port — wrong port number
NPM shows 502 Bad Gateway NPM reached something but got an invalid/empty response — usually wrong internal port, or scheme mismatch (https configured against a plain http backend)
Works via curl from the host but not from NPM container Cross-network/firewall issue — the fix in this cheat sheet (shared proxy-net)
Browser shows cert errors but backend response otherwise fine SSL/cert issue on NPM's side, unrelated to the forwarding path — check the SSL tab in NPM

7. Quick Migration Checklist (existing stacks → shared network)

For every container currently proxied by NPM via host IP:

  • [ ] Add proxy-net (external) to that service's compose file
  • [ ] Add proxy-net (external) to NPM's compose file (only needs doing once)
  • [ ] docker compose up -d in that service's folder
  • [ ] Confirm with docker inspect <container> --format '{{json .NetworkSettings.Networks}}' that it's now on proxy-net
  • [ ] In NPM, change Forward Hostname/IP from the host IP to the container name
  • [ ] Change Forward Port from the host-published port to the container's internal port
  • [ ] Test with curl from inside the NPM container before testing in the browser
  • [ ] Save the proxy host, confirm it loads correctly