fix: ntfy listens on the Tailscale address; monitor publishes to it directly
Security / Dependency vulnerabilities (push) Successful in 44s
Security / Static analysis (push) Successful in 9s
CI / Format & lint (push) Successful in 10s
CI / Test suite (push) Successful in 26s
Security / License report (push) Successful in 50s
Docs / Validate documentation (push) Successful in 16s

The reverse proxy is a separate VPS on the tailnet, so a loopback-only
ntfy was unreachable from it. install_ntfy.sh now binds the host's Tailscale
IP (NTFY_LISTEN overrides). New NTFY_PUBLISH_URL: where the monitor POSTs,
separate from the public NTFY_SERVER subscribers see, so an alert never
waits on DNS or the proxy (first cycle logged 502s from Cloudflare while
the domain was not yet proxied).
This commit is contained in:
2026-09-12 00:28:29 +02:00
parent 039d24a5c3
commit f4d42c90f4
5 changed files with 47 additions and 27 deletions
+20 -17
View File
@@ -60,12 +60,14 @@ cd /opt/thailand-water-monitor
NTFY_DOMAIN=ntfy.buildfor.life bash scripts/install_ntfy.sh
```
This installs the ntfy .deb, writes `/etc/ntfy/server.yml` (listen on
`127.0.0.1:2586`, anonymous read, token-only write, 72 h message cache,
signup/login/metrics off, tight visitor limits), enables the systemd unit,
This installs the ntfy .deb, writes `/etc/ntfy/server.yml` (listen on the
host's Tailscale address, port 2586; anonymous read, token-only write, 72 h
message cache, signup/login/metrics off, tight visitor limits), enables the
systemd unit,
creates the `monitor` user with **write-only access to `ping-*`**, mints a
token, and appends `NTFY_SERVER` / `NTFY_TOPIC_PREFIX` / `NTFY_TOKEN` to
`.env` if they are not there yet. Then:
token, and appends `NTFY_SERVER` (public URL for subscribers),
`NTFY_PUBLISH_URL` (loopback, what the monitor POSTs to), `NTFY_TOPIC_PREFIX`
and `NTFY_TOKEN` to `.env` if they are not there yet. Then:
```bash
systemctl restart water-monitor
@@ -73,30 +75,31 @@ journalctl -u water-monitor -n 20 | grep ntfy # "ntfy notifications: https:
curl -s 'https://ntfy.buildfor.life/ping-status/json?poll=1' # anonymous read works
```
Put `https://ntfy.buildfor.life` in front of `127.0.0.1:2586` with whatever
already terminates TLS for `water.buildfor.life`. Subscribers hold a
long-lived connection, so the proxy needs websockets on and no short read
timeout:
The reverse proxy is a separate VPS on the same tailnet, so ntfy listens on
the monitor host's Tailscale address and nothing is exposed on a public
interface. On the Caddy machine:
```caddyfile
ntfy.buildfor.life {
reverse_proxy 127.0.0.1:2586
reverse_proxy <monitor tailscale ip>:2586
}
```
Cloudflare tunnel: add a public hostname `ntfy.buildfor.life`
`http://127.0.0.1:2586`. Cloudflare proxies websockets by default; nothing
else to set.
Caddy proxies websockets and keeps long-poll connections open by default;
subscribers hold one open. `behind-proxy: true` makes ntfy rate-limit on
`X-Forwarded-For` rather than treating every subscriber as the proxy.
Nothing about the message pipeline needs the domain to be public before you
test: with `NTFY_SERVER=http://127.0.0.1:2586` in `.env` the monitor
publishes locally and `curl .../ping-status/json?poll=1` shows what went out.
Publishing does not depend on the domain: `NTFY_PUBLISH_URL` points the
monitor at the Tailscale address directly, so a DNS or proxy problem never
holds back an alert. Test the pipeline before the domain is live with
`curl -s 'http://<tailscale ip>:2586/ping-status/json?poll=1'`.
## Configuration
| Variable | Default | Meaning |
|---|---|---|
| `NTFY_SERVER` | *(empty = off)* | base URL of the ntfy server the monitor publishes to |
| `NTFY_SERVER` | *(empty = off)* | public base URL subscribers use; shown on the dashboard |
| `NTFY_PUBLISH_URL` | = `NTFY_SERVER` | where the monitor POSTs; the local ntfy address (`http://<tailscale ip>:2586`), so publishing never waits on DNS/proxy |
| `NTFY_TOPIC_PREFIX` | `ping` | first segment of every topic |
| `NTFY_TOKEN` | *(empty)* | bearer token if the server requires auth to publish (it does, see above) |
| `PUBLIC_URL` | `https://water.buildfor.life/` | click-through target in messages |