Any home server sooner or later raises a question: how to know something is broken if you’re not looking at the dashboard. A Telegram bot is the most common answer, and it has a downside: the delivery channel is completely someone else’s, the token lives in environment variables of half your containers, and if the bot is blocked or banned you find out about problems last.
ntfy solves the same task differently: it’s a small Go server that accepts a regular HTTP POST and turns it into a push notification on your phone. No SDK, no webhook formats — curl -d “text” server/topic, and that’s it.
Below is a roadmap by levels: from “try it in thirty seconds” to “your own server with logins, tokens and topic permissions.” Each level is self-contained: you can stop where you’re satisfied and not haul in more.
Current version at the time of writing — v2.24.0 from June 4, 2026. The project is active, but releases are infrequent: previous ones — 2.23.0 (May 18) and 2.22.0 (April 21, 2026). This is a normal cadence for a tool that does one thing and has already done it.
Level 0. чужой сервер and one curl
Public ntfy.sh does not require registration or keys. A topic is created at the moment of the first send:
curl -d "Backup finished" ntfy.sh/moi-domashnii-server-x7k2
Subscribe to the same topic in the app or at https://ntfy.sh/moi-domashnii-server-x7k2 in a browser — and the message arrives.
Right here is where the main mistake happens. The documentation states this in plain text: the topic is basically a password. Whoever guesses the name can read all your notifications and can send their own. ntfy.sh/backup, ntfy.sh/home, ntfy.sh/alerts — these are public feeds that anyone subscribes to.
The topic name is limited to 64 characters, allowed letters, digits, - and _. If you stay on a public server — generate it as a password:
head -c 24 /dev/urandom | base64 | tr -d '/+='
What this level is enough for: one-off scripts, “let me know when rsync arrives,” notifications whose content you wouldn’t mind showing to strangers. For everything else — onward.
Level 1. Your ntfy in Docker
The image binwiederhier/ntfy is built for amd64, armv6, armv7 and arm64 — so it runs smoothly on a Raspberry Pi and on any mini PC.
services:
ntfy:
image: binwiederhier/ntfy
container_name: ntfy
command: serve
environment:
- TZ=Europe/Moscow
volumes:
- ./cache:/var/cache/ntfy
- ./etc:/etc/ntfy
ports:
- 8080:80
healthcheck:
test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
Minimal etc/server.yml:
base-url: "https://ntfy.example.org"
cache-file: /var/cache/ntfy/cache.db
auth-file: /var/cache/ntfy/auth.db
attachment-cache-dir: /var/cache/ntfy/attachments
Inside the container the server listens on port 80 (listen-http: ":80" by default), there is also listen-https (:443) and listen-unix — if you prefer to serve a socket directly to a proxy rather than over TCP.
cache-file — this is a SQLite database where messages reside. Without it the server runs entirely in memory: restarting the container loses the history, and subscribers who were offline won’t receive anything. Practically always keep it set.
Level 2. Behind a reverse proxy
Here lives the trap that almost everyone steps on.
If ntfy sits behind nginx, Caddy or Traefik and you do not set behind-proxy: true, the server sees all clients as a single IP — the proxy address. And ntfy’s rate limits are calculated per “visitor,” i.e. per IP. Result: one active script eats the limit for everyone, the others get 429.
behind-proxy: true
proxy-forwarded-header: "X-Forwarded-For" # default value
Second, and essential at this level, is a correct base-url. It’s used in attachment links and in emails; if it doesn’t match the real external address, attachments will arrive with links pointing nowhere.
Third — the proxy itself. ntfy keeps long connections for subscribers, so response buffering and a short proxy timeout break delivery. In nginx this is proxy_buffering off; and an increased proxy_read_timeout, in Caddy usually enough to use a regular reverse_proxy without extra settings.
Level 3. Who can do what
By default the server is open: anyone who knows the address can publish and read. Close it with one line:
auth-file: /var/cache/ntfy/auth.db
auth-default-access: "deny-all"
Values of auth-default-access:
| Value | What it means for an anonymous guest |
|---|---|
read-write |
reads and writes everything (default) |
read-only |
only subscribes |
write-only |
only sends |
deny-all |
nothing; everything requires login or token |
Next — the CLI. Commands run inside the container (docker compose exec ntfy ntfy ...):
# admin — has access to everything
ntfy user add --role=admin admin
# regular user: by default has no rights to anything
ntfy user add homeassistant
# grant rights to specific things
ntfy access homeassistant "alerts-*" write-only
ntfy access phone "alerts-*" read-only
# token instead of a password for scripts
ntfy token add --expires=90d homeassistant
Template alerts-* works — rights are granted to a group of topics, not just a single one.
A token is safer than a password not because it’s longer, but because you can revoke it individually without changing access everywhere, and you can set a lifetime. For a service that only sends, set write-only: a compromised container will then not be able to read other people’s notifications.
In version 2.24.0, by the way, a bug was fixed in this area — incorrect topic name comparisons in ACL without case sensitivity in SQLite. If you run ntfy with access controls, updating is worth it.
Sending with a token:
curl -H "Authorization: Bearer tk_xxxxxxxxxxxx" \
-d "Disk /dev/sda is 91% full" \
https://ntfy.example.org/alerts-infra
Level 4. Phone, and why iOS requires a third-party server
This is the most underrated part. The same app delivers notifications in three different ways, and the way affects latency, privacy, and battery usage.
Diagram based on docs.ntfy.sh, subscribe/phone and config sections
Android, Google Play build. Firebase is used only for topics on the public ntfy.sh. For your own servers the Firebase app is not involved at all — it maintains a direct connection. This is a direct quote from the docs, and it dispels the main concern: your own server does not “go through Google.”
Android, F-Droid build. No Firebase at all, all subscriptions are delivered instantly by default.
Instant delivery means a persistent connection, hence a foreground service and a persistent notification in the status bar. Without it, the docs warn, messages can arrive with minutes or even hours of delay. The choice here is fair and explicit: either the status-bar icon, or unpredictable delays.
iOS — a special case. Apple does not allow apps to keep a permanent connection; everything goes through APNS. Only the certificate owner — i.e., the ntfy project — can reach APNS. So for your own server in the config add:
upstream-base-url: "https://ntfy.sh"
Your server, upon receiving a message, sends a short poll request to ntfy.sh; that wakes the phone via APNS, and the phone comes to your server for the message body. What goes out is the fact of the event, not its content.
If even this isn’t satisfactory — there is the option to “log in to the web interface and subscribe there” via Web Push, which requires VAPID keys:
web-push-public-key: "..."
web-push-private-key: "..."
web-push-file: /var/cache/ntfy/webpush.db
web-push-email-address: "admin@example.org"
Keys are generated with the command ntfy webpush keys. Note that Web Push in the browser also travels through Google/Mozilla/Apple services — there is no completely isolated path to a smartphone today.
What you can put in a notification
This is what makes ntfy worth preferring over a homemade webhook: a message is not just a string.
| Title | Short aliases | What it does |
|---|---|---|
X-Title |
t, ti |
notification title |
X-Priority |
p, prio |
priority 1…5 |
X-Tags |
tag, ta |
emoji and tags |
X-Click |
Click |
URL opened on tap |
X-Actions |
Action |
up to 3 action buttons |
X-Attach |
a |
attachment by external link |
X-Markdown |
md |
enable markup |
X-Delay |
Delay |
deferred sending |
X-Email |
Email |
duplicate to email |
Priorities affect phone behavior:
| Level | Name | Behavior |
|---|---|---|
| 5 | urgent / max |
long vibration, pop-up notification |
| 4 | high |
long vibration, pop-up |
| 3 | default |
short vibration, normal |
| 2 | low |
silent and vibrate-less, hidden until the shade is opened |
| 1 | min |
silent, “under the bend” |
The most interesting part — buttons. There are three types: view (open a link), http (make an HTTP request), and broadcast (send an Android intent to Tasker and similar). A classic home server scenario:
curl -H "Title: Garage door opened for 40 minutes" \
-H "Priority: high" \
-H "Tags: warning,house" \
-H 'Actions: http, Close, https://ha.local/api/webhook/garage_close, method=POST' \
-H "Authorization: Bearer tk_xxxxxxxxxxxx" \
-d "Close right from here?" \
https://ntfy.example.org/alerts-home
One notification — and an action is performed from the lock screen, without opening Home Assistant.
Useful tips that are rarely found right away:
X-Delayaccepts30s,5m,2hor a Unix timestamp — a ready-made reminder scheduler without cron.Markdown: yesturns the body into marked-up text: a list of failing services reads much better.- Body up to 4096 bytes. More and the server will try to turn it into an attachment.
- JSON publication to the root of the server:
curl ntfy.example.org -d '{"topic":"alerts-infra","title":"…","priority":5}'— handy when you’re sending from code, not bash.
Where this plugs in in practice
- systemd. The unit adds an
OnFailure=directive that triggers a one-shot service withcurl. A service failure immediately reaches your phone. - cron and backup scripts.
curl -d "..." -H "Priority: 5"on the error branch — and you finally learn about the failed backup not a month later. - Uptime Kuma and Healthchecks. ntfy is in the notifier lists of both out of the box.
- Home Assistant. Via
rest_commandor thentfyintegration; same area — action buttons from the example above. - Grafana, Alertmanager. A regular webhook receiver; formatting is configurable via templates.
Limits you should know in advance
Even on your own server, limits are enabled — they protect against a single runaway script filling the disk.
| Parameter | Default value | Meaning |
|---|---|---|
visitor-request-limit-burst |
60 | how many requests can be sent in a burst |
visitor-request-limit-replenish |
5s | how quickly the bucket refills |
attachment-file-size-limit |
15M | maximum per file |
attachment-total-size-limit |
5G | total cache of attachments |
attachment-expiry-duration |
3h | how long an attachment lives |
visitor-attachment-total-size-limit |
100M | quota per “visitor” |
visitor-attachment-daily-bandwidth-limit |
500M | daily attachment bandwidth |
Three hours of attachment storage means ntfy is not a dump-all. A camera screenshot will reach you, the log archive will not, and should not.
Another detail for those who expose their server publicly: in 2.23.0 a limit was added on creating new topics from a single address — specifically to prevent guessing existing topics by enumeration. If you have an open server with auth-default-access: read-write, topic-name enumeration remains a fully workable attack. This is another argument for deny-all.
What ntfy does not do
To be honest: this is not a messenger and not a duty-shift notification system.
There is no encryption of messages between sender and receiver — traffic is protected by TLS, but on the server the message sits as plaintext in SQLite. No escalations like “not confirmed in 5 minutes — call the next one,” no duty rosters, no grouping and suppression of a flood of identical alerts. If you need that — look toward Alertmanager before ntfy, and treat ntfy as the transport to the last mile.
And: with your own server you take on its availability. A notification that the server is down will not come from a server that is down. The classic solution is to keep ntfy on a different machine from the one it monitors, or an external watchdog like Healthchecks.io on the side.
Sources
- docs.ntfy.sh — Installation — installation, Docker Compose, supported platforms
- docs.ntfy.sh — Configuration — all parameters, ACL, limits, Web Push, upstream
- docs.ntfy.sh — Publishing — headers, priorities, buttons, attachments
- docs.ntfy.sh — Subscribe from phone — behavior of Android builds and instant delivery
- github.com/binwiederhier/ntfy — Releases — versions and changelog
What will you reach for when the home server goes down — a Telegram bot, ntfy, Gotify, or something completely exotic? And was there a time when the notification didn’t arrive exactly when it was most needed?

