ntfy is an open source publish-subscribe notification service. You send a message with a plain HTTP PUT or POST to a topic, and every phone, browser or script subscribed to that topic receives it. In this tutorial you will install ntfy on Ubuntu 24.04, publish it over HTTPS behind Nginx, restrict every topic to authenticated users, and send notifications from the command line and from a cron job.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has sudo privileges.
  • A domain or subdomain, such as ntfy.your_domain, with a DNS A record pointing to your server's public IP. The mobile apps need a valid HTTPS certificate.
  • Nginx installed (sudo apt install nginx) and UFW allowing HTTP and HTTPS (sudo ufw allow 'Nginx Full').

Step 1 - Adding the ntfy repository

The ntfy author publishes Debian and Ubuntu packages in a signed APT repository. Create the keyring directory and import the key:

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg

Add the repository. Change amd64 to arm64 on an ARM server:

echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main" | sudo tee /etc/apt/sources.list.d/archive.heckel.io.list

Install ntfy:

sudo apt update
sudo apt install -y ntfy

Check the installed version:

apt policy ntfy | head -n 3
ntfy:
  Installed: 2.x.y
  Candidate: 2.x.y

The package creates an ntfy system user, a default configuration at /etc/ntfy/server.yml and a systemd unit named ntfy.

Step 2 - Configuring the server

ntfy stores its message cache and user database in SQLite files. Create a directory for them owned by the ntfy user:

sudo install -d -o ntfy -g ntfy /var/lib/ntfy /var/cache/ntfy/attachments

Open the configuration file:

sudo nano /etc/ntfy/server.yml

The shipped file is fully commented. Add the following settings at the top, replacing ntfy.your_domain:

base-url: "https://ntfy.your_domain"
listen-http: "127.0.0.1:2586"
behind-proxy: true

cache-file: "/var/cache/ntfy/cache.db"
cache-duration: "12h"

attachment-cache-dir: "/var/cache/ntfy/attachments"
attachment-file-size-limit: "15M"
attachment-total-size-limit: "5G"

auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"

upstream-base-url: "https://ntfy.sh"

What these settings do:

  • base-url is the public URL. It must match the domain you use in the apps.
  • listen-http binds ntfy to localhost only, so Nginx is the only way in.
  • behind-proxy: true makes ntfy use the X-Forwarded-For header for rate limiting, so it limits clients instead of Nginx.
  • auth-default-access: "deny-all" means nobody can read or write any topic unless you grant it explicitly.
  • upstream-base-url lets the iOS app receive instant notifications. iOS only delivers background push through Apple's servers, so your server forwards a message ID (never the content) to ntfy.sh, which wakes the app up to fetch the message from your server. Leave it out if you only use Android and the web app.

Start the service and enable it at boot:

sudo systemctl enable --now ntfy

Check that it is running and listening on port 2586:

systemctl status ntfy --no-pager
curl -s http://127.0.0.1:2586/v1/health
{"healthy":true}

If the service fails, journalctl -u ntfy -n 50 --no-pager shows the reason; a YAML indentation error or a directory the ntfy user cannot write are the usual causes.

Step 3 - Publishing ntfy through Nginx with HTTPS

ntfy keeps long-lived connections open for subscribers (HTTP streaming and WebSockets), so the proxy must not buffer responses and must allow long timeouts.

Create a server block:

sudo nano /etc/nginx/sites-available/ntfy
server {
    listen 80;
    listen [::]:80;
    server_name ntfy.your_domain;

    location / {
        proxy_pass http://127.0.0.1:2586;
        proxy_http_version 1.1;

        proxy_buffering off;
        proxy_request_buffering off;
        proxy_redirect off;

        proxy_set_header Host $http_host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_connect_timeout 3m;
        proxy_send_timeout 3m;
        proxy_read_timeout 3m;

        client_max_body_size 0;
    }
}

Enable the site and test the configuration:

sudo ln -s /etc/nginx/sites-available/ntfy /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Install Certbot and request a certificate. Certbot edits the server block to listen on 443 and redirect HTTP to HTTPS:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d ntfy.your_domain

Verify from your own computer:

curl -s https://ntfy.your_domain/v1/health
{"healthy":true}

Opening https://ntfy.your_domain in a browser shows the ntfy web app.

Step 4 - Creating users and granting access

With deny-all in place, create an admin account for yourself. Admins can read and write every topic. The command prompts for a password:

sudo ntfy user add --role=admin your_admin
password: 
confirm: 
user your_admin added with role admin

Create a regular user for the servers that will send alerts:

sudo ntfy user add alerts

Give it write-only access to the topics it needs. Wildcards match topic prefixes:

sudo ntfy access alerts "server-*" write-only
sudo ntfy access alerts backups write-only

List users and their permissions:

