diff --git a/docker-compose.yml b/docker-compose.yml index 26ed5ac..1e0fd25 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docs/public-hosting.md b/docs/public-hosting.md index 604d41c..8dbc7c6 100644 --- a/docs/public-hosting.md +++ b/docs/public-hosting.md @@ -54,13 +54,23 @@ number of proxies you actually run means a forged prefix can never be selected. too HIGH and it starts trusting entries no proxy of yours wrote; too low and several callers share one rate-limit bucket, which is merely inconvenient. -**4. Stop publishing the app port.** The default compose binds `0.0.0.0:5000` so LAN -clients can reach it directly. Behind a proxy that is a second, unprotected front -door. In `.env`: +**4. Stop reaching the app except through the proxy.** The default compose binds +`0.0.0.0:5000` so LAN clients can reach it directly — which is right for a LAN install +and wrong the moment there is a proxy in front, because it leaves a second way in that +bypasses everything the proxy does. -``` -THOUGHTSYNC_BIND=127.0.0.1 -``` +Which fix depends on where your proxy runs: + +- **Proxy in Docker** (Traefik discovering the container, an nginx container): delete + the `ports:` block from `docker-compose.yml`. The proxy reaches the app over the + compose network; no published port is needed at all, and this is the safest of the + two because there is no host port to reach even from the host. +- **Proxy on the host**: set `THOUGHTSYNC_BIND=127.0.0.1` in `.env`, so the port + exists but only the host itself can use it. + +To check which you have: `docker compose ps` shows the published ports, and +`curl http://:5000/api/health` from another machine tells you whether the +app is still answering around the proxy. It should not be. **5. Have a backup that includes the files.** Attachments are files on the `thoughtsync-data` volume, not rows — a `pg_dump` restores notes whose images are all