Runbook: Set up email alerts to monitor mailcow server activity and available updates

Objective: Set up a complete Monday morning alert stack so you always know about available updates to containers, mailcow itself, and host OS packages — and receive a weekly summary of mail activity — without anything updating automatically.

When to use: Once, as part of initial mailcow setup.

Prerequisites:

  • Mailcow is running via mailcow-dockerized on a Hetzner server
  • You have SSH root access to your server
  • docker compose is working
  • curl and jq are installed (see Step 1)
  • msmtp is installed and configured — see the Install and configure msmtp runbook before continuing

Overview — Monday Morning Alert Schedule

Time Alert Method
6:00 AM Weekly mail activity summary pflogsumm + cron + msmtp
7:00 AM Container image scan Watchtower (monitor-only)
7:05 AM Container update report docker logs + cron + msmtp
7:10 AM Mailcow version check GitHub API + cron + msmtp
8:05 AM Host OS package updates apt-get -s upgrade + cron + msmtp

Part 1: Weekly Mail Activity Summary (pflogsumm)

All alerts in this runbook are sent via msmtp. If you haven’t set it up yet, follow the Install and configure msmtp runbook first, then come back here.

Step 1: Install pflogsumm

apt install pflogsumm

Step 2: Test pflogsumm manually

docker logs --since 168h mailcowdockerized-postfix-mailcow-1 2>&1 | \
  pflogsumm

You should see a plain-text mail activity report. If output is empty, confirm your postfix container name:

docker ps --format '{{.Names}}' | grep postfix

:bulb: Avoid --verbose-msg-detail — pflogsumm will include spam subject lines in the output, which can cause mailcow to reject the outgoing summary email.

Step 3: Test the full email pipeline

docker logs --since 168h mailcowdockerized-postfix-mailcow-1 2>&1 | \
  pflogsumm | \
  { printf "Subject: Weekly Mail Summary - =SERVER_HOST=\n\n"; cat; } | \
  msmtp =ALERT_EMAIL=

Check your inbox — you should receive a complete weekly summary. :white_check_mark:

Step 4: Add the weekly cron job

crontab -e

Add this line (runs every Monday at 6:00 AM UTC):

0 6 * * 1 SUBJECT="Weekly Mail Summary - =SERVER_HOST= (week $(date +%V), $(date +%Y): $(date -d '7 days ago' +%b\ %d) – $(date +%b\ %d))"; docker logs --since 168h mailcowdockerized-postfix-mailcow-1 2>&1 | pflogsumm | { printf "Subject: $SUBJECT\n\n"; cat; } | msmtp =ALERT_EMAIL=

This produces a subject line like:

Weekly Mail Summary - mail.example.org (week 21, 2026: May 11 – May 18)

:bulb: --since 168h gives exactly 7 days of logs. date +%V is the ISO week number. Since the cron runs Monday morning, date -d '7 days ago' is always last Monday — so the date range is accurate.


Part 2: Watchtower Container Monitoring

Step 5: Verify your mailcow Docker network name

docker network ls | grep mailcow

Note the full name of the mailcow-network entry — typically mailcowdockerized_mailcow-network.

Step 6: Add Watchtower to docker-compose.override.yml

nano /opt/mailcow-dockerized/docker-compose.override.yml

Add the following (create the file if it doesn’t exist):

services:
  watchtower-mailcow-notify:
    image: containrrr/watchtower
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - DOCKER_API_VERSION=1.41
      - WATCHTOWER_MONITOR_ONLY=true
      - WATCHTOWER_SCHEDULE=0 0 7 * * 1
    networks:
      - mailcow-network

networks:
  mailcow-network:
    external: true
    name: mailcowdockerized_mailcow-network

:bulb: WATCHTOWER_MONITOR_ONLY=true means Watchtower never pulls or restarts any container — it only checks for available updates and logs what it finds. WATCHTOWER_SCHEDULE=0 0 7 * * 1 runs the check every Monday at 7:00 AM UTC.

Step 7: Start Watchtower

cd /opt/mailcow-dockerized
docker compose up -d watchtower-mailcow-notify
docker compose logs watchtower-mailcow-notify

You should see clean output with no errors:

Watchtower 1.7.1
Using no notifications
Scheduling first run: 2026-05-18 07:00:00 +0000 UTC

Step 8: Add the Watchtower results cron job

crontab -e

