🏥 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