Build a Server Health Monitor in Linux

Write a bash script that checks CPU, memory, disk, and processes on Linux.

Introduction

30 Second Summary

Every cloud server needs regular health checks. Without them, a full disk or memory leak can take your entire application offline before anyone notices.

In this project, you will build a bash script that monitors system health by checking CPU, memory, disk usage, and running processes. You'll work inside a real Linux environment using the same commands that engineers use to manage production servers.

What You'll Build

A health monitoring script that runs in your WSL Ubuntu environment and outputs a formatted report covering system uptime, CPU, memory, disk space, and top resource-consuming processes.

By the end of this project, you'll have:

  • A project directory with proper ownership and permissions.
  • A health-report.sh bash script that outputs a formatted system health check.
  • A generated report.txt output file demonstrating the script works.
  • Secret Mission: Add threshold-based alerts that warn when resources are running high.

Are there any prerequisites?

This project assumes you already have WSL2 with Ubuntu installed on Windows, and VS Code installed. If you don't have these yet, set them up before starting.

Set Up VS Code with WSL

To build a health monitoring script, you need a development environment where you can write code and run commands in a real Linux shell. VS Code connected to WSL gives you both a modern code editor and a full Ubuntu terminal in one window.

Your environment is already set up with VS Code connected to WSL Ubuntu. In this step, you'll confirm everything is in place and verify the key details of your Linux system before building anything.

In this step, get ready to:
  • Confirm the WSL extension is installed and VS Code is connected to Ubuntu.
  • Verify your Linux environment details including your OS, shell, and username.
Install the WSL extension in VS Code

The WSL extension bridges VS Code on Windows with your Linux environment running inside WSL. It lets you open folders, edit files, and run terminals directly in Ubuntu.

  • In VS Code, open the Extensions panel by pressing Ctrl+Shift+X.
  • Search for WSL to confirm the extension published by Microsoft is installed.
  • Verify the extension ID shows ms-vscode-remote.remote-wsl in the extension details.

What is the WSL extension?

Without the WSL extension, VS Code only works with Windows files and commands. The extension routes everything through your Linux environment instead, so the terminal, file system, and installed tools are all Linux-native.

Connect VS Code to your Ubuntu environment

VS Code should already be connected to your WSL Ubuntu instance. Let's confirm the connection is active and your terminal is running a Linux shell.

  • Check the bottom-left corner of VS Code. It should show WSL: Ubuntu.
  • Confirm your integrated terminal is open and shows a Linux bash prompt with your Ubuntu username.

Don't see WSL: Ubuntu?

Press F1 to open the Command Palette, then type WSL: Connect to WSL and select it. VS Code will reload and connect to your Ubuntu instance.

