Tim's blah blah blah

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:

  1. Which audience should receive the notification (i.e. is it general info or more admin stuff)
  2. What is the severity of the notification (general info is always)

I have these existing notifications I want to integrate:

  1. Appliance ready (audience: all, severity: info)
  2. Appliance started (audience: admin, severity: info)
  3. Backups ready (audience: admin, severity: info)
  4. Backups failed (audience: admin, severity: error)
  5. Low disk space (audience: admin, severity: warning)
  6. High CO2/PM2.5/etc. (audience: admin, severity: warning)

Writing this down, I settled on the following topics:

and the severity I encode using message priority (ntfy.sh)

Some useful emojis for tags (ntfy.sh):

Notification topics layout depending on audience

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:

Proxmox notification webhook for ntfy
Proxmox notification matcher pane 1
Proxmox notification matcher pane 2
Proxmox notification matcher pane 3

Grafana

Add to Grafana, see instructions in ntfy docs (ntfy.sh).

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

Add ntfy to crontab (ntfy.sh)

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