DockerSelf-Hosting

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.

ntfy official logo

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.

WhatValue (official docs, read October 2026)
Current versionntfy server v2.35.0, released October 2026 (latest stable; the Android app is 1.25.2, the iOS app 1.7.0)
Image to pinbinwiederhier/ntfy:v2.35.0 (tag checked on Docker Hub; amd64, arm64, armv7, armv6)
Time neededAbout 20 minutes for the Compose route (our estimate, not measured)
Size and RAMImage about 35 MB compressed per platform (Docker Hub). The docs publish no RAM figure
PortsTCP 80 inside the container; this guide publishes it as host port 2586, then removes it behind HTTPS
LicenceDual: Apache License 2.0 and GPLv2 (project README)
Official docsdocs.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, the ntfy command, 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.

Top-to-bottom diagram: scripts and tools post with a token, optionally through Caddy or Traefik on port 443, to the ntfy container, which saves to the data folders, pushes to the Android app and web app, and sends only a wake-up to the ntfy.sh relay, which wakes the iPhone app through Apple push

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:

Run in terminalbash
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.

Run in terminalpowershell
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:

Run in terminalbash
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#

Run in terminalbash
mkdir -p ~/docker_volumes/ntfy/{config,cache/attachments,data,backups}
chmod 700 ~/docker_volumes/ntfy
cd ~/docker_volumes/ntfy

Windows (PowerShell)#

Codepowershell
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:

Run in terminalbash
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 data holds password hashes and tokens. The chmod 700 keeps 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#

Run in terminalbash
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)#

Codepowershell
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):

File contenttext
{"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 is ntfy with no default command, so a file without serve does 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#

Run in terminalbash
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:

Codepowershell
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):

Run in terminaltext
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#

Run in terminalbash
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:

File contentpowershell
$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.
Run in terminalbash
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 the curl line above. See our n8n guide.
  • ntfy command: the same ntfy binary is also a client. On Windows install it with scoop install ntfy, on Mac with the tarball from the install page, and set default-host in client.yml to your server. Then ntfy 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:

Run in terminalbash
docker network create proxy
File contentyaml
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:

Run in terminalbash
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:

File contentyaml
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):

File contenttext
ntfy.example.com {
    reverse_proxy ntfy:80
}

For a home-only name, add tls internal for Caddy's own local certificate:

File contenttext
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#

ControlHow
Default accessauth-default-access: deny-all. Check with ntfy access: the anonymous user must have no access
Topic namesUnguessable, even with ACLs. The docs call a topic "essentially a password" without them
Script usersOne write-only user per tool. Tokens grant full access to their user, so narrow the user, not the token
HTTPSRequired beyond a trusted LAN. Remove host port 2586 once the proxy works
Folderchmod 700 ~/docker_volumes/ntfy; it holds the user database
Rate limitsBuilt in: a burst of 60 requests, then one every 5 seconds per visitor. Set behind-proxy: true behind a proxy
Declarative usersPossible via auth-users (bcrypt hashes). In Compose, a $ in a hash must be written $$. The CLI route avoids this
Web appSafe 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):

Run in terminalbash
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:

File contentyaml
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.

MistakeWhy it happensDo this instead
Starting with default accessA 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 volumeIssue #1625 (closed): FATAL user database directory /var/lib/ntfy does not exist or is not accessibleMount ./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 folderchown cache and data to the container user, or run as the default user
Setting upstream-base-url to your own domainIssue #334 (closed): a Traefik setup looped into the rate limitUse https://ntfy.sh. Current servers refuse equal values
No WebSocket headers in nginxIssue #1948 (closed): WebSocket upgrade failed with HTTP 400Forward Connection and Upgrade. Caddy does it itself
Forgetting behind-proxy: trueEvery visitor looks like the proxy's IP, so all share one rate limitSet it with HTTPS in front
Proxy Basic Auth in front of ntfyIssue #1018 (closed): the phone app and /file/ breakUse ntfy's own users and tokens
Long attachment expiry, short cacheIssue #1692 (closed): attachments return 404Keep cache-duration at least attachment-expiry-duration
Treating the web app as the whole storyBackground notifications need Web Push over valid HTTPSUse the phone app for reliable alerts
AI assistants: inventing config valuesAssistants guess auth-default-access: private; valid values are read-write, read-only, write-only, deny-allCheck every key against the options table in the docs
AI assistants: leaving out serve, or using the public serverExamples for ntfy.sh use ntfy.sh/mytopic; the image needs serveUse 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 ntfy first; 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 with docker compose restart ntfy and read the log. A typo in the file stops the server at start, and the log names the setting.

Troubleshooting#

ProblemSolution
Container does not serve anything, or exits at oncecommand: serve is missing in the Compose file
FATAL user database directory /var/lib/ntfy does not exist or is not accessibleMount ./data:/var/lib/ntfy (issue #1625)
attempt to write a readonly database, HTTP 500 on publishFix owner and permissions on the cache and data folders (issue #1774)
if attachment-cache-dir is set, base-url must also be setAdd base-url to server.yml
if set, base-url must not have a pathUse https://ntfy.example.com, not a sub-path; ntfy does not support one
cannot set require-login without also setting enable-loginSet enable-login: true as well, and auth-file
base-url and upstream-base-url cannot be identicalSet upstream-base-url: "https://ntfy.sh"
Publish returns 401 or 403The token's user has no write ACL for the topic; run ntfy access
Phone shows "Reconnecting..." or late messagesSet behind-proxy, check WebSockets, and check no subscribed topic returns 403
iPhone shows "New message" onlyThe phone cannot reach your server; check the address and base-url (docs, known issues)
No iPhone push for hoursSet 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:

Run in terminalbash
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:

Run in terminalbash
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)#

Codepowershell
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:

Run in terminalbash
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/health on 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#

Checked against the official docs on 2026-10-31.