You’ve got a Docker container that’s leaking memory at 3 AM. Your backup job failed silently. Watchtower just nuked your prod image. And you’re not home to fix it.
This is where push notifications from your homelab change the game. Not “email me something eventually” — real push notifications on your phone right now. The problem: there are three very different tools that all promise to do this, and they solve the problem in completely different ways.
Let’s talk about ntfy, Gotify, and Apprise — three paths to “send me a push from my self-hosted stack when X happens.”
ntfy: The Curl-Friendly Megaphone
ntfy is the modern, opinionated kid in the notification space. It’s written in Go, it’s lightweight, and it treats HTTP as a first-class citizen.
Here’s the philosophy: every notification is just an HTTP POST. Want to send a message to a topic? Curl it.
curl -d "Database backup completed" ntfy.sh/my-homelab-alertsThat’s it. No auth tokens to manage, no API keys, no ceremony. The message appears on your phone (iOS and Android, via official apps). If nobody’s subscribed to my-homelab-alerts, the message sits on the server for a while and disappears. If you are, it lands in real-time.
ntfy operates on a pub/sub model with zero configuration. You make up topic names on the fly. Send to ntfy.sh/watchtower-updates and anyone (publicly, by default) can subscribe. Want privacy? Add authentication or run it self-hosted.
Self-Hosting ntfy
version: '3.8'services: ntfy: image: binwiederhier/ntfy:latest container_name: ntfy command: serve ports: - "8080:80" volumes: - ntfy-cache:/var/cache/ntfy - ntfy-etc:/etc/ntfy environment: NTFY_LISTEN_HTTP: ":80" NTFY_BASE_URL: "https://ntfy.example.com" NTFY_BEHIND_PROXY: "true" restart: unless-stopped
volumes: ntfy-cache: ntfy-etc:Drop this behind a reverse proxy (Caddy, Traefik, whatever), point a domain at it, and you’ve got your own ntfy instance. Slick web UI, history for each topic, zero fuss.
Sending Messages from Your Scripts
# From your backup scriptif backup_failed; then curl -d "Backup failed: $ERROR" \ -H "Priority: high" \ https://ntfy.example.com/backupsfi
# From Watchtower or any cron jobcurl -d "Watchtower updated $(docker ps | wc -l) containers" \ https://ntfy.example.com/docker-updatesYou can add headers: Priority: high (or urgent) for emergency-level alerts, Title: "Backup Failure" for a custom title, Tags: warning,alert for emoji. It’s all HTTP headers. Your 2 AM self will appreciate the simplicity.
ntfy’s Killer Feature: iOS Support
This is the massive differentiator. ntfy has official iOS and Android apps in the respective app stores. They use Apple Push Notification service (APNs) for iOS, so you actually get a real native push notification on your phone — not a buried in-app message, not a web push, a real iOS notification.
Android support is solid too. Subscribe to a topic in the app, and you get instant native notifications.
ntfy Auth Model
By default, topics are public. Anyone who knows the name can subscribe and see the message history. If you need privacy, you’ve got options:
- Access control file (
auth.json) — restrict who can publish/subscribe to specific topics - Read-only topics — allow subscriptions but require auth to publish
- Fully private instance — require auth for everything
For a homelab, the sweet spot is usually: self-hosted ntfy, enable auth on sensitive topics (like your backup alerts), leave non-sensitive channels open.
Gotify: The Polished Self-Hosted Alternative
Gotify is the older player here — been around since 2018, written in Go, built from the ground up as a self-hosted notification server with a web UI.
Where ntfy is HTTP-first and minimal, Gotify is application-first and opinionated. You create applications (think: “backup alerts”, “docker updates”, “watchtower”), each gets an app token, and you send notifications to your app token.
Self-Hosting Gotify
version: '3.8'services: gotify: image: gotify/gotify-server:latest container_name: gotify ports: - "8080:80" volumes: - gotify-data:/app/data environment: GOTIFY_DEFAULTUSER_NAME: "admin" GOTIFY_DEFAULTUSER_PASS: "changeme" restart: unless-stopped
volumes: gotify-data:Boot it up, log in with the default credentials, and you get a clean web dashboard. Create applications, get tokens, start sending.
Sending Messages to Gotify
# Basic messagecurl -X POST https://gotify.example.com/message \ -H "Content-Type: application/json" \ -d '{ "title": "Backup Alert", "message": "Daily backup completed successfully", "priority": 5 }' \ -H "Authorization: Bearer YOUR_APP_TOKEN"
# From a script with priority levels (0-10)curl -X POST https://gotify.example.com/message \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $BACKUP_TOKEN" \ -d "{ \"title\": \"Backup Failure\", \"message\": \"PostgreSQL backup failed: $(date)\", \"priority\": 10 }"The auth model is stricter: each application gets a token, you embed it in your scripts. That’s more secure than ntfy’s topic-based approach, but also requires token management.
Gotify’s Tradeoff: Android Only
Here’s the catch: Gotify has an official Android app in the Play Store, but there’s no official iOS app. You can use third-party clients, but the experience is fragmented. If you have an iPhone, Gotify is off the table unless you’re willing to compromise on app quality.
The web UI is genuinely polished, though — message history, priority levels (0-10), icon support, everything feels like a real application. For a team or a family with Android-only phones, Gotify shines.
Apprise: The Abstraction Layer
Here’s where people get confused. Apprise is not a server. It’s a Python library and CLI tool that abstracts over 70+ notification providers.
Think of Apprise as a universal translator. You give it a URL like discord://webhook_id/token or slack://tokenA/tokenB/tokenC or mailto://[email protected], and it handles the details of sending to that provider.
You can use ntfy as one of Apprise’s notification backends. Same with Gotify. Or Discord. Slack. Pushover. Telegram. Matrix. Email. XMPP. You name it.
Apprise CLI
# Send to a single ntfy topicapprise ntfy://ntfy.sh/my-topic \ -b "Disk usage on homelab is 85%"
# Send to Gotifyapprise gotify://gotify.example.com/token \ -b "Watchtower just updated 3 images"
# Send to multiple providers at onceapprise \ ntfy://ntfy.example.com/alerts \ discord://webhook_id/token \ slack://token_a/token_b/token_c \ -b "Critical: memory usage above 80%"One command, multiple providers, no code changes. This is the power of Apprise.
Apprise in Python
from apprise import Apprise
# Create an Apprise instanceapobj = Apprise()
# Add notification servicesapobj.add('ntfy://ntfy.example.com/alerts')apobj.add('gotify://gotify.example.com/token')apobj.add('discord://webhook_id/token')apobj.add('slack://token_a/token_b/token_c')
# Send the same message to all of themapobj.notify( body='Backup job failed with status 1', title='Homelab Alert',)
print("Notification sent to all channels")This pattern is powerful in your homelab. One script, flexible notification targets. If you later decide to switch from Gotify to ntfy, or add Discord to the mix, you just change the config.
Apprise’s Flexibility
Apprise supports attachments too — send files, images, links. It handles rate limiting per provider. It has plugin support for custom notification backends.
The catch: Apprise by itself doesn’t provide push notifications unless you plug it into a provider that does (like ntfy, Gotify, Pushover, or the ntfy app). Apprise is the plumbing; the providers are the endpoints.
The Decision Matrix
| Feature | ntfy | Gotify | Apprise |
|---|---|---|---|
| What it is | Self-hosted pub/sub server | Self-hosted notification server | Python library/CLI abstraction |
| iOS app support | Yes (official) | No (third-party only) | Delegates to provider |
| Android app support | Yes (official) | Yes (official) | Delegates to provider |
| Auth model | Topics + optional auth | App tokens (strict) | Provider-specific |
| Curl-friendly | Yes (HTTP-first) | Yes (REST API) | CLI + Python library |
| Pub/sub | Yes (native) | No (push-only) | Delegates to provider |
| Self-hosted setup | 5 minutes | 5 minutes | Not a server (use as library) |
| Configuration friction | Minimal | Moderate | Depends on provider setup |
| Best for | Quick homelab alerts, multi-user | Android-only teams, polished UI | Centralizing many notification services |
The Killer Combo: Apprise → ntfy → Your Phone
Here’s the pattern that wins:
- Apprise in your scripts — centralizes logic, makes it easy to change targets
- ntfy as the backend — simple, iOS support, curl-friendly
- ntfy iOS/Android apps — real native notifications on your phone
#!/usr/bin/env python3from apprise import Appriseimport sys
def notify(title, body, priority='normal'): apobj = Apprise()
# All notifications route through ntfy apobj.add(f'ntfy://ntfy.example.com/backups?priority={priority}')
# Optionally: also send to Discord for ops team visibility apobj.add('discord://webhook_id/token')
apobj.notify(body=body, title=title)
# Usage from cron, Docker health checks, whateverif __name__ == '__main__': if len(sys.argv) > 1: notify('Backup Alert', sys.argv[1], priority='high') else: print("Usage: python backup_notifier.py 'message'")Now your backup script, Watchtower, your cron jobs — they all just call this one notifier. Switch notification targets by editing one file. Add Discord for your ops team. Add email fallback. No code changes.
Real Homelab Uses
Watchtower updates:
curl -d "Watchtower updated $(docker ps -q | wc -l) running containers" \ https://ntfy.example.com/docker-updatesDisk space alerts from cron:
#!/bin/bashDISK_USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')if [ "$DISK_USAGE" -gt 80 ]; then apprise ntfy://ntfy.example.com/alerts -b "Disk usage: $DISK_USAGE%"fiHealth check failures:
# From a health check scriptif ! curl -f https://example.com > /dev/null; then curl -d "Website is down" \ -H "Priority: urgent" \ https://ntfy.example.com/critical-alertsfiPostgreSQL backup verification:
#!/bin/bashif pg_dump mydb | gzip > backup.sql.gz; then curl -d "PostgreSQL backup succeeded ($(ls -lh backup.sql.gz | awk '{print $5}'))" \ https://ntfy.example.com/backupselse curl -d "PostgreSQL backup FAILED" \ -H "Priority: urgent" \ https://ntfy.example.com/backupsfiEvery one of these works without a client library, without heavy dependencies. HTTP and curl are enough.
Attachments and Files
ntfy:
# Attach a file -d "Check the attached log" \ https://ntfy.example.com/alertsGotify:
# Limited support; mostly image previews via URLcurl -X POST https://gotify.example.com/message \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Screenshot", "message": "Error state captured", "image": "https://example.com/screenshot.png" }'Apprise:
from apprise import Apprise
apobj = Apprise()apobj.add('ntfy://ntfy.example.com/alerts')
# Attach a file (provider-dependent support)apobj.notify( body='See attached log', title='Debug Log', attach=['/var/log/myapp.log'],)Rate Limits and Reliability
ntfy: No hard rate limits by default. Self-hosted, you control the limits. Public ntfy.sh has soft limits to prevent spam.
Gotify: No built-in rate limiting. Messages are stored in the database until delivered or expired.
Apprise: Respects rate limits per provider. Discord, Slack, etc. all have their own limits; Apprise handles backoff gracefully.
For a homelab, all three are rock-solid. You’re not hitting thousands of messages per day. The friction is in choosing the right one, not in reliability.
So Which One?
Go with ntfy if:
- You want iOS support (non-negotiable for you)
- You like simplicity and curl-friendly APIs
- You’re okay with pub/sub semantics
- Your homelab is just you or a few trusted people
Go with Gotify if:
- Everyone in your household uses Android
- You want a polished web UI with message history
- You prefer strict app-token-based auth
- You don’t mind zero iOS support
Use Apprise if:
- You already run multiple notification services (Discord, Slack, email)
- You want to centralize logic and switch providers easily
- You’re building something for other people and want flexible targets
- You’re willing to set up a backend (ntfy or Gotify or something else)
The combo that wins: Apprise + ntfy. One abstraction layer for your scripts, one solid HTTP-based server that works everywhere, native apps that actually push to your phone.
Your 2 AM self — the one staring at the phone waiting for the alert — will thank you.