Skip to content

Configuring the ntfy server

The ntfy server can be configured in three ways: using a config file (typically at /etc/ntfy/server.yml, see server.yml), via command line arguments or using environment variables.

Quick start

By default, simply running ntfy serve will start the server at port 80. No configuration needed. Batteries included 😀. If everything works as it should, you'll see something like this:

$ ntfy serve
2021/11/30 19:59:08 Listening on :80

You can immediately start publishing messages, or subscribe via the Android app, the web UI, or simply via curl or your favorite HTTP client. To configure the server further, check out the config options table or simply type ntfy serve --help to get a list of command line options.

Example config

Info

Definitely check out the server.yml file. It contains examples and detailed descriptions of all the settings.

The most basic settings are base-url (the external URL of the ntfy server), the HTTP/HTTPS listen address (listen-http and listen-https), and socket path (listen-unix). All the other things are additional features.

Here are a few working sample configs using a /etc/ntfy/server.yml file:

base-url: "http://ntfy.example.com"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
base-url: "http://ntfy.example.com"
listen-http: ":80"
listen-https: ":443"
key-file: "/etc/letsencrypt/live/ntfy.example.com.key"
cert-file: "/etc/letsencrypt/live/ntfy.example.com.crt"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
base-url: "http://ntfy.example.com"
listen-http: ":2586"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
# All the things: Behind a proxy, Firebase, cache, attachments, 
# SMTP publishing & receiving

base-url: "https://ntfy.sh"
listen-http: "127.0.0.1:2586"
firebase-key-file: "/etc/ntfy/firebase.json"
cache-file: "/var/cache/ntfy/cache.db"
behind-proxy: true
attachment-cache-dir: "/var/cache/ntfy/attachments"
smtp-sender-addr: "email-smtp.us-east-2.amazonaws.com:587"
smtp-sender-user: "AKIDEADBEEFAFFE12345"
smtp-sender-pass: "Abd13Kf+sfAk2DzifjafldkThisIsNotARealKeyOMG."
smtp-sender-from: "ntfy@ntfy.sh"
smtp-server-listen: ":25"
smtp-server-domain: "ntfy.sh"
smtp-server-addr-prefix: "ntfy-"
keepalive-interval: "45s"

Alternatively, you can also use command line arguments or environment variables to configure the server. Here's an example using Docker Compose (i.e. docker-compose.yml):

version: '3'
services:
  ntfy:
    image: binwiederhier/ntfy
    restart: unless-stopped
    environment:
      NTFY_BASE_URL: http://ntfy.example.com
      NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
      NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
      NTFY_AUTH_DEFAULT_ACCESS: deny-all
      NTFY_BEHIND_PROXY: true
      NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
      NTFY_ENABLE_LOGIN: true
    volumes:
      - ./:/var/lib/ntfy
    ports:
      - 80:80
    command: serve
version: '3'
services:
  ntfy:
    image: binwiederhier/ntfy
    restart: unless-stopped
    environment:
      NTFY_BASE_URL: http://ntfy.example.com
      NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
      NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
      NTFY_AUTH_DEFAULT_ACCESS: deny-all
      NTFY_BEHIND_PROXY: true
      NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
      NTFY_ENABLE_LOGIN: true
      NTFY_UPSTREAM_BASE_URL: https://ntfy.sh
      NTFY_WEB_PUSH_PUBLIC_KEY: <public_key>
      NTFY_WEB_PUSH_PRIVATE_KEY: <private_key>
      NTFY_WEB_PUSH_FILE: /var/lib/ntfy/webpush.db
      NTFY_WEB_PUSH_EMAIL_ADDRESS: <email>
    volumes:
      - ./:/var/lib/ntfy
    ports:
      - 8093:80
    command: serve

Message cache

If desired, ntfy can temporarily keep notifications in an in-memory or an on-disk cache. Caching messages for a short period of time is important to allow phones and other devices with brittle Internet connections to be able to retrieve notifications that they may have missed.

By default, ntfy keeps messages in-memory for 12 hours, which means that cached messages do not survive an application restart. You can override this behavior using the following config settings:

  • cache-file: if set, ntfy will store messages in a SQLite based cache (default is empty, which means in-memory cache). This is required if you'd like messages to be retained across restarts.
  • cache-duration: defines the duration for which messages are stored in the cache (default is 12h).

You can also entirely disable the cache by setting cache-duration to 0. When the cache is disabled, messages are only passed on to the connected subscribers, but never stored on disk or even kept in memory longer than is needed to forward the message to the subscribers.

Subscribers can retrieve cached messaging using the poll=1 parameter, as well as the since= parameter.

Attachments

If desired, you may allow users to upload and attach files to notifications. To enable this feature, you have to simply configure an attachment cache directory and a base URL (attachment-cache-dir, base-url). Once these options are set and the directory is writable by the server user, you can upload attachments via PUT.

By default, attachments are stored in the disk-cache for only 3 hours. The main reason for this is to avoid legal issues and such when hosting user controlled content. Typically, this is more than enough time for the user (or the auto download feature) to download the file. The following config options are relevant to attachments:

  • base-url is the root URL for the ntfy server; this is needed for the generated attachment URLs
  • attachment-cache-dir is the cache directory for attached files
  • attachment-total-size-limit is the size limit of the on-disk attachment cache (default: 5G)
  • attachment-file-size-limit is the per-file attachment size limit (e.g. 300k, 2M, 100M, default: 15M)
  • attachment-expiry-duration is the duration after which uploaded attachments will be deleted (e.g. 3h, 20h, default: 3h)

Here's an example config using mostly the defaults (except for the cache directory, which is empty by default):

base-url: "https://ntfy.sh"
attachment-cache-dir: "/var/cache/ntfy/attachments"
base-url: "https://ntfy.sh"
attachment-cache-dir: "/var/cache/ntfy/attachments"
attachment-total-size-limit: "5G"
attachment-file-size-limit: "15M"
attachment-expiry-duration: "3h"
visitor-attachment-total-size-limit: "100M"
visitor-attachment-daily-bandwidth-limit: "500M"

Please also refer to the rate limiting settings below, specifically visitor-attachment-total-size-limit and visitor-attachment-daily-bandwidth-limit. Setting these conservatively is necessary to avoid abuse.

Access control

By default, the ntfy server is open for everyone, meaning everyone can read and write to any topic (this is how ntfy.sh is configured). To restrict access to your own server, you can optionally configure authentication and authorization.

ntfy's auth is implemented with a simple SQLite-based backend. It implements two roles (user and admin) and per-topic read and write permissions using an access control list (ACL). Access control entries can be applied to users as well as the special everyone user (*), which represents anonymous API access.

To set up auth, simply configure the following two options:

  • auth-file is the user/access database; it is created automatically if it doesn't already exist; suggested location /var/lib/ntfy/user.db (easiest if deb/rpm package is used)
  • auth-default-access defines the default/fallback access if no access control entry is found; it can be set to read-write (default), read-only, write-only or deny-all.

Once configured, you can use the ntfy user command to add or modify users, and the ntfy access command lets you modify the access control list for specific users and topic patterns. Both of these commands directly edit the auth database (as defined in auth-file), so they only work on the server, and only if the user accessing them has the right permissions.

Users and roles

The ntfy user command allows you to add/remove/change users in the ntfy user database, as well as change passwords or roles (user or admin). In practice, you'll often just create one admin user with ntfy user add --role=admin ... and be done with all this (see example below).

Roles:

  • Role user (default): Users with this role have no special permissions. Manage access using ntfy access (see below).
  • Role admin: Users with this role can read/write to all topics. Granular access control is not necessary.

Example commands (type ntfy user --help or ntfy user COMMAND --help for more details):

ntfy user list                     # Shows list of users (alias: 'ntfy access')
ntfy user add phil                 # Add regular user phil  
ntfy user add --role=admin phil    # Add admin user phil
ntfy user del phil                 # Delete user phil
ntfy user change-pass phil         # Change password for user phil
ntfy user change-role phil admin   # Make user phil an admin
ntfy user change-tier phil pro     # Change phil's tier to "pro"