sudo ntfy user list
user your_admin (role: admin, tier: none)
- read-write access to all topics (admin role)
user alerts (role: user, tier: none)
- write-only access to topic server-*
- write-only access to topic backups
user * (role: anonymous, tier: none)
- no topic-specific permissions
- no access to any (other) topics (server config)

Scripts should not carry a password. Create an access token for the alerts user instead:

sudo ntfy token add alerts
token tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 created for user alerts, never expires

Store the token somewhere safe. You can revoke it later with sudo ntfy token remove alerts tk_... without touching the user's password.

Step 5 - Subscribing from your phone and browser

Install the ntfy app from Google Play, F-Droid or the App Store, then:

  1. Open Settings > Manage users (Android) or Settings > Users (iOS) and add a user for https://ntfy.your_domain with the admin credentials.
  2. Tap + to subscribe to a topic, enable Use another server, enter https://ntfy.your_domain and the topic name, for example server-alerts.

In the web app, click Sign in, log in, and subscribe to the same topic. Allow browser notifications when prompted.

Step 6 - Sending notifications

Publish a test message with the token. The body is the message, and headers add a title, priority and tags:

curl -H "Authorization: Bearer tk_your_token" \
  -H "Title: Test from the server" \
  -H "Priority: high" \
  -H "Tags: white_check_mark" \
  -d "ntfy is working" \
  https://ntfy.your_domain/server-alerts
{"id":"hwQ2YpKdmg","time":1790326512,"expires":1790369712,"event":"message","topic":"server-alerts","title":"Test from the server","message":"ntfy is working","priority":4,"tags":["white_check_mark"]}

The notification appears on your phone within a second or two. Priorities range from 1 (min) to 5 (max or urgent); 4 and 5 make the phone ring or vibrate more insistently. Tags that match an emoji short code are shown as icons.

Check that permissions work. The alerts user has write-only access, so reading the topic with its token is refused:

curl -s -H "Authorization: Bearer tk_your_token" "https://ntfy.your_domain/server-alerts/json?poll=1"
{"code":40301,"http":403,"error":"forbidden","link":"https://ntfy.sh/docs/publish/#authentication"}

Reading the topic as the admin user returns the message:

curl -s -u your_admin "https://ntfy.your_domain/server-alerts/json?poll=1&since=10m"

A few other headers are useful in alerts:

HeaderExampleEffect
Clickhttps://grafana.your_domainOpens the URL when the notification is tapped
Delay30mDelivers the message later
MarkdownyesRenders the body as Markdown in the web app
Filenamebackup.logName of an uploaded attachment (send the file with -T)

Step 7 - Getting notified when a cron job fails

A common use for ntfy is to alert on failed scheduled jobs. Store the token in a root-only file so it does not appear in the crontab:

echo "tk_your_token" | sudo tee /etc/ntfy-token > /dev/null
sudo chmod 600 /etc/ntfy-token

Then add a job that only notifies when the backup command exits with an error:

sudo nano /etc/cron.d/backup
0 3 * * * root /usr/local/bin/backup.sh || curl -fsS -H "Authorization: Bearer $(cat /etc/ntfy-token)" -H "Title: Backup failed on $(hostname)" -H "Priority: urgent" -H "Tags: warning" -d "backup.sh exited with an error, check the logs" https://ntfy.your_domain/backups

Replace /usr/local/bin/backup.sh with your own job. To test the notification path without waiting until 3:00, run the part after || by hand in a root shell (sudo -i).

Many tools support ntfy natively, including Uptime Kuma, Grafana (through a webhook contact point), Healthchecks and Proxmox Backup Server, so you can point them at the same topics.

Troubleshooting

403 forbidden when publishing. The user or token has no write permission on that topic. Check with sudo ntfy access alerts and grant it with sudo ntfy access alerts topic_name write-only.

The phone only gets notifications when the app is open. On Android, make sure the subscription uses your server URL exactly as set in base-url and that battery optimization is disabled for the ntfy app. On iOS, check that upstream-base-url is set and restart the service with sudo systemctl restart ntfy.

Subscriptions drop every few minutes. Nginx is closing idle streams. Confirm the proxy_read_timeout and proxy_buffering off lines are in the HTTPS server block that Certbot created, then run sudo nginx -t && sudo systemctl reload nginx.

Permission denied for /var/lib/ntfy/user.db in the logs. The directory must belong to the ntfy user. Fix it with sudo chown -R ntfy:ntfy /var/lib/ntfy /var/cache/ntfy.

Conclusion

You now have a private ntfy server on Ubuntu 24.04, reachable over HTTPS, with every topic closed to anonymous users and scripts publishing through revocable tokens. As next steps, connect your monitoring stack (Uptime Kuma, Grafana or Prometheus Alertmanager) to dedicated topics, give each server its own token so you can revoke them individually, and back up /var/lib/ntfy/user.db together with /etc/ntfy/server.yml.