My ntfy self-hosted push notification setup
In my quest of self-hosting, I also wanted to ditch Home Assistant & Telegram notification integration. Ntfy (ntfy.sh) is a great solution: it’s open source, has a simple REST API, comes as a simple apt package, have a native iOS app, and is configurable to my needs.
Install & configure ¶
Debian package ¶
Install using Debian instructions (ntfy.sh).
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
sudo apt install apt-transport-https
sudo sh -c "echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
> /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy
sudo systemctl enable ntfy
sudo systemctl start ntfy
nginx reverse proxy ¶
Add subdomain for ntfy (local and global DNS), then add reverse proxy to nginx (ntfy.sh):
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ntfy.vanwerkhoven.org;
location / {
include snippets/nginx-server-proxy-tim.conf;
proxy_buffering off;
# Use fixed IP instead because DNS might not be up yet
# resuting in error
# "nginx: [emerg] host not found in upstream"
# https://stackoverflow.com/questions/32845674/nginx-how-to-not-exit-if-host-not-found-in-upstream
resolver 172.17.10.1 valid=30s;
set $upstream_ha ntfy.lan.vanwerkhoven.org;
proxy_pass http://$upstream_ha:2586;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection “upgrade”;
client_max_body_size 0; # Stream request body to backend
}
include snippets/nginx-server-ssl-tim.conf;
include snippets/nginx-server-cert-vanwerkhoven-tim.conf;
}
Test config and restart
sudo nginx -t
sudo systemctl restart nginx.service
ntfy ¶
Configure ntfy service /etc/ntfy/server.yml
:
base-url: "http://ntfy.vanwerkhoven.org"
listen-http: ":2586"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
attachment-total-size-limit: "256M"
attachment-file-size-limit: "15M"
attachment-expiry-duration: "3h"
behind-proxy: true
# For notifications via Firebase & APNS
upstream-base-url: "https://ntfy.sh"
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
Create users
ntfy user add --role=user app_client
ntfy user add --role=user app_pub
ntfy access app_pub t_* rw
ntfy access app_client t_* ro
sudo ntfy token add app_pub
Restart
sudo systemctl restart ntfy
Test pub/sub locally
ntfy sub -d http://ntfy.lan.vanwerkhoven.org:2586/test
ntfy pub -d http://ntfy.lan.vanwerkhoven.org:2586/test test
Test via reverse proxy
ntfy sub --token tk_xx -d https://ntfy.vanwerkhoven.org/t_all
ntfy pub --token tk_xx -t "Title hello world" -d https://ntfy.vanwerkhoven.org/t_all message
Notification setup ¶
Once you have things running, I spent some time to determine how I group my notifications. I have two dimensions:
- Which audience should receive the notification (i.e. is it general info or more admin stuff)
- What is the severity of the notification (general info is always)
I have these existing notifications I want to integrate:
- Appliance ready (audience: all, severity: info)
- Appliance started (audience: admin, severity: info)
- Backups ready (audience: admin, severity: info)
- Backups failed (audience: admin, severity: error)
- Low disk space (audience: admin, severity: warning)
- High CO2/PM2.5/etc. (audience: admin, severity: warning)
Writing this down, I settled on the following topics:
t_all
t_verbose
and the severity I encode using message priority (ntfy.sh)
- info:
default
- warning:
high
- error:
urgent
Some useful emojis for tags (ntfy.sh):
- start:
arrow_forward
- end:
stop_button
/white_check_mark
- warn:
warning
- info:
information_source
- safe:
ok
Notification topics layout depending on audience
topic t_all: general notifcations relevant for all clients
- devices finish
topic t_verbose: specific notifications
- devices start
- environment high/low
- backups ok / nok
devices start/finish –> topic dev
environmental updates (CO2, RH) –> topic env
infra updates (server backup / low memory) –> topic infra
Home Assistant ¶
Integrate in Home Assistant, either using Apprise (home-assistant.io) or REST (home-assistant.io) command (diecknet.de). I chose the latter for less dependencies, adding this to configuration.yaml
directly:
shell_command:
ntfy: >
curl
-X POST
--url 'https://ntfy.vanwerkhoven.org/{{ topic | default("t_admin") }}'
--data '{{ message }}'
--header 'X-Title: {{ title }}'
--header 'X-Tags: {{ tags }}'
--header 'X-Priority: {{ priority | default('default')}}'
--header 'X-Delay: {{ delay }}'
--header 'X-Actions: {{ actions }}'
--header 'X-Click: {{ click }}'
--header 'X-Icon: {{ icon }}'
--header 'Authorization: Bearer tk_xx'
Add notification to automations
# Appliance started
action: shell_command.ntfy
data:
tags: arrow_forward
topic: t_verbose
title: Dishwasher started
message: Started at {{now().strftime("%H:%M")}}.
# Appliance finished
action: shell_command.ntfy
data:
tags: white_check_mark
topic: t_all
title: Dishwasher finished
message: >-
Finished at {{now().strftime("%H:%M")}}, used {{
((states(power_consumption_sensor_var) | float) - (power_consumption_start
| float)) | round(2) }}{{power_consumption_unit}}
# Low battery
action: shell_command.ntfy
data:
tags: information_source
topic: t_verbose
title: Battery low
message: Battery low in {{sensors}}
Proxmox ¶
Add to proxmox via GUI:
- Method/URL:
POST
/ https://ntfy.vanwerkhoven.org/t_verbose (vanwerkhoven.org) - Headers
- Authorization:
Bearer {{ secrets.token }}
- X-Title:
{{ title }}
- Markdown:
yes
- Authorization:
- Body:
{{ message }}
- Secrets
- token:
<your token>
- token:
Grafana ¶
Add to Grafana, see instructions in ntfy docs (ntfy.sh).
- Title template:
default.title
- Message template:
default.message
- Webhook URL:
https://ntfy.vanwerkhoven.org/t_test?tpl=1&t=%7B%7B.title%7D%7D&m=%7B%7B.message%7D%7D
Optionally define a shorter message template under ‘Contact points’ -> ‘Notification template’ -> ‘+Add notification template group’ -> ‘Add example’ -> ‘Default template for notification messages’, then edit as desired, e.g.:
{{- /* This is a copy of the "default.message" template. */ -}}
{{- /* Edit the template name and template content as needed. */ -}}
{{ define "default.message.brief" }}{{ if gt (len .Alerts.Firing) 0 }}**Firing**
{{ template "__text_alert_list.brief" .Alerts.Firing }}{{ if gt (len .Alerts.Resolved) 0 }}
{{ end }}{{ end }}{{ if gt (len .Alerts.Resolved) 0 }}**Resolved**
{{ template "__text_alert_list.brief" .Alerts.Resolved }}{{ end }}{{ end }}
{{ define "__text_alert_list.brief" }}{{ range . }}
Value: {{ template "__text_values_list.brief" . }}
Labels:
{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end }}{{ end }}{{ end }}
{{ define "__text_values_list.brief" }}{{ if len .Values }}{{ $first := true }}{{ range $refID, $value := .Values -}}
{{ if $first }}{{ $first = false }}{{ else }}, {{ end }}{{ $refID }}={{ $value }}{{ end -}}
{{ else }}[no value]{{ end }}{{ end }}
Crontab ¶
30 01 * * * lego_renew_cert.sh && sudo service nginx reload || curl -H 'Authorization: Bearer tk_xx' -H tags:warning -H prio:high -d "Certificate renewal failed for <domain>" https://ntfy.vanwerkhoven.org/t_all
50 01 * * * test $(echo | openssl s_client -connect www.vanwerkhoven.org:443 -servername www.vanwerkhoven.org 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2 | xargs -I{} date -d "{}" +%s | awk -v now=$(date +%s) '{print int(($1 - now) / 86400)}') -lt 40 && curl -H 'Authorization: Bearer tk_xx' -H tags:error -H prio:high -d "Certificate expires in 40 days, renewal failed for vanwerkhoven.org" https://ntfy.vanwerkhoven.org/t_all
#Curl #Debian #Diy #Grafana #Home-Assistant #Ios #Letsencrypt #Linux #Proxmox #RaspberryPi #Server #Smarthome