Access control list (ACL)

The access control list (ACL) manages access to topics for non-admin users, and for anonymous access (everyone/*). Each entry represents the access permissions for a user to a specific topic or topic pattern.

The ACL can be displayed or modified with the ntfy access command:

ntfy access                            # Shows access control list (alias: 'ntfy user list')
ntfy access USERNAME                   # Shows access control entries for USERNAME
ntfy access USERNAME TOPIC PERMISSION  # Allow/deny access for USERNAME to TOPIC

A USERNAME is an existing user, as created with ntfy user add (see users and roles), or the anonymous user everyone or *, which represents clients that access the API without username/password.

A TOPIC is either a specific topic name (e.g. mytopic, or phil_alerts), or a wildcard pattern that matches any number of topics (e.g. alerts_* or ben-*). Only the wildcard character * is supported. It stands for zero to any number of characters.

A PERMISSION is any of the following supported permissions:

  • read-write (alias: rw): Allows publishing messages to the given topic, as well as subscribing and reading messages
  • read-only (aliases: read, ro): Allows only subscribing and reading messages, but not publishing to the topic
  • write-only (aliases: write, wo): Allows only publishing to the topic, but not subscribing to it
  • deny (alias: none): Allows neither publishing nor subscribing to a topic

Example commands (type ntfy access --help for more details):

ntfy access                        # Shows entire access control list
ntfy access phil                   # Shows access for user phil
ntfy access phil mytopic rw        # Allow read-write access to mytopic for user phil
ntfy access everyone mytopic rw    # Allow anonymous read-write access to mytopic
ntfy access everyone "up*" write   # Allow anonymous write-only access to topics "up..."
ntfy access --reset                # Reset entire access control list
ntfy access --reset phil           # Reset all access for user phil
ntfy access --reset phil mytopic   # Reset access for user phil and topic mytopic

Example ACL:

$ ntfy access
user phil (admin)
- read-write access to all topics (admin role)
user ben (user)
- read-write access to topic garagedoor
- read-write access to topic alerts*
- read-only access to topic furnace
user * (anonymous)
- read-only access to topic announcements
- read-only access to topic server-stats
- no access to any (other) topics (server config)

In this example, phil has the role admin, so he has read-write access to all topics (no ACL entries are necessary). User ben has three topic-specific entries. He can read, but not write to topic furnace, and has read-write access to topic garagedoor and all topics starting with the word alerts (wildcards). Clients that are not authenticated (called */everyone) only have read access to the announcements and server-stats topics.

Access tokens

In addition to username/password auth, ntfy also provides authentication via access tokens. Access tokens are useful to avoid having to configure your password across multiple publishing/subscribing applications. For instance, you may want to use a dedicated token to publish from your backup host, and one from your home automation system.

Info

As of today, access tokens grant users full access to the user account. Aside from changing the password, and deleting the account, every action can be performed with a token. Granular access tokens are on the roadmap, but not yet implemented.

The ntfy token command can be used to manage access tokens for users. Tokens can have labels, and they can expire automatically (or never expire). Each user can have up to 20 tokens (hardcoded).

Example commands (type ntfy token --help or ntfy token COMMAND --help for more details):

ntfy token list                      # Shows list of tokens for all users
ntfy token list phil                 # Shows list of tokens for user phil
ntfy token add phil                  # Create token for user phil which never expires
ntfy token add --expires=2d phil     # Create token for user phil which expires in 2 days
ntfy token remove phil tk_th2sxr...  # Delete token

Creating an access token:

$ ntfy token add --expires=30d --label="backups" phil
$ ntfy token list
user phil
- tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 (backups), expires 15 Mar 23 14:33 EDT, accessed from 0.0.0.0 at 13 Feb 23 13:33 EST

Once an access token is created, you can use it to authenticate against the ntfy server, e.g. when you publish or subscribe to topics. To learn how, check out authenticate via access tokens.

Example: Private instance

The easiest way to configure a private instance is to set auth-default-access to deny-all in the server.yml:

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

After that, simply create an admin user:

$ ntfy user add --role=admin phil
password: mypass
confirm: mypass
user phil added with role admin 

Once you've done that, you can publish and subscribe using Basic Auth with the given username/password. Be sure to use HTTPS to avoid eavesdropping and exposing your password. Here's a simple example:

curl \
    -u phil:mypass \
    -d "Look ma, with auth" \
    https://ntfy.example.com/mysecrets
ntfy publish \
    -u phil:mypass \
    ntfy.example.com/mysecrets \
    "Look ma, with auth"
POST /mysecrets HTTP/1.1
Host: ntfy.example.com
Authorization: Basic cGhpbDpteXBhc3M=

Look ma, with auth
fetch('https://ntfy.example.com/mysecrets', {
    method: 'POST', // PUT works too
    body: 'Look ma, with auth',
    headers: {
        'Authorization': 'Basic cGhpbDpteXBhc3M='
    }
})
req, _ := http.NewRequest("POST", "https://ntfy.example.com/mysecrets",
    strings.NewReader("Look ma, with auth"))
req.Header.Set("Authorization", "Basic cGhpbDpteXBhc3M=")
http.DefaultClient.Do(req)
requests.post("https://ntfy.example.com/mysecrets",
    data="Look ma, with auth",
    headers={
        "Authorization": "Basic cGhpbDpteXBhc3M="
    })
file_get_contents('https://ntfy.example.com/mysecrets', false, stream_context_create([
    'http' => [
        'method' => 'POST', // PUT also works
        'header' => 
            'Content-Type: text/plain\r\n' .
            'Authorization: Basic cGhpbDpteXBhc3M=',
        'content' => 'Look ma, with auth'
    ]
]));

Example: UnifiedPush

UnifiedPush requires that the application server (e.g. Synapse, Fediverse Server, …) has anonymous write access to the topic used for push messages. The topic names used by UnifiedPush all start with the up* prefix. Please refer to the UnifiedPush documentation for more details.

To enable support for UnifiedPush for private servers (i.e. auth-default-access: "deny-all"), you should either allow anonymous write access for the entire prefix or explicitly per topic:

$ ntfy access '*' 'up*' write-only
$ ntfy access '*' upYzMtZGZiYTY5 write-only

E-mail notifications

To allow forwarding messages via e-mail, you can configure an SMTP server for outgoing messages. Once configured, you can set the X-Email header to send messages via e-mail (e.g. curl -d "hi there" -H "X-Email: phil@example.com" ntfy.sh/mytopic).

As of today, only SMTP servers with PLAIN auth and STARTLS are supported. To enable e-mail sending, you must set the following settings:

  • base-url is the root URL for the ntfy server; this is needed for e-mail footer
  • smtp-sender-addr is the hostname:port of the SMTP server
  • smtp-sender-user and smtp-sender-pass are the username and password of the SMTP user
  • smtp-sender-from is the e-mail address of the sender

Here's an example config using Amazon SES for outgoing mail (this is how it is configured for ntfy.sh):

base-url: "https://ntfy.sh"
smtp-sender-addr: "email-smtp.us-east-2.amazonaws.com:587"
smtp-sender-user: "AKIDEADBEEFAFFE12345"
smtp-sender-pass: "Abd13Kf+sfAk2DzifjafldkThisIsNotARealKeyOMG."
smtp-sender-from: "ntfy@ntfy.sh"

Please also refer to the rate limiting settings below, specifically visitor-email-limit-burst and visitor-email-limit-burst. Setting these conservatively is necessary to avoid abuse.

E-mail publishing

To allow publishing messages via e-mail, ntfy can run a lightweight SMTP server for incoming messages. Once configured, users can send emails to a topic e-mail address (e.g. mytopic@ntfy.sh or myprefix-mytopic@ntfy.sh) to publish messages to a topic. This is useful for e-mail based integrations such as for statuspage.io (though these days most services also support webhooks and HTTP calls).

To configure the SMTP server, you must at least set smtp-server-listen and smtp-server-domain:

  • smtp-server-listen defines the IP address and port the SMTP server will listen on, e.g. :25 or 1.2.3.4:25
  • smtp-server-domain is the e-mail domain, e.g. ntfy.sh (must be identical to MX record, see below)
  • smtp-server-addr-prefix is an optional prefix for the e-mail addresses to prevent spam. If set to ntfy-, for instance, only e-mails to ntfy-$topic@ntfy.sh will be accepted. If this is not set, all emails to $topic@ntfy.sh will be accepted (which may obviously be a spam problem).

Here's an example config (this is how it is configured for ntfy.sh):

smtp-server-listen: ":25"
smtp-server-domain: "ntfy.sh"
smtp-server-addr-prefix: "ntfy-"

In addition to configuring the ntfy server, you have to create two DNS records (an MX record and a corresponding A record), so incoming mail will find its way to your server. Here's an example of how ntfy.sh is configured (in Amazon Route 53):

DNS records for incoming mail

DNS records for incoming mail

You can check if everything is working correctly by sending an email as raw SMTP via nc. Create a text file, e.g. email.txt

EHLO example.com
MAIL FROM: phil@example.com
RCPT TO: ntfy-mytopic@ntfy.sh
DATA
Subject: Email for you
Content-Type: text/plain; charset="UTF-8"

Hello from 🇩🇪
.

And then send the mail via nc like this. If you see any lines starting with 451, those are errors from the ntfy server. Read them carefully.

$ cat email.txt | nc -N ntfy.sh 25
220 ntfy.sh ESMTP Service Ready
250-Hello example.com
...
250 2.0.0 Roger, accepting mail from <phil@example.com>
250 2.0.0 I'll make sure <ntfy-mytopic@ntfy.sh> gets this

As for the DNS setup, be sure to verify that dig MX and dig A are returning results similar to this:

$ dig MX ntfy.sh +short 
10 mx1.ntfy.sh.
$ dig A mx1.ntfy.sh +short 
3.139.215.220

Local-only email

If you want to send emails from an internal service on the same network as your ntfy instance, you do not need to worry about DNS records at all. Define a port for the SMTP server and pick an SMTP server domain (can be anything).

smtp-server-listen: ":25"
smtp-server-domain: "example.com"
smtp-server-addr-prefix: "ntfy-"  # optional

Then, in the email settings of your internal service, set the SMTP server address to the IP address of your ntfy instance. Set the port to the value you defined in smtp-server-listen. Leave any username and password fields empty. In the "From" address, pick anything (e.g., "alerts@ntfy.sh"); the value doesn't matter. In the "To" address, put in an email address that follows this pattern: [topic]@[smtp-server-domain] (or [smtp-server-addr-prefix][topic]@[smtp-server-domain] if you set smtp-server-addr-prefix).

So if you used example.com as the SMTP server domain, and you want to send a message to the email-alerts topic, set the "To" address to email-alerts@example.com. If the topic has access restrictions, you will need to include an access token in the "To" address, such as email-alerts+tk_AbC123dEf456@example.com.

If the internal service lets you use define an email "Subject", it will become the title of the notification. The body of the email will become the message of the notification.

Behind a proxy (TLS, etc.)

Warning

If you are running ntfy behind a proxy, you must set the behind-proxy flag. Otherwise, all visitors are rate limited as if they are one.

It may be desirable to run ntfy behind a proxy (e.g. nginx, HAproxy or Apache), so you can provide TLS certificates using Let's Encrypt using certbot, or simply because you'd like to share the ports (80/443) with other services. Whatever your reasons may be, there are a few things to consider.

If you are running ntfy behind a proxy, you should set the behind-proxy flag. This will instruct the rate limiting logic to use the X-Forwarded-For header as the primary identifier for a visitor, as opposed to the remote IP address. If the behind-proxy flag is not set, all visitors will be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address.

# Tell ntfy to use "X-Forwarded-For" to identify visitors
behind-proxy: true

TLS/SSL

ntfy supports HTTPS/TLS by setting the listen-https config option. However, if you are behind a proxy, it is recommended that TLS/SSL termination is done by the proxy itself (see below).

I highly recommend using certbot. I use it with the dns-route53 plugin, which lets you use AWS Route 53 as the challenge. That's much easier than using the HTTP challenge. I've found this guide to be incredibly helpful.

nginx/Apache2/caddy

For your convenience, here's a working config that'll help configure things behind a proxy. Be sure to enable WebSockets by forwarding the Connection and Upgrade headers accordingly.

In this example, ntfy runs on :2586 and we proxy traffic to it. We also redirect HTTP to HTTPS for GET requests against a topic or the root domain:

# /etc/nginx/sites-*/ntfy
#
# This config allows insecure HTTP POST/PUT requests against topics to allow a short curl syntax (without -L
# and "https://" prefix). It also disables output buffering, which has worked well for the ntfy.sh server.
#
# This is pretty much how ntfy.sh is configured. To see the exact configuration,
# see https://github.com/binwiederhier/ntfy-ansible/

server {
  listen 80;
  server_name ntfy.sh;

  location / {
    # Redirect HTTP to HTTPS, but only for GET topic addresses, since we want 
    # it to work with curl without the annoying https:// prefix
    set $redirect_https "";
    if ($request_method = GET) {
      set $redirect_https "yes";
    }
    if ($request_uri ~* "^/([-_a-z0-9]{0,64}$|docs/|static/)") {
      set $redirect_https "${redirect_https}yes";
    }
    if ($redirect_https = "yesyes") {
      return 302 https://$http_host$request_uri$is_args$query_string;
    }

    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; # Stream request body to backend
  }
}

server {
  listen 443 ssl http2;
  server_name ntfy.sh;

  # See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6see https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
  ssl_session_timeout 1d;
  ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
  ssl_session_tickets off;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
  ssl_prefer_server_ciphers off;

  ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;

  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; # Stream request body to backend
  }
}
# /etc/nginx/sites-*/ntfy
#
# This config requires the use of the -L flag in curl to redirect to HTTPS, and it keeps nginx output buffering
# enabled. While recommended, I have had issues with that in the past.

server {
  listen 80;
  server_name ntfy.sh;

  location / {
    return 302 https://$http_host$request_uri$is_args$query_string;

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

    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; # Stream request body to backend
  }
}

server {
  listen 443 ssl http2;
  server_name ntfy.sh;

  # See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6see https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
  ssl_session_timeout 1d;
  ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
  ssl_session_tickets off;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
  ssl_prefer_server_ciphers off;

  ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;

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

    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; # Stream request body to backend
  }
}
# /etc/apache2/sites-*/ntfy.conf

<VirtualHost *:80>
    ServerName ntfy.sh

    # Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
    ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
    ProxyPassReverse / http://127.0.0.1:2586/

    SetEnv proxy-nokeepalive 1
    SetEnv proxy-sendchunked 1

    # Higher than the max message size of 4096 bytes
    LimitRequestBody 102400

    # Redirect HTTP to HTTPS, but only for GET topic addresses, since we want 
    # it to work with curl without the annoying https:// prefix (requires "a2enmod alias")
    <If "%{REQUEST_METHOD} == 'GET'">
        RedirectMatch permanent "^/([-_A-Za-z0-9]{0,64})$" "https://%{SERVER_NAME}/$1"
    </If>

</VirtualHost>

<VirtualHost *:443>
    ServerName ntfy.sh

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/ntfy.sh/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf

    # Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
    ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
    ProxyPassReverse / http://127.0.0.1:2586/

    SetEnv proxy-nokeepalive 1
    SetEnv proxy-sendchunked 1

    # Higher than the max message size of 4096 bytes 
    LimitRequestBody 102400

</VirtualHost>
# Note that this config is most certainly incomplete. Please help out and let me know what's missing
# via Discord/Matrix or in a GitHub issue.

ntfy.sh, http://nfty.sh {
    reverse_proxy 127.0.0.1:2586

    # Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
    # it to work with curl without the annoying https:// prefix
    @httpget {
        protocol http
        method GET
        path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/)
    }
    redir @httpget https://{host}{uri}
}

Firebase (FCM)

Info

Using Firebase is optional and only works if you modify and build your own Android .apk. For a self-hosted instance, it's easier to just not bother with FCM.

Firebase Cloud Messaging (FCM) is the Google approved way to send push messages to Android devices. FCM is the only method that an Android app can receive messages without having to run a foreground service.

For the main host ntfy.sh, the ntfy Android app uses Firebase to send messages to the device. For other hosts, instant delivery is used and FCM is not involved.

To configure FCM for your self-hosted instance of the ntfy server, follow these steps:

  1. Sign up for a Firebase account
  2. Create a Firebase app and download the key file (e.g. myapp-firebase-adminsdk-...json)
  3. Place the key file in /etc/ntfy, set the firebase-key-file in server.yml accordingly and restart the ntfy server
  4. Build your own Android .apk following these instructions

Example:

# If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app.
# This is optional and only required to support Android apps (which don't allow background services anymore).
#
firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json"

iOS instant notifications

Unlike Android, iOS heavily restricts background processing, which sadly makes it impossible to implement instant push notifications without a central server.

To still support instant notifications on iOS through your self-hosted ntfy server, you have to forward so called poll_request messages to the main ntfy.sh server (or any upstream server that's APNS/Firebase connected, if you build your own iOS app), which will then forward it to Firebase/APNS.

To configure it, simply set upstream-base-url like so:

upstream-base-url: "https://ntfy.sh"
upstream-access-token: "..." # optional, only if rate limits exceeded, or upstream server protected

If set, all incoming messages will publish a poll request to the configured upstream server, containing the message ID of the original message, instructing the iOS app to poll this server for the actual message contents.

If upstream-base-url is not set, notifications will still eventually get to your device, but delivery can take hours, depending on the state of the phone. If you are using your phone, it shouldn't take more than 20-30 minutes though.

In case you're curious, here's an example of the entire flow:

  • In the iOS app, you subscribe to https://ntfy.example.com/mytopic
  • The app subscribes to the Firebase topic 6de73be8dfb7d69e... (the SHA256 of the topic URL)
  • When you publish a message to https://ntfy.example.com/mytopic, your ntfy server will publish a poll request to https://ntfy.sh/6de73be8dfb7d69e.... The request from your server to the upstream server contains only the message ID (in the X-Poll-ID header), and the SHA256 checksum of the topic URL (as upstream topic).
  • The ntfy.sh server publishes the poll request message to Firebase, which forwards it to APNS, which forwards it to your iOS device
  • Your iOS device receives the poll request, and fetches the actual message from your server, and then displays it

Here's an example of what the self-hosted server forwards to the upstream server. The request is equivalent to this curl:

curl -X POST -H "X-Poll-ID: s4PdJozxM8na" https://ntfy.sh/6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b
{"id":"4HsClFEuCIcs","time":1654087955,"event":"poll_request","topic":"6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b","message":"New message","poll_id":"s4PdJozxM8na"}

Note that the self-hosted server literally sends the message New message for every message, even if your message may be Some other message. This is so that if iOS cannot talk to the self-hosted server (in time, or at all), it'll show New message as a popup.

Web Push

Web Push (RFC8030) allows ntfy to receive push notifications, even when the ntfy web app (or even the browser, depending on the platform) is closed. When enabled, the user can enable background notifications for their topics in the wep app under Settings. Once enabled by the user, ntfy will forward published messages to the push endpoint (browser-provided, e.g. fcm.googleapis.com), which will then forward it to the browser.

To configure Web Push, you need to generate and configure a VAPID keypair (via ntfy webpush keys), a database to keep track of the browser's subscriptions, and an admin email address (you):

  • web-push-public-key is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
  • web-push-private-key is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
  • web-push-file is a database file to keep track of browser subscription endpoints, e.g. /var/cache/ntfy/webpush.db
  • web-push-email-address is the admin email address send to the push provider, e.g. sysadmin@example.com
  • web-push-startup-queries is an optional list of queries to run on startup`

Limitations:

  • Like foreground browser notifications, background push notifications require the web app to be served over HTTPS. A valid certificate is required, as service workers will not run on origins with untrusted certificates.

  • Web Push is only supported for the same server. You cannot use subscribe to web push on a topic on another server. This is due to a limitation of the Push API, which doesn't allow multiple push servers for the same origin.

To configure VAPID keys, first generate them:

$ ntfy webpush keys
Web Push keys generated.
...

Then copy the generated values into your server.yml or use the corresponding environment variables or command line arguments:

web-push-public-key: AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
web-push-private-key: AA2BB1234567890abcdefzxcvbnm1234567890
web-push-file: /var/cache/ntfy/webpush.db
web-push-email-address: sysadmin@example.com

The web-push-file is used to store the push subscriptions. Unused subscriptions will send out a warning after 7 days, and will automatically expire after 9 days (not configurable). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed), subscriptions are also removed automatically.

The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription file is deleted or lost, any web apps that aren't open will not receive new web push notifications until you open then.

Changing your public/private keypair is not recommended. Browsers only allow one server identity (public key) per origin, and if you change them the clients will not be able to subscribe via web push until the user manually clears the notification permission.

Tiers

ntfy supports associating users to pre-defined tiers. Tiers can be used to grant users higher limits, such as daily message limits, attachment size, or make it possible for users to reserve topics. If payments are enabled, tiers can be paid or unpaid, and users can upgrade/downgrade between them. If payments are disabled, then the only way to switch between tiers is with the ntfy user change-tier command (see users and roles).

By default, newly created users have no tier, and all usage limits are read from the server.yml config file. Once a user is associated with a tier, some limits are overridden based on the tier.

The ntfy tier command can be used to manage all available tiers. By default, there are no pre-defined tiers.

Example commands (type ntfy token --help or ntfy token COMMAND --help for more details):

ntfy tier add pro                     # Add tier with code "pro", using the defaults
ntfy tier change --name="Pro" pro     # Update the name of an existing tier
ntfy tier del starter                 # Delete an existing tier
ntfy user change-tier phil pro        # Switch user "phil" to tier "pro"

Creating a tier (full example):

ntfy tier add \
  --name="Pro" \
  --message-limit=10000 \
  --message-expiry-duration=24h \
  --email-limit=50 \
  --call-limit=10 \
  --reservation-limit=10 \
  --attachment-file-size-limit=100M \
  --attachment-total-size-limit=1G \
  --attachment-expiry-duration=12h \
  --attachment-bandwidth-limit=5G \
  --stripe-price-id=price_123456 \
  pro

Payments

ntfy supports paid tiers via Stripe as a payment provider. If payments are enabled, users can register, login and switch plans in the web app. The web app will behave slightly differently if payments are enabled (e.g. showing an upgrade banner, or "ntfy Pro" tags).

Info

The ntfy payments integration is very tailored to ntfy.sh and Stripe. I do not intend to support arbitrary use cases.

To enable payments, sign up with Stripe, set the stripe-secret-key and stripe-webhook-key config options:

  • stripe-secret-key is the key used for the Stripe API communication. Setting this values enables payments in the ntfy web app (e.g. Upgrade dialog). See API keys.
  • stripe-webhook-key is the key required to validate the authenticity of incoming webhooks from Stripe. Webhooks are essential to keep the local database in sync with the payment provider. See Webhooks.
  • billing-contact is an email address or website displayed in the "Upgrade tier" dialog to let people reach out with billing questions. If unset, nothing will be displayed.

In addition to setting these two options, you also need to define a Stripe webhook for the customer.subscription.updated and customer.subscription.deleted event, which points to https://ntfy.example.com/v1/account/billing/webhook.

Here's an example:

stripe-secret-key: "sk_test_ZmhzZGtmbGhkc2tqZmhzYcO2a2hmbGtnaHNkbGtnaGRsc2hnbG"
stripe-webhook-key: "whsec_ZnNkZnNIRExBSFNES0hBRFNmaHNka2ZsaGR"
billing-contact: "phil@example.com"

Phone calls

ntfy supports phone calls via Twilio as a call provider. If phone calls are enabled, users can verify and add a phone number, and then receive phone calls when publishing a message using the X-Call header. See publishing page for more details.

To enable Twilio integration, sign up with Twilio, purchase a phone number (Toll free numbers are the easiest), and then configure the following options:

  • twilio-account is the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586
  • twilio-auth-token is the Twilio auth token, e.g. affebeef258625862586258625862586
  • twilio-phone-number is the outgoing phone number you purchased, e.g. +18775132586
  • twilio-verify-service is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586

After you have configured phone calls, create a tier with a call limit (e.g. ntfy tier create --call-limit=10 ...), and then assign it to a user. Users may then use the X-Call header to receive a phone call when publishing a message.

Rate limiting

Info

Be aware that if you are running ntfy behind a proxy, you must set the behind-proxy flag. Otherwise, all visitors are rate limited as if they are one.

By default, ntfy runs without authentication, so it is vitally important that we protect the server from abuse or overload. There are various limits and rate limits in place that you can use to configure the server:

  • Global limit: A global limit applies across all visitors (IPs, clients, users)
  • Visitor limit: A visitor limit only applies to a certain visitor. A visitor is identified by its IP address (or the X-Forwarded-For header if behind-proxy is set). All config options that start with the word visitor apply only on a per-visitor basis.

During normal usage, you shouldn't encounter these limits at all, and even if you burst a few requests or emails (e.g. when you reconnect after a connection drop), it shouldn't have any effect.

General limits

Let's do the easy limits first:

  • global-topic-limit defines the total number of topics before the server rejects new topics. It defaults to 15,000.
  • visitor-subscription-limit is the number of subscriptions (open connections) per visitor. This value defaults to 30.

Request limits

In addition to the limits above, there is a requests/second limit per visitor for all sensitive GET/PUT/POST requests. This limit uses a token bucket (using Go's rate package):

Each visitor has a bucket of 60 requests they can fire against the server (defined by visitor-request-limit-burst). After the 60, new requests will encounter a 429 Too Many Requests response. The visitor request bucket is refilled at a rate of one request every 5s (defined by visitor-request-limit-replenish)

  • visitor-request-limit-burst is the initial bucket of requests each visitor has. This defaults to 60.
  • visitor-request-limit-replenish is the rate at which the bucket is refilled (one request per x). Defaults to 5s.
  • visitor-request-limit-exempt-hosts is a comma-separated list of hostnames and IPs to be exempt from request rate limiting; hostnames are resolved at the time the server is started. Defaults to an empty list.

Message limits

By default, the number of messages a visitor can send is governed entirely by the request limit. For instance, if the request limit allows for 15,000 requests per day, and all of those requests are POST/PUT requests to publish messages, then that is the daily message limit.

To limit the number of daily messages per visitor, you can set visitor-message-daily-limit. This defines the number of messages a visitor can send in a day. This counter is reset every day at midnight (UTC).

Attachment limits

Aside from the global file size and total attachment cache limits (see above), there are two relevant per-visitor limits:

  • visitor-attachment-total-size-limit is the total storage limit used for attachments per visitor. It defaults to 100M. The per-visitor storage is automatically decreased as attachments expire. External attachments (attached via X-Attach, see publishing docs) do not count here.
  • visitor-attachment-daily-bandwidth-limit is the total daily attachment download/upload bandwidth limit per visitor, including PUT and GET requests. This is to protect your precious bandwidth from abuse, since egress costs money in most cloud providers. This defaults to 500M.

E-mail limits

Similarly to the request limit, there is also an e-mail limit (only relevant if e-mail notifications are enabled):

  • visitor-email-limit-burst is the initial bucket of emails each visitor has. This defaults to 16.
  • visitor-email-limit-replenish is the rate at which the bucket is refilled (one email per x). Defaults to 1h.

Firebase limits

If Firebase is configured, all messages are also published to a Firebase topic (unless Firebase: no is set). Firebase enforces its own limits on how many messages can be published. Unfortunately these limits are a little vague and can change depending on the time of day. In practice, I have only ever observed 429 Quota exceeded responses from Firebase if too many messages are published to the same topic.

In ntfy, if Firebase responds with a 429 after publishing to a topic, the visitor (= IP address) who published the message is banned from publishing to Firebase for 10 minutes (not configurable). Because publishing to Firebase happens asynchronously, there is no indication of the user that this has happened. Non-Firebase subscribers (WebSocket or HTTP stream) are not affected. After the 10 minutes are up, messages forwarding to Firebase is resumed for this visitor.

If this ever happens, there will be a log message that looks something like this:

WARN Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor

Subscriber-based rate limiting

By default, ntfy puts almost all rate limits on the message publisher, e.g. number of messages, requests, and attachment size are all based on the visitor who publishes a message. Subscriber-based rate limiting is a way to use the rate limits of a topic's subscriber, instead of the limits of the publisher.

If enabled, subscribers may opt to have published messages counted against their own rate limits, as opposed to the publisher's rate limits. This is especially useful to increase the amount of messages that high-volume publishers (e.g. Matrix/Mastodon servers) are allowed to send.

Once enabled, a client may send a Rate-Topics: <topic1>,<topic2>,... header when subscribing to topics via HTTP stream, or websockets, thereby registering itself as the "rate visitor", i.e. the visitor whose rate limits to use when publishing on this topic. Note that setting the rate visitor requires read-write permission on the topic.

UnifiedPush only: If this setting is enabled, publishing to UnifiedPush topics will lead to an HTTP 507 Insufficient Storage response if no "rate visitor" has been previously registered. This is to avoid burning the publisher's visitor-message-daily-limit.

To enable subscriber-based rate limiting, set visitor-subscriber-rate-limiting: true.

Tuning for scale

If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config, if it's not behind a proxy, the ntfy server can keep about as many connections as the open file limit allows. This limit is typically called nofile. Other than that, RAM and CPU are obviously relevant. You may also want to check out this discussion on Reddit.

Depending on how you run it, here are a few limits that are relevant:

Message cache

By default, the message cache (defined by cache-file) uses the SQLite default settings, which means it syncs to disk on every write. For personal servers, this is perfectly adequate. For larger installations, such as ntfy.sh, the write-ahead log (WAL) should be enabled, and the sync mode should be adjusted. See this article for details.

In addition to that, for very high load servers (such as ntfy.sh), it may be beneficial to write messages to the cache in batches, and asynchronously. This can be enabled with the cache-batch-size and cache-batch-timeout. If you start seeing database locked messages in the logs, you should probably enable that.

Here's how ntfy.sh has been tuned in the server.yml file:

cache-batch-size: 25
cache-batch-timeout: "1s"
cache-startup-queries: |
    pragma journal_mode = WAL;
    pragma synchronous = normal;
    pragma temp_store = memory;
    pragma busy_timeout = 15000;
    vacuum;

For systemd services

If you're running ntfy in a systemd service (e.g. for .deb/.rpm packages), the main limiting factor is the LimitNOFILE setting in the systemd unit. The default open files limit for ntfy.service is 10,000. You can override it by creating a /etc/systemd/system/ntfy.service.d/override.conf file. As far as I can tell, /etc/security/limits.conf is not relevant.

# Allow 20,000 ntfy connections (and give room for other file handles)
[Service]
LimitNOFILE=20500

Outside of systemd

If you're running outside systemd, you may want to adjust your /etc/security/limits.conf file to increase the nofile setting. Here's an example that increases the limit to 5,000. You can find out the current setting by running ulimit -n, or manually override it temporarily by running ulimit -n 50000.

# Increase open files limit globally
* hard nofile 20500

Proxy limits (nginx, Apache2)

If you are running behind a proxy (e.g. nginx, Apache), the open files limit of the proxy is also relevant. So if your proxy runs inside of systemd, increase the limits in systemd for the proxy. Typically, the proxy open files limit has to be double the number of how many connections you'd like to support, because the proxy has to maintain the client connection and the connection to ntfy.

events {
  # Allow 40,000 proxy connections (2x of the desired ntfy connection count;
  # and give room for other file handles)
  worker_connections 40500;
}
# Allow 40,000 proxy connections (2x of the desired ntfy connection count;
# and give room for other file handles)
[Service]
LimitNOFILE=40500

Banning bad actors (fail2ban)

If you put stuff on the Internet, bad actors will try to break them or break in. fail2ban and nginx's ngx_http_limit_req_module module can be used to ban client IPs if they misbehave. This is on top of the rate limiting inside the ntfy server.

Here's an example for how ntfy.sh is configured, following the instructions from two tutorials (here and here):

# Rate limit all IP addresses
http {
  limit_req_zone $binary_remote_addr zone=one:10m rate=45r/m;
}

# Alternatively, whitelist certain IP addresses
http {
  geo $limited {
    default 1;
    116.203.112.46/32 0;
    132.226.42.65/32 0;
    ...
  }
  map $limited $limitkey {
    1 $binary_remote_addr;
    0 "";
  }
  limit_req_zone $limitkey zone=one:10m rate=45r/m;
}
# For each server/location block
server {
  location / {
    limit_req zone=one burst=1000 nodelay;
  }
}    
[Definition]
failregex = limiting requests, excess:.* by zone.*client: <HOST>
ignoreregex =
[nginx-req-limit]
enabled = true
filter = nginx-req-limit
action = iptables-multiport[name=ReqLimit, port="http,https", protocol=tcp]
logpath = /var/log/nginx/error.log
findtime = 600
bantime = 14400
maxretry = 10

Health checks

A preliminary health check API endpoint is exposed at /v1/health. The endpoint returns a json response in the format shown below. If a non-200 HTTP status code is returned or if the returned healthy field is false the ntfy service should be considered as unhealthy.

{"healthy":true}

See Installation for Docker for an example of how this could be used in a docker-compose environment.

Monitoring

If configured, ntfy can expose a /metrics endpoint for Prometheus, which can then be used to create dashboards and alerts (e.g. via Grafana).

To configure the metrics endpoint, either set enable-metrics and/or set the listen-metrics-http option to a dedicated listen address. Metrics may be considered sensitive information, so before you enable them, be sure you know what you are doing, and/or secure access to the endpoint in your reverse proxy.

  • enable-metrics enables the /metrics endpoint for the default ntfy server (i.e. HTTP, HTTPS and/or Unix socket)
  • metrics-listen-http exposes the metrics endpoint via a dedicated [IP]:port. If set, this option implicitly enables metrics as well, e.g. "10.0.1.1:9090" or ":9090"
enable-metrics: true
metrics-listen-http: "10.0.1.1:9090"

In Prometheus, an example scrape config would look like this:

scrape_configs:
  - job_name: "ntfy"
    static_configs:
      - targets: ["10.0.1.1:9090"]

Here's an example Grafana dashboard built from the metrics (see Grafana JSON on GitHub):

ntfy Grafana dashboard

Profiling

ntfy can expose Go's net/http/pprof endpoints to support profiling of the ntfy server. If enabled, ntfy will listen on a dedicated listen IP/port, which can be accessed via the web browser on http://<ip>:<port>/debug/pprof/. This can be helpful to expose bottlenecks, and visualize call flows. To enable, simply set the profile-listen-http config option.

Logging & debugging

By default, ntfy logs to the console (stderr), with an info log level, and in a human-readable text format.

ntfy supports five different log levels, can also write to a file, log as JSON, and even supports granular log level overrides for easier debugging. Some options (log-level and log-level-overrides) can be hot reloaded by calling kill -HUP $pid or systemctl reload ntfy.

The following config options define the logging behavior:

  • log-format defines the output format, can be text (default) or json
  • log-file is a filename to write logs to. If this is not set, ntfy logs to stderr.
  • log-level defines the default log level, can be one of trace, debug, info (default), warn or error. Be aware that debug (and particularly trace) can be very verbose. Only turn them on briefly for debugging purposes.
  • log-level-overrides lets you override the log level if certain fields match. This is incredibly powerful for debugging certain parts of the system (e.g. only the account management, or only a certain visitor). This is an array of strings in the format:
    • field=value -> level to match a value exactly, e.g. tag=manager -> trace
    • field -> level to match any value, e.g. time_taken_ms -> debug

Logging config (good for production use):

log-level: info
log-format: json
log-file: /var/log/ntfy.log

Temporary debugging:
If something's not working right, you can debug/trace through what the ntfy server is doing by setting the log-level to debug or trace. The debug setting will output information about each published message, but not the message contents. The trace setting will also print the message contents.

Alternatively, you can set log-level-overrides for only certain fields, such as a visitor's IP address (visitor_ip), a username (user_name), or a tag (tag). There are dozens of fields you can use to override log levels. To learn what they are, either turn the log-level to trace and observe, or reference the source code.

Here's an example that will output only info log events, except when they match either of the defined overrides:

log-level: info
log-level-overrides:
  - "tag=manager -> trace"
  - "visitor_ip=1.2.3.4 -> debug"
  - "time_taken_ms -> debug"

Warning

The debug and trace log levels are very verbose, and using log-level-overrides has a performance penalty. Only use it for temporary debugging.

You can also hot-reload the log-level and log-level-overrides by sending the SIGHUP signal to the process after editing the server.yml file. You can do so by calling systemctl reload ntfy (if ntfy is running inside systemd), or by calling kill -HUP $(pidof ntfy). If successful, you'll see something like this:

$ ntfy serve
2022/06/02 10:29:28 INFO Listening on :2586[http] :1025[smtp], log level is INFO
2022/06/02 10:29:34 INFO Partially hot reloading configuration ...
2022/06/02 10:29:34 INFO Log level is TRACE

Config options

Each config option can be set in the config file /etc/ntfy/server.yml (e.g. listen-http: :80) or as a CLI option (e.g. --listen-http :80. Here's a list of all available options. Alternatively, you can set an environment variable before running the ntfy command (e.g. export NTFY_LISTEN_HTTP=:80).

Info

All config options can also be defined in the server.yml file using underscores instead of dashes, e.g. cache_duration and cache-duration are both supported. This is to support stricter YAML parsers that do not support dashes.

Config option Env variable Format Default Description
base-url NTFY_BASE_URL URL - Public facing base URL of the service (e.g. https://ntfy.sh)
listen-http NTFY_LISTEN_HTTP [host]:port :80 Listen address for the HTTP web server
listen-https NTFY_LISTEN_HTTPS [host]:port - Listen address for the HTTPS web server. If set, you also need to set key-file and cert-file.
listen-unix NTFY_LISTEN_UNIX filename - Path to a Unix socket to listen on
listen-unix-mode NTFY_LISTEN_UNIX_MODE file mode system default File mode of the Unix socket, e.g. 0700 or 0777
key-file NTFY_KEY_FILE filename - HTTPS/TLS private key file, only used if listen-https is set.
cert-file NTFY_CERT_FILE filename - HTTPS/TLS certificate file, only used if listen-https is set.
firebase-key-file NTFY_FIREBASE_KEY_FILE filename - If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See Firebase (FCM.
cache-file NTFY_CACHE_FILE filename - If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See message cache.
cache-duration NTFY_CACHE_DURATION duration 12h Duration for which messages will be buffered before they are deleted. This is required to support the since=... and poll=1 parameter. Set this to 0 to disable the cache entirely.
cache-startup-queries NTFY_CACHE_STARTUP_QUERIES string (SQL queries) - SQL queries to run during database startup; this is useful for tuning and enabling WAL mode
cache-batch-size NTFY_CACHE_BATCH_SIZE int 0 Max size of messages to batch together when writing to message cache (if zero, writes are synchronous)
cache-batch-timeout NTFY_CACHE_BATCH_TIMEOUT duration 0s Timeout for batched async writes to the message cache (if zero, writes are synchronous)
auth-file NTFY_AUTH_FILE filename - Auth database file used for access control. If set, enables authentication and access control. See access control.
auth-default-access NTFY_AUTH_DEFAULT_ACCESS read-write, read-only, write-only, deny-all read-write Default permissions if no matching entries in the auth database are found. Default is read-write.
behind-proxy NTFY_BEHIND_PROXY bool false If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection.
attachment-cache-dir NTFY_ATTACHMENT_CACHE_DIR directory - Cache directory for attached files. To enable attachments, this has to be set.
attachment-total-size-limit NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT size 5G Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected.
attachment-file-size-limit NTFY_ATTACHMENT_FILE_SIZE_LIMIT size 15M Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected.
attachment-expiry-duration NTFY_ATTACHMENT_EXPIRY_DURATION duration 3h Duration after which uploaded attachments will be deleted (e.g. 3h, 20h). Strongly affects visitor-attachment-total-size-limit.
smtp-sender-addr NTFY_SMTP_SENDER_ADDR host:port - SMTP server address to allow email sending
smtp-sender-user NTFY_SMTP_SENDER_USER string - SMTP user; only used if e-mail sending is enabled
smtp-sender-pass NTFY_SMTP_SENDER_PASS string - SMTP password; only used if e-mail sending is enabled
smtp-sender-from NTFY_SMTP_SENDER_FROM e-mail address - SMTP sender e-mail address; only used if e-mail sending is enabled
smtp-server-listen NTFY_SMTP_SERVER_LISTEN [ip]:port - Defines the IP address and port the SMTP server will listen on, e.g. :25 or 1.2.3.4:25
smtp-server-domain NTFY_SMTP_SERVER_DOMAIN domain name - SMTP server e-mail domain, e.g. ntfy.sh
smtp-server-addr-prefix NTFY_SMTP_SERVER_ADDR_PREFIX string - Optional prefix for the e-mail addresses to prevent spam, e.g. ntfy-
twilio-account NTFY_TWILIO_ACCOUNT string - Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586
twilio-auth-token NTFY_TWILIO_AUTH_TOKEN string - Twilio auth token, e.g. affebeef258625862586258625862586
twilio-phone-number NTFY_TWILIO_PHONE_NUMBER string - Twilio outgoing phone number, e.g. +18775132586
twilio-verify-service NTFY_TWILIO_VERIFY_SERVICE string - Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
keepalive-interval NTFY_KEEPALIVE_INTERVAL duration 45s Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that.
manager-interval NTFY_MANAGER_INTERVAL duration 1m Interval in which the manager prunes old messages, deletes topics and prints the stats.
global-topic-limit NTFY_GLOBAL_TOPIC_LIMIT number 15,000 Rate limiting: Total number of topics before the server rejects new topics.
upstream-base-url NTFY_UPSTREAM_BASE_URL URL https://ntfy.sh Forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers
upstream-access-token NTFY_UPSTREAM_ACCESS_TOKEN string tk_zyYLYj... Access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth
visitor-attachment-total-size-limit NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT size 100M Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See attachment-expiry-duration.
visitor-attachment-daily-bandwidth-limit NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT size 500M Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding.
visitor-email-limit-burst NTFY_VISITOR_EMAIL_LIMIT_BURST number 16 Rate limiting:Initial limit of e-mails per visitor
visitor-email-limit-replenish NTFY_VISITOR_EMAIL_LIMIT_REPLENISH duration 1h Rate limiting: Strongly related to visitor-email-limit-burst: The rate at which the bucket is refilled
visitor-message-daily-limit NTFY_VISITOR_MESSAGE_DAILY_LIMIT number - Rate limiting: Allowed number of messages per day per visitor, reset every day at midnight (UTC). By default, this value is unset.
visitor-request-limit-burst NTFY_VISITOR_REQUEST_LIMIT_BURST number 60 Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has
visitor-request-limit-replenish NTFY_VISITOR_REQUEST_LIMIT_REPLENISH duration 5s Rate limiting: Strongly related to visitor-request-limit-burst: The rate at which the bucket is refilled
visitor-request-limit-exempt-hosts NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS comma-separated host/IP list - Rate limiting: List of hostnames and IPs to be exempt from request rate limiting
visitor-subscription-limit NTFY_VISITOR_SUBSCRIPTION_LIMIT number 30 Rate limiting: Number of subscriptions per visitor (IP address)
visitor-subscriber-rate-limiting NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING bool false Rate limiting: Enables subscriber-based rate limiting
web-root NTFY_WEB_ROOT path, e.g. / or /app, or disable / Sets root of the web app (e.g. /, or /app), or disables it entirely (disable)
enable-signup NTFY_ENABLE_SIGNUP boolean (true or false) false Allows users to sign up via the web app, or API
enable-login NTFY_ENABLE_LOGIN boolean (true or false) false Allows users to log in via the web app, or API
enable-reservations NTFY_ENABLE_RESERVATIONS boolean (true or false) false Allows users to reserve topics (if their tier allows it)
stripe-secret-key NTFY_STRIPE_SECRET_KEY string - Payments: Key used for the Stripe API communication, this enables payments
stripe-webhook-key NTFY_STRIPE_WEBHOOK_KEY string - Payments: Key required to validate the authenticity of incoming webhooks from Stripe
billing-contact NTFY_BILLING_CONTACT email address or website - Payments: Email or website displayed in Upgrade dialog as a billing contact
web-push-public-key NTFY_WEB_PUSH_PUBLIC_KEY string - Web Push: Public Key. Run ntfy webpush keys to generate
web-push-private-key NTFY_WEB_PUSH_PRIVATE_KEY string - Web Push: Private Key. Run ntfy webpush keys to generate
web-push-file NTFY_WEB_PUSH_FILE string - Web Push: Database file that stores subscriptions
web-push-email-address NTFY_WEB_PUSH_EMAIL_ADDRESS string - Web Push: Sender email address
web-push-startup-queries NTFY_WEB_PUSH_STARTUP_QUERIES string - Web Push: SQL queries to run against subscription database at startup

The format for a duration is: <number>(smh), e.g. 30s, 20m or 1h.
The format for a size is: <number>(GMK), e.g. 1G, 200M or 4000k.

Command line options

NAME:
   ntfy serve - Run the ntfy server

USAGE:
   ntfy serve [OPTIONS..]

CATEGORY:
   Server commands

DESCRIPTION:
   Run the ntfy server and listen for incoming requests

   The command will load the configuration from /etc/ntfy/server.yml. Config options can 
   be overridden using the command line options.

   Examples:
     ntfy serve                      # Starts server in the foreground (on port 80)
     ntfy serve --listen-http :8080  # Starts server with alternate port

OPTIONS:
   --debug, -d                                                                                                            enable debug logging (default: false) [$NTFY_DEBUG]
   --trace                                                                                                                enable tracing (very verbose, be careful) (default: false) [$NTFY_TRACE]
   --no-log-dates, --no_log_dates                                                                                         disable the date/time prefix (default: false) [$NTFY_NO_LOG_DATES]
   --log-level value, --log_level value                                                                                   set log level (default: "INFO") [$NTFY_LOG_LEVEL]
   --log-level-overrides value, --log_level_overrides value [ --log-level-overrides value, --log_level_overrides value ]  set log level overrides [$NTFY_LOG_LEVEL_OVERRIDES]
   --log-format value, --log_format value                                                                                 set log format (default: "text") [$NTFY_LOG_FORMAT]
   --log-file value, --log_file value                                                                                     set log file, default is STDOUT [$NTFY_LOG_FILE]
   --config value, -c value                                                                                               config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
   --base-url value, --base_url value, -B value                                                                           externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL]
   --listen-http value, --listen_http value, -l value                                                                     ip:port used as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
   --listen-https value, --listen_https value, -L value                                                                   ip:port used as HTTPS listen address [$NTFY_LISTEN_HTTPS]
   --listen-unix value, --listen_unix value, -U value                                                                     listen on unix socket path [$NTFY_LISTEN_UNIX]
   --listen-unix-mode value, --listen_unix_mode value                                                                     file permissions of unix socket, e.g. 0700 (default: system default) [$NTFY_LISTEN_UNIX_MODE]
   --key-file value, --key_file value, -K value                                                                           private key file, if listen-https is set [$NTFY_KEY_FILE]
   --cert-file value, --cert_file value, -E value                                                                         certificate file, if listen-https is set [$NTFY_CERT_FILE]
   --firebase-key-file value, --firebase_key_file value, -F value                                                         Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE]
   --cache-file value, --cache_file value, -C value                                                                       cache file used for message caching [$NTFY_CACHE_FILE]
   --cache-duration since, --cache_duration since, -b since                                                               buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
   --cache-batch-size value, --cache_batch_size value                                                                     max size of messages to batch together when writing to message cache (if zero, writes are synchronous) (default: 0) [$NTFY_BATCH_SIZE]
   --cache-batch-timeout value, --cache_batch_timeout value                                                               timeout for batched async writes to the message cache (if zero, writes are synchronous) (default: 0s) [$NTFY_CACHE_BATCH_TIMEOUT]
   --cache-startup-queries value, --cache_startup_queries value                                                           queries run when the cache database is initialized [$NTFY_CACHE_STARTUP_QUERIES]
   --auth-file value, --auth_file value, -H value                                                                         auth database file used for access control [$NTFY_AUTH_FILE]
   --auth-startup-queries value, --auth_startup_queries value                                                             queries run when the auth database is initialized [$NTFY_AUTH_STARTUP_QUERIES]
   --auth-default-access value, --auth_default_access value, -p value                                                     default permissions if no matching entries in the auth database are found (default: "read-write") [$NTFY_AUTH_DEFAULT_ACCESS]
   --attachment-cache-dir value, --attachment_cache_dir value                                                             cache directory for attached files [$NTFY_ATTACHMENT_CACHE_DIR]
   --attachment-total-size-limit value, --attachment_total_size_limit value, -A value                                     limit of the on-disk attachment cache (default: 5G) [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT]
   --attachment-file-size-limit value, --attachment_file_size_limit value, -Y value                                       per-file attachment size limit (e.g. 300k, 2M, 100M) (default: 15M) [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT]
   --attachment-expiry-duration value, --attachment_expiry_duration value, -X value                                       duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: 3h) [$NTFY_ATTACHMENT_EXPIRY_DURATION]
   --keepalive-interval value, --keepalive_interval value, -k value                                                       interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL]
   --manager-interval value, --manager_interval value, -m value                                                           interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
   --disallowed-topics value, --disallowed_topics value [ --disallowed-topics value, --disallowed_topics value ]          topics that are not allowed to be used [$NTFY_DISALLOWED_TOPICS]
   --web-root value, --web_root value                                                                                     sets root of the web app (e.g. /, or /app), or disables it (disable) (default: "/") [$NTFY_WEB_ROOT]
   --enable-signup, --enable_signup                                                                                       allows users to sign up via the web app, or API (default: false) [$NTFY_ENABLE_SIGNUP]
   --enable-login, --enable_login                                                                                         allows users to log in via the web app, or API (default: false) [$NTFY_ENABLE_LOGIN]
   --enable-reservations, --enable_reservations                                                                           allows users to reserve topics (if their tier allows it) (default: false) [$NTFY_ENABLE_RESERVATIONS]
   --upstream-base-url value, --upstream_base_url value                                                                   forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers [$NTFY_UPSTREAM_BASE_URL]
   --upstream-access-token value, --upstream_access_token value                                                           access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth [$NTFY_UPSTREAM_ACCESS_TOKEN]
   --smtp-sender-addr value, --smtp_sender_addr value                                                                     SMTP server address (host:port) for outgoing emails [$NTFY_SMTP_SENDER_ADDR]
   --smtp-sender-user value, --smtp_sender_user value                                                                     SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER]
   --smtp-sender-pass value, --smtp_sender_pass value                                                                     SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS]
   --smtp-sender-from value, --smtp_sender_from value                                                                     SMTP sender address (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_FROM]
   --smtp-server-listen value, --smtp_server_listen value                                                                 SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN]
   --smtp-server-domain value, --smtp_server_domain value                                                                 SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN]
   --smtp-server-addr-prefix value, --smtp_server_addr_prefix value                                                       SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-') [$NTFY_SMTP_SERVER_ADDR_PREFIX]
   --twilio-account value, --twilio_account value                                                                         Twilio account SID, used for phone calls, e.g. AC123... [$NTFY_TWILIO_ACCOUNT]
   --twilio-auth-token value, --twilio_auth_token value                                                                   Twilio auth token [$NTFY_TWILIO_AUTH_TOKEN]
   --twilio-phone-number value, --twilio_phone_number value                                                               Twilio number to use for outgoing calls [$NTFY_TWILIO_PHONE_NUMBER]
   --twilio-verify-service value, --twilio_verify_service value                                                           Twilio Verify service ID, used for phone number verification [$NTFY_TWILIO_VERIFY_SERVICE]
   --global-topic-limit value, --global_topic_limit value, -T value                                                       total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT]
   --visitor-subscription-limit value, --visitor_subscription_limit value                                                 number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
   --visitor-attachment-total-size-limit value, --visitor_attachment_total_size_limit value                               total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT]
   --visitor-attachment-daily-bandwidth-limit value, --visitor_attachment_daily_bandwidth_limit value                     total daily attachment download/upload bandwidth limit per visitor (default: "500M") [$NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT]
   --visitor-request-limit-burst value, --visitor_request_limit_burst value                                               initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST]
   --visitor-request-limit-replenish value, --visitor_request_limit_replenish value                                       interval at which burst limit is replenished (one per x) (default: 5s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
   --visitor-request-limit-exempt-hosts value, --visitor_request_limit_exempt_hosts value                                 hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit [$NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS]
   --visitor-message-daily-limit value, --visitor_message_daily_limit value                                               max messages per visitor per day, derived from request limit if unset (default: 0) [$NTFY_VISITOR_MESSAGE_DAILY_LIMIT]
   --visitor-email-limit-burst value, --visitor_email_limit_burst value                                                   initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST]
   --visitor-email-limit-replenish value, --visitor_email_limit_replenish value                                           interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
   --visitor-subscriber-rate-limiting, --visitor_subscriber_rate_limiting                                                 enables subscriber-based rate limiting (default: false) [$NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING]
   --behind-proxy, --behind_proxy, -P                                                                                     if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
   --stripe-secret-key value, --stripe_secret_key value                                                                   key used for the Stripe API communication, this enables payments [$NTFY_STRIPE_SECRET_KEY]
   --stripe-webhook-key value, --stripe_webhook_key value                                                                 key required to validate the authenticity of incoming webhooks from Stripe [$NTFY_STRIPE_WEBHOOK_KEY]
   --billing-contact value, --billing_contact value                                                                       e-mail or website to display in upgrade dialog (only if payments are enabled) [$NTFY_BILLING_CONTACT]
   --enable-metrics, --enable_metrics                                                                                     if set, Prometheus metrics are exposed via the /metrics endpoint (default: false) [$NTFY_ENABLE_METRICS]
   --metrics-listen-http value, --metrics_listen_http value                                                               ip:port used to expose the metrics endpoint (implicitly enables metrics) [$NTFY_METRICS_LISTEN_HTTP]
   --profile-listen-http value, --profile_listen_http value                                                               ip:port used to expose the profiling endpoints (implicitly enables profiling) [$NTFY_PROFILE_LISTEN_HTTP]
   --web-push-public-key value, --web_push_public_key value                                                               public key used for web push notifications [$NTFY_WEB_PUSH_PUBLIC_KEY]
   --web-push-private-key value, --web_push_private_key value                                                             private key used for web push notifications [$NTFY_WEB_PUSH_PRIVATE_KEY]
   --web-push-file value, --web_push_file value                                                                           file used to store web push subscriptions [$NTFY_WEB_PUSH_FILE]
   --web-push-email-address value, --web_push_email_address value                                                         e-mail address of sender, required to use browser push services [$NTFY_WEB_PUSH_EMAIL_ADDRESS]
   --web-push-startup-queries value, --web_push_startup-queries value                                                     queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES]   
   --help, -h                                                                                                             show help