How to Self-Host Uptime Kuma with ntfy Push Alerts

2026-11-05 · Unix3r

A step-by-step guide to deploying Uptime Kuma and ntfy in Docker, wiring them together, and getting a push notification on your phone within seconds of a service going down — including the monitors worth setting up first and the ones that are noise

Monitoring is the thing every self-hoster skips until the first time a service is down for two days and nobody noticed. This tutorial fixes that in one sitting: by the end you'll have a status dashboard, a self-hosted push notification service, and your phone buzzing when something breaks.

What you'll build:

Uptime Kuma — checks your services every minute (HTTP, TCP, ping, DNS, Docker containers, certificates)
ntfy — a tiny pub/sub server that delivers push notifications to Android/iOS without going through Google or Apple's cloud (well, iOS still needs APNs, but your data doesn't transit a third party)
A public status page you can share, and private alerts you can't miss

You'll need:

A Linux server with Docker and Docker Compose (any VPS or a home box)
A domain with two subdomains you control — we'll use status.example.com and ntfy.example.com
A reverse proxy handling TLS (Caddy shown; Traefik/Nginx/Apache work the same way)
20 minutes

> Important: run the monitor on a different machine from the things it monitors, if you can. A monitor that goes down with the server it's watching tells you nothing. A €4 VPS, a Raspberry Pi at home, or a free Oracle instance all work.

---

Step 1 — Deploy ntfy

ntfy goes first so Uptime Kuma has somewhere to send alerts.

Create a user and a topic

Topic names are just strings; alerts is what we'll use. You can create more later (backups, security) with different notification sounds.

Put it behind the proxy

Install the app and subscribe

Android: ntfy from F-Droid or Play Store. Settings → Default server → https://ntfy.example.com, log in as your admin user. Then + → subscribe to alerts.
iOS: ntfy from the App Store. Same steps; iOS requires the server's upstream-base-url to be set for push to work reliably:

Test it from the server:

Your phone should buzz within a second. If it does, ntfy is done.

---

Step 2 — Deploy Uptime Kuma

Open https://status.example.com. The first visit asks you to create the admin account — do that now, before anyone else finds the URL.

> Tip: if you'd rather keep the admin UI private and only publish the status page, serve the UI on a Tailscale-only hostname and the status page on the public one. Kuma's status pages work on any hostname that reaches the container.

---

Step 3 — Connect Uptime Kuma to ntfy

In Kuma: Settings → Notifications → Setup Notification.

| Field | Value |
|---|---|
| Notification Type | ntfy |
| Friendly Name | ntfy alerts |
| Server URL | https://ntfy.example.com |
| Topic | alerts |
| Priority | 4 (high) |
| Authentication Method | Access Token |
| Access Token | tkxxxxxxxx… (the kuma token from Step 1) |
| Default enabled | ✅ |
| Apply on all existing monitors | ✅ |

Click Test — your phone should get a "Testing" notification. Save.

Setting Default enabled means every monitor you create from now on alerts through ntfy without you ticking a box each time.

---

Step 4 — Add the monitors that matter

Click + Add New Monitor. Here's a sensible starting set, from most to least important.

4a. Your public websites (HTTP(s) – Keyword)

Don't just check that the site returns 200 — a broken app often still returns 200 with an error page. Use HTTP(s) – Keyword and look for a string only a working page contains.

| Field | Value |
|---|---|
| Monitor Type | HTTP(s) – Keyword |
| Friendly Name | Blog |
| URL | https://www.example.com/ |
| Keyword | <title>My Blog |
| Heartbeat Interval | 60 |
| Retries | 2 |
| Heartbeat Retry Interval | 30 |

Retries = 2 means a single blip doesn't page you; three consecutive failures over 90 seconds does.

4b. Nextcloud, Gitea, anything with a status endpoint

Most apps expose a health URL. Use it instead of the login page:

Nextcloud: https://cloud.example.com/status.php → keyword "installed":true
Gitea: https://git.example.com/api/healthz → keyword "status":"pass"
Immich: https://photos.example.com/api/server/ping → keyword pong
Mailu admin: https://mail.example.com/admin/ → keyword Mailu

4c. TLS certificate expiry

Every HTTP monitor has a Certificate Expiry Notification toggle in its settings. Turn it on; Kuma warns at 21, 14 and 7 days. If you use Caddy or Traefik with Let's Encrypt this should never fire — which is exactly why you want to know if it does.

