Merge pull request 'docs(ntfy): ntfy publishes a host port for the external reverse proxy' (#37) from fix/ntfy-published-port into main

Reviewed-on: #37
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
This commit is contained in:
2026-07-22 08:59:19 +00:00
2 changed files with 25 additions and 12 deletions

View File

@@ -169,9 +169,12 @@ none (keeps §11's zero-interaction promise).
registration and every publish validate it is HTTPS and its origin is in the shard's ntfy registration and every publish validate it is HTTPS and its origin is in the shard's ntfy
allow-set (`NTFY_BASE_URL` / `NTFY_ALLOWED_ORIGINS`), rejecting loopback/private hosts. allow-set (`NTFY_BASE_URL` / `NTFY_ALLOWED_ORIGINS`), rejecting loopback/private hosts.
- **ntfy** added to `website/docker-compose.yml` as a pinned upstream image with a committed - **ntfy** added to `website/docker-compose.yml` as a pinned upstream image with a committed
declarative `./ntfy/server.yml` and named volume, **no published host port** (reached via the declarative `./ntfy/server.yml` and named volume, **published on a host port** (`NTFY_HOST_PORT`,
reverse proxy; internal-only for the publisher), anonymous read-write to unguessable topics (no default `2586` → container `:80`) so the public reverse proxy — which runs *outside* the compose
per-user accounts — safe because tickles are content-free). network — can forward the notification subdomain to it, anonymous read-write to unguessable topics
(no per-user accounts — safe because tickles are content-free). Both the app (SSE subscribe) and the
backend (POSTing tickles to registered device endpoints) reach ntfy on that public origin, so all
ntfy traffic transits the proxy — there is no separate internal publish port.
**Part 2 — the Android app — ✅ LANDED** (2026-07-20, `RunicGateway/Android-app#15` + a small **Part 2 — the Android app — ✅ LANDED** (2026-07-20, `RunicGateway/Android-app#15` + a small
`RunicGateway/website#79` settings addition + this docs PR). Built exactly to the plan below, with `RunicGateway/website#79` settings addition + this docs PR). Built exactly to the plan below, with
@@ -911,8 +914,11 @@ first release. Users **opt in per stream**: nothing is pushed unless subscribed.
path/tag at implementation. path/tag at implementation.
- **All config is declarative** — a committed `ntfy` config file and/or `NTFY_*` env vars baked into - **All config is declarative** — a committed `ntfy` config file and/or `NTFY_*` env vars baked into
compose. No `docker exec`, no interactive `ntfy user add`, no post-deploy manual steps. Bringing the compose. No `docker exec`, no interactive `ntfy user add`, no post-deploy manual steps. Bringing the
stack up provisions a working push relay. Reachable to devices via the existing reverse proxy on its stack up provisions a working push relay. Reachable via the existing reverse proxy on its own
own hostname/path; internal-only for the backend publisher. hostname/path — the container publishes `:80` on a host port (`NTFY_HOST_PORT`, default `2586`)
because the proxy runs *outside* the compose network and can only reach a service through a
published host port (the same reason `app` publishes `3000`). Both devices and the backend publisher
reach ntfy on that public origin.
- **No per-user ntfy accounts to administer.** The security model (below) removes the need for ntfy ACL - **No per-user ntfy accounts to administer.** The security model (below) removes the need for ntfy ACL
provisioning, which is exactly what keeps setup interaction-free. ntfy topics are the random, provisioning, which is exactly what keeps setup interaction-free. ntfy topics are the random,
unguessable endpoints UnifiedPush hands out; the backend treats ntfy as an **untrusted relay**. unguessable endpoints UnifiedPush hands out; the backend treats ntfy as an **untrusted relay**.
@@ -1002,8 +1008,10 @@ it is a UX convenience, not a v1 requirement — deferred at M3, descoped at M6;
See [`APP_LINKS.md`](./APP_LINKS.md). See [`APP_LINKS.md`](./APP_LINKS.md).
- ntfy: exact upstream image + pinned tag (Part-1 landed the compose service — confirm the tag), and - ntfy: exact upstream image + pinned tag (Part-1 landed the compose service — confirm the tag), and
its reverse-proxy hostname/path. The hostname must land in `NTFY_ALLOWED_ORIGINS` before M7 Part 2 is its reverse-proxy hostname/path. The hostname must land in `NTFY_ALLOWED_ORIGINS` before M7 Part 2 is
end-to-end testable (the app registers an endpoint on that origin; the SSRF guard rejects others). No end-to-end testable (the app registers an endpoint on that origin; the SSRF guard rejects others). The
backend publish token — **decided** (the content-free-tickle design does not require one; optional reverse proxy forwards that hostname to the ntfy container's published host port (`NTFY_HOST_PORT`,
default `2586`) — the container publishes `:80` because the proxy runs outside the compose network.
No backend publish token — **decided** (the content-free-tickle design does not require one; optional
`NTFY_PUBLISH_TOKEN` is honored if ever set). `NTFY_PUBLISH_TOKEN` is honored if ever set).
- FCM flavor: build it for the Play release or ship Play on UnifiedPush too? Decide at M8. (The M7 - FCM flavor: build it for the Play release or ship Play on UnifiedPush too? Decide at M8. (The M7
Part 2 `PushTransport` seam keeps this swap cheap.) Part 2 `PushTransport` seam keeps this swap cheap.)

View File

@@ -511,11 +511,13 @@ subsystem (`[server]`, `[http]`, `[db]`, `[auth]`, `[admin]`, `[ratelimit]`, …
`env_file: .env`, `DB_HOST=db`, `depends_on: db (healthy)`, volume `uploads:/app/uploads`, `env_file: .env`, `DB_HOST=db`, `depends_on: db (healthy)`, volume `uploads:/app/uploads`,
`ports: "3000:3000"`**binds 0.0.0.0** (no `127.0.0.1:` prefix) so Pangolin reaches it. `ports: "3000:3000"`**binds 0.0.0.0** (no `127.0.0.1:` prefix) so Pangolin reaches it.
- `ntfy` (M7): pinned upstream `binwiederhier/ntfy` image, declarative config only - `ntfy` (M7): pinned upstream `binwiederhier/ntfy` image, declarative config only
(`./ntfy/server.yml` mounted `:ro` + `NTFY_BASE_URL`), volume `ntfydata:/var/lib/ntfy`, **no (`./ntfy/server.yml` mounted `:ro` + `NTFY_BASE_URL`), volume `ntfydata:/var/lib/ntfy`,
published host port** — devices reach it via the reverse proxy; the backend publisher reaches it **publishes `:80` on a host port** (`${NTFY_HOST_PORT:-2586}:80`, binds 0.0.0.0) so Pangolin — which
over the private compose network. Anonymous read-write to unguessable topics (no accounts to runs outside the compose network — can forward the notification subdomain to it, the same reason
provision) — safe because pushes are content-free tickles. Bringing the stack up provisions a `app` publishes `3000`. Both devices (SSE subscribe) and the backend publisher (POSTing tickles to
working push relay with **zero interactive setup**. registered device endpoints) reach ntfy on that public origin. Anonymous read-write to unguessable
topics (no accounts to provision) — safe because pushes are content-free tickles. Bringing the stack
up provisions a working push relay with **zero interactive setup**.
- Volumes: `dbdata`, `uploads`, `ntfydata`. - Volumes: `dbdata`, `uploads`, `ntfydata`.
Express listens on `0.0.0.0:${PORT||3000}`. Pangolin terminates TLS and proxies to `app`. Express listens on `0.0.0.0:${PORT||3000}`. Pangolin terminates TLS and proxies to `app`.
@@ -548,6 +550,9 @@ NTFY_BASE_URL=https://ntfy.example.com
# shows push as unavailable for the shard. # shows push as unavailable for the shard.
NTFY_PUBLIC_URL=https://ntfy.example.com NTFY_PUBLIC_URL=https://ntfy.example.com
NTFY_ALLOWED_ORIGINS=https://ntfy.example.com NTFY_ALLOWED_ORIGINS=https://ntfy.example.com
# Host port the ntfy container publishes :80 on (default 2586); the reverse proxy
# forwards the notification subdomain to host:NTFY_HOST_PORT. Change on a conflict.
NTFY_HOST_PORT=2586
``` ```
`.gitignore`: `node_modules/`, `.env`, `_reference/`, `client/dist/`, `uploads/`. `.gitignore`: `node_modules/`, `.env`, `_reference/`, `client/dist/`, `uploads/`.