I wanted to know when my media stack grabbed something, without routing my notifications through Discord or a third-party push service. The answer was ntfy, self-hosted.
ntfy in a container
ntfy is a dead-simple pub/sub notification server: publish to a topic over HTTP, subscribe from a phone or browser app. Self-hosted, it's one container behind Traefik. I set it to deny-all by default with a single user scoped to one topic, so it isn't an open relay.
That default matters more than it sounds. Out of the box ntfy is deliberately wide open, because the hosted service is meant to be: any topic name works and anyone who guesses it can read or write. On your own box, set the default access to deny-all, create a user, and grant that user read-write on exactly the topics it needs. A topic name is not a secret, and an open relay on your own domain is somebody else's spam problem to hand you.
Publishing is a plain HTTP POST, which is why everything can talk to it:
curl -u user:pass \
-H "X-Title: Disk space" \
-H "X-Priority: high" \
-H "X-Tags: warning" \
-d "Root volume at 91%" \
https://ntfy.example.com/homelab
The headers are the whole API surface. Priority drives whether your phone buzzes, tags become the icon, and X-Click puts a URL behind the notification so tapping it opens the dashboard that raised it.
Wiring up the *arr apps
Sonarr, Radarr, Lidarr and Readarr all have a native ntfy connection under Settings, Connect. Point it at the server, set the topic, pick your events (grab, import, upgrade, health), done.
The gotcha that cost me an hour
Creating the connection via the API kept failing with:
Unable to send test message: Value cannot be null. (Parameter 'source')
The *arr ntfy connector calls .Select() on its Tags field, and if you omit that field it's null, so the whole request throws before a single packet leaves the box. The message names source because that is what LINQ calls the collection it was handed, which is why the error tells you nothing about tags. The fix: include an empty tags array in the payload even if you don't use tags. Once I did, every test message went straight to my phone.
One more thing if you use iOS. Instant delivery on iPhone goes through Apple's push network, which a server on your own domain has no way to reach, so a purely self-hosted instance cannot wake the app. The workaround built into ntfy is to point your server at the public instance as an upstream purely for the wake-up poke, which carries no message content. Android has no such problem, since the app can hold its own connection open.
No Discord, no cloud, no monthly anything. Just my own little notification bus.