Add this line (runs every Monday at 7:05 AM, just after Watchtower’s scan):

5 7 * * 1 docker logs --since 10m mailcowdockerized-watchtower-mailcow-notify-1 2>&1 | grep -i "found new\|error\|warn" | { read -r first; [ -z "$first" ] && exit 0; { printf "Subject: Container Updates Available - =SERVER_HOST=\n\n"; printf "$first"; cat; } | msmtp =ALERT_EMAIL=; }

:bulb: This only sends an email if Watchtower found something noteworthy — Found new means an update is available. If everything is up to date the email is suppressed. --since 10m grabs just the most recent run’s output.


Part 3: Mailcow Version Check

Step 9: Verify jq is installed

jq --version

If missing:

apt install -y jq

Step 10: Create the version check script

nano /usr/local/bin/mailcow-version-check.sh

Paste the following:

#!/bin/bash
# Checks installed mailcow version against latest GitHub release
# Sends email via msmtp if an update is available

MAILCOW_DIR="/opt/mailcow-dockerized"
ALERT_EMAIL="=ALERT_EMAIL="
SERVER_HOST="=SERVER_HOST="

# Get installed version from mailcow.conf
INSTALLED=$(grep "^MAILCOW_GIT_VERSION=" "$MAILCOW_DIR/mailcow.conf" 2>/dev/null | cut -d= -f2 | tr -d '"')

# Get latest release from GitHub API
LATEST=$(curl -sf "https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest" | jq -r '.tag_name')

# Bail out if either value is empty (network error etc.)
if [ -z "$INSTALLED" ] || [ -z "$LATEST" ]; then
  exit 0
fi

# Send email only if versions differ
if [ "$INSTALLED" != "$LATEST" ]; then
  printf "Subject: Mailcow Update Available - $SERVER_HOST\n\nInstalled: $INSTALLED\nLatest: $LATEST\n\nRelease notes: https://github.com/mailcow/mailcow-dockerized/releases/tag/$LATEST\n\nTo update, run:\n  cd $MAILCOW_DIR && ./update.sh" \
    | msmtp "$ALERT_EMAIL"
fi

Make it executable:

chmod +x /usr/local/bin/mailcow-version-check.sh

Step 11: Test the script

Run the script directly to confirm it works:

/usr/local/bin/mailcow-version-check.sh

To test the email pipeline even if you’re already up to date:

INSTALLED="2024-01a" LATEST="2024-02" \
  && printf "Subject: Mailcow Update Available - =SERVER_HOST=\n\nInstalled: $INSTALLED\nLatest: $LATEST\n\nRelease notes: https://github.com/mailcow/mailcow-dockerized/releases/tag/$LATEST\n\nTo update, run:\n  cd /opt/mailcow-dockerized && ./update.sh" \
  | msmtp =ALERT_EMAIL=

Step 12: Add the cron job

crontab -e

Add this line (runs every Monday at 7:10 AM):

10 7 * * 1 /usr/local/bin/mailcow-version-check.sh

Part 4: Host OS Package Update Notifications

Step 13: Install unattended-upgrades

apt install unattended-upgrades apt-listchanges

Step 14: Configure unattended-upgrades

nano /etc/apt/apt.conf.d/50unattended-upgrades

Find and update these lines (uncomment them if needed):

Unattended-Upgrade::Mail "=ALERT_EMAIL=";
Unattended-Upgrade::MailReport "only-on-error";
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";

:warning: Automatic-Reboot "false" is critical — you never want a mail server rebooting itself automatically.

Step 15: Enable the unattended-upgrades timer

dpkg-reconfigure -plow unattended-upgrades

Select Yes when prompted. This enables automatic security-only patches. Verify the timer is active:

systemctl status apt-daily.timer apt-daily-upgrade.timer

Both should show active (waiting).

Step 16: Add the package updates cron job

crontab -e

Add this line (runs every day at 8:05 AM):

5 8 * * * apt-get -s upgrade 2>/dev/null | grep "^Inst" | { read -r first; [ -z "$first" ] && exit 0; { printf "Subject: Package Updates Available - =SERVER_HOST=\n\nThe following packages can be updated:\n\n"; printf "$first"; cat; } | msmtp =ALERT_EMAIL=; }

:bulb: apt-get -s upgrade is a dry run — the -s flag means “simulate”, nothing is ever installed. The email is only sent if at least one package is waiting.

Step 17: Test the cron command manually

