Behind a reverse proxy

Terminate TLS at your own reverse proxy or load balancer and proxy HTTP to Password Pusher Pro Self-Hosted on port 80.

This article applies to: Pro Self-Hosted (Docker Compose)

By default, Self-Hosted Pro terminates TLS inside the container using Let’s Encrypt (via TLS_DOMAIN). Many environments instead terminate TLS at a corporate reverse proxy, load balancer, API gateway, or ingress controller, then forward plain HTTP to the app.

This page covers that setup for Docker Compose. For in-container port defaults and HTTP_PORT, see Container Ports. For Kubernetes Ingress, see Deploy on Kubernetes.


Layer Role
Your proxy / LB Public HTTPS endpoint, certificates, HTTP→HTTPS redirect
Password Pusher Pro HTTP only on container port 80
Forwarded headers Tell the app the original host and that the client used HTTPS
Client (HTTPS)
    → Reverse proxy / load balancer (TLS termination)
        → http://pwpush-pro:80  (or host-mapped http://127.0.0.1:80)

Do not use port 5100 for Pro. That port is used by the open-source edition. Pro Self-Hosted listens on port 80 for HTTP (and 443 only when container TLS is enabled).


Quick checklist

  1. Unset TLS_DOMAIN in .env (comment out or delete the line).
  2. Point your proxy upstream at http://<app-host>:80.
  3. Send X-Forwarded-Host, X-Forwarded-Proto, and X-Forwarded-For (and preferably X-Forwarded-Port).
  4. Optionally remove the unused 443:443 publish from docker-compose.yml.
  5. Restart: docker compose up -d.
  6. Confirm the site works over your public HTTPS URL (create a push and check the shared link).

1. Disable container TLS (TLS_DOMAIN)

Your install .env usually includes a line like:

TLS_DOMAIN=pwpush.example.com

When that variable is set, the container:

  • Obtains and serves certificates with Let’s Encrypt (built-in HTTP proxy on ports 80/443)
  • Forces HTTPS for browser sessions
  • Does not forward inbound X-Forwarded-* headers by default

For external TLS termination, remove or comment out TLS_DOMAIN:

# TLS_DOMAIN=pwpush.example.com

With TLS_DOMAIN unset:

  • The app serves HTTP on port 80 only
  • Inbound X-Forwarded-* headers are forwarded to the application by default
  • The app does not force HTTPS itself — your proxy must redirect HTTP→HTTPS
  • Page / UI links use X-Forwarded-Proto from the live request
  • Email links (auto-dispatch, invitations, password reset, etc.) default to HTTPS because background jobs have no request. Override with HOST_PROTOCOL=http only for rare plain-HTTP installs

Keep your licensed hostname assigned in Billing / Admin as usual. Clearing TLS_DOMAIN only disables container certificate management; it does not change your license domain.

After editing .env:

docker compose up -d

2. Docker Compose ports

Default install compose publishes both 80 and 443:

ports:
  - "80:80"
  - "443:443"

Option A — Proxy on the same host

Keep HTTP published (or bind it to localhost only) and drop public 443 on the app container:

ports:
  - "127.0.0.1:80:80"
  # - "443:443"   # not needed when the proxy owns TLS

Point the proxy at http://127.0.0.1:80.

Option B — Proxy on the same Docker network

Put the proxy and pwpush-pro on a shared Compose network, omit host port publishing for the app, and proxy to http://pwpush-pro:80. The healthcheck still uses http://localhost:80/up inside the container, so it continues to work without publishing ports to the host.

Option C — Leave "80:80" / "443:443" as-is

Harmless if nothing binds host 443 besides the unused mapping. The important part is that the proxy targets HTTP :80, not HTTPS :443 on the app.

Do not add a 5100:5100 mapping for Pro — the Pro image does not listen on 5100.


3. Required proxy headers

Configure your reverse proxy to set:

Header Purpose
Host Public hostname
X-Forwarded-Host Hostname the client used
X-Forwarded-Proto Must be https when the client connected with TLS
X-Forwarded-For Real client IP (audit logs, rate limiting)
X-Forwarded-Port Optional; public port (usually 443)

Without these — especially X-Forwarded-Proto: https — the app may generate http:// links, mishandle secure cookies, or record the proxy IP instead of the client.

Nginx

