How to Install ntfy (Push Notifications to Your Phone from Any Script)

Email (tutorial 49) is fine for reports. For "the backup finished", "someone's at the front door" or "disk at 91%" you want a buzz in your pocket, now. ntfy (pronounced notify) is a tiny server where sending a notification is one curl to a URL, and the phone app shows it instantly. Self-hosted, no account with anyone, and every tool in this series can talk to it.
Quick reference
| Task | What |
|---|---|
| Folder | ~/docker/ntfy/compose.yaml |
| Web / API | http://server-ip:8090 |
| Send | curl -d "Backup done" https://ntfy.example.com/server |
| Subscribe | ntfy app (iOS, Android) → + → topic server, your server URL |
| Priority, title, tags | -H "Priority: high" -H "Title: Disk" -H "Tags: warning" |
| Lock it down | NTFY_AUTH_DEFAULT_ACCESS: deny-all + users |
1. The compose file
mkdir -p ~/docker/ntfy && cd ~/docker/ntfy
nano compose.yaml
services:
ntfy:
image: binwiederhier/ntfy:latest
container_name: ntfy
command: serve
ports:
- "8090:80"
environment:
TZ: Europe/London
NTFY_BASE_URL: https://ntfy.example.com
NTFY_CACHE_FILE: /var/cache/ntfy/cache.db
NTFY_AUTH_FILE: /var/lib/ntfy/user.db
NTFY_AUTH_DEFAULT_ACCESS: deny-all
NTFY_BEHIND_PROXY: "true"
NTFY_ATTACHMENT_CACHE_DIR: /var/cache/ntfy/attachments
NTFY_ENABLE_LOGIN: "true"
volumes:
- ./cache:/var/cache/ntfy
- ./data:/var/lib/ntfy
restart: unless-stopped
deny-all means nobody can send or read without a user — the right default once it's reachable from outside.
docker compose up -d
2. Users and topics
docker compose exec ntfy ntfy user add --role=admin john
docker compose exec ntfy ntfy user add scripts
docker compose exec ntfy ntfy access scripts "server" write-only
docker compose exec ntfy ntfy token add scripts
That's you as admin (read everything), a scripts user that can only write to the server topic, and a token for it to use. Save the token in .env on the server (tutorial 59).
3. The phone app
Install ntfy (App Store / Play Store). Settings → Default server: https://ntfy.example.com (through the reverse proxy with HTTPS — tutorial 28 — or the LAN address if you'll only use it at home over the VPN). Log in as john. + → Subscribe to topic: server.
On iOS, push notifications need the proxy to be reachable from the internet (Apple relays them); on Android the app keeps its own connection and works over the VPN.
4. Send something
curl -H "Authorization: Bearer tk_your_token" -d "Hello from the server" https://ntfy.example.com/server
Phone buzzes. Now the useful forms:
# title, priority, emoji tags
curl -H "Authorization: Bearer $NTFY_TOKEN" -H "Title: Backup" -H "Priority: low" -H "Tags: white_check_mark" \
-d "restic finished in 4m12s" https://ntfy.example.com/server
# urgent, with a button
curl -H "Authorization: Bearer $NTFY_TOKEN" -H "Priority: urgent" -H "Tags: rotating_light" \
-H "Actions: view, Open Grafana, https://grafana.example.com" \
-d "Disk at 91%" https://ntfy.example.com/server
# a picture (Frigate snapshot, say)
curl -H "Authorization: Bearer $NTFY_TOKEN" -T /tmp/front_door.jpg -H "Filename: door.jpg" https://ntfy.example.com/server
Put a notify() function at the top of your scripts and call it at the end. In cron, && notify "ok" || notify "FAILED".
5. Connect the rest
- Uptime Kuma: Settings → Notifications → ntfy, with the URL, topic and token.
- Home Assistant: the ntfy integration (or a rest_command) — "person at front door" with the Frigate snapshot.
- Watchtower: WATCHTOWER_NOTIFICATION_URL: ntfy://:token@ntfy.example.com/server
- Grafana: a webhook contact point to https://ntfy.example.com/server with the Authorization header.
- Any Android app can post to it via Tasker; any script in any language can curl.
Troubleshooting
"unauthorized". Topic access isn't granted to that user, or the token is wrong. ntfy access lists the rules.
iPhone gets nothing when the app is closed. The server must be reachable over HTTPS from the internet for Apple's push relay; LAN-only works for Android.
Notifications delayed on Android. Battery optimisation killed the app's connection. Exempt ntfy in Android's battery settings, or enable the app's instant delivery option.
Through the proxy, "Bad Request" on the web UI. Missing NTFY_BEHIND_PROXY and the proxy's X-Forwarded-* headers.
Wrapping up
One container, a user and a write-only token, the app subscribed to a topic. From then on any script, cron job, monitor or automation reaches your pocket with a single curl — and the "did the backup run?" question answers itself every morning.
