Skip to main content
RunBook Academy

Proxmox VEXVI · MonitoringNotifications

The PVE notification system: targets and matchers

Intermediate⏱ ~28 minpvesh

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

Not yet marked complete on this device.

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:

EventtypeSeverityMetadata fields
System updates availablepackage-updatesinfohostname
Cluster node fencedfencingerrorhostname
Storage replication failedreplicationerrorhostname, job-id
Backup succeededvzdumpinfohostname, job-id
Backup failedvzdumperrorhostname, job-id
Mail addressed to rootsystem-mailunknownhostname

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

TypeDelivery pathRetries on failureUse it when
sendmailHands the mail to the local MTA (Postfix)Yes — the MTA queues and retriesMail is your channel and the node has a working relay configured
smtpTalks to an SMTP relay directly, no local MTANo — no queueing, no retryYou want mail without configuring Postfix on every node, and you accept that a relay outage means the notification is gone
gotifyPush to a self-hosted Gotify serverNoYou want a phone notification without an email round trip
webhookHTTP request to an arbitrary URLNoIntegrating 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.

Configuration changean SMTP target, created from the CLI
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 yaml
Configuration changea webhook target for a chat or paging system
set -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:

RuleSyntaxMatches on
match-fieldexact:type=vzdump or regex:hostname=^pve-0[123]$Notification metadata
match-severityerror or warning,errorSeverity level
match-calendarmon..fri 09:00-17:00Wall-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.

Configuration changeroute backup failures to the paging channel, and nothing else
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 yaml

The 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.

Read-only / Safefind out where your cluster is currently sending things
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 system sendmail, to an address configured on the job, bypassing the notification system entirely. This reproduces the pre-8.1 behaviour and is deprecated.
Read-only / Safefind backup jobs that bypass the notification system
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 yaml

Testing 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.

Service impact possibleend-to-end test: make a real backup fail and confirm the page arrives
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 error as the safety net. It excludes unknown, 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-matcher enabled 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.
  • sendmail queues and retries through the local MTA; smtp does neither. During an incident that difference decides whether the alert arrives.
  • Severity unknown covers system-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-sendmail bypass 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

  1. 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?

  2. Q2. Which statements about matcher evaluation in PVE are correct? Select all that apply.

  3. 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.

  4. 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?

  5. 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.