Run your own ntfy server

Keep your messages on your own hardware, add logins, set your own limits, and still get instant notifications on your iPhone with NTFY me.

The short version

Run the standard, unmodified ntfy server and add one line to its config:

upstream-base-url: "https://ntfy-me.com"

That's the only NTFY me-specific part. Everything else is a normal ntfy install. Already running ntfy? Jump to Switch an existing server.

How instant notifications work

iPhones only accept push notifications from Apple, and Apple only delivers them for the app's own push service. Your server can't reach your phone directly, so it sends a tiny "wake up" signal through ntfy-me.com:

Your script→Your server→ntfy-me.com (a hash and an ID only)→Apple→iPhone wakes up→App fetches the message from your server

The message itself never leaves your server except to go straight to your phone. This is the same mechanism the official ntfy app uses with ntfy.sh, pointed at ntfy-me.com because NTFY me has its own push channel.

Without the upstream-base-url line, NTFY me still works with your server, but messages show up only when you open the app.

What you need

Install with Docker

Replace ntfy.example.com with your own domain everywhere below.

  1. Create a folder and a compose.yml file.

    mkdir ntfy && cd ntfy

    Put this in compose.yml:

    services:
      ntfy:
        image: binwiederhier/ntfy
        container_name: ntfy
        command: serve
        environment:
          NTFY_BASE_URL: https://ntfy.example.com
          NTFY_UPSTREAM_BASE_URL: https://ntfy-me.com
          NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
          NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
          NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
          NTFY_BEHIND_PROXY: "true"
        volumes:
          - ./ntfy-data:/var/lib/ntfy
        ports:
          - 127.0.0.1:8080:80
        restart: unless-stopped

    NTFY_BASE_URL must be exactly the address you'll type into the app, with the same https://, the same host name and no trailing slash. The push signal is matched on it, so a mismatch is the most common reason notifications don't arrive instantly.

    Prefer a config file? Every variable has a server.yml equivalent: base-url, upstream-base-url, cache-file and so on. See the ntfy config reference.

  2. Start it.

    docker compose up -d

    Messages, logins and attachments are stored in the ntfy-data folder next to the file.

  3. Put HTTPS in front of it. The server listens on 127.0.0.1:8080. Any reverse proxy works. With Caddy, which gets a certificate for you, the whole Caddyfile is:

    ntfy.example.com {
        reverse_proxy localhost:8080
    }

    Using nginx, Traefik or Cloudflare Tunnel instead? Point it at http://localhost:8080. The ntfy config reference has nginx and Apache examples.

  4. Check it from outside.

    curl https://ntfy.example.com/v1/health

    You should see {"healthy":true}.

  5. Subscribe in NTFY me. Tap +, turn on Use another server, enter https://ntfy.example.com and a topic name. To make your server the default for every new topic, set it in Settings → Default server instead.

  6. Send a test.

    curl -d "Hello from my own server" https://ntfy.example.com/your-topic

    A banner should appear within a second or two, even with the app closed.

Updating later: docker compose pull && docker compose up -d.

Optional: require a login

By default anyone who knows a topic name can use your server. To lock it down:

  1. Deny anonymous access. Add this line under environment: in compose.yml, then run docker compose up -d again:

          NTFY_AUTH_DEFAULT_ACCESS: deny-all
  2. Create a user. It asks for a password.

    docker exec -it ntfy ntfy user add --role=admin alice

    Admins can use every topic. For per-topic permissions, create a normal user and grant access with ntfy access; see access control.

  3. Sign in from the app. In NTFY me, open Settings → Users → Add user and enter your server address, username and password. The app uses that login for every topic on that server, including when it fetches a message after a push.

  4. Send with credentials. Either a password:

    curl -u alice:your-password -d "Backup finished" https://ntfy.example.com/backups

    or, better for scripts, an access token you can revoke:

    docker exec ntfy ntfy token add alice
    curl -H "Authorization: Bearer tk_..." -d "Backup finished" https://ntfy.example.com/backups

Behind Cloudflare Access or another login proxy? Add its headers in Settings → Server headers. They are stored in the Keychain and only sent to that server.

Switch an existing server

If your server already has upstream-base-url: "https://ntfy.sh" for the official ntfy iOS app, change it to ntfy-me.com:

# server.yml
upstream-base-url: "https://ntfy-me.com"

# or, with Docker environment variables
NTFY_UPSTREAM_BASE_URL: https://ntfy-me.com

Restart the server. Nothing else changes: topics, users, tokens, URLs and your scripts all stay the same.

One server, one upstream. ntfy forwards to a single upstream. After the switch, NTFY me gets instant notifications from your server and the official ntfy iOS app no longer does: it then shows messages only when opened. Android, desktop and web clients aren't affected; they connect to your server directly.

Then add your topics in NTFY me (+ → Use another server) and your login (Settings → Users). The migration guide has a checklist.

Troubleshooting

Messages only show up when I open the app

The push signal isn't reaching your phone. Check, in this order:

  1. The upstream line is active. It's set and the server was restarted. With Docker: docker compose config | grep UPSTREAM.
  2. The base URL matches the app. base-url on the server and the server address in the app must be identical (https vs http, www., a port number all count). If they differ, change one and re-add the topic.
  3. The server can reach ntfy-me.com. docker exec ntfy wget -qO- https://ntfy-me.com/v1/health should print {"healthy":true}.
  4. Notifications are allowed in iPhone Settings → Notifications → NTFY me.
  5. You're under the relay limit. ntfy-me.com accepts up to 2,000 relayed messages a day from one server (one IP address), in bursts of up to 120. Beyond that the messages still reach the app, just without an instant banner.
The banner only says "New message"

The phone was woken up but couldn't fetch the message from your server. Usually the server isn't reachable from where the phone is (for example a home-only address while you're on mobile data), or the server needs a login the app doesn't have yet (Settings → Users). Open the app on a network that can reach the server and the message appears.

The app can't subscribe, or says unauthorized

Open https://ntfy.example.com/v1/health in Safari on the phone. If it doesn't load, the phone can't reach the server (DNS, firewall, VPN). If it loads but subscribing fails, add your login under Settings → Users, or your proxy's headers under Settings → Server headers.

The official ntfy iOS app stopped getting notifications

Expected after switching upstream-base-url to ntfy-me.com; see One server, one upstream.

Does it work over plain http?

Yes, the app accepts http:// servers, but messages and passwords then travel unencrypted. Use HTTPS for anything beyond a quick test.

What ntfy-me.com sees

For each message on your server, ntfy-me.com receives only:

It never receives the title, message, tags or attachments. The same goes for Apple and Google, which carry the push. Full details: Privacy Policy.