How to Send Yourself a Weekly Server Report (One Script, Every Monday, Everything That Matters)

Every Monday, one message: disk, updates, failed services, logins, backups, drives, certificates. The monthly review, automated.

Tutorial 39's monthly review is seven commands you have to remember to run. Alerts (35, 93, 104) tell you when something breaks. What's missing is the quiet middle: the slow trends and the small things that aren't alarms yet — disk creeping up, a container that restarted twice, a certificate with three weeks left, three failed SSH logins from your own IP. A script that collects all of it into one message every Monday morning takes an hour to write and then runs for years.

Quick reference

SectionCommand behind it
Uptime, load, rebootsuptime, last reboot
Diskdf -h with a warning over 80%
Memory and swapfree -h
Pending updates, reboot requiredapt list --upgradable, /var/run/reboot-required
Failed servicessystemctl --failed
Containersdocker ps with restart counts
Logins and failureslast -n, lastb, journalctl -u ssh
Backupstail of backup.log / restic snapshots
Drivessmartctl -H per drive
Certificatescertbot certificates
Deliverymail (49), ntfy (93) or Telegram (118)

1. The script

sudo nano /usr/local/bin/weekly-report.sh

#!/bin/bash
set -uo pipefail
H=$(hostname)
R=$(mktemp)
section() { printf '\n=== %s ===\n' "$1" >> "$R"; }

section "System"
{ uptime; echo "Last reboots:"; last reboot -n 3 | head -3; } >> "$R"

section "Disk"
df -h --output=target,size,used,pcent -x tmpfs -x devtmpfs -x overlay | awk 'NR==1 || $4+0 >= 0' >> "$R"
df --output=target,pcent -x tmpfs -x devtmpfs -x overlay | awk 'NR>1 && $2+0 > 80 {print "WARNING: " $1 " at " $2}' >> "$R"

section "Memory"
free -h >> "$R"

section "Updates"
apt list --upgradable 2>/dev/null | grep -c upgradable | sed 's/^/Packages upgradable: /' >> "$R"
[ -f /var/run/reboot-required ] && echo "REBOOT REQUIRED" >> "$R"
grep -h "Installed\|Upgrade" /var/log/unattended-upgrades/unattended-upgrades.log 2>/dev/null | tail -n 3 >> "$R"

section "Failed services"
systemctl --failed --no-legend >> "$R"
[ -z "$(systemctl --failed --no-legend)" ] && echo "none" >> "$R"

section "Containers"
docker ps -a --format '{{.Names}}\t{{.Status}}' 2>/dev/null | column -t >> "$R"
for c in $(docker ps -q 2>/dev/null); do
n=$(docker inspect -f '{{.Name}} {{.RestartCount}}' "$c"); [ "${n##* }" -gt 0 ] && echo "RESTARTED: $n" >> "$R"
done

section "Logins (7 days)"
last -s -7days -a | grep -v "^$\|wtmp\|reboot" | awk '{print $1, $4, $5, $6, $NF}' | sort | uniq -c >> "$R"
echo "SSH failures: $(journalctl -u ssh --since '7 days ago' 2>/dev/null | grep -c 'Failed password\|Invalid user')" >> "$R"
journalctl -u ssh --since '7 days ago' 2>/dev/null | grep Accepted | awk '{print $NF}' | sort | uniq -c | sed 's/^/Accepted keys: /' >> "$R"

section "Backups"
tail -n 3 /var/log/backup.log 2>/dev/null >> "$R"
[ -f /root/.restic.env ] && { source /root/.restic.env; restic snapshots --latest 1 --compact 2>/dev/null | tail -n 4 >> "$R"; }

section "Drives"
for d in $(lsblk -dno NAME,TYPE | awk '$2=="disk"{print $1}'); do
echo "$d: $(smartctl -H /dev/$d 2>/dev/null | grep -i 'overall\|result' | sed 's/.*: //') $(smartctl -A /dev/$d 2>/dev/null | awk '/Reallocated_Sector|Current_Pending/ {printf "%s=%s ", $2, $10}')" >> "$R"
done

section "Certificates"
certbot certificates 2>/dev/null | grep -E 'Certificate Name|Expiry' | paste - - | awk '{print $3, $(NF-1), $NF}' >> "$R"

# --- deliver ---
SUBJECT="Weekly report: $H $(date +%F)"
mail -s "$SUBJECT" you@example.com < "$R"
curl -s -H "Title: $SUBJECT" -H "Authorization: Bearer $NTFY_TOKEN" --data-binary @"$R" https://ntfy.example.com/server > /dev/null
rm -f "$R"

sudo chmod +x /usr/local/bin/weekly-report.sh. Put NTFY_TOKEN in an env file the script sources, or drop the curl line and keep email only. Run it once by hand: sudo /usr/local/bin/weekly-report.sh — read the message, fix anything it reveals.

2. Every Monday

A systemd timer (69) so a server that was off at 7 am sends it when it's next up:

sudo nano /etc/systemd/system/weekly-report.timer

[Unit]
Description=Weekly server report
[Timer]
OnCalendar=Mon 07:00
Persistent=true
[Install]
WantedBy=timers.target

and a matching weekly-report.service with ExecStart=/usr/local/bin/weekly-report.sh. sudo systemctl enable --now weekly-report.timer. Or root's crontab: 0 7 * * 1 /usr/local/bin/weekly-report.sh.

3. Make it yours

4. Read it

Two minutes on Monday with coffee. Disk up 3% a week → look at Immich. A container restarted → check its log in Dozzle (125). An Accepted key fingerprint you don't recognise → tutorial 71, now. Certificate expiring in 10 days → renewal is failing. Nothing odd → close it. That's the review, done by the server.

Troubleshooting

Empty sections. The command needs root (smartctl, journalctl, restic) — run from root's timer/cron, not yours.

mail sends nothing. msmtp (49) not configured; test with echo hi | mail -s test you@example.com.

Report arrives late or not at all after a weekend off. Persistent=true on the timer handles it; cron doesn't.

Too long. Trim sections, or send a short summary by ntfy and the full report by email.

Wrapping up

One script that runs the commands you'd otherwise forget, a timer for Monday morning, email and ntfy delivery. The slow problems — the ones that never trigger an alert — show up as a line in a message you actually read, weeks before they'd have become an outage.

Call us