Most server monitoring setups involve installing an agent, configuring a daemon, opening ports, and maintaining a separate process on every server you run.
That's a lot of moving parts for something that should be simple — check if the server is healthy, send the numbers somewhere useful, alert if something goes wrong.
Here's a lighter approach. Pure bash. No agents. No installs. Just curl and a cron job.
What this monitors
- CPU usage — actual percentage, sent as a numeric value
- Memory usage — actual percentage, sent as a numeric value
- Disk usage — used percentage and free GB, both sent as numeric values
- Process status — whether nginx, postgres, redis, or any process you care about is running Every metric is sent as an actual number, not a string. This matters for ML anomaly detection — NotiLens uses these values to learn your server's normal baseline automatically and alert when something drifts. No thresholds to configure.
How it works
The script runs every 5 minutes via cron. Each run:
- Collects CPU, memory, and disk metrics using standard Linux tools
- Sends all metrics in one payload to NotiLens via curl webhook
- Checks each process in your list
- Sends a healthy ping for running processes
- Sends an urgent, actionable alert for any process that's down — repeats every 5 minutes until acknowledged No dependencies beyond curl, which ships with every Linux server.
Setup
Step 1 — Get your credentials
- Create a free account at notilens.com
- Go to app.notilens.com → Custom Integration → create a topic
- Copy your Topic ID and Secret Key
Step 2 — Download the script
curl -o notilens-server-health-monitor.sh \
https://gist.githubusercontent.com/notilens/f6414a8d58f80bd50fb9e04fd59b4eec/raw/notilens-server-health-monitor.sh
chmod +x notilens-server-health-monitor.sh
Or copy the full script from the gist directly:
👉 gist.github.com/notilens/f6414a8d58f80bd50fb9e04fd59b4eec
Step 3 — Configure
Edit the config section at the top:
NOTILENS_TOPIC_ID="YOUR_TOPIC_ID" # from app.notilens.com → Custom Integration
NOTILENS_SECRET_KEY="YOUR_SECRET_KEY" # from app.notilens.com → Custom Integration
SERVER_NAME="prod-server-1" # shown in all NotiLens alerts
PROCESSES=("nginx" "postgres" "redis") # processes to check
Step 4 — Add to crontab
crontab -e
Add this line to run every 5 minutes:
*/5 * * * * /bin/bash /path/to/notilens-server-health-monitor.sh >> /var/log/notilens-health.log 2>&1
The script — section by section
Collect metrics
CPU=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1 | cut -d',' -f1)
MEM=$(free | grep Mem | awk '{printf "%.0f", $3/$2 * 100}')
DISK_USED=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
DISK_FREE_GB=$(df -BG / | tail -1 | awk '{print $4}' | tr -d 'G')
Standard Linux tools — top, free, df. No additional packages needed.
Send metrics payload
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "X-NOTILENS-KEY: $NOTILENS_SECRET_KEY" \
-d "{
\"title\": \"$SERVER_NAME — Health Check\",
\"message\": \"CPU: ${CPU}% | Memory: ${MEM}% | Disk: ${DISK_USED}% used (${DISK_FREE_GB}GB free)\",
\"type\": \"info\",
\"event\": \"server.health\",
\"tags\": \"$SERVER_NAME\",
\"meta\": {
\"server\": \"$SERVER_NAME\",
\"cpu_percent\": $CPU,
\"memory_percent\": $MEM,
\"disk_used_percent\": $DISK_USED,
\"disk_free_gb\": $DISK_FREE_GB
}
}"
The key detail — cpu_percent, memory_percent, disk_used_percent, and disk_free_gb are sent as actual numbers in the meta object, not strings. NotiLens ML reads these numeric values to build your server's baseline over time.
After a few days of runs, NotiLens knows what normal CPU looks like on a Tuesday afternoon vs a Sunday night. Anomaly detection fires automatically when values drift from that learned baseline — no manual threshold needed.
Process check — healthy
if pgrep -x "$PROCESS_NAME" > /dev/null; then
curl -s -X POST "$WEBHOOK_URL" \
-d "{
\"title\": \"$SERVER_NAME — $PROCESS_NAME\",
\"message\": \"$PROCESS_NAME is running\",
\"type\": \"info\",
\"event\": \"server.process.up\",
\"meta\": {
\"server\": \"$SERVER_NAME\",
\"process\": \"$PROCESS_NAME\",
\"status\": 1
}
}"
Sends a status ping for every healthy process. NotiLens tracks the frequency — if a process that normally pings every 5 minutes goes quiet, that's a silence alert.
Process check — down
else
curl -s -X POST "$WEBHOOK_URL" \
-d "{
\"title\": \"$SERVER_NAME — $PROCESS_NAME DOWN\",
\"message\": \"$PROCESS_NAME is not running on $SERVER_NAME\",
\"type\": \"urgent\",
\"event\": \"server.process.down\",
\"meta\": {
\"server\": \"$SERVER_NAME\",
\"process\": \"$PROCESS_NAME\",
\"status\": 0
},
\"is_actionable\": true,
\"force_send\": true
}"
fi
Two important flags here:
is_actionable: true — NotiLens resends this alert every 5 minutes until someone acknowledges it. A process down alert that fires once and gets missed is useless. This keeps firing until the on-call engineer ACKs it.
force_send: true — bypasses ML filtering and fires immediately. For a process down event you don't want the ML deciding whether this is anomalous enough to alert on. It fires regardless.
What you get in NotiLens
Every 5 minutes — a health check entry with CPU, memory, and disk values. NotiLens builds a baseline from these over time. When CPU spikes to 94% on a server that normally runs at 12% — that's the anomaly alert. When disk free drops below your historical normal — that's the drift alert.
Process down — immediate push notification to your phone. Repeats every 5 minutes until acknowledged. Escalation policy fires if nobody ACKs within your configured window.
No dashboard to watch. No thresholds to tune. No YAML.
Customisation
Monitor a different disk partition:
DISK_USED=$(df /data | tail -1 | awk '{print $5}' | tr -d '%')
DISK_FREE_GB=$(df -BG /data | tail -1 | awk '{print $4}' | tr -d 'G')
Add more processes:
PROCESSES=("nginx" "postgres" "redis" "your-app" "celery")
Change the check frequency:
Every minute instead of every 5:
* * * * * /bin/bash /path/to/notilens-server-health-monitor.sh >> /var/log/notilens-health.log 2>&1
Add network connectivity check:
if ping -c 1 8.8.8.8 &> /dev/null; then
NETWORK_STATUS=1
else
NETWORK_STATUS=0
fi
Add \"network_status\": $NETWORK_STATUS to the meta object.
Full script
Copy the complete boilerplate from the gist:
👉 gist.github.com/notilens/f6414a8d58f80bd50fb9e04fd59b4eec
Why pure bash
No agent to install means no agent to maintain, update, restart, or debug. No extra process running on your server consuming memory. No dependency on a language runtime. No version conflicts.
The script runs, sends the data, exits. Cron handles the scheduling. curl handles the delivery. That's the entire dependency tree.
Works on any Linux server. Tested on Ubuntu 20.04, 22.04, 24.04, Debian, and Amazon Linux.
notilens.com — 7-day free trial, no credit card required.
Top comments (0)