🏥 Server Health Check Script
One script that checks everything. Run it every Sunday. Takes 30 seconds. Catches disk full, SSL expiry, malware signs, and broken backups before they become emergencies.
⚡ Complete Health Check Script — Copy & Save
PASS — all good
WARN — attention needed
FAIL — fix this now
INFO — for your reference
📄 health-check.sh  ·  Save to /opt/health-check.sh on your server
#!/bin/bash
# ============================================================
# WEBMASTER SERVER HEALTH CHECK
# Save: /opt/health-check.sh
# Run:  bash /opt/health-check.sh
# Cron: 0 9 * * 0 bash /opt/health-check.sh
# ============================================================

# ── CONFIG (edit these) ──────────────────────────────────
DOMAIN="yourdomain.com"
WP_PATH="/var/www/html"
BACKUP_DIR="/backups"
WARN_DISK=80          # % — warn if disk above this
CRIT_DISK=90          # % — fail if disk above this
WARN_SSH_FAILS=20     # daily failed logins before warning
CRIT_SSH_FAILS=100    # daily failed logins before critical

RED='\033[0;31m'; YEL='\033[1;33m'; GRN='\033[0;32m'
BLU='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'

pass() { echo -e "${GRN}[PASS]${NC} $1"; }
warn() { echo -e "${YEL}[WARN]${NC} $1"; }
fail() { echo -e "${RED}[FAIL]${NC} $1"; }
info() { echo -e "${BLU}[INFO]${NC} $1"; }
section() { echo -e "\n${BOLD}━━━ $1 ━━━${NC}"; }

echo -e "${BOLD}═══════════════════════════════════════════════${NC}"
echo -e "${BOLD}  SERVER HEALTH CHECK — $(date '+%Y-%m-%d %H:%M:%S')${NC}"
echo -e "${BOLD}  Host: $(hostname) | Domain: ${DOMAIN}${NC}"
echo -e "${BOLD}═══════════════════════════════════════════════${NC}"

# ─────────────────────────────────────────────────────────
# 1. DISK USAGE
# ─────────────────────────────────────────────────────────
section "1. DISK USAGE"
while IFS= read -r line; do
    pct=$(echo "$line" | awk '{gsub(/%/,"",$5); print $5}')
    mnt=$(echo "$line" | awk '{print $6}')
    [[ "$pct" =~ ^[0-9]+$ ]] || continue
    [[ "$mnt" == "Mounted" ]] && continue
    if   [ "$pct" -ge "$CRIT_DISK" ]; then fail "$mnt is ${pct}% full — CRITICAL"
    elif [ "$pct" -ge "$WARN_DISK" ]; then warn "$mnt is ${pct}% full"
    else                                    pass "$mnt: ${pct}% used"
    fi
done < <(df -h | grep -v tmpfs | grep -v udev)

# ─────────────────────────────────────────────────────────
# 2. MEMORY
# ─────────────────────────────────────────────────────────
section "2. MEMORY"
read TOTAL USED FREE_MEM < <(free -m | awk '/^Mem:/{print $2, $3, $7}')
SWAP_USED=$(free -m | awk '/^Swap:/{print $3}')
PCT=$((USED * 100 / TOTAL))
if   [ "$PCT" -ge 90 ]; then fail "RAM: ${PCT}% (${USED}/${TOTAL}MB) — critical pressure"
elif [ "$PCT" -ge 75 ]; then warn "RAM: ${PCT}% (${USED}/${TOTAL}MB)"
else                         pass "RAM: ${PCT}% (${USED}/${TOTAL}MB, ${FREE_MEM}MB free)"
fi
if [ "$SWAP_USED" -gt 100 ] 2>/dev/null; then
    warn "Swap in use: ${SWAP_USED}MB — RAM may be too small"
else
    pass "Swap: ${SWAP_USED}MB in use"
fi