To open the integrated terminal, press Ctrl+`. The prompt should show your Ubuntu username.

Create the project directory and verify your environment

Your project directory ~/projects/health-monitor is already created and open in VS Code. The last thing to do is verify the key details of your Linux environment.

  • In your integrated terminal, run these commands to verify your environment:
cat /etc/os-release
echo $SHELL
whoami

What should I see?

  • cat /etc/os-release displays details about your Ubuntu installation, including the version name and ID.
  • echo $SHELL should output /bin/bash, confirming you're using the bash shell.
  • whoami prints your Linux username. This is the user that owns your project files.

Seeing something unexpected?

  • If cat /etc/os-release does not mention Ubuntu, you may be connected to a different WSL distro. Press F1, type WSL: Connect to WSL using Distro, and select Ubuntu.
  • If echo $SHELL shows something other than /bin/bash, the project will still work, but your prompt may look slightly different from the screenshots.

Help me verify my WSL Ubuntu environment in VS Code.

Your environment is verified and ready to go. Next up, you'll explore the Linux filesystem and discover how it organizes everything from configuration files to system logs.

Explore the Linux Filesystem

Your VS Code is connected to WSL and your project directory is ready. Now it's time to learn how to navigate the Linux filesystem.

Linux uses a single directory tree rooted at /, where everything from system configs to hardware info lives as a file. Understanding this structure is essential because every command in your health script reads from specific locations in this tree.

In this step, get ready to:
  • Navigate key Linux directories to understand the filesystem layout.
  • Create your project's file structure with proper directories.
  • Read system information from virtual files in /proc.
Navigate key directories

The Linux filesystem starts at a single root directory called /. Every file and folder on the system branches off from here.

  • Explore the top-level filesystem structure by running these commands one at a time:
ls /
ls /home
ls /etc
ls /var/log
ls /proc

What do these directories contain?

  • /home contains your user directory. Your files and projects live here.
  • /etc holds system configuration files. Think of it as the settings folder for the entire machine.
  • /var/log stores system log files. This is where you look when troubleshooting server issues.
  • /proc is a virtual filesystem that exposes kernel and process information as readable files.
  • Practice navigating between directories by running:
cd /var/log
pwd

The pwd command prints your current working directory so you always know where you are.

  • Return to your home directory by running:
cd ~
  • Navigate back to your project directory by running:
cd ~/projects/health-monitor

Shortcuts for getting around

The ~ symbol is a shortcut for your home directory (/home/your-username). You can combine it with any path to jump directly to a folder inside your home.

Work with files and directories

Your health monitoring project needs a place for scripts and for output files. You'll create this structure now using mkdir and touch.

  • Create the project structure by running these commands:
mkdir -p scripts output
touch scripts/health-report.sh
touch output/.gitkeep

What do these commands do?

  • mkdir -p scripts output creates two directories inside your project folder. The -p flag means "create parent directories if needed" and avoids errors if the folder already exists.
  • touch scripts/health-report.sh creates an empty file where your bash script will live.
  • touch output/.gitkeep creates a hidden placeholder file. The dot prefix makes it hidden from normal ls output. This is a common convention to keep an otherwise-empty folder tracked in version control.
  • Confirm the files and directories exist by running:
ls -la

You should see both scripts/ and output/ listed in the output.

  • Check inside the scripts directory by running:
ls -la scripts/

You should see health-report.sh listed with a size of 0 bytes.

How to read ls -la output

Each line shows seven columns: file type and permissions, hard link count, owner name, group name, file size, modification time, and filename.

The first character indicates the type. A d means directory, and a - means a regular file. The next nine characters are permissions, which you'll explore in a later step.

Don't see your files?

  • Make sure you are in the ~/projects/health-monitor directory. Run pwd to check your current location.
  • If you accidentally ran the commands in a different directory, navigate back with cd ~/projects/health-monitor and try the mkdir and touch commands again.

Help me figure out why my files aren't showing up.

Inspect system information files

One of the most powerful ideas in Linux is that system information is exposed as readable files. The /proc directory contains virtual files that let you read hardware and kernel data just like you would read a text file.

  • Read CPU information from the system by running:
cat /proc/cpuinfo | head -20

You should see details about your CPU model, cores, and cache size.

  • Read memory information by running:
cat /proc/meminfo | head -10

You should see MemTotal, MemFree, and other memory values reported in kilobytes.

  • Check system uptime by running:
cat /proc/uptime

This outputs two numbers. The first is the total seconds your system has been running. The second is the total idle time across all CPU cores.

What does 'everything is a file' mean?

In Linux, hardware info, running processes, and system stats are all represented as files you can read with standard commands like cat. There is no special API needed.

This is why Linux is so popular for servers and cloud instances. Monitoring tools just read from files in /proc and /sys. Your health report script will do exactly the same thing.

Your project structure is in place and you know your way around the filesystem. Next up, you'll run the actual system monitoring commands that will power your health report.

Inspect System Resources

In the previous step, you explored the Linux filesystem and discovered that system information lives in virtual files under /proc. Now it is time to use dedicated commands that read and summarize that information into something actionable.

On any Linux server (local or cloud), the first thing you do when something feels slow is check three things: CPU load, memory usage, and disk space. In this step, you will run the exact commands that Cloud and DevOps engineers reach for when diagnosing a struggling server.

In this step, get ready to:
  • Check CPU information and system load averages.
  • Inspect memory and disk usage.
  • Examine running processes sorted by resource consumption.
Check CPU and system load

The first question to ask any server is: how hard is it working right now? Linux provides commands that answer this instantly.

  • Check how long the system has been running and its current load by running this command:
uptime

You should see output showing the current time, how long the system has been up, the number of users, and three load average numbers.

What are load averages?

The three numbers at the end of the uptime output are load averages over 1 minute, 5 minutes, and 15 minutes. They represent how many processes are waiting to use the CPU.

On a cloud server, you want these numbers to stay below your CPU core count. If you have 4 cores and a load average of 6, your server is overloaded.

  • Get a snapshot of real-time CPU usage and the top processes by running:
top -bn1 | head -15

What do the flags mean?

The -b flag runs top in batch mode (non-interactive, suitable for scripting). The -n1 flag tells it to run one iteration and then exit. Without these flags, top would take over your terminal with a live-updating display.

  • Check how many CPU cores are available by running:
nproc

This prints a single number representing the processing units available. Compare this to your load averages from uptime to gauge whether the system is under pressure.

Check memory and disk

CPU pressure is one problem. Running out of memory or disk space is another. On cloud servers, these two are the most common cause of outages.

  • Check memory usage in a human-readable format by running:
free -h

You should see a table with columns for total, used, free, shared, buff/cache, and available memory.

Which column matters most?

The "available" column is what you should watch. It shows how much memory is actually usable, including memory that the kernel can reclaim from caches when needed. The "free" column is misleading because Linux intentionally uses free memory for caching.

  • Check disk space usage for all mounted filesystems by running:
df -h

You should see a table showing each filesystem's size, how much is used, how much is available, and the use percentage.

  • Check disk usage for the root filesystem specifically by running:
df -h /
  • Check how much space your projects folder is using by running:
du -sh ~/projects

What is the difference between df and du?

The df command reports disk space at the filesystem level (how full is the entire drive). The du command reports disk usage at the directory level (how much space does this specific folder consume).

Inspect running processes

When a server is slow, you need to find out which process is eating up resources. The ps command gives you a snapshot of everything running on the system.

  • View a snapshot of all running processes by running:
ps aux | head -20

You should see a table with many columns. This is a lot of information at once, so next you will filter for what matters most.

  • Find the top 10 processes consuming the most CPU by running:
ps -eo pid,user,%cpu,%mem,comm --sort=-%cpu | head -10
  • Find the top 10 processes consuming the most memory by running:
ps -eo pid,user,%cpu,%mem,comm --sort=-%mem | head -10

How do I read ps output?

The custom format you used shows five columns:

  • PID: the unique process ID (used to kill or inspect a specific process).
  • USER: which user owns the process.
  • %CPU: how much CPU the process is currently using.
  • %MEM: how much memory the process is consuming.
  • COMM: the command name (what program is running).

The --sort=-%cpu flag sorts descending by CPU usage so the hungriest processes appear first. Swapping to --sort=-%mem shows the memory hogs instead.

Same commands, any Linux server

The commands you just ran (uptime, free -h, df -h, ps) work identically on an EC2 instance, a Docker container, or a bare-metal server. You now have the same toolkit a DevOps engineer uses when SSH-ing into a production machine.

You now know how to check every vital sign on a Linux system. Next up, you will learn how to control who can access and run files using Linux permissions.

Manage File Permissions

You can now inspect CPU, memory, disk, and processes on a Linux system. The next step is controlling who can access and run your files.

Every file and directory in Linux has permissions that determine who can read, write, or execute it. On a cloud server, misconfigured permissions are one of the most common causes of "Permission denied" errors.

In this step, get ready to:
  • Read and interpret Linux file permission strings.
  • Set permissions on your script and output directory using chmod.
  • Change file ownership using chown.
Understand permission notation

Linux assigns a permission string to every file and directory. This string tells you exactly who can do what.

  • Inspect the current permissions on your script by running this command:
ls -la scripts/health-report.sh

You should see output similar to -rw-r--r-- followed by your username, the file size, and the filename.

How do I read a permission string?

The permission string has 10 characters. The first character shows the file type: - for a regular file, d for a directory.

The remaining 9 characters split into three groups of three. Characters 2-4 are the owner's permissions. Characters 5-7 are the group's permissions. Characters 8-10 are everyone else's permissions.

Each position is either a letter or a dash: r = read (value 4), w = write (value 2), x = execute (value 1). A dash means that permission is not granted.

What are common permission modes?

  • 755 = owner rwx (4+2+1=7), group r-x (4+0+1=5), others r-x (5). Used for scripts others can run but not edit.
  • 644 = owner rw (4+2=6), group r (4), others r (4). Used for regular files that don't need to be executed.
  • 700 = owner rwx (7), group nothing (0), others nothing (0). Private. Only you can access it.
Set permissions on your project files

Your health report script needs execute permission so you can run it directly. Your output folder should be private since reports could contain sensitive system information.

  • Make your script executable by running this command:
chmod 755 scripts/health-report.sh

What does this command do?

chmod changes file permissions. 755 gives the owner full control (read, write, execute) while letting group members and others read and execute the file but not modify it.

  • Verify the permissions changed by running:
ls -la scripts/health-report.sh

You should see -rwxr-xr-x at the beginning of the line. This confirms your script is now executable.

  • Secure your output directory so only you can access it by running:
chmod 700 output/
  • Verify the output directory permissions by running:
ls -la

The output directory should show drwx------ in its permission string. The d means it is a directory, and the dashes confirm no one else has any access.

Permissions didn't change?

Make sure you are running the commands from inside your ~/projects/health-monitor directory. Run pwd to check your current location.

If you see "No such file or directory", verify the file exists with ls scripts/ first.

Help me troubleshoot why my chmod command isn't working in WSL.

Now try the symbolic mode syntax, which uses letters instead of numbers.

  • Practice adding execute permission for the owner using symbolic mode:
chmod u+x scripts/health-report.sh

What does symbolic mode look like?

Symbolic mode uses targets and operators. Targets: u (user/owner), g (group), o (others), a (all). Operators: + (add), - (remove), = (set exactly).

So u+x means "add execute permission for the owner." Since you already set 755 earlier, the owner already has execute. This command has the same effect.

  • Remove write permission from group and others by running:
chmod go-w scripts/health-report.sh

This ensures no one else can modify your script. Octal mode and symbolic mode are two ways to achieve the same result.

Understand ownership

Permissions control what actions are allowed. Ownership controls who those permission groups apply to. Every file has an owner and a group.

  • Check your user identity by running:
id

This shows your user ID, group ID, and all group memberships. The owner and group columns in ls -la output correspond to these values.

  • Confirm your username by running:
whoami

Now practice changing ownership. The chown command changes who owns a file. You need sudo (superuser privileges) to change a file's owner to someone else.

  • Change the owner of your script to root by running:
sudo chown root:root scripts/health-report.sh
  • Verify the ownership change by running:
ls -la scripts/

You should see root root in the owner and group columns for health-report.sh.

  • Change ownership back to yourself by running:
sudo chown $(whoami):$(whoami) scripts/health-report.sh
  • Confirm it is back to your username by running:
ls -la scripts/

The owner and group columns should now show your username again.

Why does ownership matter on cloud servers?

On cloud servers, services like web servers and databases run as specific users. If a script is owned by root but the web server runs as www-data, the web server might not be able to execute it. Knowing chmod and chown saves hours of debugging "Permission denied" errors.

Getting a password prompt or permission error?

The sudo command may ask for your Linux password. This is the password you set when you first created your WSL Ubuntu user. Type it and press Enter. The characters will not appear on screen as you type.

Help me fix a sudo permission error in WSL.

Your project files are locked down with proper permissions. Next up, you will write the actual health monitoring script that brings all those system commands together into one automated report.

Write the Health Monitoring Script

You now know how to read permissions, set them with chmod, and change ownership with chown. Your script file is secured and ready for code.

In this step, you will write a Bash script that pulls together every monitoring command you practiced in earlier steps into one automated report. Instead of running uptime, free, df, and ps individually, the script runs them all and formats the output into a clean report.

In this step, get ready to:
  • Write the health monitoring script that outputs a formatted system report.
  • Run and test the script to confirm it produces correct output.
  • Save the report to a file and verify your complete project setup.
Write the report header and system stats

The script starts with a banner identifying which server generated the report and when. Then it collects basic system statistics using the same commands you ran manually in earlier steps.

  • Open scripts/health-report.sh in the VS Code editor by clicking it in the Explorer sidebar.
  • Copy the following code into the file:
#!/bin/bash
# Server Health Monitoring Script
# Generates a formatted health report for the local system

echo "========================================"
echo "  SERVER HEALTH REPORT"
echo "========================================"
echo "Hostname: $(hostname)"
echo "Date:     $(date)"
echo "========================================"

echo ""
echo "--- SYSTEM UPTIME ---"
uptime

echo ""
echo "--- CPU INFORMATION ---"
echo "Model: $(grep 'model name' /proc/cpuinfo | head -1 | awk -F': ' '{print $2}')"
echo "Cores: $(nproc)"

echo ""
echo "--- MEMORY USAGE ---"
free -h

echo ""
echo "--- DISK USAGE ---"
df -h / | awk 'NR==1 || /\/$/'

What does this code do?

The first line #!/bin/bash is called a shebang. It tells Linux which interpreter to use when running the script.

The $(command) syntax runs a command and inserts its output into the surrounding text. For example, $(hostname) is replaced with your machine's name.

The CPU model line uses grep to search /proc/cpuinfo for the model name, head -1 to take only the first match, and awk to extract the value after the colon. The disk line filters df output to show only the header row and the root filesystem.

Do not save the file yet. You will add the rest of the script next.

Add process monitoring and the report footer

The second half of the script lists the top resource-consuming processes. This is the most useful section for diagnosing performance problems on a live server.

  • Add the following code directly below the disk usage section (after the df -h / | awk 'NR==1 || /\/$/' line):
echo ""
echo "--- TOP 5 PROCESSES BY CPU ---"
printf "%-8s %-12s %-6s %-6s %s\n" "PID" "USER" "%CPU" "%MEM" "COMMAND"
ps -eo pid,user,%cpu,%mem,comm --sort=-%cpu | head -6 | tail -5 | awk '{printf "%-8s %-12s %-6s %-6s %s\n", $1, $2, $3, $4, $5}'

echo ""
echo "--- TOP 5 PROCESSES BY MEMORY ---"
printf "%-8s %-12s %-6s %-6s %s\n" "PID" "USER" "%CPU" "%MEM" "COMMAND"
ps -eo pid,user,%cpu,%mem,comm --sort=-%mem | head -6 | tail -5 | awk '{printf "%-8s %-12s %-6s %-6s %s\n", $1, $2, $3, $4, $5}'

echo ""
echo "========================================"
echo "  END OF REPORT"
echo "========================================"

What does this code do?

The printf line prints a formatted column header so the output is easy to read. The %-8s format means left-aligned string padded to 8 characters.

The ps -eo pid,user,%cpu,%mem,comm --sort=-%cpu command lists all processes with custom columns, sorted by CPU usage descending. Then head -6 | tail -5 skips the header line and keeps only the top 5 data rows.

  • Save scripts/health-report.sh by pressing Ctrl+S.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

Here is the complete scripts/health-report.sh file for comparison:

#!/bin/bash
# Server Health Monitoring Script
# Generates a formatted health report for the local system

echo "========================================"
echo "  SERVER HEALTH REPORT"
echo "========================================"
echo "Hostname: $(hostname)"
echo "Date:     $(date)"
echo "========================================"

echo ""
echo "--- SYSTEM UPTIME ---"
uptime

echo ""
echo "--- CPU INFORMATION ---"
echo "Model: $(grep 'model name' /proc/cpuinfo | head -1 | awk -F': ' '{print $2}')"
echo "Cores: $(nproc)"

echo ""
echo "--- MEMORY USAGE ---"
free -h

echo ""
echo "--- DISK USAGE ---"
df -h / | awk 'NR==1 || /\/$/' 

echo ""
echo "--- TOP 5 PROCESSES BY CPU ---"
printf "%-8s %-12s %-6s %-6s %s\n" "PID" "USER" "%CPU" "%MEM" "COMMAND"
ps -eo pid,user,%cpu,%mem,comm --sort=-%cpu | head -6 | tail -5 | awk '{printf "%-8s %-12s %-6s %-6s %s\n", $1, $2, $3, $4, $5}'

echo ""
echo "--- TOP 5 PROCESSES BY MEMORY ---"
printf "%-8s %-12s %-6s %-6s %s\n" "PID" "USER" "%CPU" "%MEM" "COMMAND"
ps -eo pid,user,%cpu,%mem,comm --sort=-%mem | head -6 | tail -5 | awk '{printf "%-8s %-12s %-6s %-6s %s\n", $1, $2, $3, $4, $5}'

echo ""
echo "========================================"
echo "  END OF REPORT"
echo "========================================"
Run and test the script

Your script is saved. Time to execute it and see the full health report in action.

  • Make sure you are in the project directory by running:
cd ~/projects/health-monitor
  • Run the script by executing:
./scripts/health-report.sh
  • You should see a formatted report in your terminal with clearly separated sections for uptime, CPU, memory, disk, and processes.

Seeing a permission denied or syntax error?

  • If you see Permission denied, confirm the file is executable by running ls -la scripts/health-report.sh. You should see -rwxr-xr-x. If not, run chmod 755 scripts/health-report.sh.
  • If you see a syntax error, check that all quotation marks are matched and that you have not accidentally deleted the shebang line at the top.

My health report script is failing when I run it

  • Save the output to a file by running:
./scripts/health-report.sh > output/report.txt

What does the > symbol do?

The > operator redirects the script's terminal output into a file instead of printing it on screen. If the file already exists, it gets overwritten. This is how you save reports for later review or send them to a monitoring dashboard.

  • View the saved report by running:
cat output/report.txt
  • You should see the same formatted report you saw in the terminal, now saved inside output/report.txt.
Verify the complete setup

Before wrapping up, confirm your entire project structure and permissions are correct.

  • Check your project structure by running:
find . -type f | sort
  • You should see these files listed:
./output/.gitkeep
./output/report.txt
./scripts/health-report.sh
  • Verify permissions across the project by running:
ls -laR
  • Confirm that scripts/health-report.sh shows -rwxr-xr-x and the output/ directory shows drwx------.
  • Run the script one final time to confirm clean output:
./scripts/health-report.sh
  • You should see all sections rendered cleanly: the header with your hostname and date, system uptime, CPU model and core count, memory table, disk usage, and the top 5 processes by CPU and memory.

Where would this go next?

On a real cloud server, you would schedule this script with cron to run automatically every hour or every day. The saved reports build a history of server health you can review when something goes wrong.

Secret mission

Add Threshold Alerts

Your health report shows raw numbers, but it does not tell you when something is wrong. Add conditional logic that checks whether disk and memory usage exceed warning thresholds. The script will print alerts before the main report so critical issues are impossible to miss.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally in WSL2, so there are no ongoing costs.

Resources you used:

  • Project directory ~/projects/health-monitor (scripts, output files).
  • VS Code WSL extension (ms-vscode-remote.remote-wsl).

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend your health monitoring script further.

  • Your project directory stays at ~/projects/health-monitor and is ready to use anytime.
  • The VS Code WSL extension remains installed so you can reconnect to WSL at any time.
  • You can re-run your script whenever you want with ./scripts/health-report.sh.

Pause - I'll come back to this later

Close VS Code to free up memory. Your project files and the WSL extension stay intact for next time.

  • Close VS Code.
  • Your project files remain in ~/projects/health-monitor inside WSL. They will be there when you reconnect.
  • To resume later, open VS Code and use F1 then select WSL: Connect to WSL to reconnect.

Delete - I don't want to use this again

Remove all project files and the VS Code extension to start completely fresh.

  • Delete the project directory by running this command in your WSL terminal:
rm -rf ~/projects/health-monitor
  • Confirm deletion by running:
ls ~/projects/health-monitor

You should see a "No such file or directory" message confirming the folder is gone.

  • To uninstall the VS Code WSL extension, open the Extensions panel (Ctrl+Shift+X), find WSL in your installed extensions list, and click Uninstall.

Nice Work!

Nice Work!

Nice work! You just built a server health monitoring script on a real Linux environment, using the same commands and techniques that Cloud and DevOps engineers rely on every day.

You've learned how to:

  • Navigate the Linux filesystem and inspect system information through virtual files in /proc, understanding the "everything is a file" philosophy.
  • Inspect system resources using uptime, free, df, ps, and top, and manage file permissions with chmod and chown.
  • Write a bash script that automates a full server health check covering CPU, memory, disk, and top processes.
  • Secret Mission: Add threshold-based alerting with bash conditionals that warn when resources exceed safe levels.

Ready to quiz yourself?