Skip to content

Behind a proxy

Point your proxy at the published WEB_PORT and let it terminate HTTPS. Two variables in .env then have to follow, and the stack has to be restarted: PUBLIC_URL is the public address you serve the app on, and COOKIE_SECURE=true marks the session cookie HTTPS-only.

The upstream depends on where the proxy itself runs.

The published port is reached at 127.0.0.1:8080. With BIND_ADDRESS=127.0.0.1 it listens on that address only, which closes the instance to everything but the proxy.

The application takes requests up to 25 MB, the size a JSON export of a full instance can reach and still has to be re-imported at. nginx accepts 1 MB by default and answers anything larger with its own 413 Request Entity Too Large page. The request never reaches Badlen, so the application log holds nothing about it. Put client_max_body_size 25m; in the proxied location to match the two.

Badlen’s own server only speaks HTTP/1.1, and over HTTP/1.1 a browser fetches six files at a time. The first screen is two dozen files, so they arrive in waves, each paying the network’s delay. A proxy serving HTTP/2 sends them all at once. On a phone over a slow 4G connection, measured on the Overview: the first paint came at 3.6 s instead of 5.7 s, and the screen was complete at 8.4 s instead of 13.5 s.

nginx needs http2 on; in the server block that listens on 443 (listen 443 ssl http2; before nginx 1.25.1). Caddy serves HTTP/2 by default. Reaching the published port directly stays on HTTP/1.1, so the gain is only had through the proxy.

Tell the app how many proxies are in front

Section titled “Tell the app how many proxies are in front”

Behind a proxy every request seems to come from the proxy, and two features read the address a request comes from. Rate limits are counted per address, so the ten-a-minute limit on the routes that check a password becomes ten a minute for the whole household, and one person mistyping twice locks everybody else out. And each session under Settings → Security is shown at the address it was opened from, so every device in the house is listed at the same one.

TRUSTED_PROXY_HOPS=1 gives both of them the reader’s address again. The number is one for a single nginx or Caddy, one more for each further proxy of yours behind it. It is read as a count from the end of the X-Forwarded-For header, because each proxy appends the address it received the request from: with one proxy in front, the last address in that header is the one your proxy wrote. A client inventing a header of its own only pushes that invention further left, which is why the count comes from you and not from the header.

Too low and the outer proxy is what gets counted. Too high and the count would reach past your proxies into the part of the chain a client writes, so the number alone is not trusted. With one proxy nothing more is needed: the app reads the address that proxy wrote. With two or more, name the proxies behind the first in TRUSTED_PROXY_ADDRESSES, by address or CIDR range, comma-separated (TRUSTED_PROXY_ADDRESSES=172.18.0.0/16): every hop between the address read and the app has to be one of them. Being on a private network is not enough, since a client in the house has a private address too. A count that reaches further than the proxies named, like one that reaches further than the header, falls back to the connecting address and the log says so, once a minute:

Terminal window
docker compose logs app | grep TRUSTED_PROXY_HOPS

COOKIE_SECURE=true is the one setting that can send you back to the sign-in page for ever. The browser keeps a cookie marked that way on an HTTPS origin and throws it away on a cleartext one, localhost excepted. Reaching the same instance directly at http://<lan-ip>:8080 while it is set signs you in and loses the session on the spot: the sign-in answers 201, the app opens, the first navigation lands back on the sign-in page, and nothing anywhere says why. Go through the proxy, or set COOKIE_SECURE=false while there is no HTTPS in front.

One last thing the browser decides alone: with a certificate it does not trust, a self-signed one included, you lose the installable app. It refuses to register the service worker and prints an SSL error in the console. The interface and the data are unaffected.