Setting Up Proxmox Alerts with ntfy (Sendmail + .forward)
If you run Proxmox in production (or even a busy homelab), you already know the built-in notifications are useful… if you actually see them.
This is the setup I use to forward Proxmox alerts into a ntfy topic so they pop up where I’ll notice them.
This approach is simple and dependable:
- Proxmox notifications → Sendmail
- Mail lands in root
- root’s
~/.forwardpipes the mail into a script - the script pushes to ntfy using Basic Auth
It’s lightweight, doesn’t require an external SMTP provider, and works great even in isolated networks.
What you’ll need
- A working ntfy server (self-hosted or hosted)
- An ntfy username + password
- A topic for your Proxmox alerts
- Shell access to the Proxmox node
Step 1 — Create the script
Open the shell on the Proxmox node you want to configure.
Create a file called ntfy.sh in /usr/bin.
I usually use nano:
nano /usr/bin/ntfy.sh
Step 2 — Paste my enhanced script
Replace these values inside the script:
your_ntfy_user→ your ntfy usernameyour_ntfy_pass→ your ntfy user’s passwordyour_nfty_fqdn→ your ntfy domain (FQDN)your_ntfy_subject→ your ntfy topic
Here’s the full script I’m using:
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# Proxmox -> Sendmail -> root -> .forward -> ntfy.sh -> ntfy topic
# Enhanced parsing + severity mapping + multiline message support
# -----------------------------------------------------------------------------
# Set DEBUG=1 temporarily if you want to see parsing output
DEBUG=0
# Static label for ntfy title prefix (optional)
ALERTSOURCE="Proxmox"
# ---- ntfy Basic Auth credentials ----
BASIC_USER="your_ntfy_user"
BASIC_PASS="your_ntfy_pass"
# ---- ntfy endpoint ----
NTFY_FQDN="your_nfty_fqdn"
NTFY_TOPIC="your_ntfy_subject"
# ---- optional default tags for ntfy (comma-separated) ----
# Example: "proxmox,alerts,backup"
NTFY_TAGS="proxmox"
# -----------------------------------------------------------------------------
# Read full input (the email piped via .forward)
# -----------------------------------------------------------------------------
RAW="$(cat)"
# -----------------------------------------------------------------------------
# Helper: extract a single-line "KEY: value" from the body
# Looks for exact uppercase keys like SUBJECT:, SEVERITY:, NODE:, HOST:, etc.
# -----------------------------------------------------------------------------
extract_kv() {
local key="$1"
echo "$RAW" | awk -v k="$key" '
$0 ~ "^"k":[[:space:]]*" {
sub("^"k":[[:space:]]*", "", $0)
print $0
exit
}
'
}
# -----------------------------------------------------------------------------
# Helper: extract a multi-line block starting at "MESSAGE:"
# Stops when another KEY: line begins or end of input
# -----------------------------------------------------------------------------
extract_message_block() {
echo "$RAW" | awk '
BEGIN {found=0}
/^MESSAGE:[[:space:]]*/ {
found=1
sub(/^MESSAGE:[[:space:]]*/, "")
print
next
}
found && /^[A-Z_]+[[:space:]]*:/ { exit }
found { print }
'
}
# -----------------------------------------------------------------------------
# Helper: fallback parse standard email "Subject:" header if needed
# -----------------------------------------------------------------------------
extract_email_subject_header() {
echo "$RAW" | awk '
BEGIN{IGNORECASE=1}
/^Subject:[[:space:]]*/ {
sub(/^Subject:[[:space:]]*/, "")
print
exit
}
'
}
# -----------------------------------------------------------------------------
# Parse known fields (body-style)
# -----------------------------------------------------------------------------
SUBJECT_BODY="$(extract_kv "SUBJECT")"
SEVERITY="$(extract_kv "SEVERITY")"
NODE_FIELD="$(extract_kv "NODE")"
HOST_FIELD="$(extract_kv "HOST")"
MESSAGE_BODY="$(extract_message_block)"
# -----------------------------------------------------------------------------
# Fallbacks
# -----------------------------------------------------------------------------
SUBJECT_HEADER="$(extract_email_subject_header)"
NODE_LOCAL="$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo "proxmox")"
ALERT_SUBJECT="${SUBJECT_BODY:-${SUBJECT_HEADER:-"${ALERTSOURCE} Alert"}}"
# Prefer NODE:, then HOST:, then local hostname
NODE_NAME="${NODE_FIELD:-${HOST_FIELD:-$NODE_LOCAL}}"
# If MESSAGE: block doesn't exist, grab a short excerpt as fallback
if [[ -z "${MESSAGE_BODY// /}" ]]; then
MESSAGE_BODY="$(echo "$RAW" | awk '
BEGIN{n=0}
/^[[:space:]]*$/ {next}
# Skip common email headers if present
/^[A-Za-z-]+:/ && n==0 {next}
{print; n++}
n>=12 {exit}
')"
fi
# -----------------------------------------------------------------------------
# Severity -> ntfy priority mapping
# ntfy priorities are usually 1..5
# -----------------------------------------------------------------------------
severity_to_priority() {
local s="${1,,}" # lowercase
case "$s" in
emergency|alert|critical|fatal|panic)
echo 5 ;;
error|failed|failure)
echo 4 ;;
warning|warn)
echo 3 ;;
notice)
echo 3 ;;
info|information|ok|success)
echo 2 ;;
*)
echo 3 ;;
esac
}
PRIORITY="$(severity_to_priority "${SEVERITY:-}")"
# -----------------------------------------------------------------------------
# Build final message
# -----------------------------------------------------------------------------
FINAL_MESSAGE="Node: ${NODE_NAME}"
if [[ -n "$SEVERITY" ]]; then
FINAL_MESSAGE="${FINAL_MESSAGE}
Severity: ${SEVERITY}"
fi
FINAL_MESSAGE="${FINAL_MESSAGE}
${MESSAGE_BODY}"
# -----------------------------------------------------------------------------
# Build base64(user:password), strip newline
# -----------------------------------------------------------------------------
BASIC_AUTH="$(printf '%s' "${BASIC_USER}:${BASIC_PASS}" | base64 | tr -d '\n')"
# -----------------------------------------------------------------------------
# Send to ntfy
# -----------------------------------------------------------------------------
CURL_ARGS=(
-sS
-X POST
"https://${NTFY_FQDN}/${NTFY_TOPIC}"
-H "Authorization: Basic ${BASIC_AUTH}"
-F "title=${ALERTSOURCE} - ${ALERT_SUBJECT}"
-F "message=${FINAL_MESSAGE}"
-F "priority=${PRIORITY}"
)
# Add tags if set
if [[ -n "${NTFY_TAGS// /}" ]]; then
CURL_ARGS+=(-F "tags=${NTFY_TAGS}")
fi
if [[ "$DEBUG" -eq 1 ]]; then
echo "---- DEBUG ntfy.sh ----"
echo "NODE_NAME=$NODE_NAME"
echo "SEVERITY=$SEVERITY"
echo "PRIORITY=$PRIORITY"
echo "ALERT_SUBJECT=$ALERT_SUBJECT"
echo "----- MESSAGE -----"
echo "$FINAL_MESSAGE"
echo "-------------------"
curl -v "${CURL_ARGS[@]}"
else
curl "${CURL_ARGS[@]}" > /dev/null 2>&1
fi
exit 0
Step 3 — Make it executable
chmod +x /usr/bin/ntfy.sh
Step 4 — Pipe root mail into the script
Edit root’s .forward file:
nano ~/.forward
Add this line exactly:
|/usr/bin/ntfy.sh
That means anything delivered to root will be sent into the script.
Step 5 — Test it
This is the test I run to confirm everything is wired correctly:
printf "SUBJECT: Test from terminal\nMESSAGE: This is a multi-line test\nLine two\nLine three\nSEVERITY: warning\n" | /usr/bin/ntfy.sh
If that’s working, you should see a notification arrive in your chosen ntfy topic.
Step 6 — Configure Sendmail in Proxmox
In the Proxmox GUI:(Should be build in but if its not there)
- Click Datacenter
- Go to Notifications
- Click Add
- Choose Sendmail
- Enter the details from the Screenshot below

