Ntfy Self-Hosted Push Notification Server on VPS

Install Ntfy Self-Hosted Push Notifications on VPS

Published: September 26, 2026 · 12 min read · Ubuntu 22.04 / 24.04

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

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.

Featurentfy.sh (Public)Self-hosted
Free to use✅✅ (VPS cost only)
Private data❌✅
Unlimited topics❌ (tier limits)✅
Unlimited subscribers❌✅
Full authenticationLimited✅
File attachmentsSize limitedConfigurable

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"
Tip: Replace 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
Tip: Users with 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:

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

ValueNameBehavior
1minNo sound, low priority
2lowQuiet notification
3defaultNormal (default)
4highLoud sound, large banner
5urgent/maxRepeated 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
Tip: The /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