ntfy is one of the most inconspicuous yet useful services on my server.
It does something very simple: you send it a message via an HTTP request, and it pushes that message to your phone.
No complex configuration interface, no fancy dashboards. Just a push pipeline.
But this pipeline has more uses than you might imagine:
- Backup script finished, tells you if it succeeded or failed.
- Server cost alert triggered, notifies you to check it out.
- Scheduled task failed, sends you a push.
- Gatus monitoring found a service down, notifies you.
- Certificate is about to expire, reminds you.
- You can also customize some commands to send for remote, temporary control.
Official project:
https://github.com/binwiederhier/ntfy
Official documentation:
Why Self-Host?
ntfy has an official public server, ntfy.sh, which you can use without installing anything.
But I still chose to self-host, because:
- Topics on the public server are public. Anyone who knows your topic name can send messages to it.
- Self-hosting allows for user authentication.
- Self-hosting allows control over retention and logs.
- My student server has space anyway, and ntfy is very lightweight.
If you’re just trying it out temporarily, using the public ntfy.sh is perfectly fine. But for long-term use, self-hosting is more reliable.
Deployment
Prepare the directory:
sudo mkdir -p /srv/stacks/ntfysudo chown -R $USER:$USER /srv/stacks/ntfycd /srv/stacks/ntfyWrite docker-compose.yml:
services: ntfy: image: binwiederhier/ntfy container_name: ntfy restart: unless-stopped command: serve environment: TZ: Asia/Shanghai NTFY_BASE_URL: "https://ntfy.example.com" NTFY_AUTH_DEFAULT_ACCESS: "deny-all" volumes: - ./cache:/var/cache/ntfy - ./data:/var/lib/ntfy - ./etc:/etc/ntfy ports: - "127.0.0.1:8090:80"A few key points:
- Port
127.0.0.1:8090:80: Only listens on localhost. Do not expose directly to the public internet. NTFY_AUTH_DEFAULT_ACCESS: "deny-all": Denies all unauthenticated requests by default. This means only you (with a token or username/password) can send and receive.NTFY_BASE_URL: Fill in the domain you’ll eventually use, for example, I usentfy.sayori.org.
Start
docker compose up -ddocker compose logs -fLocal check:
curl http://127.0.0.1:8090/You should see ntfy’s Web interface responding.
Create User
Since we set deny-all, we need to create a user for ourselves:
docker compose exec ntfy ntfy user add --role=admin your_usernameIt will ask you to enter a password. Remember this username and password; you’ll need it later for sending notifications and the mobile app.
You can also generate a token:
docker compose exec ntfy ntfy token add your_usernameTokens are more convenient than passwords; you can just include them in the header in scripts.
Connect Domain
Similar to Vaultwarden, use Cloudflare Tunnel or Caddy/Nginx reverse proxy.
Cloudflare Tunnel route:
ntfy.example.com -> Cloudflare Tunnel -> 127.0.0.1:8090 -> ntfyAfter confirming HTTPS access is working, open https://ntfy.example.com and log in to the Web interface with the account you just created.
Send Your First Notification
The simplest way:
curl -H "Authorization: Bearer your_token" \ -d "Server is still alive" \ https://ntfy.example.com/testHere, /test is the topic name. You can name it anything you like, such as /backup, /alert, /server.
You can also include a title and priority:
curl -H "Authorization: Bearer your_token" \ -H "Title: Backup Complete" \ -H "Priority: default" \ -d "Vaultwarden backup successful, size 2.3MB" \ https://ntfy.example.com/backupMobile App
Android:
- ntfy client is available on F-Droid.
- Also on Google Play.
iOS:
- Search for ntfy in the App Store.
After opening the App:
- In settings, add your self-hosted server address.
- Enter your username and password or token.
- Subscribe to your topics (e.g.,
backup,alert).
From now on, notifications sent from your server will be pushed to your phone.
Use in Scripts
Add a line at the end of your backup script:
NTFY_URL="https://ntfy.example.com/backup"NTFY_TOKEN="your_token"
# On successful backupcurl -s -H "Authorization: Bearer $NTFY_TOKEN" \ -H "Title: ✅ Backup Successful" \ -d "Vaultwarden backup completed $(date +%F)" \ "$NTFY_URL"
# On failed backupcurl -s -H "Authorization: Bearer $NTFY_TOKEN" \ -H "Title: ❌ Backup Failed" \ -H "Priority: high" \ -d "Vaultwarden backup error, please check logs" \ "$NTFY_URL"Scheduled task failure notification:
some_command || curl -s -H "Authorization: Bearer $NTFY_TOKEN" \ -H "Title: Scheduled Task Failed" \ -H "Priority: high" \ -d "$(hostname): some_command failed to execute" \ "$NTFY_URL"Integrating with Gatus
If you’re running Gatus for health checks, you can add ntfy as an alert channel in your Gatus configuration.
Gatus supports native ntfy alerts; just configure the server address, topic, and token.
This way, if a service goes down, Gatus will automatically send a notification to ntfy, and your phone will ring.
Resource Usage
ntfy is very lightweight.
It’s running on my server, usually consuming about 20-30MB of RAM. CPU usage is practically zero. For a 2C2G server, it’s no burden at all.
It doesn’t require a database. Messages are cached in local files and automatically expire after a certain period.
Important Notes
- Do not write your ntfy token into public repositories.
- If you set
deny-allbut some external services (e.g., Gatus sending from the same machine) need to send notifications without authentication, you can open permissions for specific topics. - Messages are not permanently retained. If you need historical records, the sender should log them itself.
- ntfy’s Web interface does not need to be exposed to the public internet. The mobile app subscribes using the API.
Is it suitable for a 2C2G server?
Absolutely. It might be one of the most cost-effective services for a 2C2G server.
Once installed, you’ll find that almost every script’s final step becomes “send me a notification with the result.” This is much more comfortable than SSHing in to check logs every day.
Some information may be outdated