I keep this simple and target local mail delivery (typically root).
Step 7 — Enable email notifications where you want them
For backups and other tasks that support notification targets:
- Select Email
- Ensure the recipient is a mailbox you are forwarding
(for this guide, that’s usually root)
From here, Proxmox will generate local emails, and my .forward rule will push them into ntfy.
How I map severity to ntfy priority
The script does a best-effort mapping:
critical/fatal/emergency→ priority 5error/failed→ priority 4warning→ priority 3notice→ priority 3info/success→ priority 2- anything unknown → priority 3
This keeps the high-urgency alerts loud without making everything feel like a fire.
Optional hardening (highly recommended)
I don’t love storing credentials in /usr/bin, even on a box I control.
A quick improvement is to move secrets into a root-only config file.
Create:
nano /root/.ntfy-proxmox.conf
Example contents:
BASIC_USER="EDIT_ME"
BASIC_PASS="EDIT_ME"
NTFY_FQDN="EDIT_ME"
NTFY_TOPIC="EDIT_ME"
NTFY_TAGS="proxmox,alerts,backup"
Lock it down:
chmod 600 /root/.ntfy-proxmox.conf
Then add this line near the top of /usr/bin/ntfy.sh:
[[ -f /root/.ntfy-proxmox.conf ]] && source /root/.ntfy-proxmox.conf
Now your script can stay generic while your secrets remain protected.
Optional debug mode
If you’re troubleshooting parsing, set:
DEBUG=1
Then run:
printf "SUBJECT: Debug test\nMESSAGE: Hello world\nSEVERITY: error\n" | /usr/bin/ntfy.sh
You’ll see how the script interprets the message before it posts.
What this setup gives you
This is the full pipeline:
Proxmox event
-> Sendmail notification
-> root mailbox
-> ~/.forward
-> /usr/bin/ntfy.sh
-> ntfy topic (Basic Auth)
It’s clean, easy to replicate across nodes, and requires minimal moving parts.
Final notes
Once this is in place, I usually forget it exists — which is exactly what I want from alerting infrastructure.
Alerts show up in ntfy reliably, and I can fine-tune which Proxmox jobs send email without touching the script again.