Contents

Ntfy: Self-Hosted Push Notifications That Replace Twelve SaaS Webhooks

Send a phone notification with a single curl, from anything, to anywhere

Contents

Count the ways your homelab currently tries to get your attention. The backup script emails you, which means you now check email for alerts, which means you’ve made email worse. Prometheus pages through some webhook you configured eighteen months ago and no longer remember the shape of. One app posts to a Discord server you joined only for its notifications, another insists on Telegram, a third wants a Pushover token you bought once and can’t find. Each new tool arrives with its own notification mechanism bolted on, and pretty soon your phone is being prodded by half a dozen different SaaS middlemen — several of which you’re quietly trusting with messages like “the front door sensor opened at 3am.” That is a lot of third parties in the loop for the crime of telling you a disk is full.

Ntfy is the antidote, and it’s the rare piece of software that’s exactly as simple as it claims. It’s a stupidly simple pub-sub notification service: things publish messages to named topics, your phone subscribes to those topics, and notifications appear. The genius is entirely in the publishing interface — it’s just HTTP. If a thing can run curl, it can send you a push notification. No SDK to import, no API client to keep up to date, no account with anybody, no OAuth dance. That constraint is the whole design, and it’s why ntfy ends up absorbing every other notification channel you own.

How simple is “simple”

Advertisement

This simple. To send yourself a notification, you POST a message body to a topic URL:

1
curl -d "Backup finished, 4.2 GB written" ntfy.sh/my-backups-a8f3

That’s a working notification on your phone, assuming you’ve subscribed to the my-backups-a8f3 topic in the app. There’s no registration step and no schema; topics are created on first use, which is also why — on the public server — you pick an unguessable topic name. Anyone who knows the name can read and write it, so a topic called alerts on ntfy.sh is effectively a public bulletin board, whereas alerts-7f3a9c is functionally private through obscurity until you move to authenticated self-hosting.

You can dress the message up with headers for a title, a priority, tags (which render as emoji), and even tap-actions:

1
2
3
4
5
6
7
curl \
  -H "Title: Disk almost full" \
  -H "Priority: urgent" \
  -H "Tags: warning,floppy_disk" \
  -H "Click: https://grafana.example.com/d/disk" \
  -d "/ is at 92% on nas01" \
  https://ntfy.example.com/alerts

That Priority: urgent will buzz through a phone’s do-not-disturb if you grant it the permission, which is exactly what you want for “the array is degrading” and exactly what you don’t want for “backup finished” — and ntfy lets you set that per topic on the phone. The Click header makes tapping the notification jump straight to the relevant dashboard. All of that, from a shell one-liner. That’s the entire appeal, and it doesn’t get more complicated as you scale up; it just gets used in more places.

Self-hosting it

The hosted ntfy.sh is genuinely generous and free, and it’s the right place to start. But the whole point of this exercise is owning your own infrastructure — and once your messages include phrases like “garage door open” or “VPN login from a new IP”, you really do want to be the only human who can see them. This is the same reasoning that pushes people off convenient hosted everything in the first place: the per-seat, per-account, your-data-on-their-servers model of SaaS is exactly what a homelab exists to escape, and a notification pipe carrying your security events is a place you feel that acutely. The server is, mercifully, a single Go binary:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
services:
  ntfy:
    image: binwiederhier/ntfy:latest
    command: serve
    environment:
      NTFY_BASE_URL: https://ntfy.example.com
      NTFY_AUTH_FILE: /var/lib/ntfy/user.db
      NTFY_AUTH_DEFAULT_ACCESS: deny-all
    volumes:
      - ./ntfy_cache:/var/cache/ntfy
      - ./ntfy_lib:/var/lib/ntfy
    ports:
      - "80:80"

Setting NTFY_AUTH_DEFAULT_ACCESS: deny-all is the single most important line for a private instance. It flips the server from its permissive default — anyone can read and write any topic — to “nobody can, until you explicitly grant them.” You then create users and hand out per-topic access with the ntfy user add and ntfy access commands, so your alerts topic is genuinely, cryptographically yours rather than obscure-name-yours. Put a real reverse proxy with TLS in front of it in production; the bare 80:80 above is fine on a trusted LAN but you don’t want unauthenticated alerts crossing the open internet in cleartext.

Where it slots into everything

Advertisement

The reason ntfy ends up at the centre of a homelab is that almost everything can already talk to it, either natively or through a generic webhook. Alertmanager, Healthchecks, Grafana, the various *arr apps — most either ship an ntfy integration or accept a webhook you can point straight at ntfy’s API. Uptime Kuma, in particular, has a first-class ntfy notification target, so the “is this service up” checks land in the same app as everything else with about four clicks. Suddenly all those scattered notification channels collapse into one app on your phone, organised by topic — alerts, backups, doorbell, whatever you like — each one muted, prioritised, and coloured independently.

