If Gotify is the neat self-hosted notification dashboard, ntfy is the thing you reach for when you want notifications to be as boring as curl.

Publish to a topic URL. Subscribe from a phone, browser, desktop, CLI, or another script. That is the core idea.

That makes ntfy especially useful for backups, cron jobs, monitoring checks, Home Assistant automations, CI scripts, Docker update alerts, and small homelab tasks where adding a heavier workflow tool would be overkill.

What is ntfy?

ntfy is an open-source HTTP-based pub-sub notification service.

The project description is direct:

Send push notifications to your phone or desktop using PUT/POST

The public service runs at ntfy.sh , but the interesting part for self-hosters is that the same server can run locally with Docker.

The repository includes:

  • a Go server
  • a CLI for publishing and subscribing
  • a React web app
  • Android and iOS clients
  • SQLite and PostgreSQL-backed persistence options
  • Web Push, SMTP, Matrix Push Gateway, Twilio, Firebase/APNS, and Prometheus support

For a first homelab deployment, you do not need most of that. A single container with a persistent cache is enough to start.

Why Self-Host ntfy?

The biggest reason is control over your notification path.

With the public ntfy.sh service, topic names are effectively shared secrets. That is convenient, but you are still sending messages through someone else’s server.

With self-hosted ntfy, you decide where the messages are stored, how long they are cached, whether accounts are enabled, whether topics require auth, and how the instance is exposed.

It also makes local-only alerting easy. A NAS, Raspberry Pi, mini PC, or monitoring box can publish to http://ntfy.local/topic-name without needing a SaaS notification account.

Self-Hosting ntfy with Docker

This compose file pins ntfy to v2.28.0, publishes the web UI and API on port 8099, and keeps the message cache in a Docker volume.

name: ntfy

services:
  ntfy:
    image: binwiederhier/ntfy:v2.28.0
    container_name: ntfy
    command:
      - serve
      - --cache-file
      - /var/cache/ntfy/cache.db
    environment:
      TZ: ${TZ:-Europe/Madrid}
    ports:
      - "${NTFY_PORT:-8099}:80"
    volumes:
      - ntfy_cache:/var/cache/ntfy
    healthcheck:
      test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"[[:space:]]*:[[:space:]]*true' || exit 1"]
      interval: 60s
      timeout: 10s
      retries: 3
      start_period: 20s
    restart: unless-stopped

volumes:
  ntfy_cache:

The matching Home-Lab config is here:

Home-Lab ntfy compose

Start it with:

cd /home/jalcocert/Desktop/Home-Lab/ntfy
cp .env.sample .env
docker compose --env-file .env up -d

Then check the server:

curl -s http://localhost:8099/v1/health

Expected response:

{"healthy":true}

The web UI is available at:

http://localhost:8099

Sending a Test Notification

The simplest ntfy publish command is just a POST body to a topic:

curl -d "Backup finished" http://localhost:8099/my-homelab-topic

For a slightly richer message:

curl \
  -H "Title: Backup completed" \
  -H "Priority: default" \
  -H "Tags: white_check_mark" \
  -d "The nightly archive finished successfully." \
  http://localhost:8099/my-homelab-topic

To verify from the command line without a phone app, poll the topic:

curl -s "http://localhost:8099/my-homelab-topic/json?poll=1"

Field Note: Local Docker Test

I tested the Home-Lab compose locally on port 8099.

The container started with binwiederhier/ntfy:v2.28.0, /v1/health returned {"healthy":true}, and this publish-and-poll flow worked:

topic="foss-ntfy-test-$(date +%s)"

curl -s \
  -H "Title: Foss Engineer ntfy test" \
  -H "Priority: default" \
  -d "local ntfy compose works" \
  "http://localhost:8099/$topic"

curl -s "http://localhost:8099/$topic/json?poll=1"

The poll endpoint returned the same message, title, topic, and priority.

Reverse Proxy Notes

For a private LAN-only notifier, localhost:8099 or an internal DNS name may be enough.

For a public instance, do not just throw it onto the internet and hope topic names are enough.

At minimum:

  • put ntfy behind HTTPS
  • set a public base-url
  • set behind-proxy: true if a reverse proxy is in front
  • use long, unguessable topic names
  • consider enabling auth and access control
  • protect metrics if you enable them

If iOS devices need timely push behavior from a self-hosted ntfy server, read the upstream push notes carefully. The config usually needs upstream-base-url: "https://ntfy.sh" so the iOS app can be nudged to poll your server.

ntfy vs Gotify

Use ntfy when the integration should be a URL.

It shines for shell scripts, health checks, Docker update alerts, cron jobs, CI, and monitoring systems that already know how to make HTTP requests.

Use Gotify when you want a more account-centered notification server with application tokens, a clear dashboard, and persistent app identities.

The one-line version:

ntfy is topic-first and curl-first. Gotify is app-token-first and dashboard-first.

For a homelab, both are valid. I would pick ntfy for simple script alerts and Gotify when I want a more structured notification admin UI.

Conclusion

ntfy is one of those tools that stays useful because it does not ask for much.

You can run it as a single container, send messages with curl, subscribe from a phone or browser, and add auth, Web Push, e-mail, Matrix, or PostgreSQL only when the deployment actually needs them.

For self-hosters already using Uptime Kuma, Gotify, Home Assistant, or cron-based maintenance scripts, ntfy is worth having in the toolbox.

The ntfy Project


FAQ