4d. Mail server ports (TCP Port)

| Monitor Type | Hostname | Port | Name |
|---|---|---|---|
| TCP Port | mail.example.com | 25 | SMTP |
| TCP Port | mail.example.com | 587 | Submission |
| TCP Port | mail.example.com | 993 | IMAPS |

If 587/993 are Tailscale-only (they should be), run the monitor from a box on the tailnet and use the Tailscale IP.

4e. DNS

| Field | Value |
|---|---|
| Monitor Type | DNS |
| Hostname | example.com |
| Resolver Server | 1.1.1.1 |
| Record Type | A |
| Expected value | 203.0.113.10 |

This catches the "I changed DNS and forgot" and "registrar did something" cases.

4f. The other servers (Ping)

One Ping monitor per machine in the lab, by Tailscale IP. If the monitor box can't reach a host over the tailnet, the host is either down or Tailscale on it is — both are things you want to hear about.

4g. Docker containers (Docker Container)

Because we mounted the Docker socket: Settings → Docker Hosts → Add → socket /var/run/docker.sock. Then monitors of type Docker Container can watch individual containers on this host and alert when one isn't running.

4h. Backups (Push)

This one's inverted: the backup job tells Kuma it ran. Create a Push monitor named Nightly backup, heartbeat 86400 (1 day), retries 0. Kuma shows you a URL like:

Append to the end of your backup script:

If the script fails or doesn't run, the ping never arrives, and after 24 hours Kuma pages you. This is the single most useful monitor on the list — a backup that silently stopped is worse than no backup.

---

Step 5 — Build a status page

Status Pages → New Status Page. Give it a slug (public), a title, add the monitors you're happy to show the world, and Save. It lives at https://status.example.com/status/public.

To make it the root of the hostname:

Keep internal monitors (Tailscale pings, mail ports, backup heartbeats) off the public page — it's a map of your infrastructure.

---

Step 6 — Tune the alerts so you don't start ignoring them

A monitoring setup that cries wolf gets muted within a week. Three settings keep it honest:

Retries on everything. Retries: 2, Retry Interval: 30 on every HTTP/TCP/ping monitor. Transient network blips are normal; three in a row is an outage.

Resend interval. In each monitor's Advanced section, set Resend Notification if Down X times consecutively to 0. One alert when it goes down, one when it comes back. You don't need a buzz every minute during a 2-hour outage — you already know.

Priority per topic. Create a second ntfy topic, warnings, for things that aren't urgent (cert expiry, a dev box ping). In the ntfy app set alerts to a loud sound that breaks Do Not Disturb, and warnings to silent. Add a second Kuma notification pointing at warnings, and assign it per monitor instead of the default.

---

Step 7 — Back up Kuma itself

Everything is in ./data/kuma.db (SQLite). It's small:

Or just include the data/ directory in Restic — the DB uses WAL mode and a live copy is fine for restore in practice.

---

Troubleshooting

Test notification works but real alerts don't arrive
Check the monitor's Notifications tab — if you created monitors before the notification, they're not linked unless you ticked "Apply on all existing monitors".

ntfy app shows "connection error" on mobile data
Your proxy is timing out the long-poll connection. Confirm the readtimeout 0 block in Caddy, or proxyreadtimeout 3600s in Nginx.

Kuma says everything is down after a reboot
The monitor container started before the network was up. Add startperiod: 180s to the healthcheck (already in the Compose file above) and, if it persists, a dependson or a systemd ordering on the Docker service.

Monitors flap at exactly the same time every day
That's your backup saturating the link or the DB. Either move the backup window or raise retries on those monitors to 3.

iOS gets notifications late or not at all
Set upstream-base-url: "https://ntfy.sh" in server.yml and restart. iOS can't hold a background connection; ntfy.sh is used only as a wake-up signal, the message body still comes from your server.

---

What you have now

A dashboard that shows at a glance what's up, a status page you can hand to anyone, and a phone that tells you within about 90 seconds when something is down — and, just as important, tells you again when it's back. Total cost: one small container host and about 300 MB of RAM.

Next step, when you're ready: point Grafana + Prometheus at the same hosts for metrics over time. Uptime Kuma tells you that something broke; Prometheus tells you why. But if you only ever run one of them, run this one.