Two features quietly turn it from a toy into a daily driver. First, there’s a proper Android and iOS app (and a web app) that holds a persistent connection open, so notifications are genuinely instant rather than polled on a five-minute cron. Second, ntfy can also receive: you can publish from your phone, or subscribe a script to a topic so that a message triggers an action somewhere. That makes it a tiny bidirectional message bus, not merely a one-way pager — I have a topic that, when I publish to it from my phone, kicks off a job on a server, which is a two-line shell subscription and no infrastructure at all.

Wiring it into a real script

The theory is nice; here’s what it looks like in anger. Say you have a nightly backup and you want to know two things: that it ran, and — far more importantly — if it failed. A silent backup that stopped working three weeks ago is the worst kind of failure, so the pattern I use publishes success quietly and failure loudly, with a priority that will actually wake me:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/bin/env bash
set -euo pipefail
NTFY="https://ntfy.example.com/backups"

if restic backup /data >/tmp/backup.log 2>&1; then
  size=$(grep -oP 'Added \K[0-9.]+ \w+' /tmp/backup.log | tail -1)
  curl -s -H "Tags: white_check_mark" \
       -d "Backup OK, ${size:-unknown} added" "$NTFY"
else
  curl -s -H "Priority: urgent" -H "Tags: rotating_light" \
       -H "Title: BACKUP FAILED" \
       --data-binary @/tmp/backup.log "$NTFY"
fi

Two things make that better than an email alert. The failure case sends the actual log as the body via --data-binary @file, so the notification isn’t “something broke, go and SSH in to find out” — it’s the error itself, on my phone, with the priority to interrupt me. And the success case is deliberately low-key with no priority header, so it lands in the app’s history for reassurance without buzzing. That distinction — loud failures, quiet successes — is most of what good alerting is, and ntfy makes expressing it a one-line difference rather than a configuration project.

For Prometheus, Alertmanager talks to ntfy through its generic webhook receiver, or more cleanly through a tiny bridge, but the shape is the same idea: the alert’s labels become the title and the tags, the severity becomes the priority. Once you’ve done it once for backups, every other alerting source in the lab is the same three headers pointed at a different topic.

How it compares to the alternatives

To be fair to the incumbents you’re replacing: Discord and Telegram bots work, Pushover is reliable and cheap, Gotify is a close self-hosted cousin, and Apprise is a Python library that abstracts over all of them at once. Ntfy’s distinct advantage is the publishing model. Every alternative wants an SDK, a bot token, a chat ID, or a client library; ntfy wants a curl. That means anything that can make an HTTP request — a cron job, a router’s limited firmware, a systemd OnFailure hook, a one-line shell script inside a container — can notify you with zero dependencies added. Gotify is the closest and genuinely good, but ntfy’s tag-to-emoji rendering, tap-actions, and bidirectional topics edge it ahead for me, and the mobile apps feel more finished in daily use. If you’re already deep in Apprise, ntfy is simply one more target it supports, so it isn’t even an either/or — you can point Apprise at a self-hosted ntfy and keep every integration you already wrote, which is about as low-friction a migration path as any tool offers.

The honest caveats

It’s a notification pipe, not a mail server, and pretending otherwise leads to disappointment. There’s no threading, no read receipts, no clever inbox management. Messages are cached for a configurable window and then they’re gone — it’s built for transient alerts, not for an archive you’ll grep in a year. On iOS specifically, instant delivery leans on infrastructure that means messages briefly transit Apple’s push servers and ntfy’s own, which the project documents honestly rather than hiding; if that transit bothers you for genuinely sensitive alerts, the workarounds exist but they cost you some of the simplicity that made you choose ntfy. And, as ever with a project that carries a strong single-maintainer flavour, you are placing a bet on its continued health — mitigated by the fact that it’s Apache-2.0 licensed and you can self-host the exact version you have, forever, if upstream ever stalls.

A couple of practical gotchas to save you an evening. If self-hosted notifications simply never arrive on the phone, check that NTFY_BASE_URL matches the URL the app is actually subscribed to — a mismatch there breaks the push registration silently. And if deny-all locks you out too, that’s working as designed: you haven’t created a user and granted access yet, so add yourself with ntfy user add before wondering why your own topic rejects you.

The verdict

If your homelab nags you through more than two different channels, ntfy will measurably improve your life within an afternoon. It’s the connective tissue I didn’t know my setup was missing: one app, one server, one curl pattern, and every script, alert, and sensor I own funnels its shouting into a single tidy place. I self-hosted it on a whim and now it’s load-bearing — the backups, the disk alerts, the “something logged into the VPN” pings all land in the same app, sorted by topic, instant, and answerable to no third party but me. For a tool you can fully understand in five minutes, that’s a genuinely remarkable amount of value, and it’s one of the very few things I’d tell any homelabber to install this week rather than “eventually”.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.