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.
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.
Recommended architecture
| 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
- Unset
TLS_DOMAINin.env(comment out or delete the line). - Point your proxy upstream at
http://<app-host>:80. - Send
X-Forwarded-Host,X-Forwarded-Proto, andX-Forwarded-For(and preferablyX-Forwarded-Port). - Optionally remove the unused
443:443publish fromdocker-compose.yml. - Restart:
docker compose up -d. - 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-Protofrom the live request - Email links (auto-dispatch, invitations, password reset, etc.) default to HTTPS because background jobs have no request. Override with
HOST_PROTOCOL=httponly 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.
5. Optional: keep TLS_DOMAIN (not recommended)
If you must leave TLS_DOMAIN set and sit behind a proxy (for example, dual TLS or transitional setups):
- Set
FORWARD_HEADERS=1in.envso the built-in HTTP proxy passesX-Forwarded-*through to the application. - Ensure only a trusted upstream can reach the container (otherwise clients could spoof headers).
- 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
Page / UI links show http:// instead of https://
These links are built from the live browser request.
- Confirm
TLS_DOMAINis unset and the container was recreated (docker compose up -d). - Confirm the proxy sets
X-Forwarded-Prototohttps. - Confirm the proxy connects from a trusted private range.
Email links show http:// instead of https://
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.
- Upgrade to an image that includes the HTTPS mailer default.
- Confirm
HOST_PROTOCOLis unset, or set tohttps(nothttp). - 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.
Related
| 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 |