Proxmox VEXVI · MonitoringNotifications
The PVE notification system: targets and matchers
What you'll learn
- Describe the target and matcher model, and explain why a notification with no matching matcher is silently discarded
- Configure sendmail, SMTP, Gotify and webhook targets, and choose between them on operational grounds
- Write matchers that route by type, severity and calendar rather than sending everything everywhere
- Prove that an alert actually arrives, rather than assuming it from a saved configuration
Prerequisites
Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-12
Every other lesson in this part is about collecting information — metrics, logs, exporters, dashboards. This one is about the last hop, the one where something a computer noticed becomes something a person knows. In Proxmox VE that hop has exactly one mechanism, and since PVE 8.1 it is a first-class subsystem with its own configuration, its own permissions and its own failure modes.
It has two objects, and understanding the relationship between them is most of the lesson:
- A target is a place a notification can be delivered to. Four types:
sendmail,smtp,gotify,webhook. - A matcher decides which notifications go to which targets. It holds rules and a list of targets.
The critical property, and the one that produces silent failures: a notification that matches no matcher is discarded. Not queued, not logged as undelivered, not surfaced anywhere. Delivery is opt-in through matchers, and a target with no matcher pointing at it is decoration.
What generates a notification
The set is small and finite, which is both a strength and a limitation. These are the events PVE emits, with the metadata each carries:
| Event | type | Severity | Metadata fields |
|---|---|---|---|
| System updates available | package-updates | info | hostname |
| Cluster node fenced | fencing | error | hostname |
| Storage replication failed | replication | error | hostname, job-id |
| Backup succeeded | vzdump | info | hostname, job-id |
| Backup failed | vzdump | error | hostname, job-id |
| Mail addressed to root | system-mail | unknown | hostname |
Two things to notice immediately.
job-id is only populated for scheduled backups. A backup you trigger by
hand does not carry one. That matters because a matcher rule against a field
that does not exist on a given notification fails to match — so a matcher
keyed on job-id will silently ignore every manual backup.
system-mail is the catch-all, and it is more important than it looks.
Local daemons that mail root — smartd reporting a failing disk, mdadm
reporting a degraded array, cron reporting a failed job — become
system-mail notifications with severity unknown. If your matchers key on
match-severity error, none of them will ever match a system-mail
notification, and you will never hear about the failing disk.
The four target types, and how to choose
| Type | Delivery path | Retries on failure | Use it when |
|---|---|---|---|
sendmail | Hands the mail to the local MTA (Postfix) | Yes — the MTA queues and retries | Mail is your channel and the node has a working relay configured |
smtp | Talks to an SMTP relay directly, no local MTA | No — no queueing, no retry | You want mail without configuring Postfix on every node, and you accept that a relay outage means the notification is gone |
gotify | Push to a self-hosted Gotify server | No | You want a phone notification without an email round trip |
webhook | HTTP request to an arbitrary URL | No | Integrating with Slack, Teams, Opsgenie, PagerDuty, or your own handler |
Configuration lives in two files
Targets and matchers are stored in /etc/pve/notifications.cfg, which is in
pmxcfs and therefore replicated to every node. Secrets — SMTP passwords,
Gotify tokens, webhook secrets — live separately in
/etc/pve/priv/notifications.cfg, which is root-only.
That split is why you can safely include notifications.cfg in a
configuration backup or a diff without exporting credentials, and why a
restore of /etc/pve that skipped priv/ leaves you with targets that are
configured and cannot authenticate.
set -euo pipefail
# mode is one of insecure, starttls or tls. Default is tls.
# Default ports follow the mode: 25 insecure, 465 tls, 587 starttls.
pvesh create /cluster/notifications/endpoints/smtp \
--name relay-ops \
--server smtp.example.com \
--port 587 \
--mode starttls \
--username 'pve-notify@example.com' \
--password 'REPLACE_ME' \
--from-address 'pve-notify@example.com' \
--author 'Proxmox VE prod-cluster' \
--mailto 'ops@example.com' \
--comment 'Primary mail path for cluster alerts'
# Confirm it exists.
pvesh get /cluster/notifications/endpoints/smtp --output-format yamlset -euo pipefail
# Handlebars placeholders available in a webhook body:
# {{ title }} {{ message }} {{ severity }} {{ timestamp }}
# {{ fields.<name> }} metadata such as fields.type, fields.hostname
# {{ secrets.<name> }} values stored in the root-only config
# Helpers: {{ url-encode v }} {{ escape v }} {{ json v }}
#
# The braces below are intentional and are what the target expects.
pvesh create /cluster/notifications/endpoints/webhook \
--name chat-ops \
--method POST \
--url 'https://chat.example.com/hooks/proxmox' \
--header 'name=Content-Type,value=application/json' \
--secret 'name=token,value=REPLACE_ME' \
--comment 'Cluster alerts into the ops channel'
# The body is a templated JSON document. Build it in the GUI if the quoting
# gets awkward from the shell - it is the same field either way.Matchers: the routing layer
A matcher holds rules and a list of targets. The rules are of three kinds and they can be combined:
| Rule | Syntax | Matches on |
|---|---|---|
match-field | exact:type=vzdump or regex:hostname=^pve-0[123]$ | Notification metadata |
match-severity | error or warning,error | Severity level |
match-calendar | mon..fri 09:00-17:00 | Wall-clock time |
mode decides how multiple rules combine: all requires every rule to
match, any requires at least one. invert-match reverses the whole result.
A matcher with no rules matches everything. That is not a bug; it is how the default configuration works.
set -euo pipefail
pvesh create /cluster/notifications/matchers \
--name backup-failures \
--mode all \
--match-field 'exact:type=vzdump' \
--match-severity error \
--target chat-ops \
--comment 'Failed backups page the on-call channel'
# A second matcher as the safety net: anything that is not routine,
# including system-mail from smartd and mdadm, which carries severity
# unknown and would be missed by an error-only rule.
pvesh create /cluster/notifications/matchers \
--name catch-all-non-info \
--mode all \
--match-severity 'warning,error,unknown' \
--target relay-ops \
--comment 'Safety net - everything that is not routine goes to mail'
pvesh get /cluster/notifications/matchers --output-format yamlThe default configuration, and why it is not enough
A fresh PVE installation ships with a built-in sendmail target named
mail-to-root and a built-in matcher named default-matcher that routes to
it. The built-ins can be disabled but not deleted, which is deliberate — it
guarantees there is always some path for a notification to take.
mail-to-root delivers to the email address configured on the root@pam
user. On a freshly installed cluster that address is frequently unset or
still root@localhost, which means every notification the cluster has ever
generated is sitting in /var/mail/root on a node nobody logs into.
set -euo pipefail
# What targets and matchers exist, including the built-ins?
pvesh get /cluster/notifications/endpoints/sendmail --output-format yaml
pvesh get /cluster/notifications/matchers --output-format yaml
# Where does mail-to-root actually go? Check the address on root@pam.
pvesh get /access/users/root@pam --output-format yaml | grep -i email
# Is there undelivered mail piling up locally? A large mbox here is a
# direct measure of how long notifications have been going nowhere.
ls -lh /var/mail/root 2>/dev/null || echo 'no local mail spool'
# Is the local MTA queueing because the relay is unreachable?
postqueue -p 2>/dev/null | tail -5 || echo 'postfix not installed or no queue'Per-job overrides
Backup jobs carry a notification-mode option with two values:
notification-system— the job’s notifications go through targets and matchers, like everything else.legacy-sendmail— the job sends mail directly through the systemsendmail, to an address configured on the job, bypassing the notification system entirely. This reproduces the pre-8.1 behaviour and is deprecated.
set -euo pipefail
# The notification-mode column is what you are reading. A job showing
# legacy-sendmail is outside every matcher; a job with the column empty is
# using the current default, which is the notification system.
pvesh get /cluster/backup \
--output-format json-pretty \
| grep -E '"(id|notification-mode|mailto)"'
# Or the whole job list in a readable form, if you prefer to eyeball it.
pvesh get /cluster/backup --output-format yamlTesting that an alert actually arrives
Configuration that has never delivered a message is a hypothesis. The GUI
provides a Test button per target — Datacenter → Notifications → select
the target → Test — which sends a test notification through the full
delivery path for that target. Using it requires Mapping.Use together with
Mapping.Audit or Mapping.Modify on the ACL path
/mapping/notifications.
But a target test only proves the target works. It does not exercise the matcher, and the matcher is where the routing mistakes are. To test the whole path you need to generate a real notification of the type you care about.
set -euo pipefail
VMID=9999
# The honest test is a genuine failure of the type your matcher keys on.
# Backing up a VMID that does not exist produces a vzdump notification with
# severity error and type vzdump - exactly what the matcher is written for.
vzdump "$VMID" --storage pbs-main --mode snapshot || true
# Then verify, in this order:
# 1. The notification was generated. Check the task log and the journal.
journalctl -u pvedaemon --since '5 min ago' --no-pager | grep -i vzdump
# 2. It was delivered. Check the target's own logs - the mail log for
# sendmail and smtp, the Gotify server, the webhook receiver.
journalctl -u postfix --since '5 min ago' --no-pager | tail -20
# 3. A human received it. This step cannot be automated and is the only
# one that actually matters. Ask the person on call whether their
# phone lit up.PBS has the same system, configured separately
Proxmox Backup Server implements the same target and matcher model with its own configuration and its own CLI. It is a separate system: a matcher on the PVE cluster does not route PBS notifications, and PBS generates event types that PVE does not — garbage collection, verification jobs, sync jobs, prune, and datastore health.
This matters because the events most worth being paged about are on the PBS side. A failed verification job means a backup that exists and may not restore. Configure both, and treat “PBS notifications are configured” as a separate line item from “PVE notifications are configured”.
Common mistakes
- Configuring a target and never writing a matcher. The target is reachable, tests pass, and nothing is ever routed to it.
- Keying a matcher on
job-id. Manual backups do not carry the field, so the rule fails to match and manual jobs are never reported. match-severity erroras the safety net. It excludesunknown, which is where SMART and RAID warnings live.- Assuming matchers stop at the first hit. They do not, and duplicate alerts are the result.
- Leaving
default-matcherenabled alongside real matchers, then wondering why every alert arrives twice. - Testing the target but not the path. The Test button does not exercise matcher rules.
- No heartbeat. A silent alerting system and a healthy cluster look identical.
Key takeaways
- Targets are destinations, matchers are routing, and an unmatched notification is silently discarded.
sendmailqueues and retries through the local MTA;smtpdoes neither. During an incident that difference decides whether the alert arrives.- Severity
unknowncoverssystem-mail, which carries SMART, mdadm and cron failures. Include it. - Every matcher is evaluated against every notification; there is no first-match-wins.
- Backup jobs set to
legacy-sendmailbypass matchers entirely — audit for them after an upgrade. - Test the full path with a real event, and run a heartbeat so that silence is detectable.
Knowledge check
Knowledge check · 5 questions
Q1. An administrator configures a Gotify target, tests it successfully from the GUI, and considers alerting done. Two weeks later a backup fails and no notification arrives. What is the most likely cause?
Q2. Which statements about matcher evaluation in PVE are correct? Select all that apply.
Q3. A sendmail target survives a temporary relay outage better than an smtp target, because it hands the message to the local MTA which queues and retries.
Q4. A cluster has a matcher with match-severity error routing to the on-call channel. smartd has been reporting a failing disk daily for three weeks and nobody has been paged. Why?
Q5. What is the purpose of storing a webhook token with the secret option rather than writing it into the header or body directly?
Passing score: 75%. Answers are checked in this browser.