Pingtower Docs
Docs Self-hosting Reverse proxy & TLS

Reverse proxy & TLS

The daemon serves plain HTTP on localhost. One line of Caddy, or a small nginx block, puts a certificate in front of it.

Caddy

/etc/caddy/Caddyfile:

alerts.example.com {
    reverse_proxy 127.0.0.1:8391 {
        header_up X-Real-IP {remote_host}
    }
}

Caddy issues and renews the certificate itself. The header_up line is for client addresses.

nginx

server {
    listen 443 ssl http2;
    server_name alerts.example.com;
    ssl_certificate     /etc/letsencrypt/live/alerts.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/alerts.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8391;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_read_timeout 60s;
    }
}
Long-poll needs a patient proxyGET /v1/pull?wait=25 holds the connection open for up to 25 seconds. A proxy read timeout under 30 seconds turns every quiet poll into a 504. Caddy’s default is fine; nginx’s default of 60 seconds is fine; a stricter default in front of either is not.

Client addresses

Sign-up, sign-in, account deletion, password-reset requests and seat redemption are rate-limited per client address. Behind a proxy on the same host, every request reaches the daemon from 127.0.0.1, so the daemon cannot tell callers apart: it skips the per-client limit altogether, keeps the per-email limits, and counts everyone’s failed sign-ins as one caller’s. Set -client-ip-header X-Real-IP (or PINGTOWER_CLIENT_IP_HEADER=X-Real-IP) to the header the proxy sets, as both examples above do.

The daemon reads that header only from a loopback peer, so a caller who reaches the port directly cannot spoof it, and it takes the rightmost address if the header holds a list. Name a header your proxy sets on every request, overwriting whatever the client sent, not one it passes through. A name that is not a valid header stops startup with invalid configuration.

Behind Cloudflare

Works as-is. Set SSL mode to Full (strict) and give the origin a Cloudflare origin certificate, which is how api.pingtower.com itself runs. Every request then arrives from a Cloudflare edge, so have the proxy take the client address from CF-Connecting-IP for requests from Cloudflare’s published ranges only (nginx’s real_ip module does this), and set X-Real-IP from the result.

Check it

curl -s https://alerts.example.com/healthz
# {"ok":true,"role":"self-host","tenants":1,"version":"…"}
Last updated 24 Sep 2026 Report a problem with this page