Setting Up Proxmox Alerts with ntfy (Sendmail + .forward)

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 ~/.forward pipes 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 username
  • your_ntfy_pass → your ntfy user’s password
  • your_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)

  1. Click Datacenter
  2. Go to Notifications
  3. Click Add
  4. Choose Sendmail
  5. Enter the details from the Screenshot below
    Screenshot 2025-12-07 at 4.36.13 pm.png

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 5
  • error/failed → priority 4
  • warning → priority 3
  • notice → priority 3
  • info/success → priority 2
  • anything unknown → priority 3

This keeps the high-urgency alerts loud without making everything feel like a fire.


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.