compose: say which of the three deployment shapes you're in

The operator asked why `THOUGHTSYNC_BIND` isn't just defaulted to the safe value.
Fair question, and the answer exposed that my own advice was incomplete: I told
them to set it to 127.0.0.1 without asking where their proxy runs, and for a
proxy inside Docker that is the wrong fix.

There are three shapes, not two:

1. **LAN, no proxy** — the default. Binds every interface so a phone and a desktop
   can reach the server. This is why the default is NOT the locked-down value: a
   server reachable only from the machine it runs on isn't hardened, it's broken,
   and that is the primary documented use of this app.
2. **Proxy in Docker** — delete the `ports:` block entirely. The proxy reaches the
   app over the compose network; publishing a host port is a second,
   unauthenticated way in that bypasses whatever the proxy does about TLS. Safer
   than 127.0.0.1, because there is no host port to reach even from the host.
3. **Proxy on the host** — `THOUGHTSYNC_BIND=127.0.0.1`.

The compose file now spells out all three where the decision is made, and
`docs/public-hosting.md` item 4 asks where your proxy runs before telling you what
to do, plus how to check: `curl http://<lan-ip>:5000/api/health` from another
machine should NOT answer once you're proxied.

No default changed. Changing it would silently break every LAN install on the next
`docker compose pull` — the phone would just stop syncing, with nothing saying why.
This commit is contained in:
2026-08-23 15:30:01 -04:00
parent 09b5f874b6
commit abe01da5f7
2 changed files with 37 additions and 9 deletions
+21 -3
View File
@@ -71,10 +71,28 @@ services:
# Uploaded attachments. /var/thoughtsync is fixed in the app (Config.DATA_DIR),
# not configurable — mount it or lose every image on container recreation.
- thoughtsync-data:/var/thoughtsync
# WHERE THE APP IS REACHABLE FROM. Three shapes, and the right answer is
# different for each — the default serves the first.
#
# 1. LAN, no proxy (the default). Binds every interface so your phone and your
# desktop can reach the server. This is what makes a self-hosted install work
# out of the box, and it is why the default is NOT the locked-down value: a
# server only reachable from the machine it runs on is not hardened, it is
# broken.
#
# 2. Reverse proxy in Docker, on this network (Traefik discovering the container,
# an nginx container, etc). DELETE the `ports:` block below entirely. The proxy
# reaches the app over the compose network without any port being published,
# and publishing one is a second, unauthenticated way in that bypasses the
# proxy — including whatever the proxy is doing about TLS and auth.
#
# 3. Reverse proxy on the HOST (not in Docker). Set THOUGHTSYNC_BIND=127.0.0.1 in
# .env, so the port exists but only the host itself can reach it.
#
# If you are exposing this to the internet, you want 2 or 3. Leaving it at 1
# means the app is reachable directly on port 5000, past everything your proxy
# does.
ports:
# Default binds every interface, which is what lets desktop clients on the LAN
# reach it. Behind a reverse proxy, set THOUGHTSYNC_BIND=127.0.0.1 so only the
# proxy can talk to it.
- "${THOUGHTSYNC_BIND:-0.0.0.0}:${THOUGHTSYNC_PORT:-5000}:5000"
healthcheck:
# python rather than curl: the runtime image is python:3.12-slim and carries no