Custom Reverse Proxy
Run Portr behind your own reverse proxy such as Caddy or nginx with manually managed TLS certificates, instead of the bundled Caddy service.
The production compose file bundles a Caddy service that provisions wildcard certificates via DNS-01 (Cloudflare or Route53) and proxies traffic to Portr. This is a convenience, not a requirement — Portr has no dependency on Caddy and never terminates TLS itself. If you already run a reverse proxy on your server, or your DNS provider doesn't support automated DNS-01 validation, you can bring your own proxy and certificates.
How traffic is routed
Portr listens on plain HTTP/TCP behind the proxy:
| Port | Purpose |
|---|---|
8000 | Admin dashboard and API, serves example.com |
8001 | Tunnel proxy, serves *.example.com |
2222 | SSH ingress for tunnel clients (raw TCP, never proxied) |
Your reverse proxy needs exactly two HTTPS vhosts: the apex domain to port 8000 and the wildcard to port 8001. Per-subdomain routing happens inside Portr based on the Host header.
TLS must terminate at your proxy. Portr assumes it is served over HTTPS in
production — session cookies are marked Secure and tunnel URLs are
generated with https://.
Remove the bundled Caddy
Remove the caddy service, the labels block, and the caddy_data volume from docker-compose.prod.yaml:
You can also drop CLOUDFLARE_API_TOKEN from your .env file — it is only consumed by the Caddy labels.
The server service uses network_mode: "host", so ports 8000, 8001, and 2222 are bound directly on the host and reachable at localhost from your proxy.
Example: Caddy with your own certificates
If you already run Caddy, add two site blocks. The tls directive with certificate paths disables automatic provisioning — no DNS plugin or API token needed:
Example: nginx
Proxy requirements
Whatever proxy you use, on the wildcard vhost:
- Forward the
Hostheader verbatim, without a port. Portr routes to tunnels by extracting the subdomain fromHost; forwardingsub.example.com:443results in a 404. - Pass websocket upgrades through (
Upgrade/Connectionheaders). - Don't buffer responses — streaming responses and server-sent events depend on it.
- Don't intercept error responses or add proxy-level authentication. Portr's health checks rely on its own 404 responses and
X-Portr-*headers reaching clients unmodified, and theAuthorizationheader is passed end to end to your local service. - Set
X-Forwarded-Proto: httpsand overwriteX-Forwarded-Forfrom the real client address so tunneled applications generate correct absolute URLs and can't be fed spoofed client IPs. - The tunnel proxy speaks HTTP/1.1 only.
The admin dashboard must be served from the root of the apex domain — it cannot be mounted under a path prefix.
Certificates
- The certificates must cover both
example.comand*.example.com(a single cert with both SANs, or one per vhost). - Use certificates from a trusted CA. The Portr client verifies TLS when talking to the server (
portr auth set, request replay), and the server's own health checks dialhttps://<subdomain>.<domain>— self-signed certificates break these.
DNS and firewall
DNS records must point directly at the server. Don't route them through a proxying CDN — tunnel clients connect to port 2222 over raw TCP:
| Type | Name | Value |
|---|---|---|
| A | @ | your-server-ipv4 |
| A | * | your-server-ipv4 |
Firewall rules:
| Port | Action |
|---|---|
| 80/443 | Allow (your proxy) |
| 2222 | Allow (SSH tunnel ingress) |
| 30001-40001 | Allow if you use TCP tunnels |
| 5432, 8000, 8001 | Block from the internet (postgres and portr only need local access) |
| 20000-30000 | Block from the internet |
Ports 20000-30000 are used internally for HTTP tunnel connections and bind on all interfaces. If left open, tunnel traffic can be reached directly over plain HTTP, bypassing your proxy and TLS. Make sure your firewall blocks them.
Troubleshooting
- 404 with an
X-Portr-Errorheader on tunnel URLs: theHostheader is being rewritten or forwarded with a port. Forward it verbatim. - Login loop on the admin dashboard: the dashboard is being served over plain HTTP. The session cookie is
Secure-only; serve it over HTTPS. - Websockets disconnect after ~60 seconds: your proxy's read timeout is closing idle connections. Increase it (e.g.
proxy_read_timeoutin nginx).
Cloudflare API Token
Create a Cloudflare API token for Portr to issue wildcard TLS certificates, enabling HTTPS on every subdomain of your tunnel server.
GitHub OAuth App Setup
Set up a GitHub OAuth app for Portr admin login, so your team can authenticate to the self-hosted dashboard using their GitHub accounts.