Self-Host ntfy: Push Notifications for Everything 2026
TL;DR
ntfy (Apache 2.0, ~16K GitHub stars, Go) is the simplest self-hosted push notification service. Subscribe to a topic, then send notifications to it with a single curl command — no API keys, no SDKs, no setup on the sender side. Send alerts from cron jobs, shell scripts, CI pipelines, Home Assistant, Uptime Kuma, Grafana, or any tool that can make an HTTP request. PagerDuty charges $19/user/month; ntfy self-hosted is free with no per-notification limits.
Key Takeaways
- ntfy: Apache 2.0, ~16K stars, Go — HTTP pub/sub push notifications
- Zero sender setup: Send with a single
curlto any topic URL — no SDK or API key required - iOS and Android apps: Native apps for receiving notifications with rich features
- Priority levels: Min/low/default/high/urgent — control notification sound and delivery
- Actions: Add clickable action buttons to notifications (URLs, intents, HTTP callbacks)
- Access control: User/password protection for private topics
How ntfy Works
Sender (curl/script/app) → ntfy server → ntfy iOS/Android app
(pub/sub) (subscriber)
Topics are just URL paths — anyone who knows the topic URL can publish or subscribe to it (unless protected with auth).
Part 1: Docker Setup
# docker-compose.yml
services:
ntfy:
image: binwiederhier/ntfy:latest
container_name: ntfy
restart: unless-stopped
command: serve
ports:
- "8080:80"
volumes:
- ntfy_cache:/var/cache/ntfy
- ntfy_data:/etc/ntfy
environment:
TZ: America/Los_Angeles
# For config file (see below), mount it:
# volumes:
# - ./server.yml:/etc/ntfy/server.yml:ro
volumes:
ntfy_cache:
ntfy_data:
docker compose up -d
Part 2: HTTPS with Caddy
ntfy.yourdomain.com {
reverse_proxy localhost:8080
}
Part 3: Configuration File
For production use, create a config file:
# /etc/ntfy/server.yml (or mount ./server.yml)
# Public base URL (required for iOS notifications):
base-url: "https://ntfy.yourdomain.com"
# Cache for offline message delivery:
cache-file: "/var/cache/ntfy/cache.db"
cache-duration: "12h"
# Auth (enable to protect topics):
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all" # Deny anonymous by default
# Rate limiting:
visitor-request-limit-burst: 60
visitor-request-limit-replenish: "1m"
visitor-subscription-limit: 30
visitor-message-daily-limit: 250
# Attachment support:
attachment-cache-dir: "/var/cache/ntfy/attachments"
attachment-total-size-limit: "5G"
attachment-file-size-limit: "15M"
attachment-expiry-duration: "3h"
# Optional: upstream Firebase for iOS background delivery
# (uses ntfy.sh as relay — set up-stream-base-url to avoid this)
# upstream-base-url: "https://ntfy.sh"
# docker-compose.yml with config file:
services:
ntfy:
image: binwiederhier/ntfy:latest
command: serve --config /etc/ntfy/server.yml
volumes:
- ./server.yml:/etc/ntfy/server.yml:ro
- ntfy_cache:/var/cache/ntfy
- ntfy_data:/var/lib/ntfy
Part 4: Sending Notifications
Simplest possible notification
curl -d "Backup completed" ntfy.yourdomain.com/my-alerts
That's it. Any subscriber to my-alerts receives the notification immediately.
With title, priority, and tags
curl \
-H "Title: Server Alert" \
-H "Priority: high" \
-H "Tags: warning,computer" \
-d "Disk usage on prod-1 is at 95%" \
ntfy.yourdomain.com/server-alerts
Priority levels
| Priority | Value | Behavior |
|---|---|---|
| Min | 1 | No notification sound, no badge |
| Low | 2 | No notification sound |
| Default | 3 | Default notification |
| High | 4 | Loud notification, stays in tray |
| Urgent | 5 | Max volume, bypasses DND |
Notification with action buttons
# Add a "View Logs" button that opens a URL:
curl \
-H "Title: Deploy Failed" \
-H "Priority: high" \
-H "Actions: view, View Logs, https://logs.yourdomain.com/deploy-42" \
-d "Production deployment #42 failed at build step" \
ntfy.yourdomain.com/deployments
Notification with image attachment
# Send a screenshot or image with the notification:
curl \
-H "Title: Security Camera Alert" \
-H "Filename: camera-snapshot.jpg" \
-T /tmp/snapshot.jpg \
ntfy.yourdomain.com/cameras
Part 5: iOS and Android Apps
iOS
- Install ntfy from App Store
- Add subscription:
- Server:
https://ntfy.yourdomain.com - Topic:
my-alerts
- Server:
- Enable notifications
For background push delivery on iOS, ntfy uses Apple Push Notifications (APNs). Self-hosted servers require proxying through ntfy.sh's Firebase relay unless you configure your own Firebase project.
Android
- Install ntfy from F-Droid or Play Store
- Add subscription:
- Server:
https://ntfy.yourdomain.com - Topic:
my-alerts
- Server:
- Notifications work without Firebase (UnifiedPush)
Web
Subscribe in a browser:
const es = new EventSource("https://ntfy.yourdomain.com/my-alerts/sse");
es.onmessage = (e) => {
const notification = JSON.parse(e.data);
console.log(notification.message);
};
Part 6: Access Control
Create users
# Add a user with publish-only access to a specific topic:
docker exec ntfy ntfy user add --role=user alice
docker exec ntfy ntfy access alice server-alerts rw # read+write
docker exec ntfy ntfy access alice public-feed ro # read-only
# Add an admin user:
docker exec ntfy ntfy user add --role=admin admin
# List users:
docker exec ntfy ntfy user list
Sending with authentication
# Basic auth:
curl -u alice:password \
-d "Authorized alert" \
ntfy.yourdomain.com/server-alerts
# Or Bearer token:
curl \
-H "Authorization: Bearer tk_yourtoken" \
-d "Authorized alert" \
ntfy.yourdomain.com/server-alerts
Part 7: Integrations
Home Assistant
# configuration.yaml:
notify:
- name: ntfy
platform: rest
resource: "https://ntfy.yourdomain.com/home-assistant"
method: POST_JSON
headers:
Authorization: "Bearer tk_yourtoken"
message_param_name: message
title_param_name: title
data:
priority: high
# Automation example:
automation:
- alias: Notify on motion
trigger:
- platform: state
entity_id: binary_sensor.front_door_motion
to: "on"
action:
- service: notify.ntfy
data:
title: "Motion Detected"
message: "Front door motion sensor triggered"
Uptime Kuma
- Uptime Kuma → Notifications → Add → ntfy
- ntfy Server URL:
https://ntfy.yourdomain.com - Topic:
uptime-alerts - Priority: High
Cron job alerts
#!/bin/bash
# Wrap any command to get notified on failure:
COMMAND="$@"
OUTPUT=$($COMMAND 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
curl \
-H "Title: Cron Job Failed" \
-H "Priority: high" \
-H "Tags: x,computer" \
-d "Command: $COMMAND
Exit code: $EXIT_CODE
Output: $(echo "$OUTPUT" | tail -20)" \
ntfy.yourdomain.com/cron-alerts
fi
Grafana alerts
# In Grafana → Contact Points → New → Webhook
URL: https://ntfy.yourdomain.com/grafana-alerts
Method: POST
Headers:
Authorization: Bearer tk_yourtoken
Title: Grafana Alert
Priority: high
Watchtower (Docker update notifications)
# docker-compose.yml:
services:
watchtower:
image: containrrr/watchtower
environment:
WATCHTOWER_NOTIFICATION_URL: "ntfy://ntfy.yourdomain.com/watchtower?auth=Basic&password=tk_yourtoken"
Part 8: ntfy CLI
# Install ntfy CLI:
brew install ntfy # macOS
# or download from: https://github.com/binwiederhier/ntfy/releases
# Subscribe and watch for notifications:
ntfy subscribe --from-config
# Subscribe to a topic and run a command on notification:
ntfy subscribe ntfy.yourdomain.com/my-alerts \
'notify-send "$m" "$t"' # Linux desktop notification
# Publish:
ntfy publish ntfy.yourdomain.com/my-alerts "Hello from CLI"
Maintenance
# Update:
docker compose pull
docker compose up -d
# Backup:
tar -czf ntfy-backup-$(date +%Y%m%d).tar.gz \
$(docker volume inspect ntfy_ntfy_data --format '{{.Mountpoint}}')
# Check cache size:
du -sh $(docker volume inspect ntfy_ntfy_cache --format '{{.Mountpoint}}')
# View logs:
docker compose logs -f ntfy
# Test from server:
curl -d "Server is healthy" ntfy.yourdomain.com/health-check
See also: Gotify — alternative push notification service with persistent message history
See all open source productivity tools at OSSAlt.com/categories/productivity.