Skip to content
Go back

ntfy vs Gotify vs Apprise: Push Stacks

By SumGuy 10 min read
ntfy vs Gotify vs Apprise: Push Stacks
Contents

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.

Terminal window
curl -d "Database backup completed" ntfy.sh/my-homelab-alerts

That’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

docker-compose.yml
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

Terminal window
# From your backup script
if backup_failed; then
curl -d "Backup failed: $ERROR" \
-H "Priority: high" \
https://ntfy.example.com/backups
fi
# From Watchtower or any cron job
curl -d "Watchtower updated $(docker ps | wc -l) containers" \
https://ntfy.example.com/docker-updates

You 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:

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

docker-compose.yml
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

Terminal window
# Basic message
curl -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

Terminal window
# Send to a single ntfy topic
apprise ntfy://ntfy.sh/my-topic \
-b "Disk usage on homelab is 85%"
# Send to Gotify
apprise gotify://gotify.example.com/token \
-b "Watchtower just updated 3 images"
# Send to multiple providers at once
apprise \
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

notify.py
from apprise import Apprise
# Create an Apprise instance
apobj = Apprise()
# Add notification services
apobj.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 them
apobj.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

FeaturentfyGotifyApprise
What it isSelf-hosted pub/sub serverSelf-hosted notification serverPython library/CLI abstraction
iOS app supportYes (official)No (third-party only)Delegates to provider
Android app supportYes (official)Yes (official)Delegates to provider
Auth modelTopics + optional authApp tokens (strict)Provider-specific
Curl-friendlyYes (HTTP-first)Yes (REST API)CLI + Python library
Pub/subYes (native)No (push-only)Delegates to provider
Self-hosted setup5 minutes5 minutesNot a server (use as library)
Configuration frictionMinimalModerateDepends on provider setup
Best forQuick homelab alerts, multi-userAndroid-only teams, polished UICentralizing many notification services

The Killer Combo: Apprise → ntfy → Your Phone

Here’s the pattern that wins:

  1. Apprise in your scripts — centralizes logic, makes it easy to change targets
  2. ntfy as the backend — simple, iOS support, curl-friendly
  3. ntfy iOS/Android apps — real native notifications on your phone
backup_notifier.py
#!/usr/bin/env python3
from apprise import Apprise
import 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, whatever
if __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:

Terminal window
curl -d "Watchtower updated $(docker ps -q | wc -l) running containers" \
https://ntfy.example.com/docker-updates

Disk space alerts from cron:

Terminal window
#!/bin/bash
DISK_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%"
fi

Health check failures:

Terminal window
# From a health check script
if ! curl -f https://example.com > /dev/null; then
curl -d "Website is down" \
-H "Priority: urgent" \
https://ntfy.example.com/critical-alerts
fi

PostgreSQL backup verification:

Terminal window
#!/bin/bash
if 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/backups
else
curl -d "PostgreSQL backup FAILED" \
-H "Priority: urgent" \
https://ntfy.example.com/backups
fi

Every one of these works without a client library, without heavy dependencies. HTTP and curl are enough.

Attachments and Files

ntfy:

Terminal window
# Attach a file
curl -F "[email protected]" \
-d "Check the attached log" \
https://ntfy.example.com/alerts

Gotify:

Terminal window
# Limited support; mostly image previews via URL
curl -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:

send_with_attachment.py
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:

Go with Gotify if:

Use Apprise if:

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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Dragonfly vs Redis: Single-Binary Performance
Next Post
Syncthing Through Untrusted VPS Relays

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts