What you are setting up
ntfy is a publish-subscribe server over plain HTTP. Your scripts, cron jobs and monitoring tools send a message to a topic with a simple POST, and every phone, browser or desktop subscribed to that topic shows it. You end up with four pieces:
- the ntfy container, listening only on the server itself;
- two SQLite files it creates on its own: a message cache and a user database;
- nginx holding a free Let's Encrypt cert, the only part the internet talks to;
- optionally, a relay setting so iPhones get alerts the moment they are sent.
How big a server does it need?
A small one. ntfy's documentation names no CPU or memory figure and says that if ntfy runs for your home, scale is not something to think about; what runs out first is the open-files limit, since each subscriber keeps a connection open. That makes it a natural fit for the one-job cartridges on our dedicated servers page, which the page describes as ideal when you have one job to run. Ready servers are handed over within 30 minutes of payment approval.
Here is our own view on where to put it: away from whatever it watches. If the alerting box shares hardware with your website, one crash silences both. A separate machine also has its own port, unmetered, so attachments never turn into a bandwidth bill.
Two things to steer around. The eight-core X-Gene ARM cartridge runs only Ubuntu 16.04 or 18.04, too old for this guide, so pick one of the x86 servers running a recent Ubuntu or Debian. And if you would like a console to fall back on after a firewall mistake, choose a rack server, a blade or an m510 or m710x cartridge; the rest of the cartridges come with remote power control and no console.
Step 1: the Compose file
First install Docker Engine, release 28 or newer, plus its Compose plugin. Create the folder with sudo mkdir -p /srv/ntfy and save this as /srv/ntfy/compose.yaml, with your own domain in the first variable. Every value is quoted, so YAML hands Compose plain strings rather than booleans. These variables are the documented environment forms of the server.yml settings.
services:
ntfy:
image: binwiederhier/ntfy
command: serve
restart: unless-stopped
environment:
NTFY_BASE_URL: "https://push.example.com"
NTFY_BEHIND_PROXY: "true"
NTFY_CACHE_FILE: "/var/cache/ntfy/cache.db"
NTFY_ATTACHMENT_CACHE_DIR: "/var/cache/ntfy/attachments"
NTFY_AUTH_FILE: "/var/lib/ntfy/user.db"
NTFY_AUTH_DEFAULT_ACCESS: "deny-all"
NTFY_ENABLE_LOGIN: "true"
NTFY_UPSTREAM_BASE_URL: "https://ntfy.sh"
volumes:
- /srv/ntfy/cache:/var/cache/ntfy
- /srv/ntfy/lib:/var/lib/ntfy
ports:
- "127.0.0.1:2586:80"
Why 127.0.0.1 in the port line? Without an address, Docker publishes a port on every address the host has, IPv4 and IPv6, and on IPv4 that traffic slips past ufw's rules (on IPv6 too, if you have enabled IPv6 in Docker's daemon configuration). Bound to 127.0.0.1, only nginx, running on this same server, can connect to it, and from Docker Engine 28 onward that binding really is local-only (moby issue 45610).
NTFY_AUTH_DEFAULT_ACCESS: deny-allis the lock. The docs say that without access control everybody may post to and read from every topic.NTFY_BEHIND_PROXYtells ntfy to take the visitor's address from the proxy's X-Forwarded-For header; otherwise all your users share one rate limit.NTFY_CACHE_FILEkeeps the last 12 hours of messages through a restart, so phones that were out of signal can catch up.NTFY_UPSTREAM_BASE_URLis only for iPhone users. Drop it if no one will subscribe from an iPhone.
Step 2: firewall, start, first accounts
Turn the firewall on before the container starts. The sed line comes first because ufw only adds IPv6 versions of rules created while IPv6 is enabled. nginx will run directly on the server, so it needs 80 and 443 open on both IPv4 and IPv6.
sudo apt install -y ufw
sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH
sudo ufw --force enable
sudo ufw allow 80,443/tcp
sudo mkdir -p /srv/ntfy/cache /srv/ntfy/lib
cd /srv/ntfy
sudo docker compose up -d
curl -s http://127.0.0.1:2586/v1/health
- Make yourself an admin, who can use every topic:
sudo docker compose exec ntfy ntfy user add --role=admin maria. You are asked for the password, so it never appears on screen or in your shell history. - Add a separate account for your scripts:
sudo docker compose exec ntfy ntfy user add alerts. - Let it post, but not read, to topics starting with server-:
sudo docker compose exec ntfy ntfy access alerts 'server-*' wo. - Give it a token:
sudo docker compose exec ntfy ntfy token add --label=scripts alerts. The docs warn that tokens currently carry the full rights of their user, so keep that user this narrow. - Sign in to the web app or the phone app as maria and subscribe to
server-disk,server-backupsand so on.
Rather than pasting the token into commands, where it lands in shell history and the process list, keep Authorization: Bearer tk_... in a file only root can read (chmod 600) and send it with curl -H @/root/ntfy.header -d "Disk at 90%" https://push.example.com/server-disk.
Prefer packages to containers? The docs' Debian/Ubuntu repository at archive.ntfy.sh installs the same server with a systemd unit; settings then go in /etc/ntfy/server.yml as lowercase names with dashes (base-url rather than NTFY_BASE_URL), and the commands above drop the docker compose exec ntfy part.
HTTPS in front, without breaking live updates
Phones and browsers stay connected to ntfy for a long time, over streaming HTTP or websockets, and nginx must hand messages through as they come. Install nginx and certbot, then build the site from the documented example. The parts that matter:
proxy_pass http://127.0.0.1:2586;withproxy_http_version 1.1;- the websocket pair,
proxy_set_header Upgrade $http_upgrade;andproxy_set_header Connection "upgrade";, present in both of the nginx examples in the docs; - the
Hostheader set to$http_hostandX-Forwarded-Forset to$proxy_add_x_forwarded_for; proxy_buffering off;andproxy_request_buffering off;so nothing is held back;proxy_connect_timeout,proxy_send_timeoutandproxy_read_timeoutat3m;client_max_body_size 0;so attachments stream through to ntfy.
Send every plain-HTTP request to HTTPS, because your login travels with each request; the stricter of the two nginx examples in the docs does exactly that. Want visitors to reach you over IPv6? The docs point to the proxy for listening on both protocols, so add listen [::]:443 ssl; beside the IPv4 line. IPv6 comes with the server at no charge.
Getting alerts on an iPhone
Apple limits what apps can do in the background, so the iOS app cannot hold a connection to your server the way the Android app does. ntfy works around it with the relay setting from Step 1. Each time a message arrives, your server sends ntfy.sh a poll request. The docs spell out what is in it: an X-Poll-ID header holding the message's ID, plus a SHA-256 fingerprint of the full address of your topic, nothing more; its text is always the stand-in "New message", whatever you actually wrote. ntfy.sh passes that to Firebase and APNS, the iPhone wakes up, collects the actual text from your server and shows you that. You only see the stand-in if your phone cannot get through to your server in time.
Without the relay, alerts still come through, just late: the docs mention delays of up to hours, depending on what the phone is doing, and at most 20-30 minutes while you are using it. You only need upstream-access-token if you go over the relay's rate limits, or if the upstream server you relay through asks for a login.
Keeping your accounts safe
The file you cannot lose is /srv/ntfy/lib/user.db, which holds every account, permission and token; keep compose.yaml with it. The message cache only spans 12 hours by default and attachments expire after 3, so there is little point saving those. You manage the server; we keep the hardware and the network running, so the copies are yours to make. The .backup command belongs to SQLite and copies the database safely while ntfy is running. The last command creates /etc/cron.d/ntfy-backup through printf, once the folder it saves into exists.
sudo apt install -y sqlite3
sudo mkdir -m 700 -p /srv/ntfy-backups
printf '%s\n' '30 2 * * * root umask 077; sqlite3 /srv/ntfy/lib/user.db ".backup /srv/ntfy-backups/user-$(date +\%F).db" && cp /srv/ntfy/compose.yaml /srv/ntfy-backups/compose-$(date +\%F).yaml' | sudo tee /etc/cron.d/ntfy-backup
Copy /srv/ntfy-backups to another machine regularly, since a copy kept on the same disk fails along with it. To restore, run this from /srv/ntfy, and it only proceeds if the copy is really there: f=/srv/ntfy-backups/user-2026-10-20.db; [ -s "$f" ] && sudo docker compose stop ntfy && sudo cp "$f" /srv/ntfy/lib/user.db && sudo docker compose start ntfy.
Staying up to date
Read the release notes on docs.ntfy.sh first. Then, inside /srv/ntfy, sudo docker compose pull downloads the newer image and sudo docker compose up -d restarts ntfy on that image, keeping your data in /srv/ntfy. With the apt package, a normal apt upgrade does it, and the package script in the GitHub repository restarts the service when it was already active. One recent example of why the notes are worth a glance: version 2.33.0 shipped amd64 builds that crashed on older chips lacking the x86-64-v3 instruction level (AVX2, BMI2), and 2.33.1 fixed that the same day. A check of our own, not from the ntfy docs: grep -c avx2 /proc/cpuinfo tests for AVX2, one of the x86-64-v3 features. If it prints 0, go straight to 2.33.1 or later.
When the public ntfy.sh is enough
If you only send yourself the odd alert and do not mind a shared public server, ntfy.sh needs no installation at all. Running your own makes sense when you want topics nobody else can see, retention you decide, and no outside service storing what you send. Two honest limits: iPhone wake-ups still go through ntfy.sh, and the docs call clustering experimental, so think of this as one server, well looked after. Watching websites too? Uptime Kuma is one of ntfy's official integrations, and the data centre list shows which cities you can choose.
Questions people ask
- Can strangers use my ntfy server?
- Out of the box, yes: the docs say a server without access control lets anybody post to and read from every topic. Setting the auth file and deny-all as the default closes it, and from then on only the accounts you create get in.
- Do I have to use Docker?
- No. The official Debian/Ubuntu repository installs the same server as a systemd service, and the docs also cover rpm packages and plain binaries. Docker is handy if the server already runs other containers.
- Will my iPhone notifications pass through someone else's server?
- A wake-up signal will, if you turn on the relay. The docs say it carries only the message ID and a hash of the topic URL, with "New message" as stand-in text. Your phone then fetches the real content from your server to display; the stand-in appears only when the phone fails to get through to your server in time.
- Is ntfy free to use commercially?
- ntfy is dual licensed under Apache 2.0 and GPLv2, according to its README. What follows is a general reading, not legal advice. Using it on your own server for your own alerts needs nothing from you. Distributing it, changed or unchanged, means meeting the terms of whichever licence you pick, such as keeping notices and marking changes under Apache 2.0, or making the source available under GPLv2.
- Why do all my users get rate limited together?
- ntfy sees every request coming from nginx's address. Turn on the behind-proxy setting and make sure nginx passes the X-Forwarded-For header, and each visitor is counted separately again.