From d10bc099c886c5ded9533a06546b773258f8783e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 29 Aug 2026 18:21:14 +0200 Subject: [PATCH 1/2] docs: add Tailscale serve and funnel as a reverse-proxy option --- docs/self-hosting/getting-started.md | 1 + docs/self-hosting/reverse-proxy.md | 10 +++ docs/self-hosting/tailscale.md | 102 +++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 docs/self-hosting/tailscale.md diff --git a/docs/self-hosting/getting-started.md b/docs/self-hosting/getting-started.md index 9053dae7c..0d7c8871d 100644 --- a/docs/self-hosting/getting-started.md +++ b/docs/self-hosting/getting-started.md @@ -171,6 +171,7 @@ NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false pnpm build | Next step | File | | ------------------------------------------------------------------- | ------------------------------------- | | TLS + reverse-proxy configuration | `docs/self-hosting/reverse-proxy.md` | +| Tailnet-only HTTPS via `tailscale serve` (no reverse proxy) | `docs/self-hosting/tailscale.md` | | Push notifications — Web Push, Telegram, ntfy, webhook, email, APNs | `docs/self-hosting/notifications.md` | | Web/worker process split for horizontal scale | `docs/self-hosting/scaling.md` | | Off-host encrypted backups to S3/R2/B2 | `docs/ops/backup-restore.md` | diff --git a/docs/self-hosting/reverse-proxy.md b/docs/self-hosting/reverse-proxy.md index d1df00629..72b1fd18c 100644 --- a/docs/self-hosting/reverse-proxy.md +++ b/docs/self-hosting/reverse-proxy.md @@ -9,6 +9,11 @@ through unbuffered with a generous read timeout. This guide gives copy-paste blocks for five common fronts. Pick one; the env-var pairing is identical across them. +If the instance only needs to be reachable from your own devices, none +of them is required: `tailscale serve` terminates trusted HTTPS on the +tailnet with no extra container. See +[`docs/self-hosting/tailscale.md`](tailscale.md). + ## Pre-flight — env vars the proxy interacts with | Variable | Required for | Notes | @@ -248,6 +253,11 @@ login works over HTTP. The trade-off is explicit: the session cookie crosses the wire unencrypted, so the network it traverses needs to be one you control. +For the Tailscale case specifically there is a better option than +lifting the flag: `tailscale serve` gives the same surface real HTTPS +with a tailnet-trusted certificate, and `SESSION_COOKIE_SECURE` stays +unset. See [`docs/self-hosting/tailscale.md`](tailscale.md). + **Do not set this on a deployment that ever serves plain HTTP to the open internet.** Anyone with passive network access can capture the session cookie and impersonate the user. The right answer for any diff --git a/docs/self-hosting/tailscale.md b/docs/self-hosting/tailscale.md new file mode 100644 index 000000000..f0fa83912 --- /dev/null +++ b/docs/self-hosting/tailscale.md @@ -0,0 +1,102 @@ +# HealthLog over Tailscale + +For a single Docker box that only you and your household need to reach, +`tailscale serve` can stand in for the reverse proxy entirely. Tailscale +terminates HTTPS with a certificate every device on your tailnet already +trusts, there is no extra container to run and nothing to renew, and the +free plan covers all of it. The trade is scope: the instance is reachable +only from devices signed into your tailnet, which for a personal health +record is usually the point. + +Two modes exist, and the difference matters more here than for most apps: + +- **`tailscale serve`** publishes HealthLog inside your tailnet only. + Every device that should reach it runs the Tailscale client and signs + into the same tailnet, including the iPhone if you use the iOS app. + The instance is invisible from the public internet. This is the right + default for a personal health record. +- **`tailscale funnel`** publishes the same HTTPS endpoint to the public + internet. No client is needed on the phone, but anyone on the internet + can reach the sign-in page, so the instance becomes a public deployment + and needs to be treated like one. Funnel is the deliberate opt-in, not + the default. + +## Set up `tailscale serve` + +1. Install Tailscale on the Docker host and sign in. On macOS, the + standalone app from tailscale.com is more full-featured than the App + Store build; either works for this page. + +2. With the compose stack running on `localhost:3000`, run: + + ```bash + tailscale serve --bg localhost:3000 + ``` + +3. If HTTPS certificates are not yet enabled for your tailnet, the CLI + says so and prints the admin-console link that enables them. Follow + that link once; it is a one-time, per-tailnet step. + +4. The CLI prints the HTTPS URL it now serves, of the form + `https://..ts.net`. `--bg` persists the + configuration across restarts, so this is a run-once command, not + something to add to a boot script. + +## Point HealthLog at the tailnet URL + +Set the URL pair in `.env` to the exact HTTPS URL the CLI printed: + +```env +NEXT_PUBLIC_APP_URL="https://your-machine.your-tailnet.ts.net" +APP_URL="https://your-machine.your-tailnet.ts.net" +``` + +Both variables are on the bundled compose `environment:` whitelist, so +values in `.env` reach the container. Restart to pick them up: + +```bash +docker compose up -d +``` + +## Cookies and TLS + +`tailscale serve` terminates real HTTPS, so the session cookie's +`Secure` flag works exactly as it does behind any TLS-terminating proxy. +Leave `SESSION_COOKIE_SECURE` unset. The plain-HTTP escape hatch in the +[reverse-proxy guide](reverse-proxy.md) exists for serving the raw +`http://host:3000` surface across a trusted network without a TLS front; +once `serve` fronts the app you do not need it. + +`serve` is a single proxy hop in front of the app, which is the topology +the default `TRUST_PROXY_HOPS=1` assumes. If the wide-event log shows a +null client IP after the switch, or every caller lands in one shared +rate-limit bucket, the "Verifying the proxy chain" section of the +reverse-proxy guide shows how to read the mismatch warning and adjust. + +## The iOS app and Apple Health + +With `serve`, the iPhone reaches the instance the same way every other +device does: install Tailscale from the App Store on the phone, sign +into the same tailnet, then point the HealthLog iOS app at the +`https://..ts.net` URL. Apple Health sync works over +this path; from the app's perspective the tailnet URL is an ordinary +HTTPS origin with a trusted certificate. + +## Public exposure with `tailscale funnel` + +If the instance must be reachable without the Tailscale client, for +example for someone outside your household's tailnet, `tailscale funnel` +publishes the endpoint to the public internet. The command shape mirrors +`serve`; follow the CLI's own output, which walks through any +admin-console enablement funnel needs on your tailnet. + +Before opening it, accept what it means: the sign-in page is now +internet-facing. Close public registration once every household member +has an account (the toggle lives in `/admin`, see +[`getting-started.md`](getting-started.md)), and keep the image current +via `pull_policy: always` as documented there. Everything the other +guides say about publicly reachable hosts applies unchanged. + +## Costs + +The free Tailscale plan is enough for everything on this page. From 1bb1ee0e555fc4e25271351f7e52073299d5533c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 29 Aug 2026 18:23:10 +0200 Subject: [PATCH 2/2] docs: the compose whitelist now carries TRUST_PROXY_HOPS The note predates the whitelist fix that landed alongside the restore permission change; keep the manual-edit instruction as a version note for older compose files. --- docs/self-hosting/reverse-proxy.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/self-hosting/reverse-proxy.md b/docs/self-hosting/reverse-proxy.md index 72b1fd18c..7f18095ec 100644 --- a/docs/self-hosting/reverse-proxy.md +++ b/docs/self-hosting/reverse-proxy.md @@ -26,11 +26,12 @@ Set the URL pair to the public origin (`https://your-instance.example.com`) and restart the `app` container. The image reads both at startup; no rebuild needed. -Note for bundled-compose users: `TRUST_PROXY_HOPS` is not on the -shipped `docker-compose.yml` `environment:` whitelist, and a var not on -the whitelist never reaches the container. If you need a non-default -value, add `TRUST_PROXY_HOPS: "${TRUST_PROXY_HOPS:-1}"` to the `app` -service's `environment:` block yourself. +All three variables are on the shipped `docker-compose.yml` +`environment:` whitelist, so a value in `.env` reaches the container +without editing the compose file. (Before v1.37.33 the whitelist was +missing `TRUST_PROXY_HOPS`; on those versions add +`TRUST_PROXY_HOPS: "${TRUST_PROXY_HOPS:-}"` to the `app` service's +`environment:` block yourself.) ### `TRUST_PROXY_HOPS` — what to set it to