#!/bin/bash # Disk-space guardrail. Alerts via Telegram BEFORE the root filesystem fills up # and crashes Postgres/Listmonk (as happened 2026-06-27 when an orphaned 15GB # forgejo-dump.zip + uncapped Docker logs pushed / to 100% and Postgres # crash-looped on "No space left on device"). Silent on healthy days. # # Also proactively cleans the two things that caused that outage: # 1. orphaned /tmp/forgejo-dump.zip left behind by an interrupted backup # 2. Docker build cache (safe, fully regenerable) # ...but ONLY once usage crosses a soft threshold, so normal days are untouched. # # Schedule: every 15 min via cron (see infra/cron/pw-disk-space-alert). set -uo pipefail ENV_FILE=/opt/performancewest/.env WARN_PCT=${PW_DISK_WARN_PCT:-90} # Telegram warning at/above this % CRIT_PCT=${PW_DISK_CRIT_PCT:-94} # auto-cleanup kicks in at/above this % MOUNT=/ BOT=$(grep -E '^TELEGRAM_BOT_TOKEN=' "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-) CHAT=$(grep -E '^TELEGRAM_CHAT_ID=' "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-) tg() { [ -n "${BOT:-}" ] && [ -n "${CHAT:-}" ] || return 0 curl -s --max-time 10 "https://api.telegram.org/bot${BOT}/sendMessage" \ --data-urlencode "chat_id=${CHAT}" --data-urlencode "text=$1" >/dev/null 2>&1 } usage_pct() { df --output=pcent "$MOUNT" | tail -1 | tr -dc '0-9'; } PCT=$(usage_pct) [ -n "$PCT" ] || exit 0 if [ "$PCT" -lt "$WARN_PCT" ]; then exit 0 # healthy, stay silent fi FREED="" if [ "$PCT" -ge "$CRIT_PCT" ]; then # 1. Drop any orphaned forgejo dump still sitting in the container's /tmp. if docker exec performancewest-forgejo test -f /tmp/forgejo-dump.zip 2>/dev/null; then docker exec performancewest-forgejo rm -f /tmp/forgejo-dump.zip 2>/dev/null \ && FREED="${FREED}orphaned forgejo-dump.zip; " fi # 2. Reclaim Docker build cache (safe, regenerable). RECLAIMED=$(docker builder prune -af 2>/dev/null | grep -oE 'Total reclaimed space: .*' | head -1) [ -n "$RECLAIMED" ] && FREED="${FREED}build-cache (${RECLAIMED#Total reclaimed space: }); " fi NEWPCT=$(usage_pct) AVAIL=$(df -h "$MOUNT" | tail -1 | awk '{print $4}') TOPDIRS=$(sudo du -shx /var/lib/docker /opt/backups /var/log 2>/dev/null | sort -rh | head -3 | awk '{printf "%s %s; ", $1, $2}') LEVEL="⚠️ WARNING"; [ "$NEWPCT" -ge "$CRIT_PCT" ] && LEVEL="🔴 CRITICAL" MSG=$(printf '%s Performance West disk space\n\nRoot %s: %s%% used (%s free)\n%s\nTop: %s' \ "$LEVEL" "$MOUNT" "$NEWPCT" "$AVAIL" \ "${FREED:+Auto-freed: ${FREED}now ${NEWPCT}%}" "$TOPDIRS") tg "$MSG" # Mirror to the same report log the warmup check uses, for history. echo "[$(date '+%F %T')] disk ${PCT}% -> ${NEWPCT}% (free ${AVAIL}) ${FREED:+freed: $FREED}" \ >> /opt/performancewest/logs/pw-disk-space-alert.log 2>&1