# ─────────────────────────────────────────────────────────
# 3. CPU LOAD
# ─────────────────────────────────────────────────────────
section "3. CPU LOAD"
LOAD=$(uptime | awk -F'load average:' '{print $2}' | awk -F',' '{gsub(/ /,"",$1); print $1}')
CORES=$(nproc)
LOAD_X10=$(echo "$LOAD * 10" | bc 2>/dev/null | cut -d. -f1)
THRESHOLD=$((CORES * 10))
info "Load: ${LOAD} on ${CORES} cores (1-min average)"
if   [ "${LOAD_X10:-0}" -ge $((THRESHOLD * 2)) ] 2>/dev/null; then fail "Load critically high: ${LOAD}"
elif [ "${LOAD_X10:-0}" -ge "$THRESHOLD" ] 2>/dev/null;        then warn "Load above core count: ${LOAD}"
else                                                                 pass "Load normal: ${LOAD}"
fi

# ─────────────────────────────────────────────────────────
# 4. SERVICES
# ─────────────────────────────────────────────────────────
section "4. SERVICES"
for svc in nginx mysql php8.2-fpm; do
    if systemctl is-active --quiet "$svc" 2>/dev/null; then
        pass "$svc is running"
    elif systemctl list-units --all 2>/dev/null | grep -q "${svc}.service"; then
        fail "$svc is STOPPED"
    fi
done

# ─────────────────────────────────────────────────────────
# 5. SSL CERTIFICATE
# ─────────────────────────────────────────────────────────
section "5. SSL CERTIFICATE"
EXPIRY=$(echo | timeout 5 openssl s_client -servername "${DOMAIN}" \
         -connect "${DOMAIN}:443" 2>/dev/null \
         | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -n "$EXPIRY" ]; then
    EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null \
                  || date -j -f "%b %d %H:%M:%S %Y %Z" "$EXPIRY" +%s 2>/dev/null)
    DAYS=$(( (EXPIRY_EPOCH - $(date +%s)) / 86400 ))
    if   [ "$DAYS" -le 7  ]; then fail "SSL expires in ${DAYS} days — RENEW NOW"
    elif [ "$DAYS" -le 30 ]; then warn "SSL expires in ${DAYS} days"
    else                          pass "SSL valid for ${DAYS} days (expires: $EXPIRY)"
    fi
else
    warn "Could not check SSL — verify ${DOMAIN} is reachable"
fi