location / {
    proxy_pass http://127.0.0.1:80;
    proxy_http_version 1.1;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Port $server_port;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;

    proxy_set_header Connection "";
    proxy_read_timeout 3600;
}

If TLS terminates in Nginx and $scheme is unreliable in your setup, force:

proxy_set_header X-Forwarded-Proto https;

Caddy

pwpush.example.com {
    reverse_proxy pwpush-pro:80 {
        header_up X-Forwarded-Host {host}
        header_up X-Forwarded-Proto {scheme}
        header_up X-Forwarded-Port {port}
        header_up X-Real-IP {remote}
    }
}

Traefik

services:
  pwpush-pro:
    labels:
      - "traefik.enable=true"
      - "traefik.http.services.pwpush-pro.loadbalancer.server.port=80"
      - "traefik.http.routers.pwpush-pro.rule=Host(`pwpush.example.com`)"
      - "traefik.http.routers.pwpush-pro.entrypoints=websecure"
      - "traefik.http.routers.pwpush-pro.tls.certresolver=letsencrypt"

Traefik typically forwards X-Forwarded-* automatically when configured as the edge proxy.


4. Trusted proxies

Pro trusts X-Forwarded-* only from private / local ranges by default (loopback, RFC 1918, link-local, CGNAT). That covers typical Docker bridge and LAN proxies.

Proxy location Action
Same Docker network / private LAN Usually works with no extra config
Public cloud LB with a public IP as seen by the app Headers may be ignored; prefer placing the app on a private network so the LB connects from a private address

Unlike the open-source edition, Pro Self-Hosted does not expose PWP__TRUSTED_PROXIES or PWP__CLOUDFLARE_PROXY. Keep the proxy→app hop on a private network whenever possible.


If you must leave TLS_DOMAIN set and sit behind a proxy (for example, dual TLS or transitional setups):

  1. Set FORWARD_HEADERS=1 in .env so the built-in HTTP proxy passes X-Forwarded-* through to the application.
  2. Ensure only a trusted upstream can reach the container (otherwise clients could spoof headers).
  3. Remember the container may still attempt ACME on ports 80/443, which conflicts with an external TLS terminator binding those ports.

Preferred path: unset TLS_DOMAIN and terminate TLS only at the proxy.


Troubleshooting

These links are built from the live browser request.

  1. Confirm TLS_DOMAIN is unset and the container was recreated (docker compose up -d).
  2. Confirm the proxy sets X-Forwarded-Proto to https.
  3. Confirm the proxy connects from a trusted private range.

Email auto-dispatch and other mailers run in background jobs with no request, so they cannot use X-Forwarded-Proto. Current images default mailer URLs to HTTPS.

  1. Upgrade to an image that includes the HTTPS mailer default.
  2. Confirm HOST_PROTOCOL is unset, or set to https (not http).
  3. Restart: docker compose up -d.

For a rare plain-HTTP deploy (no TLS at all), set HOST_PROTOCOL=http in .env.

Audit logs show the proxy IP

The proxy must send X-Forwarded-For, and the app must trust the proxy address. With TLS_DOMAIN still set, enable FORWARD_HEADERS=1 or (better) unset TLS_DOMAIN.

Port 5100 / connection refused

You are likely following OSS proxy examples. Pro listens on 80, not 5100. Update proxy_pass / upstream / Traefik server.port accordingly.

HTTP 422 on login (Nginx Proxy Manager)

Avoid Referrer-Policy: no-referrer on Password Pusher routes. Prefer strict-origin-when-cross-origin or omit the header. See the OSS Proxies troubleshooting note for NPM details.

Healthcheck / container unhealthy

Compose healthchecks hit http://localhost:80/up inside the container. That path must stay reachable on port 80 regardless of how you publish (or don’t publish) ports to the host. If you change HTTP_PORT, override the healthcheck — see Troubleshooting and Operations — Healthcheck.

listen tcp :80: bind: permission denied

Host port remaps (e.g. "8095:80") do not change the in-container bind. On Podman (especially rootless), set HTTP_PORT and a matching publish/healthcheck. See Troubleshooting.


Topic Doc
Port map & HTTP_PORT Container Ports
Install & first login Getting started checklist
Day-2 Compose commands Operations
Port bind / remap errors Troubleshooting
Kubernetes Ingress TLS Deploy on Kubernetes
OSS proxy deep-dive (port 5100) Proxies & Password Pusher
Admin settings Configuration