Skip to content

Networking & Reverse Proxy

Trobar speaks plain HTTP on its container port and relies on a TLS-terminating reverse proxy for HTTPS. The container port must never be exposed directly to the internet.

Reverse proxy

Any proxy works — Traefik, Caddy, nginx. The docker-compose.yaml in this repo binds the app only to 127.0.0.1:5000 and expects your own reverse-proxy config in front of it; it doesn't ship configuration for any specific proxy. The app honours X-Forwarded-Proto / -Host from the proxy and reads X-Forwarded-For for per-IP rate limiting, so the proxy must set those headers.

In forward auth mode the proxy carries an even heavier responsibility: it must authenticate and gate every route, because the app performs no auth of its own in that mode.

Trusted proxy and rate limiting

Trusting X-Forwarded-For only makes sense if a trusted proxy is the one setting it — TROBAR_TRUSTED_PROXY (default *, meaning "any peer") is what tells waitress which one that is.

This same trust is what the per-IP brute-force backoff (login, device pairing, device-token auth) relies on to know who a request is really from. If this container's port is reachable by anything other than your reverse proxy, an attacker can send their own X-Forwarded-For and rotate it every request, and the rate limiter never accumulates a failure against any one bucket. It looks identical to a limiter that's simply never been triggered; nothing about it is loud.

* is the correct, sufficient default for the docker-compose.yaml this repo actually ships — it binds the container port to 127.0.0.1:5000 and expects your own reverse proxy in front of it. That loopback bind is a real barrier (nothing on the network, only a process on the host itself, can reach the port), just a narrower one than a fully internal-only Docker network with no published port at all — an alternative topology you can set up yourself. Don't treat pinning as a hardening step to reach for by default on either topology; it's for a deployment that can't get some form of that isolation in the first place (the port genuinely reachable from elsewhere on the LAN or the internet).

A pin that doesn't match is worse than leaving it at *

When it fails to match, waitress strips X-Forwarded-For from every request rather than trusting none of it selectively — REMOTE_ADDR then stays the proxy's own address for every real user behind it, and they all collapse into one shared rate-limit bucket. One household member mistyping a password, or one bot hitting /login, can lock out everyone else for the backoff window. It fails in the confusing direction: everything works until someone gets a 429 they can't explain.

If you do need to pin it: getting the value right

Do not use 127.0.0.1. It looks like the obvious value for the loopback-bound port above, but it's wrong: Docker's port publishing doesn't preserve the connecting address as seen by the container. Behind a 127.0.0.1:<port>:<port> mapping, a request to 127.0.0.1:5000 on the host arrives inside the container from a Docker bridge gateway address (something in the 172.x.0.1 shape), never from 127.0.0.1. waitress matches by raw string equality against the connecting socket peer — never a hostname, container name, DNS lookup, or "the address I dialed" — so 127.0.0.1 here never matches anything, ever.

The value that actually matches is your own deployment's Docker bridge
gateway — find it with:

```bash
docker network inspect $(docker compose ps -q trobar \
  | xargs docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{end}}') \
  --format '{{ (index .IPAM.Config 0).Gateway }}'
```

It's stable across docker compose up --force-recreate (the network itself isn't recreated, only the container), but it's assigned by Docker per project/host, not a fixed value you can copy from these docs — re-run the command above and update TROBAR_TRUSTED_PROXY if you ever fully tear the network down (docker compose down) rather than just recreating the container.

If you're instead running a genuinely internal-network-only topology (your
own reverse proxy joined to a Docker network with no published port at
all), pin to that proxy's container IP on that network the same way — the
same "exact match, verify it, re-check after a full network teardown"
rules apply.

If the app is reachable through a public hostname (proxy + TLS), make LAN clients resolve that hostname directly to the server's LAN IP:

  • AdGuard Home / Pi-hole: add a DNS rewrite for trobar.example.com → LAN IP.
  • Or your router's DNS override, if it has one.

Same hostname, same TLS certificate, zero app configuration — but sync traffic at home goes straight to the server instead of out and back through your WAN.

Private DNS bypasses this

A phone using Private DNS (DoT/DoH) bypasses your local resolver and keeps using the public route. Either disable Private DNS on the home Wi-Fi or accept the detour.