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 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 (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.
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 Thruster passesX-Forwarded-*through to Rails. - Ensure only a trusted upstream can reach the container (otherwise clients could spoof headers).
- 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
Links show http:// instead of https://
- 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.
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.
Related
| 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 |