# ─────────────────────────────────────────────────────────
# 6. BACKUPS
# ─────────────────────────────────────────────────────────
section "6. BACKUPS"
if [ -d "$BACKUP_DIR" ]; then
    LATEST=$(find "$BACKUP_DIR" \
             \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.sql" -o -name "*.sql.gz" \) \
             2>/dev/null | xargs ls -t 2>/dev/null | head -1)
    if [ -n "$LATEST" ]; then
        MOD=$(stat -c %Y "$LATEST" 2>/dev/null || stat -f %m "$LATEST" 2>/dev/null)
        AGE=$(( ($(date +%s) - MOD) / 3600 ))
        if   [ "$AGE" -gt 168 ]; then fail "Backup is ${AGE}h old (>7 days): $(basename "$LATEST")"
        elif [ "$AGE" -gt 48 ];  then warn "Backup is ${AGE}h old: $(basename "$LATEST")"
        else                          pass "Latest backup: $(basename "$LATEST") (${AGE}h ago)"
        fi
    else
        fail "No backup files found in ${BACKUP_DIR}"
    fi
else
    warn "Backup dir ${BACKUP_DIR} not found — set BACKUP_DIR variable"
fi

# ─────────────────────────────────────────────────────────
# 7. FAILED LOGIN ATTEMPTS
# ─────────────────────────────────────────────────────────
section "7. SECURITY — FAILED LOGINS"
if [ -f /var/log/auth.log ]; then
    TODAY=$(date +"%b %e" | sed 's/  / /')
    FAILS=$(grep "Failed password" /var/log/auth.log 2>/dev/null \
            | grep "$TODAY" | wc -l)
    if   [ "$FAILS" -gt "$CRIT_SSH_FAILS" ]; then fail "Brute force: ${FAILS} SSH failures today"
    elif [ "$FAILS" -gt "$WARN_SSH_FAILS" ]; then warn "${FAILS} SSH failures today"
    else                                           pass "SSH failures today: ${FAILS}"
    fi
    # Top attacking IPs:
    TOP_IP=$(grep "Failed password" /var/log/auth.log 2>/dev/null \
             | grep "$TODAY" | awk '{print $(NF-3)}' | sort | uniq -c \
             | sort -rn | head -3)
    [ -n "$TOP_IP" ] && info "Top IPs:\n$TOP_IP"
fi

# ─────────────────────────────────────────────────────────
# 8. SUSPICIOUS PROCESSES
# ─────────────────────────────────────────────────────────
section "8. SECURITY — PROCESSES"
SUSP=$(ps aux 2>/dev/null \
       | grep -E "base64|/tmp/[a-z0-9]{6,}\s|perl.*socket|python.*-c.*socket|xmrig|cryptonight|stratum\+" \
       | grep -v grep | grep -v "health-check")
if [ -n "$SUSP" ]; then
    fail "SUSPICIOUS PROCESSES:"
    echo "$SUSP" | while read -r ln; do echo "  ↳ $ln"; done
else
    pass "No suspicious process signatures found"
fi
DEL=$(lsof +L1 2>/dev/null | grep -vc "^COMMAND")
if [ "${DEL:-0}" -gt 0 ] 2>/dev/null; then
    warn "${DEL} deleted-but-still-running file(s) (possible rootkit)"
else
    pass "No deleted-but-running files"
fi

# ─────────────────────────────────────────────────────────
# 9. RECENTLY MODIFIED FILES
# ─────────────────────────────────────────────────────────
section "9. RECENTLY MODIFIED PHP FILES (24h)"
if [ -d "$WP_PATH" ]; then
    COUNT=$(find "$WP_PATH" -name "*.php" -mtime -1 2>/dev/null | wc -l)
    if   [ "$COUNT" -gt 20 ]; then
        fail "${COUNT} PHP files modified in 24h — possible webshell drop"
        find "$WP_PATH" -name "*.php" -mtime -1 2>/dev/null | head -5 \
            | while read -r f; do echo "  ↳ $f"; done
    elif [ "$COUNT" -gt 5 ]; then
        warn "${COUNT} PHP files modified in 24h:"
        find "$WP_PATH" -name "*.php" -mtime -1 2>/dev/null \
            | while read -r f; do echo "  ↳ $f"; done
    else
        pass "${COUNT} PHP file(s) modified in 24h (normal)"
        [ "$COUNT" -gt 0 ] && find "$WP_PATH" -name "*.php" -mtime -1 2>/dev/null \
            | while read -r f; do echo "  ↳ $f"; done
    fi
fi

# ─────────────────────────────────────────────────────────
# 10. OPEN PORTS
# ─────────────────────────────────────────────────────────
section "10. OPEN PORTS"
PORTS=$(ss -tnlp 2>/dev/null | grep LISTEN \
        | awk '{print $4}' | rev | cut -d: -f1 | rev | sort -n | uniq | tr '\n' ' ')
info "Listening: $PORTS"
UNEXPECTED=$(ss -tnlp 2>/dev/null | grep LISTEN \
             | awk '{print $4}' | grep -vE ":(22|80|443|3306|6379|8080|8443|9000)$")
[ -n "$UNEXPECTED" ] && warn "Non-standard ports: $UNEXPECTED"

# ─────────────────────────────────────────────────────────
# SUMMARY
# ─────────────────────────────────────────────────────────
section "SUMMARY"
info "Check complete in $SECONDS seconds"
info "Next run: $(date -d 'next sunday 09:00' '+%Y-%m-%d %H:%M' 2>/dev/null || echo 'next Sunday 09:00')"
echo ""
🛠 Setup & Scheduling
💾Save Script to Server
SSH in and save the script once. Then run with a single command from anywhere.
nano /opt/health-check.sh
# Paste the script above → Ctrl+X → Y → Enter

chmod +x /opt/health-check.sh

# Test run:
bash /opt/health-check.sh
⚙️Edit Config Variables
Change the four lines at the top of the script to match your server.
DOMAIN="yourdomain.com"      # your actual domain
WP_PATH="/var/www/html"     # path to WordPress root
BACKUP_DIR="/backups"       # where your backups live
WARN_DISK=80                # warn at 80% disk usage
🕘Schedule Weekly with Cron
Runs every Sunday at 9:00 AM. Output logged to file for review.
crontab -e

# Add this line:
0 9 * * 0 bash /opt/health-check.sh >> /var/log/health-check.log 2>&1

# View last run:
tail -80 /var/log/health-check.log
📧Email Report (Optional)
Install mailutils to get the health check emailed to you each week.
apt install mailutils -y

# Cron line for email delivery:
0 9 * * 0 bash /opt/health-check.sh | mail -s "[$(hostname)] Weekly Health Check" you@example.com
🔍 Individual Checks — Run Anytime
💾Disk Usage Check
See disk usage by partition. Find what is consuming the most space.
df -h | grep -v tmpfs
du -sh /var/log/* | sort -h | tail -10
du -sh /var/www/* | sort -h
⚠️ Fail threshold: 90%+. Common culprits: /var/log (Nginx logs), /var/www (large uploads), /tmp (PHP sessions).
🧠Memory Check
Current RAM and swap usage at a glance.
free -h
# Top memory-hungry processes:
ps aux --sort=-%mem | head -8
⚠️ If swap > 100MB: your VPS needs more RAM. Upgrade, or tune MySQL/PHP-FPM memory limits.
⚡CPU Load Check
Load average over 1/5/15 minutes. Should stay below your core count.
uptime
nproc  # your core count — load should stay below this

# See what is using CPU:
ps aux --sort=-%cpu | head -8
🔒SSL Expiry Check
Check how many days until your SSL certificate expires.
echo | openssl s_client -servername yourdomain.com \
  -connect yourdomain.com:443 2>/dev/null \
  | openssl x509 -noout -enddate

# Shows exact expiry date.
# Renew before 30 days: certbot renew --force-renewal
Let's Encrypt certs expire every 90 days. Auto-renew with certbot should handle it — but still verify.
🛡️Failed Login Check
How many failed SSH attempts today? Who is hammering your SSH port?
grep "Failed password" /var/log/auth.log \
  | grep "$(date +"%b %e")" | wc -l

# Top attacking IPs:
grep "Failed password" /var/log/auth.log \
  | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -5
Over 100 failures/day = active brute force. Install fail2ban or change your SSH port to something above 10000.
🔍Webshell Quick Scan
Scan PHP files for common webshell signatures: eval+base64, system($_POST), etc.
find /var/www/html -name "*.php" \
  | xargs grep -l "eval.*base64\|assert.*\$_\|system.*\$_POST\|passthru\|shell_exec" \
  2>/dev/null

# PHP files in uploads (should never exist):
find /var/www/html/wp-content/uploads -name "*.php" 2>/dev/null
Any result here means a webshell is present. Check the file immediately.
📅Recently Modified Files
PHP files modified in the last 24 hours. After a hack, modified count spikes.
find /var/www/html -name "*.php" -mtime -1 | wc -l
find /var/www/html -name "*.php" -mtime -1 | head -20
Normal after a plugin update: 5-15 files. More than 20 without a known update = investigate.
🌐Outbound Connections Check
What is your server connecting to right now? Malware often calls home.
ss -tnp | grep ESTABLISHED

# Resolve IPs to hostnames:
ss -tnp | grep ESTABLISHED | awk '{print $5}' \
  | cut -d: -f1 | sort -u | while read ip; do
    host "$ip" 2>/dev/null | head -1
done
Your server should only connect to: your CDN, email relay (port 587/25), WP update servers (api.wordpress.org), and known APIs.
🔧 When a Check Fails — Quick Fix Reference
💾FAIL: Disk is 90%+ Full
Truncate logs, remove large /tmp files, compress old backups.
truncate -s 0 /var/log/nginx/access.log
truncate -s 0 /var/log/nginx/error.log
find /tmp -type f -mtime +7 -delete
find /var/log -name "*.gz" -mtime +30 -delete
# Check WP uploads for huge files:
find /var/www/html/wp-content/uploads -size +50M | sort -k5 -h
🔒FAIL: SSL Expiry under 7 Days
Force-renew Let's Encrypt certificate immediately.
certbot renew --force-renewal
systemctl reload nginx

# Verify renewal worked:
certbot certificates
💀FAIL: Backup Over 7 Days Old
Run a manual backup now via WP-CLI or UpdraftPlus.
# WP-CLI backup (files + database):
wp db export /backups/manual-$(date +%Y%m%d).sql
tar czf /backups/wp-files-$(date +%Y%m%d).tar.gz \
    /var/www/html/wp-content/

# UpdraftPlus from WP admin:
wp eval 'UpdraftPlus_Backup_History::do_backup(0, false, false);'
🤖FAIL: Brute Force > 100/day
Install fail2ban to auto-block after 5 failures. Or change SSH port.
apt install fail2ban -y

# Basic config — /etc/fail2ban/jail.local:
cat > /etc/fail2ban/jail.local << 'EOF'
[sshd]
enabled = true
maxretry = 5
bantime = 86400
findtime = 600
EOF

systemctl restart fail2ban
fail2ban-client status sshd
🦠FAIL: Suspicious Process Found
Identify the process, kill it, find its source file, and delete it.
# Find the process:
ps aux | grep [SUSPICIOUS_KEYWORD]

# Check what file it is running from:
ls -la /proc/[PID]/exe
cat /proc/[PID]/cmdline | tr '\0' ' '

# Kill it:
kill -9 [PID]

# Find the dropper:
find /var/www /tmp /var/tmp -name "*.php" -mtime -7 \
  | xargs grep -l "eval.*base64" 2>/dev/null
🧠WARN: High Memory Usage
Tune MySQL and PHP-FPM memory limits to match your VPS size.
# MySQL — for 1GB RAM VPS:
# /etc/mysql/mysql.conf.d/mysqld.cnf
# innodb_buffer_pool_size = 128M
# max_connections = 50

# PHP-FPM max children:
# /etc/php/8.2/fpm/pool.d/www.conf
# pm.max_children = 10   (for 1GB VPS)

# Restart after editing:
systemctl restart mysql php8.2-fpm
📧 Send Report by Email
📬Email via SMTP (Postfix)
Full cron setup with email delivery every Sunday morning.
apt install postfix mailutils -y
# Choose "Internet Site" during setup

# Cron for weekly email report:
crontab -e
# Add:
0 9 * * 0 bash /opt/health-check.sh \
  | mail -s "[$(hostname)] Health Check $(date +%Y-%m-%d)" \
  you@example.com
🔔Alert Only on FAIL/WARN
Only send email if something fails — no email = everything is fine.
cat > /opt/health-check-alert.sh << 'SCRIPT'
#!/bin/bash
OUTPUT=$(bash /opt/health-check.sh)
if echo "$OUTPUT" | grep -qE "\[FAIL\]|\[WARN\]"; then
    echo "$OUTPUT" | mail -s "[ALERT] $(hostname) Health Issues" \
        you@example.com
fi
SCRIPT
chmod +x /opt/health-check-alert.sh

# Cron: 0 9 * * 0 bash /opt/health-check-alert.sh
Silent success is a feature, not a bug — you only get woken up when something actually needs attention.
VPS Hosting for Webmasters: Hostinger KVM VPS starts at $4.99/month — SSD storage, 1-click server snapshot backups, and built-in hPanel for easy server management.
Get VPS →
Cite or share this tool
Server Health Check Script — https://ordinarymantrying.com/tools/toolkit/toolkit-health-check.html
Last updated: August 2026