ntfy Module
The ntfy module runs a self-hosted ntfy notification server on your rig and configures it to send push notifications to your phone.
Security: tailnet-IP binding
ntfy must never bind to 0.0.0.0
Abysslink enforces that ntfy binds only to your Tailscale tailnet IP address.
Binding to 0.0.0.0 or a public IP would expose your notification server to
the internet. A wildcard bind is a fatal abysslink doctor finding (exit 2).
The listen address is derived from tailscale ip --4 at apply time.
What it configures
Native mode (Linux):
- Installs ntfy via the system package manager
- Writes
~/.config/ntfy/server.ymlwith: listen-http: "<tailnet-ip>:2586"base-url: "http://<tailnet-ip>:2586"auth-file: ~/.local/state/abysslink/ntfy/user.dbauth-default-access: "deny-all"- Installs a user service (
dev.abysslink.ntfy, managed viasystemctl --user)
Docker mode (macOS, when Docker Desktop is available):
- Writes
~/.config/ntfy/server-docker.ymland runs theabysslink-ntfycontainer (image pinned by digest, not a floating tag) - ntfy listens on
0.0.0.0:80inside the container; the host binding is restricted to the tailnet IP viadocker run -p <tailnet-ip>:2586:80 base-urluses the MagicDNS hostname so phone URLs survive IP changes
Authentication is enabled in both modes: auth-default-access: "deny-all",
with an admin user whose password is generated on first apply and stored in
the OS keychain (never written to disk or placed on argv). Rotate it with
abysslink rotate ntfy-creds --apply.
No topic is pre-created — ntfy topics are implicit. The notify module's default
topic is rig (modules.notify.default_topic).
Configuration
Those are the only two keys the schema accepts — the config loader uses strict
decoding, so unknown keys under modules.ntfy fail the whole config load.
Phone setup
Because the server runs deny-all auth, an anonymous subscription to a topic URL is rejected. Enroll the phone instead:
This mints a tagged auth key and walks through pairing step by step (install
Tailscale → join tailnet → subscribe → save credentials), delivering the
per-device credentials the phone needs. Add --qr to render the credentials as
scannable QR codes. The server is reachable only while the phone is on your
tailnet.
abysslink doctor checks
| Check | Failure behaviour |
|---|---|
installed — ntfy present |
fatal on Linux (doctor exits 2); warning on macOS (Docker installs it on apply, or Docker Desktop is missing) |
config_exists — ~/.config/ntfy/server.yml present |
warning (doctor exits 1) |
listen_address — config binds a wildcard (0.0.0.0, [::], :PORT) |
fatal (doctor exits 2); skipped in Docker mode, where the -p flag enforces the tailnet binding |
reachable — ntfy answers GET /v1/health on <tailnet-ip>:<port> |
warning; catches the Docker-Desktop-for-Mac trap where the container runs but the port never publishes |
docker_ip_drift — running ntfy Docker container is bound to a stale tailnet IP (macOS Docker mode) |
warning; the container's -p binding is fixed at creation time, so a Tailscale IP reassignment leaves it reachable only on the old address — run abysslink repair --apply to rebind. Emitted only on macOS when the container is running and the current tailnet IP resolves |