apt-get -s upgrade 2>/dev/null | grep "^Inst" | \
  { printf "Subject: Package Updates Available - =SERVER_HOST=\n\nThe following packages can be updated:\n\n"; cat; } | \
  msmtp =ALERT_EMAIL=

:bulb: The test command sends unconditionally (no early-exit check) so you’ll get an email even if there are no updates, confirming the pipeline works. The cron version only sends if there’s something to report.


Part 5: Applying Updates When Notified

Host OS packages

:warning: Take a Hetzner snapshot before applying updates — especially when systemd, Docker, or kernel packages are involved. Log into console.hetzner.com, select your server, go to Snapshots and click Take Snapshot before proceeding.

apt-get upgrade -y

If a kernel or systemd update was applied, reboot:

reboot

After reconnecting, verify all containers came back up:

cd /opt/mailcow-dockerized && docker compose ps

Clean up orphaned packages periodically:

apt autoremove -y

For held-back packages (shown as kept back):

apt-get dist-upgrade -y

Mailcow version

:warning: Take a Hetzner snapshot before running ./update.sh — mailcow updates can include database migrations that are not easily reversible. Always read the release notes at the link in the notification email first.

cd /opt/mailcow-dockerized
./update.sh

Container images (postgres, redis, ghost etc.)

Watchtower reports these but does not update them automatically. To apply:

:warning: Take a Hetzner snapshot first for any production service.

cd /opt/mailcow-dockerized
docker compose pull
docker compose up -d

For non-mailcow containers (Ghost, Nextcloud, etc.) follow the update procedure for that service.


:white_check_mark: Done!

Your complete Monday morning alert stack is running. You will receive:

  • :e_mail: 6:00 AM — Weekly mail activity summary
  • :e_mail: 7:05 AM — Container image update report (only if updates found)
  • :e_mail: 7:10 AM — Mailcow version alert (only if update available)
  • :e_mail: 8:05 AM — Host OS package update report (only if updates found)

:link: Related runbooks:

I made an update this morning after receiving my first email. It was a bit munged because “echo -e” did not create the properly formatted output. I changed it to “printf” instead. Updated runbook accordingly.

My weekly mail summary was being rejected by mailcow, likely because it contained alot of spam subject lines! :person_facepalming:

Fixed it by removing --verbose-msg-detail from pflogsumm. msmtp helpfully provides a log that helped me troubleshoot the problem. tail -50 /var/log/msmtp.log

My current cron:

0 8 * * * df -h | grep -vE '^(overlay|tmpfs)' | awk '$5+0 > 85 {print}' | { printf "Subject: Disk Warning on mail.tobiaseigen.org\n\nFilesystems above 85%:\n\n"; cat; } | msmtp tobias@tobiaseigen.org
0 6 * * 1 SUBJECT="Weekly Mail Summary - mail.tobiaseigen.org (week $(date +%V), $(date +%Y): $(date -d '7 days ago' +%b\ %d) – $(date +%b\ %d))"; docker logs --since 168h mailcowdockerized-postfix-mailcow-1 2>&1 | pflogsumm | { printf "Subject: $SUBJECT\n\n"; cat; } | msmtp tobias@tobiaseigen.org
5 7 * * 1 docker logs --since 10m mailcowdockerized-watchtower-mailcow-notify-1 2>&1 | grep -i "found new" | { read -r first; [ -z "$first" ] && exit 0; { printf "Subject: Container Updates Available - mail.tobiaseigen.org\n\n"; echo "$first"; cat; } | msmtp tobias@tobiaseigen.org; }
5 8 * * * apt-get -s upgrade 2>/dev/null | grep "^Inst" | { read -r first; [ -z "$first" ] && exit 0; { printf "Subject: Package Updates Available - mail.tobiaseigen.org\n\nThe following packages can be updated:\n\n"; echo "$first"; cat; printf "\n\nTo apply updates:\n  1. Take a Hetzner snapshot first (console.hetzner.com)\n  2. Run: apt-get upgrade -y\n  3. Run: docker compose ps (in /opt/mailcow-dockerized)\n  4. Reboot if kernel or systemd was updated\n"; } | msmtp tobias@tobiaseigen.org; }
10 7 * * 1 /usr/local/bin/mailcow-version-check.sh

I learned from this runbook to set up automated OS updates and alerting on my other servers besides mailcow. Just skipped over the mailcow-specific parts.