Merge pull request #850 from MBombeck/docs/tailscale

docs: add Tailscale serve and funnel as a reverse-proxy option
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-08-29 19:02:39 +02:00
commit 8c7ae08fe4
3 files changed
+119 -5

No files matched your search

+1
View File
@@ -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` |
+16 -5
View File
@@ -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 |
@@ -21,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
@@ -248,6 +254,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
+102
View File
@@ -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://<machine>.<tailnet>.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://<machine>.<tailnet>.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.