💾 Archived View for gmi.runtimeterror.dev › easy-push-notifications-with-ntfy › index.gmi captured on 2024-05-10 at 10:46:46. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
2023-09-17 ~ 2023-12-22
Wouldn't it be great if there was a simple way to send a notification to your phone(s) with just a `curl` call? Then you could get notified when a script completes, a server reboots, a user logs in to a system, or a sensor connected to Home Assistant changes state. How great would that be??
ntfy.sh [1] (pronounced *notify*) provides just that. It's an open-source [2], easy-to-use, HTTP-based notification service, and it can notify using mobile apps for Android (Play [3] or F-Droid [4]) or iOS (App Store [5]) or a web app [6].
I thought it sounded pretty compelling - and *then* I noticed that ntfy's docs [7] made it sound really easy to self-host the server component, which would give me a bit more control and peace of mind.
<-- note -->
Ntfy leverages uses a pub-sub [8] approach, and (by default) all topics are public. This means that anyone can write to or read from any topic, which makes it important to use a topic name that others aren't likely to guess.
Self-hosting lets you define ACLs [9] to protect sensitive topics.
<-- /note -->
So let's take it for a spin!
I'm going to use the Docker setup [10] on a small cloud server and use Caddy [11] as a reverse proxy. I'll also configure ntfy to require authentication so that randos (*hi!*) won't be able to harass me with notifications.
So I'll start by creating a new directory at `/opt/ntfy/` to hold the goods, and create a compose config.
sudo mkdir -p /opt/ntfy sudo vim /opt/ntfy/docker-compose.yml
# /opt/ntfy/docker-compose.yml version: "2.3" services: ntfy: image: binwiederhier/ntfy container_name: ntfy command: - serve environment: - TZ=UTC # optional, set desired timezone volumes: - ./cache/ntfy:/var/cache/ntfy - ./etc/ntfy:/etc/ntfy - ./lib/ntf:/var/lib/ntfy ports: - 2586:80 healthcheck: # this should be the port inside the container, not the host port 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
This config will create/mount folders in the working directory to store the ntfy cache and config. It also maps `localhost:2586` to port `80` on the container, and enables a simple healthcheck against the ntfy health API endpoint. This will ensure that the container will be automatically restarted if it stops working.
I can go ahead and bring it up:
sudo docker-compose up -d Creating network "ntfy_default" with the default driver Pulling ntfy (binwiederhier/ntfy:)... latest: Pulling from binwiederhier/ntfy 7264a8db6415: Pull complete 1ac6a3b2d03b: Pull complete Digest: sha256:da08556da89a3f7317557fd39cf302c6e4691b4f8ce3a68aa7be86c4141e11c8 Status: Downloaded newer image for binwiederhier/ntfy:latest Creating ntfy ... done
I'll also want to add the following [12] to my Caddy config:
# /etc/caddy/Caddyfile ntfy.runtimeterror.dev, http://ntfy.runtimeterror.dev { reverse_proxy localhost:2586 => https://docs.ntfy.sh/config/#nginxapache2caddy [12] the following # Redirect HTTP to HTTPS, but only for GET topic addresses, since we want # it to work with curl without the annoying https:// prefix @httpget { protocol http method GET path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/) } redir @httpget https://{host}{uri} }
And I'll restart Caddy to apply the config:
sudo systemctl restart caddy
Now I can point my browser to `https://ntfy.runtimeterror.dev` and see the web interface:
I can subscribe to a new topic:
Image: Subscribing to a public topic
And publish a message to it:
curl -d "Hi" https://ntfy.runtimeterror.dev/testy {"id":"80bUl6cKwgBP","time":1694981305,"expires":1695024505,"event":"message","topic":"testy","message":"Hi"}
Which will then show up as a notification in my browser:
So now I've got my own ntfy server, and I've verified that it works for unauthenticated notifications. I don't really want to operate *anything* on the internet without requiring authentication, though, so I'm going to configure ntfy to prevent unauthenticated reads and writes.
I'll start by creating a `server.yml` config file which will be mounted into the container. This config will specify where to store the user database and switch the default ACL to `deny-all`:
# /opt/ntfy/etc/ntfy/server.yml auth-file: "/var/lib/ntfy/user.db" auth-default-access: "deny-all" base-url: "https://ntfy.runtimeterror.dev"
I can then restart the container, and try again to subscribe to the same (or any other topic):
sudo docker-compose down && sudo docker-compose up -d
Now I get prompted to log in:
I'll need to use the ntfy CLI to create/manage entries in the user DB, and that means first grabbing a shell inside the container:
sudo docker exec -it ntfy /bin/sh
For now, I'm going to create three users: one as an administrator, one as a "writer", and one as a "reader". I'll be prompted for a password for each:
ntfy user add --role=admin administrator user administrator added with role admin ntfy user add writer user writer added with role user ntfy user add reader user reader added with role user
The admin user has global read+write access, but right now the other two can't do anything. Let's make it so that `writer` can write to all topics, and `reader` can read from all topics:
ntfy access writer '*' write ntfy access reader '*' read
I could lock these down further by selecting specific topic names instead of `'*'` but this will do fine for now.
Let's go ahead and verify the access as well:
ntfy access user administrator (role: admin, tier: none)
While I'm at it, I also want to configure an access token to be used with the `writer` account. I'll be able to use that instead of username+password when publishing messages.
ntfy token add writer token tk_mm8o6cwxmox11wrnh8miehtivxk7m created for user writer, never expires
I can go back to the web, subscribe to the `testy` topic again using the `reader` credentials, and then test sending an authenticated notification with `curl`:
curl -H "Authorization: Bearer tk_mm8o6cwxmox11wrnh8miehtivxk7m" \ -d "Once more, with auth!" \ https://ntfy.runtimeterror.dev/testy {"id":"0dmX9emtehHe","time":1694987274,"expires":1695030474,"event":"message","topic":"testy","message":"Once more, with auth!"}
Image: Authenticated notification
Pushing notifications from the command line is neat, but how can I use this to actually make my life easier? Let's knock out quick quick configurations for a couple of the use cases I pitched at the top of the post: alerting me when a server has booted, and handling Home Assistant notifications in a better way.
I'm sure there are a bunch of ways to get a Linux system to send a simple `curl` call on boot. I'm going to create a simple script that will be triggered by a systemd service definition.
I may want to wind up having servers notify for a variety of conditions so I'll start with a generic script which will accept a notification title and message as arguments:
`/usr/local/bin/ntfy_push.sh`:
#!/usr/bin/env bash curl \ -H "Authorization: Bearer tk_mm8o6cwxmox11wrnh8miehtivxk7m" \ -H "Title: $1" \ -d "$2" \ https://ntfy.runtimeterror.dev/server_alerts
Note that I'm using a new topic name now: `server_alerts`. Topics are automatically created when messages are posted to them. I just need to make sure to subscribe to the topic in the web UI (or mobile app) so that I can receive these notifications.
Okay, now let's make it executable and then give it a quick test:
chmod +x /usr/local/bin/ntfy_push.sh /usr/local/bin/ntfy_push.sh "Script Test" "This is a test from the magic script I just wrote."
I don't know an easy way to tell a systemd service definition to pass arguments to a command, so I'll use a quick wrapper script to pass in the notification details:
`/usr/local/bin/ntfy_boot_complete.sh`:
#!/usr/bin/env bash TITLE="$(hostname -s)" MESSAGE="System boot complete" /usr/local/bin/ntfy_push.sh "$TITLE" "$MESSAGE"
And this one should be executable as well:
chmod +x /usr/local/bin/ntfy_boot_complete.sh
Finally I can create and register the service definition so that the script will run at each system boot.
`/etc/systemd/system/ntfy_boot_complete.service`:
[Unit] After=network.target [Service] ExecStart=/usr/local/bin/ntfy_boot_complete.sh [Install] WantedBy=default.target
sudo systemctl daemon-reload sudo systemctl enable --now ntfy_boot_complete.service
And I can test it by rebooting my server. I should get a push notification shortly...
Nice! Now I won't have to continually ping a server to see if it's finished rebooting yet.
I've been using Home Assistant [13] for years, but have never been completely happy with the notifications built into the mobile app. Each instance of the app registers itself as a different notification endpoint, and it can become kind of cumbersome to keep those updated in the Home Assistant configuration.
Enabling ntfy as a notification handler is pretty straight-forward, and it will allow me to receive alerts on all my various devices without even needing to have the Home Assistant app installed.
I'll add ntfy to Home Assistant by using the RESTful Notifications [14] integration. For that, I just need to update my instance's `configuration.yaml` to configure the connection.
# configuration.yaml notify: - name: ntfy platform: rest method: POST_JSON headers: Authorization: !secret ntfy_token data: topic: home_assistant title_param_name: title message_param_name: message resource: https://ntfy.runtimeterror.dev
The `Authorization` line references a secret stored in `secrets.yaml`:
# secrets.yaml ntfy_token: Bearer tk_mm8o6cwxmox11wrnh8miehtivxk7m
After reloading the Home Assistant configuration, I can use **Developer Tools > Services** to send a test notification:
Image: Home Assistant Test Send
Image: Home Assistant Test Receive
I'll use the Home Assistant UI to push a notification through ntfy if any of my three water leak sensors switch from `Dry` to `Wet`:
Image: Home Assistant Automation Notify
The business end of this is the service call at the end:
service: notify.ntfy data: title: Leak detected! message: "{{ trigger.to_state.attributes.friendly_name }} detected."
That was pretty easy, right? It didn't take a lot of effort to set up a self-hosted notification server that can be triggered by a simple authenticated HTTP POST, and now my brain is fired up thinking about all the other ways I can use this to stay informed about what's happening on my various systems.
Maybe my notes can help you get started with ntfy.sh, and I hope you'll let me know in the comments if you come up with any other killer use cases. Thanks for reading.
---
Self-Hosted Gemini Capsule with gempost and GitHub Actions
Dynamically Generating OpenGraph Images With Hugo
---