How I Built a Server Health-Check & Slack Alerting Script Using Only Bash

How I Built a Server Health-Check & Slack Alerting Script Using Only Bash

In this tutorial, we will build a simple bash script that monitors server health and sends alerts to Slack via a webhook. The script checks four things: disk, memory, CPU, and a running service. If any of them crosses a threshold you set, the script sends a Slack message. Before you start You will need: A Working Laptop (this tutorial uses macOS & macOS commands) A terminal with bash A Slack account ⚠️ A note on macOS vs Linux. Some of the commands here are macOS-specific. For instance, vm_stat and top -l 1 do not exist on Linux. You would use free -m and top -bn1 instead. df -h and pgrep work on both. I will flag each one as we go. I will be using the vim text editor in this tutorial. If you prefer another editor like VS Code, nano, or Cursor, copy the final code from the public repo and follow along. Step 1: Create a Slack Webhook URL Go to api.slack.com/apps and click Create New App Choose Blank App, give the app a name (e.g. Server Health Bot), and pick the workspace you want alerts to land in (create a workspace if you haven't) In the sidebar, open Incoming Webhooks and toggle Activate Incoming Webhooks on Scroll down and click Add New Webhook to Workspace Choose the channel the alerts should post to, then click Allow Copy the webhook URL that appears. It will look like this: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX Enter fullscreen mode Exit fullscreen mode ⚠️ Treat this URL like a password. Anyone who has it can post to your channel. Step 2: Add the Webhook URL to .env In your terminal, run this command: echo "SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" > .env Enter fullscreen mode Exit fullscreen mode 💡 The > operator is called output redirection. It takes whatever echo prints and writes it to .env instead of your screen. If .env doesn't exist, it gets created. If it does exist, > overwrites it completely, use >> when you want to append instead. Next, protect .env from being accidentally committed to git by listing it in .gitignore: echo ".env" >> .gitignore Enter fullscreen mode Exit fullscreen mode 💡 Note the >> here. Your project may already have a .gitignore, and > would wipe it. >> appends a new line and still creates the file if it's missing. Step 3: Write the Bash Script We will build the script in stages, testing as we go: Create the file with a shebang Write the alert function, then test it Add the disk check Add the memory check Add the CPU check Add the service check Building it this way means that when something breaks, you know exactly which piece broke. 3.1 Create the script file Run: vim health_check.sh Enter fullscreen mode Exit fullscreen mode This opens the vim text editor with the filename health_check.sh. Press i to enter INSERT mode, then type the shebang at the top of the file: #!/bin/bash Enter fullscreen mode Exit fullscreen mode 💡 The shebang tells your system which interpreter should run the file. You have to place this at the very first line. If you place a blank line or comment above it, it stops working. Also note: bash scripts conventionally end in .sh, but the extension is optional, and the script runs fine without it. The shebang, on the other hand, is required if you want to execute the script directly with ./health_check.sh. You can skip it when you always run bash health_check.sh, but leaving it out is a habit that will bite you later. Once done, press esc, then type :wq to write and quit the editor. 3.2 Write the alert function Before we touch any monitoring logic, we will set up the alerting side first: a reusable function that sends a message to Slack from the terminal. Doing this first means every check we write afterwards has somewhere to report to, and we can confirm Slack works before adding anything that might obscure the problem. source {THE FULL PATH TO YOUR .env file} # e.g: source /Users/mac/MyProjects/linux_lessons/ServerHealthStatusAlert/.env send_alert(){ msg=$1; curl -X POST -H 'Content-type: application/json' --data "{\"text\": \"$msg\"}" $SLACK_WEBHOOK_URL } Enter fullscreen mode Exit fullscreen mode 💡 Watch your quotes in the curl payload. Single quotes in bash prevent variable expansion, so '{"text": "$MESSAGE"}' sends the literal string $MESSAGE to Slack rather than your message. You need double quotes around the parts that contain variables. Test it before moving on: send_alert "Bash Scripting is Cool!" Enter fullscreen mode Exit fullscreen mode If a message shows up in your Slack channel, the hard part is done. Your script should now look like this: code so far #!/bin/bash source /Users/mac/MyProjects/Devops_Mastery/linux_lessons/ServerHealthStatusAlert/.env send_alert(){ msg=$1; curl -X POST -H 'Content-type: application/json' --data "{\"text\": \"$msg\"}" $SLACK_WEBHOOK_URL } send_alert "Bash Scripting is Cool!" Enter fullscreen mode Exit fullscreen mode 3.3 Disk check To read disk usage, we will use df -h. The disk check Logic disk_usage=$(df -h | grep '/$' | awk '{ print $5 }' | sed "s/%//") disk_threshold=80 if [ "$disk_usage" -gt $disk_threshold ]; then send_alert "🚨 [ALERT] Disk usage high on $(hostname): $disk_usage% (threshold: $disk_threshold%) - $(date)" fi Enter fullscreen mode Exit fullscreen mode 3.4 Memory check To read memory usage on macOS, we will use vm_stat. ⚠️ macOS only. Linux exposes memory through /proc/meminfo (usually read via free -m). vm_stat reports memory in pages rather than megabytes, so we have to do a little arithmetic to get a usable percentage. mem_threshold=80 pages_free=$(vm_stat | grep "Pages free" | awk '{ print $3 }' | sed "s/.$//") pages_active=$(vm_stat | grep "Pages active" | awk '{ print $3 }' | sed "s/.$//") pages_inactive=$(vm_stat | grep "Pages inactive" | awk '{ print $3 }' | sed "s/.$//") pages_speculative=$(vm_stat | grep "Pages speculative" | awk '{ print $3 }' | sed "s/.$//") pages_wired=$(vm_stat | grep "Pages wired down" | awk '{ print $4 }' | sed "s/.$//") pages_compressed=$(vm_stat | grep "Pages occupied by compressor:" | awk '{ print $5 }' | sed "s/.$//") free_group=$(echo "$pages_free + $pages_speculative" | bc) used=$(echo "$pages_active + $pages_inactive + $pages_wired + $pages_compressed" | bc) total=$(echo "$free_group + $used" | bc) used_percent=$(echo "scale=2; $used / $total * 100" | bc) mem_alert=$(echo "$used_percent > $mem_threshold" | bc) if [ "$mem_alert" -eq 1 ]; then send_alert "🚨 [ALERT] Memory usage high on $(hostname): $used_percent% (threshold: $mem_threshold%) - $(date)" fi Enter fullscreen mode Exit fullscreen mode 💡 Bash can only compare integers. [ 78.4 -gt 80 ] will throw an error, not a wrong answer. That's why we pipe (using |) the calculation through bc, which handles decimals and returns 1 for true and 0 for false. 3.5 CPU check To read CPU usage, we will use top -l 1. ⚠️ macOS only. The -l 1 flag means "take one sample and exit". On Linux, the equivalent is top -bn1. top reports CPU idle percentage, not usage, so we get usage by subtracting from 100: usage = 100 - idle Enter fullscreen mode Exit fullscreen mode cpu_threshold=80 cpu_idle=$(top -l 1 | grep "CPU usage:" | awk '{ print $7 }' | sed 's/%//') cpu_usage=$(echo "100 - $cpu_idle" | bc) cpu_alert=$(echo "$cpu_usage > $cpu_threshold"| bc) if [ $cpu_alert -eq 1 ]; then send_alert "🚨 [ALERT] CPU usage high on $(hostname): $cpu_usage% (threshold: $cpu_threshold%) - $(date)" fi Enter fullscreen mode Exit fullscreen mode 3.6 Service check To check whether a service is running, we will use pgrep. service_name="sshd" if pgrep $service_name > /dev/null; then echo "running" else echo "not running" send_alert "🚨 [ALERT] Service $service_name is not running on $(hostname) - $(date)" fi Enter fullscreen mode Exit fullscreen mode ⚠️ A real limitation worth knowing. pgrep sshd will come back empty on macOS even when Remote Login is enabled. macOS uses launchd socket activation — sshd isn't kept running in the background, it's started on demand when a connection actually arrives. So there's no persistent process for pgrep to find. On a Linux server, pgrep sshd works exactly as you'd expect. To test the logic locally on a Mac, point it at a process that is always running, like Finder by setting service_name="finder". Escape (with esc) then save the bash file with :wq Step 4: Run the script bash health_check.sh Enter fullscreen mode Exit fullscreen mode You should see your checks run, and any that breach a threshold should show up in Slack. Step 5: Schedule it with cron Running the script by hand defeats the point. Let's have cron run it on a schedule. Open your crontab: crontab -e Enter fullscreen mode Exit fullscreen mode This opens a text editor where you can write your cron schedules. Press i to enter INSERT mode if the editor doesn't do it for you. Then add your schedule: 0 * * * * /absolute/path/to/health_check.sh Enter fullscreen mode Exit fullscreen mode This runs the script at the top of every hour. 💡 Always use absolute paths in cron. Cron runs with a minimal environment and doesn't start in your project directory, so a relative path like ./health_check.sh fails silently. This applies to every path in your script, including your log file. If you're unsure what a schedule does, paste it into crontab.guru — it translates cron expressions into plain English. ⚠️ MacOS Note: macOS sandboxing may block cron from reading files in protected directories. If your script runs fine by hand but does nothing on a schedule, grant Full Disk Access to cron under System Settings → Privacy & Security → Full Disk Access. The final script #!/bin/bash source /Users/mac/MyProjects/Devops_Mastery/linux_lessons/ServerHealthStatusAlert/.env send_alert(){ msg=$1; curl -X POST -H 'Content-type: application/json' --data "{\"text\": \"$msg\"}" $SLACK_WEBHOOK_URL } send_alert "Bash Scripting is Cool!" ## DISK CHECK LOGIC disk_usage=$(df -h | grep '/$' | awk '{ print $5 }' | sed "s/%//") disk_threshold=80 if [ "$disk_usage" -gt $disk_threshold ]; then send_alert "🚨 [ALERT] Disk usage high on $(hostname): $disk_usage% (threshold: $disk_threshold%) - $(date)" fi ## MEMORY CHECK LOGIC mem_threshold=80 pages_free=$(vm_stat | grep "Pages free" | awk '{ print $3 }' | sed "s/.$//") pages_active=$(vm_stat | grep "Pages active" | awk '{ print $3 }' | sed "s/.$//") pages_inactive=$(vm_stat | grep "Pages inactive" | awk '{ print $3 }' | sed "s/.$//") pages_speculative=$(vm_stat | grep "Pages speculative" | awk '{ print $3 }' | sed "s/.$//") pages_wired=$(vm_stat | grep "Pages wired down" | awk '{ print $4 }' | sed "s/.$//") pages_compressed=$(vm_stat | grep "Pages occupied by compressor:" | awk '{ print $5 }' | sed "s/.$//") free_group=$(echo "$pages_free + $pages_speculative" | bc) used=$(echo "$pages_active + $pages_inactive + $pages_wired + $pages_compressed" | bc) total=$(echo "$free_group + $used" | bc) used_percent=$(echo "scale=2; $used / $total * 100" | bc) mem_alert=$(echo "$used_percent > $mem_threshold" | bc) if [ "$mem_alert" -eq 1 ]; then send_alert "🚨 [ALERT] Memory usage high on $(hostname): $used_percent% (threshold: $mem_threshold%) - $(date)" fi ## CPU CHECK LOGIC cpu_threshold=80 cpu_idle=$(top -l 1 | grep "CPU usage:" | awk '{ print $7 }' | sed 's/%//') cpu_usage=$(echo "100 - $cpu_idle" | bc) cpu_alert=$(echo "$cpu_usage > $cpu_threshold"| bc) if [ $cpu_alert -eq 1 ]; then send_alert "🚨 [ALERT] CPU usage high on $(hostname): $cpu_usage% (threshold: $cpu_threshold%) - $(date)" fi ## SERVICE CHECK LOGIC service_name="sshd" if pgrep $service_name > /dev/null; then echo "running" else echo "not running" send_alert "🚨 [ALERT] Service $service_name is not running on $(hostname) - $(date)" Enter fullscreen mode Exit fullscreen mode You can also find the full code on github

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.