🏥 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 -5Over 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
doneYour 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.shSilent 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