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 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 (Thruster on ports 80/443)
  • Enables Rails force_ssl / assume_ssl
  • 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 Rails by default
  • Rails does not force HTTPS itself — your proxy must redirect HTTP→HTTPS

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 Thruster passes X-Forwarded-* through to Rails.
  2. Ensure only a trusted upstream can reach the container (otherwise clients could spoof headers).
  3. Remember Thruster 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

  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.

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.


Topic Doc
Install & first login Getting started checklist
Day-2 Compose commands Operations
Kubernetes Ingress TLS Deploy on Kubernetes
OSS proxy deep-dive (port 5100) Proxies & Password Pusher
Admin settings Configuration