ntfy with Docker: Self-Hosted Push Notifications for Scripts and Servers (Linux, Windows, Mac)
Before you start
- 21 min read
Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#
ntfy (say "notify") is a free, open-source notification server: a script sends one HTTP request to a topic, and your phone or browser shows a push notification, with no account and no cloud service in between.

Logo: the official ntfy project (github.com/binwiederhier/ntfy)
Use it to hear when a backup finishes or fails, a service goes down, or a long job ends. On the public server ntfy.sh, "the topic is essentially a password". On your own server you can do better: keep everything private with a deny-all rule, give each script its own login, and keep messages on your hardware.
| What | Value (official docs, read October 2026) |
|---|---|
| Current version | ntfy server v2.35.0, released October 2026 (latest stable; the Android app is 1.25.2, the iOS app 1.7.0) |
| Image to pin | binwiederhier/ntfy:v2.35.0 (tag checked on Docker Hub; amd64, arm64, armv7, armv6) |
| Time needed | About 20 minutes for the Compose route (our estimate, not measured) |
| Size and RAM | Image about 35 MB compressed per platform (Docker Hub). The docs publish no RAM figure |
| Ports | TCP 80 inside the container; this guide publishes it as host port 2586, then removes it behind HTTPS |
| Licence | Dual: Apache License 2.0 and GPLv2 (project README) |
| Official docs | docs.ntfy.sh and the repository |
At a glance:
- What it is: a small publish-subscribe server. Topics are created on the fly; anyone allowed to read a topic gets its messages.
- What you send from:
curl, thentfycommand, or any tool that can make an HTTP POST (Uptime Kuma, n8n, restic scripts). - What receives: the Android app (Google Play or F-Droid), the iPhone app, and the web app in any browser.
- Private by default here:
auth-default-access: deny-all, one admin for you, one write-only user for scripts. - iPhone caveat: instant iPhone push needs a wake-up relay on ntfy.sh. Only a message ID and a hash of the topic URL go there, not your text.
- Platforms: Linux server with Docker is the tested route in the docs. Windows has a native server; macOS only the command-line client.
How it fits together#
A sender posts to a topic on your server. The server checks the token, stores the message, and pushes it to every app subscribed to that topic.
Drawn for this guide from the official docs. The dashed boxes are optional: the HTTPS layer, and the relay that only the iPhone app needs.
Before you start#
You need a machine that stays on, Docker with Compose, and a phone or browser that can reach it.
Linux: Docker Engine and Docker Compose (the docs show amd64, arm64, armv7 and armv6 images). Check:
docker --version docker compose version
Windows: Docker Desktop with the same Compose file. The ntfy docs do not cover Docker Desktop, and we did not test it. The docs do support a native Windows server (ZIP file, scoop install ntfy, or a service created with sc); this guide uses Docker so every system follows the same steps.
docker --version docker compose version
Mac: Docker Desktop as well. The docs say the macOS server is not supported outside Docker; only the CLI client is. Same check in Terminal:
docker --version docker compose version
Checklist:
- [ ] Docker and Compose work (the three commands above)
- [ ] Host port 2586 is free
- [ ] You know the server's address on your network (a name or a LAN IP your phone can reach)
- [ ] For the HTTPS section only: a domain, a DNS record for the server, and ports 80 and 443 free
- [ ] A password manager ready for the admin password and the script token
Tested by DIY Lab Hub: not yet. The commands come from the official docs, Dockerfile and CLI help read in October 2026. We did not run them.
Step 1: Create the folders#
ntfy needs three folders: one for server.yml, one for the message cache and attachments, one for the user database. We use ~/docker_volumes/ntfy, as all DIY Lab Hub guides do.
Linux#
mkdir -p ~/docker_volumes/ntfy/{config,cache/attachments,data,backups}
chmod 700 ~/docker_volumes/ntfy
cd ~/docker_volumes/ntfy
Windows (PowerShell)#
New-Item -ItemType Directory -Force "$HOME\docker_volumes\ntfy\config","$HOME\docker_volumes\ntfy\cache\attachments","$HOME\docker_volumes\ntfy\data","$HOME\docker_volumes\ntfy\backups" Set-Location "$HOME\docker_volumes\ntfy"
Mac (Terminal)#
Same commands as Linux:
mkdir -p ~/docker_volumes/ntfy/{config,cache/attachments,data,backups}
chmod 700 ~/docker_volumes/ntfy
cd ~/docker_volumes/ntfy
Check it worked. The folders exist: ls ~/docker_volumes/ntfy (Linux, Mac) or Get-ChildItem $HOME\docker_volumes\ntfy (Windows) lists backups, cache, config and data.
Tip: The user database in
dataholds password hashes and tokens. Thechmod 700keeps other local users out of the whole tree.
Step 2: Write the config and the Compose file, then start ntfy#
Two files do the work. config/server.yml is the server config; the image ships none, so we mount ours. The Compose file is the official example with a pinned tag, our three folders, and host port 2586.
The key setting is auth-default-access: deny-all. Without it the docs warn that "everyone can read and write to any topic". enable-login and require-login make the web app ask for a login; they only work when auth-file is set.
Linux#
cd ~/docker_volumes/ntfy
cat > config/server.yml << 'EOF'
base-url: "http://<your-server-address>:2586"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
enable-login: true
require-login: true
EOF
cat > docker-compose.yml << 'EOF'
services:
ntfy:
image: binwiederhier/ntfy:v2.35.0
container_name: ntfy
command:
- serve
environment:
- TZ=UTC
volumes:
- ./config:/etc/ntfy
- ./cache:/var/cache/ntfy
- ./data:/var/lib/ntfy
ports:
- "2586:80"
healthcheck:
test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
init: true
EOF
docker compose up -d
docker compose ps
docker compose logs --tail=50 ntfy
Replace <your-server-address> with the name or IP your phone will use. If you only test on this machine, localhost works for the browser but not for a phone.
Windows (PowerShell)#
Set-Location "$HOME\docker_volumes\ntfy"
@'
base-url: "http://<your-server-address>:2586"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
enable-login: true
require-login: true
'@ | Set-Content -Encoding ascii config\server.yml
@'
services:
ntfy:
image: binwiederhier/ntfy:v2.35.0
container_name: ntfy
command:
- serve
environment:
- TZ=UTC
volumes:
- ./config:/etc/ntfy
- ./cache:/var/cache/ntfy
- ./data:/var/lib/ntfy
ports:
- "2586:80"
healthcheck:
test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
init: true
'@ | Set-Content -Encoding ascii docker-compose.yml
docker compose up -d
docker compose ps
docker compose logs --tail=50 ntfy
Mac (Terminal)#
Same commands as Linux (Docker Desktop).
Check it worked. docker compose ps shows ntfy running; after about a minute the health check turns it healthy. The health endpoint answers (shape from the docs):
{"healthy":true,"role":"active"}
Run curl -s http://localhost:2586/v1/health (Linux, Mac; use curl.exe or Invoke-RestMethod in PowerShell). The docs give no expected log text, so read the log for lines starting with FATAL.
Tip: Keep
command: serve. The image's entrypoint isntfywith no default command, so a file withoutservedoes not start the server.
Step 3: Create your users and a token#
With deny-all, nobody can do anything until you create users. We make one admin for you (full access, used on the phone and web) and one <script-user> that may only write to your topic. Tokens grant full access to their user, so a script user with a narrow ACL limits the damage if a token leaks.
Pick a topic name nobody can guess, such as backups- plus random letters. Topic names may contain letters, digits, _ and -, up to 64 characters.
Linux#
cd ~/docker_volumes/ntfy docker compose exec ntfy ntfy user add --role=admin <admin-name> docker compose exec ntfy ntfy user add <script-user> docker compose exec ntfy ntfy access <script-user> "<your-topic>" write-only docker compose exec ntfy ntfy token add --label="scripts" <script-user> docker compose exec ntfy ntfy access
The first two commands ask for a password. Save the token that token add prints (it starts with tk_) in your password manager.
Windows (PowerShell)#
The ntfy command runs inside the container, so the commands are the same:
Set-Location "$HOME\docker_volumes\ntfy" docker compose exec ntfy ntfy user add --role=admin <admin-name> docker compose exec ntfy ntfy user add <script-user> docker compose exec ntfy ntfy access <script-user> "<your-topic>" write-only docker compose exec ntfy ntfy token add --label="scripts" <script-user> docker compose exec ntfy ntfy access
Mac (Terminal)#
Same commands as Linux.
Check it worked. ntfy access lists both users. The docs' example shows this shape (ours should list your names and topic, and the anonymous user with no access):
user <admin-name> (admin) - read-write access to all topics (admin role) user <script-user> (user) - write-only access to topic <your-topic> user * (anonymous) - no access to any (other) topics (server config)
Tip: Never put a token in a command you paste into a chat or a ticket. Keep it in a file only you can read (
chmod 600) and read it from the script.
Step 4: Subscribe on your phone and in the browser#
Sign in with the admin user and subscribe to your topic.
Linux#
Open http://<your-server-address>:2586 in a browser, sign in, and click the subscribe button to add your topic. For the phone, install the Android app from Google Play or F-Droid, or the iPhone app from the App Store. In the app, add a subscription, choose your own server instead of ntfy.sh, enter the base URL from server.yml, and sign in as the admin user. The app's wording may differ by version; the docs do not name each button.
Windows (PowerShell)#
Open the same address in Edge or Chrome and sign in. To get notifications when the tab is closed, the docs say you must enable Web Push (VAPID keys and HTTPS). This guide does not set that up; the phone app is the simplest route.
Mac (Terminal)#
Open the same address in Safari or Chrome and sign in. The same Web Push note applies: background notifications from a self-hosted web app need Web Push, which this guide does not cover.
Check it worked. After Step 5 your topic shows the message in the web app and on the phone. On Android with a self-hosted server, the app keeps its own connection open ("instant delivery") and shows a permanent notification; the docs say Firebase is used only for ntfy.sh.
Tip: If every topic in the phone app says "Reconnecting...", one subscription may be getting 403. The docs say all subscribed topics share one connection, so a single forbidden topic blocks all of them.
Step 5: Send your first notification#
Use the token from Step 3 so the write-only user can publish.
Linux#
curl \ -H "Authorization: Bearer <YOUR_TOKEN>" \ -H "Title: Hello from my server" \ -H "Priority: high" \ -H "Tags: tada" \ -d "ntfy works" \ http://<your-server-address>:2586/<your-topic>
Windows (PowerShell)#
The docs show Invoke-RestMethod. This form works in PowerShell 5 and 7:
$Request = @{
Method = "POST"
URI = "http://<your-server-address>:2586/<your-topic>"
Headers = @{ Authorization = "Bearer <YOUR_TOKEN>"; Title = "Hello from my server"; Priority = "high"; Tags = "tada" }
Body = "ntfy works"
}
Invoke-RestMethod @Request
Mac (Terminal)#
Same command as Linux.
Check it worked. The server answers with one line of JSON that includes the message ID, time, event, topic and your text, and the phone buzzes. A 401 or 403 means the token is wrong or its user has no write access to that topic name; check the spelling against ntfy access.
Tip: Priorities run from 1 (min) to 5 (max/urgent). The docs say priority 4 and 5 pop up on Android and 1 and 2 stay quiet. Use high priority only for things worth waking you for.
Send alerts from your other tools#
Make one write-only user per tool (for example kuma) so you can revoke each independently.
- Uptime Kuma: it has a built-in ntfy notification. Its form asks for the topic, the server URL, a priority, and an authentication method: none, username and password, or access token. Choose the token. See our Uptime Kuma guide.
- restic: wrap the backup in a check and post the result. This is our own pattern, not from the ntfy docs. See our restic guide.
TOKEN="$(cat ~/.config/ntfy-token)" if restic backup <your-path>; then curl -s -H "Authorization: Bearer $TOKEN" -H "Tags: heavy_check_mark" -d "restic backup finished" https://ntfy.<your-domain>/<your-topic> else curl -s -H "Authorization: Bearer $TOKEN" -H "Priority: high" -H "Tags: rotating_light" -d "restic backup FAILED" https://ntfy.<your-domain>/<your-topic> fi
- n8n: add an HTTP Request node with method POST, your topic URL, a header
Authorization: Bearer <YOUR_TOKEN>, and the text as the body. It is a normal HTTP call, so the settings match thecurlline above. See our n8n guide. - ntfy command: the same
ntfybinary is also a client. On Windows install it withscoop install ntfy, on Mac with the tarball from the install page, and setdefault-hostinclient.ymlto your server. Thenntfy publish --token <YOUR_TOKEN> <your-topic> "text"works.
Put it behind HTTPS (with or without Traefik)#
Basic-auth passwords and tokens travel in every request. The docs say to use HTTPS "to avoid eavesdropping and exposing your password". Anything beyond your home network needs it. Local-only users can stay on plain HTTP in a trusted LAN and skip this section. Both routes need a domain with a DNS record to the server, and both remove the published port so only the proxy reaches ntfy.
First change config/server.yml: set base-url to "https://ntfy.<your-domain>" and add behind-proxy: true. The docs warn that without that flag "all visitors are rate limited as if they are one". Then replace docker-compose.yml as shown below.
With Traefik#
On a server that already runs Traefik v3 with the Docker provider, add labels and drop the host port.
This assumes an external network proxy and an entrypoint named https. Create the network once:
docker network create proxy
services:
ntfy:
image: binwiederhier/ntfy:v2.35.0
container_name: ntfy
command:
- serve
environment:
- TZ=UTC
volumes:
- ./config:/etc/ntfy
- ./cache:/var/cache/ntfy
- ./data:/var/lib/ntfy
healthcheck:
test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 40s
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.ntfy.rule=Host(`ntfy.<your-domain>`)"
- "traefik.http.routers.ntfy.entrypoints=https"
- "traefik.http.routers.ntfy.tls=true"
- "traefik.http.services.ntfy.loadbalancer.server.port=80"
restart: unless-stopped
init: true
networks:
proxy:
external: true
Run docker compose up -d in the ntfy folder. Check it worked. curl -I https://ntfy.<your-domain>/ returns an HTTP status line over HTTPS.
Without Traefik: Caddy#
Choose this if you have no Traefik. Caddy gets and renews its own certificates. This is a short version; the full walk-through is in our Caddy guide.
Use the same Compose file as in the Traefik route but delete the labels: block. Create the proxy network if you skipped the Traefik route (docker network create proxy), then the Caddy folders:
mkdir -p ~/docker_volumes/caddy/{conf,data,config}
cd ~/docker_volumes/caddy
Save as docker-compose.yml. We mount the folder conf, not a single file, because Caddy's Docker docs warn that a single-file mount breaks reloads when an editor replaces the file:
services:
caddy:
image: caddy:2.11.7
container_name: caddy
restart: unless-stopped
ports:
- "80:80/tcp"
- "443:443/tcp"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- ./data:/data
- ./config:/config
networks:
- proxy
networks:
proxy:
external: true
Save as conf/Caddyfile for a public domain (Let's Encrypt, automatic):
ntfy.example.com {
reverse_proxy ntfy:80
}
For a home-only name, add tls internal for Caddy's own local certificate:
ntfy.home.arpa {
tls internal
reverse_proxy ntfy:80
}
Start both stacks (docker compose up -d in each folder). Caddy's docs say it handles WebSockets itself with reverse_proxy. For tls internal, trust Caddy's root certificate on each device. Our Caddy guide has the exact commands for Linux, Windows and Mac. The Android app also accepts a server certificate under Settings, Advanced, Manage certificates. The ntfy docs do not describe the iPhone case, so check it before relying on tls internal there.
Check it worked. curl -I https://ntfy.example.com/ returns a status line over HTTPS. For a home-only name the browser shows a padlock only after the root is trusted.
Tip: Do not also put your proxy's Basic Auth in front of ntfy. Issue #1018 shows it breaks the phone app and the
/file/downloads. ntfy has its own users and tokens.
Security and testing#
| Control | How |
|---|---|
| Default access | auth-default-access: deny-all. Check with ntfy access: the anonymous user must have no access |
| Topic names | Unguessable, even with ACLs. The docs call a topic "essentially a password" without them |
| Script users | One write-only user per tool. Tokens grant full access to their user, so narrow the user, not the token |
| HTTPS | Required beyond a trusted LAN. Remove host port 2586 once the proxy works |
| Folder | chmod 700 ~/docker_volumes/ntfy; it holds the user database |
| Rate limits | Built in: a burst of 60 requests, then one every 5 seconds per visitor. Set behind-proxy: true behind a proxy |
| Declarative users | Possible via auth-users (bcrypt hashes). In Compose, a $ in a hash must be written $$. The CLI route avoids this |
| Web app | Safe behind ACLs, per the FAQ. web-root: disable removes it |
Quick tests after each change (Linux, Mac; on Windows run the same docker compose lines):
cd ~/docker_volumes/ntfy
docker compose ps
docker compose logs --tail=100 ntfy
docker compose exec ntfy ntfy access
curl -s -o /dev/null -w "%{http_code}\n" -d "anonymous test" http://localhost:2586/<your-topic>
The last line should print 401 or 403, never 200: an anonymous publish is refused. That is the proof that deny-all works.
What leaves your server: nothing by default. The server publishes to Firebase only if firebase-key-file is set, which this guide does not do. The iPhone relay (below) is optional. The FAQ says the public server's logs contain topic names and IP addresses; your own logs stay on your machine.
iPhone and the ntfy.sh relay#
Apple limits background work, so the docs say instant iPhone notifications "without a central server" are impossible. A self-hosted server can forward a wake-up to ntfy.sh. Add this to server.yml only if you use the iPhone app:
upstream-base-url: "https://ntfy.sh"
What goes upstream: the message ID and a SHA256 hash of your topic URL. The upstream server sees the text "New message", never yours. Without it, the docs say delivery can take "hours", though usually 20 to 30 minutes. Two rules from the docs: set it to https://ntfy.sh, not your own domain, and make base-url match exactly what you entered as the server in the iPhone app.
Common mistakes#
These come from the official docs, the issue tracker and common AI assistant answers.
| Mistake | Why it happens | Do this instead |
|---|---|---|
| Starting with default access | A fresh server is open: "everyone can read and write to any topic" | Set auth-default-access: deny-all, then add users and ACLs |
Enabling auth-file without a data volume | Issue #1625 (closed): FATAL user database directory /var/lib/ntfy does not exist or is not accessible | Mount ./data:/var/lib/ntfy, as in Step 2 |
Wrong folder owner after using user: | Issue #1774 (closed, v2.24.0): attempt to write a readonly database and HTTP 500 on publish; the cause was a root-owned cache folder | chown cache and data to the container user, or run as the default user |
Setting upstream-base-url to your own domain | Issue #334 (closed): a Traefik setup looped into the rate limit | Use https://ntfy.sh. Current servers refuse equal values |
| No WebSocket headers in nginx | Issue #1948 (closed): WebSocket upgrade failed with HTTP 400 | Forward Connection and Upgrade. Caddy does it itself |
Forgetting behind-proxy: true | Every visitor looks like the proxy's IP, so all share one rate limit | Set it with HTTPS in front |
| Proxy Basic Auth in front of ntfy | Issue #1018 (closed): the phone app and /file/ break | Use ntfy's own users and tokens |
| Long attachment expiry, short cache | Issue #1692 (closed): attachments return 404 | Keep cache-duration at least attachment-expiry-duration |
| Treating the web app as the whole story | Background notifications need Web Push over valid HTTPS | Use the phone app for reliable alerts |
| AI assistants: inventing config values | Assistants guess auth-default-access: private; valid values are read-write, read-only, write-only, deny-all | Check every key against the options table in the docs |
AI assistants: leaving out serve, or using the public server | Examples for ntfy.sh use ntfy.sh/mytopic; the image needs serve | Use your own host in every URL and command: serve |
Tip: The server's own startup checks give plain error messages. Read
docker compose logs --tail=50 ntfyfirst; it names the setting that is wrong.
Tip: Test from a second device on mobile data, not only on your Wi-Fi. A phone on your LAN hides DNS and proxy mistakes that a phone outside would hit.
Tip: After you edit
server.yml, restart withdocker compose restart ntfyand read the log. A typo in the file stops the server at start, and the log names the setting.
Troubleshooting#
| Problem | Solution |
|---|---|
| Container does not serve anything, or exits at once | command: serve is missing in the Compose file |
FATAL user database directory /var/lib/ntfy does not exist or is not accessible | Mount ./data:/var/lib/ntfy (issue #1625) |
attempt to write a readonly database, HTTP 500 on publish | Fix owner and permissions on the cache and data folders (issue #1774) |
if attachment-cache-dir is set, base-url must also be set | Add base-url to server.yml |
if set, base-url must not have a path | Use https://ntfy.example.com, not a sub-path; ntfy does not support one |
cannot set require-login without also setting enable-login | Set enable-login: true as well, and auth-file |
base-url and upstream-base-url cannot be identical | Set upstream-base-url: "https://ntfy.sh" |
| Publish returns 401 or 403 | The token's user has no write ACL for the topic; run ntfy access |
| Phone shows "Reconnecting..." or late messages | Set behind-proxy, check WebSockets, and check no subscribed topic returns 403 |
| iPhone shows "New message" only | The phone cannot reach your server; check the address and base-url (docs, known issues) |
| No iPhone push for hours | Set upstream-base-url: "https://ntfy.sh" and match base-url exactly; remove and re-add the subscription |
Turn on debug logging when the table does not help: add log-level: debug to server.yml (or trace for more), recreate the container and read the log. Diagnostics: docker compose ps, docker compose logs --tail=100 ntfy, docker inspect ntfy.
Known bugs (open, October 2026)#
- Issue #2124: the web app can show a duplicate notification after reload when two messages arrive in the same second (since v2.31.0). The maintainer calls delivery "at-least-once", so clients must tolerate duplicates.
- Issue #1972: a valid bearer token reported to get an intermittent 401 on v2.27.0. The v2.35.0 notes fix a related race that caused intermittent 403s; if you still see 401s on v2.35.0, report it there.
- Issue #2117: an iOS 27 report of missing push; the reporter later wrote that it works again.
Updates and backups#
Update. The project ships often (two releases in one week this month). Read the release notes first, change the tag in docker-compose.yml, then:
cd ~/docker_volumes/ntfy docker compose pull docker compose up -d docker compose logs --tail=100 ntfy
Back up. The important files are config/server.yml and data/user.db. The cache only holds temporary messages. Stop the container first so the database is consistent:
cd ~/docker_volumes/ntfy docker compose down tar -czf "backups/ntfy-$(date +%F).tar.gz" config data chmod 600 backups/ntfy-*.tar.gz docker compose up -d
Schedule the same lines at 03:00 with crontab -e (Linux, Mac). The archive holds password hashes and tokens: keep copies off the machine, and see our restic guide for encrypted backups.
Windows (PowerShell)#
Set-Location "$HOME\docker_volumes\ntfy" docker compose down tar -czf "backups\ntfy-$(Get-Date -Format yyyy-MM-dd).tar.gz" config data docker compose up -d
tar.exe ships with Windows 10 and 11. Run it nightly with Task Scheduler.
Restore test. Unpack one archive into a new empty folder, never over the live one:
mkdir -p /tmp/restore-test && tar -xzf backups/ntfy-<date>.tar.gz -C /tmp/restore-test ls /tmp/restore-test/data
The folder should contain user.db.
Performance and resources#
The docs give no RAM or CPU numbers, so we give none. Measure on your machine:
- Live use:
docker stats ntfy. - Battery: the FAQ says the Android app's constant connection to a self-hosted server used about 0-1% battery over 17 hours on the author's phone.
- Limits per visitor: 4,096 bytes per message, 30 open subscriptions, 15 MB per attachment, 500 MB of daily traffic. Attachments expire after 3 hours by default.
- Health:
/v1/healthon the server, and the Compose health check above. - Metrics: ntfy can expose Prometheus metrics; this guide does not cover them.
FAQ#
Is ntfy free? Yes. The server and apps are open source under Apache 2.0 and GPLv2. Paid plans exist only for the public ntfy.sh service, not for your own server.
Can I just use ntfy.sh? Yes. Without a paid plan, a topic name is the only protection ("essentially a password"). Your own server lets you use deny-all and per-script users.
Does it need Firebase or Google? No. The Android app uses Firebase only for ntfy.sh topics, never for self-hosted servers, and the F-Droid build has none. A self-hosted server publishes to Firebase only if you set firebase-key-file.
Can I run the server on Windows or a Mac? Windows: yes, natively per the docs, or in Docker Desktop (untested by us). Mac: the docs say the macOS server is unsupported; use Docker Desktop (untested by us) or a Linux machine.
What happens to messages when the server restarts? With cache-file set, as here, they survive for the cache duration (12 hours by default). Without it they live only in memory.
Can several people use it? Yes. Add a user per person with ntfy user add and give each topics with ntfy access.
Sources#
- ntfy release v2.35.0, README, Dockerfile and server.yml - read October 2026
- ntfy docs at v2.35.0: install, configuration, publishing, subscribe from your phone, web app, FAQ, troubleshooting, known issues and release notes - read October 2026
- ntfy source: cmd/serve.go (option checks and error texts) - read October 2026
- ntfy image on Docker Hub (tag
v2.35.0checked) - October 2026 - Uptime Kuma ntfy notification form - read October 2026
- Caddy release v2.11.7 and Caddy running in Docker - read October 2026
- GitHub issues #334, #1018, #1625, #1692, #1774, #1948, #1972, #2113, #2117, #2124 - read October 2026
Checked against the official docs on 2026-10-31.