Install Ntfy Self-Hosted Push Notifications on VPS
Ntfy (pronounced "notify") is a self-hosted push notification server that delivers alerts to mobile devices (Android/iOS), desktop, and browsers using simple HTTP requests — no Firebase, APNs, or third-party cloud services required. It's 100% free and ideal for alerting from scripts, cron jobs, server events, or CI/CD pipelines on your VPS.
This guide covers: installing Ntfy on Ubuntu, configuring Nginx reverse proxy with SSL, adding authentication with access control lists, and sending notifications from the command line, shell scripts, and Python.
Prerequisites
- VPS running Ubuntu 22.04 or 24.04 (512MB RAM is sufficient)
- A domain name with DNS pointing to your VPS
- Nginx installed (see Nginx Reverse Proxy guide)
- Let's Encrypt SSL certificate (see SSL Let's Encrypt guide)
- Basic Linux command line knowledge
Why Self-Host Ntfy?
When your VPS finishes a task — backup completed, deploy done, or disk nearly full at 90% — you want to know immediately. Ntfy can notify you with a single curl command:
curl -d "Backup complete!" ntfy.sh/mytopic
However, using the public ntfy.sh means your data is publicly readable by anyone. Self-hosting on your own VPS keeps your notifications private and gives you unlimited capacity.
| Feature | ntfy.sh (Public) | Self-hosted |
|---|---|---|
| Free to use | ✅ | ✅ (VPS cost only) |
| Private data | ❌ | ✅ |
| Unlimited topics | ❌ (tier limits) | ✅ |
| Unlimited subscribers | ❌ | ✅ |
| Full authentication | Limited | ✅ |
| File attachments | Size limited | Configurable |
Install Ntfy
Install via the official Ntfy APT repository:
# Add Ntfy APT repository
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
sudo sh -c "echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' > /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy -y
# Verify installation
ntfy version
Alternatively, install from binary directly (useful for ARM VPS):
# Get latest version
NTFY_VER=$(curl -s https://api.github.com/repos/binwiederhier/ntfy/releases/latest | grep tag_name | cut -d'"' -f4)
# Download for x86_64/amd64
wget https://github.com/binwiederhier/ntfy/releases/download/${NTFY_VER}/ntfy_${NTFY_VER#v}_linux_amd64.tar.gz
tar xzf ntfy_*.tar.gz
sudo cp -a ntfy_*/ntfy /usr/local/bin/ntfy
ntfy version
Configure Ntfy Server
Create configuration directory and main config file:
sudo mkdir -p /etc/ntfy /var/cache/ntfy/attachments
sudo chown ntfy:ntfy /var/cache/ntfy/attachments 2>/dev/null || true
Create /etc/ntfy/server.yml:
sudo nano /etc/ntfy/server.yml
# /etc/ntfy/server.yml
base-url: "https://notify.yourdomain.com"
listen-http: ":2586"
# Cache & storage
cache-file: "/var/cache/ntfy/cache.db"
cache-duration: "12h"
# Attachment storage
attachment-cache-dir: "/var/cache/ntfy/attachments"
attachment-total-size-limit: "5G"
attachment-file-size-limit: "15M"
attachment-expiry-duration: "3h"
# Authentication
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
# Rate limiting
visitor-subscription-limit: 30
visitor-request-limit-burst: 60
visitor-request-limit-replenish: "5s"
# Logging
log-level: "info"
notify.yourdomain.com with your actual domain. Setting auth-default-access: "deny-all" forces all requests to authenticate with a username and password.Configure Systemd Service
sudo nano /etc/systemd/system/ntfy.service
[Unit]
Description=ntfy server
After=network.target
[Service]
Type=simple
User=ntfy
Group=ntfy
ExecStart=/usr/bin/ntfy serve --config /etc/ntfy/server.yml
Restart=on-failure
RestartSec=5s
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
# Create ntfy system user (APT installation already creates this)
sudo useradd --system --no-create-home --shell /usr/sbin/nologin ntfy 2>/dev/null || true
# Create auth database directory
sudo mkdir -p /var/lib/ntfy
sudo chown ntfy:ntfy /var/lib/ntfy
# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable --now ntfy
sudo systemctl status ntfy
Configure Nginx Reverse Proxy
sudo nano /etc/nginx/sites-available/ntfy
server {
listen 80;
server_name notify.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name notify.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/notify.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/notify.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://127.0.0.1:2586;
proxy_http_version 1.1;
# WebSocket + SSE (Server-Sent Events) support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Extended timeout for long-polling subscriptions
proxy_read_timeout 2620s;
proxy_send_timeout 2620s;
# Disable buffering for SSE
proxy_buffering off;
proxy_cache off;
proxy_redirect off;
client_max_body_size 20m;
}
}
sudo ln -s /etc/nginx/sites-available/ntfy /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Create Users and Access Control
Ntfy uses a user/password system with ACLs to control which users can access which topics:
# Create an admin user (access to all topics)
sudo ntfy user add --role=admin myadmin
# You will be prompted for a password
# Create a regular user
sudo ntfy user add myuser
# Grant topic-specific access
sudo ntfy access myuser server-alerts rw # read + write
sudo ntfy access myuser backups ro # read only
sudo ntfy access myuser '*' deny # deny all other topics
# View current ACLs
sudo ntfy access
role=admin automatically have publish/subscribe access to all topics. Regular users require explicit ACL entries per topic.Sending Notifications
From Command Line (curl)
# Send a basic message
curl -u myuser:mypassword \
-d "Backup completed at $(date)" \
https://notify.yourdomain.com/server-alerts
# Send with title, priority, and tags
curl -u myuser:mypassword \
-H "Title: Deploy Successful" \
-H "Priority: high" \
-H "Tags: white_check_mark,rocket" \
-d "Version 2.1.0 deployed successfully" \
https://notify.yourdomain.com/deployments
# Send as JSON
curl -u myuser:mypassword \
-H "Content-Type: application/json" \
-d '{
"topic": "server-alerts",
"message": "Disk usage exceeds 90%!",
"title": "Server Warning",
"priority": 5,
"tags": ["warning","hard_drive"]
}' \
https://notify.yourdomain.com
From a Shell Script (for Cron)
#!/bin/bash
# /usr/local/bin/ntfy-notify.sh
NTFY_URL="https://notify.yourdomain.com"
NTFY_USER="myuser"
NTFY_PASS="mypassword"
NTFY_TOPIC="server-alerts"
ntfy_send() {
local message="$1"
local title="${2:-Server Notification}"
local priority="${3:-default}"
curl -s -u "${NTFY_USER}:${NTFY_PASS}" \
-H "Title: ${title}" \
-H "Priority: ${priority}" \
-d "${message}" \
"${NTFY_URL}/${NTFY_TOPIC}" > /dev/null
}
# Example usage
ntfy_send "Database backup completed" "Backup OK" "default"
# Check disk and alert if critical
DISK_USAGE=$(df -h / | awk 'NR==2{print $5}' | tr -d '%')
if [ "$DISK_USAGE" -gt 85 ]; then
ntfy_send "Disk usage: ${DISK_USAGE}% — please investigate" "Disk Warning" "high"
fi
Mobile App (Android / iOS)
Install the Ntfy app on your phone:
- Android: Available on F-Droid or Google Play (search "ntfy")
- iOS: Available on the App Store (search "ntfy")
In the app, tap "Add subscription" → select "Use another server" → enter your server URL with the username and password you created.
Web Browser (PWA)
Open https://notify.yourdomain.com in a browser — Ntfy includes a built-in web UI with Web Push notification support and can be installed as a Progressive Web App.
Cron Job Integration
Automatically notify on cron job results:
# /etc/cron.d/backup-with-notify
0 2 * * * root /usr/local/bin/backup.sh && \
curl -s -u backup_user:pass \
-H "Title: Backup OK" \
-H "Tags: white_check_mark" \
-d "Daily backup $(date +%Y-%m-%d) completed" \
https://notify.yourdomain.com/backups \
|| \
curl -s -u backup_user:pass \
-H "Title: Backup FAILED" \
-H "Priority: urgent" \
-H "Tags: x,rotating_light" \
-d "Backup $(date +%Y-%m-%d) failed! Check immediately." \
https://notify.yourdomain.com/backups
Priority Levels
| Value | Name | Behavior |
|---|---|---|
| 1 | min | No sound, low priority |
| 2 | low | Quiet notification |
| 3 | default | Normal (default) |
| 4 | high | Loud sound, large banner |
| 5 | urgent/max | Repeated vibration, breaks Do Not Disturb |
Verify Ntfy Is Working Correctly
# Check service status
sudo systemctl status ntfy
# Verify listening port
ss -tlnp | grep 2586
# Test HTTP API directly (bypassing Nginx)
curl -v http://127.0.0.1:2586/v1/health
# Test through Nginx (HTTPS)
curl -v https://notify.yourdomain.com/v1/health
# Send a test notification
curl -u myadmin:adminpassword \
-H "Title: Test Notification" \
-H "Priority: default" \
-d "Ntfy is working correctly at $(date)" \
https://notify.yourdomain.com/test-topic
# Watch logs in real time
sudo journalctl -u ntfy -f
# List all users
sudo ntfy user list
/v1/health endpoint should return {"healthy":true} when Ntfy is running correctly. Use it as a health check URL in Uptime Kuma for monitoring.Troubleshooting Common Issues
Error: "403 Forbidden — unauthorized"
This occurs when a request is sent without credentials, or when the user lacks ACL access to the topic.
# Verify the user exists
sudo ntfy user list
# Check user's current ACL
sudo ntfy access myuser
# Grant access to the topic
sudo ntfy access myuser server-alerts rw
# Test with the admin user first to isolate the issue
curl -u myadmin:adminpass -d "test" https://notify.yourdomain.com/test-topic
Error: "502 Bad Gateway" from Nginx
Nginx cannot connect to Ntfy. Usually caused by the Ntfy service not running or a port mismatch.
# Check if Ntfy is running
sudo systemctl status ntfy
sudo systemctl restart ntfy
# Confirm the listening port
sudo grep listen-http /etc/ntfy/server.yml
ss -tlnp | grep ntfy
# Check Nginx configuration
sudo nginx -t
sudo tail -20 /var/log/nginx/error.log
Mobile App Receives Notifications Slowly or Not at All
This is usually caused by proxy_read_timeout in Nginx being too short, causing Server-Sent Events (SSE) connections to drop.
# Check the timeout setting
grep proxy_read_timeout /etc/nginx/sites-available/ntfy
# Should be 2620s or higher
# Fix and reload
sudo nano /etc/nginx/sites-available/ntfy
sudo nginx -t && sudo systemctl reload nginx
# Test SSE connection
curl -u myuser:pass \
"https://notify.yourdomain.com/server-alerts/json?poll=1"
Error: "disk quota exceeded" for Attachments
The attachment cache is full. You can clear old files or increase the limit in the config.
# Check attachment cache size
du -sh /var/cache/ntfy/attachments/
# Manually remove old attachments (Ntfy auto-expires based on attachment-expiry-duration)
sudo find /var/cache/ntfy/attachments -mtime +1 -delete
# Or increase the limit in server.yml
sudo nano /etc/ntfy/server.yml
# Change: attachment-total-size-limit: "10G"
sudo systemctl restart ntfy
Cannot Send Notifications from Python
Make sure the message body is UTF-8 encoded, especially if it contains non-ASCII characters.
import requests
def ntfy_send(message, title="", priority="default"):
requests.post(
"https://notify.yourdomain.com/server-alerts",
data=message.encode(encoding='utf-8'),
headers={
"Title": title.encode(encoding='utf-8'),
"Priority": priority,
"Tags": "bell"
},
auth=("myuser", "mypassword")
)
ntfy_send("Deploy successful!", "Deploy OK")
Frequently Asked Questions
How does Ntfy differ from Gotify?
Both are self-hosted notification servers, but Ntfy stands out because it accepts plain HTTP PUT/POST requests without requiring a special API token. It also supports native iOS push notifications via UnifiedPush, has a cleaner web UI, and works fully with the mobile app on a self-hosted instance without additional configuration.
How many resources does Ntfy use on a VPS?
Ntfy is written in Go and is extremely lightweight. It typically uses only 20–50MB of RAM under normal load and minimal CPU when idle. A VPS with 512MB RAM can comfortably run Ntfy alongside other services without any performance impact.
Can I send notifications from GitHub Actions?
Yes. Store your Ntfy URL and credentials as GitHub Secrets, then call them in your workflow:
- name: Notify Deploy Success
run: |
curl -u ${{ secrets.NTFY_USER }}:${{ secrets.NTFY_PASS }} \
-H "Title: Deploy Successful" \
-H "Tags: rocket,white_check_mark" \
-d "Branch ${{ github.ref_name }} deployed" \
${{ secrets.NTFY_URL }}/deployments