Manage a Cloud Server with Linux
Investigate logs, fix permissions, and write a bash health-check script.
Introduction
30 Second Summary
When cloud engineers start a new job, their first task is often "here's a server, figure out what's wrong." Knowing how to navigate, investigate, and fix a Linux system is the baseline skill every cloud role expects on Day 1.
In this project, you will simulate being a cloud engineer who just got access to a Linux server that needs attention. You'll investigate application logs, fix broken file permissions, manage services, and write an automated health-check script that reports on system status.
What You'll Build
You'll build a working server health-check bash script while practicing the core Linux commands cloud engineers use daily.
By the end of this project, you'll have:
- A project directory structure mimicking a real server environment with logs, scripts, and configs.
- Hands-on experience investigating logs using grep, awk, pipes, and redirection.
- A complete bash health-check script that monitors disk usage, memory, processes, and services.
- Secret Mission: extend your script with multi-service monitoring, warning aggregation, and journalctl error checks.
Are there any prerequisites?
This project assumes you have WSL 2 installed with Ubuntu on your Windows machine. You should also be familiar with basic commands like cd, ls, cat, and grep.
If you haven't installed WSL 2 yet, run wsl --install in PowerShell as Administrator. This installs Ubuntu by default.
Before We Start
Before We Start...
Verify Your Environment
Your first task as a cloud engineer on any new server is making sure everything is set up correctly. Before you start investigating logs or writing scripts, you need to confirm your tools are ready and your system is current.
In this step, you'll verify that WSL 2 is running, confirm systemd is active (required for service management later), update your packages, and create the project workspace.
In this step, get ready to:
- Confirm your WSL 2 instance is running and systemd is enabled.
- Update system packages to their latest versions.
- Create the project directory structure.
Confirm your WSL 2 instance and systemd
Every cloud server you manage runs Linux. WSL 2 gives you the same environment locally. Before doing anything else, you need to confirm it's running the right version and that systemd is active.
- Press the Windows key to open your search bar.
- Type Windows Terminal and press Enter to open it.
Windows Terminal opens with a PowerShell tab by default.
- Confirm you're running WSL 2 by running this command in the PowerShell tab:
wsl -l -v
You should see a table with your Ubuntu distribution listed with VERSION showing 2.
Seeing VERSION 1 or no output?
If your distribution shows VERSION 1, you need to convert it by running wsl --set-version Ubuntu 2 in PowerShell.
If you see no distributions listed, WSL is not installed. Refer to your WSL 2 setup from the prerequisites before continuing.
Help me fix my WSL version
- Launch your Ubuntu instance by running:
wsl
Your prompt changes to show your Linux username and hostname. You're now inside Ubuntu.
- Check that systemd is running as PID 1 by running:
ps -p 1 -o comm=
What does this command do?
This asks Linux to show the name of the process running as PID 1 (the very first process that starts when the system boots). On a properly configured system, this should be systemd.
systemd manages all services on the machine. You'll use it later to start, stop, and check services with systemctl.
✔️ I see systemd
Your system is ready. systemd is active and you can manage services with systemctl. Skip ahead to the next substep.
ⓧ I see init
systemd is not enabled yet. You need to turn it on by editing a configuration file.
- Open the WSL configuration file with:
sudo nano /etc/wsl.conf
Your terminal asks for your Linux password. Type it and press Enter (you won't see characters appear as you type).
- Add these two lines to the file:
[boot]
systemd=true
- Save the file by pressing Ctrl+O, then press Enter to confirm.
- Exit nano by pressing Ctrl+X.
- Type exit to leave your Ubuntu session and return to PowerShell.
- Shut down WSL completely by running:
wsl.exe --shutdown
What does this do?
This terminates all running WSL distributions and the WSL 2 lightweight utility VM. When you relaunch, it reboots with systemd as PID 1.
- Wait a few seconds, then relaunch Ubuntu by running:
wsl
- Verify systemd is now active by running:
ps -p 1 -o comm=
You should now see systemd in the output.
Still seeing init?
Make sure you saved /etc/wsl.conf correctly. Open it again with sudo nano /etc/wsl.conf and confirm [boot] is on its own line with systemd=true directly below it.
Help me enable systemd in WSL
Update your system packages
The first thing a cloud engineer does on any server is update packages. Outdated packages can have security vulnerabilities and missing features.
- Update your package index and upgrade all installed packages by running:
sudo apt update && sudo apt upgrade -y
What does this command do?
This is actually two commands joined by &&. The first (apt update) refreshes the list of available packages. The second (apt upgrade -y) installs newer versions of everything currently installed.
The -y flag auto-confirms the upgrade so you don't have to type 'yes' for each one.
This may take a minute depending on how many packages need updating. You'll see a stream of download and installation progress.
Seeing a lock error?
If you see a message about a lock file, another process is already using the package manager. Wait a minute and try again.
If sudo asks for your password, type your Linux user password and press Enter. No characters will appear as you type.
Help me fix my package update
Create your project workspace
Real servers organize files into specific directories. You'll create a workspace that mimics this structure, giving you separate folders for logs, scripts, and configuration files.
- Create your project directory and all subdirectories in one command by running:
mkdir -p ~/server-admin/{logs,scripts,configs}
What does this command do?
The -p flag tells mkdir to create parent directories as needed (so it creates server-admin and its children in one shot).
The curly-brace syntax {logs,scripts,configs} is bash expansion. It creates three subdirectories inside ~/server-admin/ without needing three separate commands.
- Verify the directory structure was created by running:
ls -la ~/server-admin/
You should see configs, logs, and scripts listed as directories.
Don't see the directories?
Make sure you typed the curly braces {} correctly with no spaces between the folder names and commas. Spaces inside the braces will create incorrectly named directories.
Help me create my project directories
Your environment is locked and loaded. Next up, you'll create a realistic application log and practice the investigation techniques cloud engineers use to track down problems.
Investigate Application Logs
Your environment is ready and your workspace is set up. Now it's time to do what cloud engineers do on day one: dig into the logs.
Application logs are the first place you look when something goes wrong on a server. In this step, you'll create a realistic log file and investigate it using grep, awk, and pipes to find exactly what's broken.
In this step, get ready to:
- Create a realistic application log that simulates entries from multiple services.
- Investigate the log using grep, awk, sort, uniq, and pipes to find errors.
- Practice real-time log monitoring with tail -f.
Create a sample application log
Before you can investigate a log, you need one to investigate. You'll use a heredoc to write a multi-line log file in one command.
What is a heredoc?
A heredoc lets you pass multiple lines of text to a command without creating a file first. The << 'EOF' syntax tells bash to keep reading lines until it hits the EOF marker. Wrapping the delimiter in quotes ('EOF') prevents any variable expansion inside the content.
- Create your sample application log by running this command in your terminal:
cat << 'EOF' > ~/server-admin/logs/app.log
2026-05-20 09:15:23 [INFO] nginx - Connection established from 192.168.1.45
2026-05-20 09:15:24 [INFO] app-server - Request processed successfully
2026-05-20 09:16:01 [WARNING] mysql - Slow query detected (2.3s)
2026-05-20 09:16:45 [ERROR] auth - Authentication failed for user admin
2026-05-20 09:17:02 [INFO] nginx - Connection established from 10.0.0.12
2026-05-20 09:17:33 [ERROR] app-server - Timeout occurred waiting for database
2026-05-20 09:18:15 [INFO] nginx - Request processed successfully
2026-05-20 09:18:44 [WARNING] disk-monitor - Disk space below 20% on /var
2026-05-20 09:19:01 [INFO] cron - Scheduled backup started
2026-05-20 09:19:30 [ERROR] auth - Authentication failed for user root
2026-05-20 09:20:12 [INFO] app-server - Connection established from 172.16.0.5
2026-05-20 09:20:45 [WARNING] mysql - Connection pool at 85% capacity
2026-05-20 09:21:03 [INFO] nginx - Request processed successfully
2026-05-20 09:21:30 [ERROR] app-server - OutOfMemoryError in worker thread 3
2026-05-20 09:22:00 [INFO] cron - Scheduled backup completed
2026-05-20 09:22:15 [ERROR] auth - Authentication failed for user deploy
2026-05-20 09:23:01 [INFO] nginx - Connection established from 192.168.1.100
2026-05-20 09:23:33 [WARNING] app-server - Response time above threshold (1.8s)
2026-05-20 09:24:00 [INFO] mysql - Connection pool recovered to 60%
2026-05-20 09:24:45 [ERROR] nginx - 502 Bad Gateway - upstream connection refused
EOF
What does this command do?
This creates a log file at ~/server-admin/logs/app.log with 20 entries that simulate a real server.
The log includes entries from 6 different services (nginx, mysql, app-server, auth, cron, disk-monitor) across 3 severity levels (INFO, WARNING, ERROR). This is exactly what you'd see when you SSH into a production server.
- Verify the file was created by running:
head -5 ~/server-admin/logs/app.log
You should see the first 5 log entries starting with the nginx connection line.
See "No such file or directory"?
Make sure you typed EOF on its own line with no spaces before or after it. The closing delimiter must appear alone on the line for the heredoc to work.
Also confirm the ~/server-admin/logs/ directory exists from Step 1. Run ls ~/server-admin/ to check.
help me fix my heredoc command
Search and filter with grep and pipes
Now comes the real work. You'll use the same investigation patterns cloud engineers use when troubleshooting a server at 2am.
- Count the total number of errors in the log by running:
grep -c "ERROR" ~/server-admin/logs/app.log
You should see 6 as the output. That tells you how many fires you need to investigate.
- Narrow your search to authentication failures by piping two grep commands together:
grep "auth" ~/server-admin/logs/app.log | grep "ERROR"
What is piping?
The | character (pipe) takes the output of one command and feeds it as input to the next. Here, the first grep finds all lines containing "auth", then the second grep filters those results down to only the ERROR lines.
You should see 3 authentication errors for users admin, root, and deploy.
- Find out which services are producing the most errors by running this pipeline:
grep "ERROR" ~/server-admin/logs/app.log | awk '{print $4}' | sort | uniq -c | sort -rn
What does this pipeline do?
This chains 5 commands together to produce a ranked error report:
- grep "ERROR" filters to error lines only.
- awk '{print $4}' extracts the 4th field (the service name like auth, app-server, nginx).
- sort groups identical names together.
- uniq -c counts how many times each name appears.
- sort -rn sorts the counts in reverse numerical order (highest first).
You should see auth with 3 errors at the top, followed by app-server with 2, then nginx with 1.
- Save a filtered error report to its own file using output redirection:
grep "ERROR" ~/server-admin/logs/app.log > ~/server-admin/logs/errors_only.log
What does the > symbol do?
The > operator redirects a command's output to a file instead of printing it on screen. If the file doesn't exist, it creates it. If it already exists, it overwrites it.
You'll see >> later in the project. That appends to the file instead of overwriting.
- Confirm the error report was created by running:
cat ~/server-admin/logs/errors_only.log
You should see all 6 ERROR lines from the original log, now isolated in their own file.
Not seeing 6 lines?
Check that you typed ERROR in all caps. grep is case-sensitive by default, so "error" won't match "ERROR" in the log.
help me debug my grep command
Monitor logs in real-time with tail -f
On a real server, logs don't sit still. New entries stream in constantly as your application handles requests. The tail -f command lets you watch this happen live.
- Start watching the log file in real-time by running:
tail -f ~/server-admin/logs/app.log
You'll see the last 10 lines of the file, and the command keeps running. It's waiting for new content to appear.
- Open a second terminal pane. In Windows Terminal, hold Alt and click the + button in the tab bar to split the pane.
- In the new pane, simulate a new log entry arriving by running:
echo '2026-05-20 09:25:00 [ERROR] app-server - Connection pool exhausted' >> ~/server-admin/logs/app.log
What is happening here?
The echo command writes a new log line, and >> appends it to the end of the file without overwriting existing content. Switch back to the first pane. You'll see the new entry appear instantly.
This is how you'd monitor a live web server, a database, or any service writing to a log file.
- Switch back to the pane running tail -f and confirm the new entry appeared.
- Press Ctrl+C to stop tail -f and return to your prompt.
Don't see the new entry in your tail output?
Make sure you used >> (append) in the echo command, not > (overwrite). Also verify both panes are in the same WSL instance by running hostname in each pane. They should show the same name.
help me get tail -f working
You've just used the same investigation toolkit that cloud engineers use every day. Next up, you'll learn to control who can access these files using Linux permissions.
Master File Permissions
In the previous step, you investigated application logs like a detective hunting for errors. Now you know what's going wrong on the server.
But knowing about problems isn't enough. On a real server, misconfigured file permissions are one of the most common causes of application failures and security breaches. In this step, you'll learn to control exactly who can read, write, and execute every file on the system.
In this step, get ready to:
- Create test files and read their permission strings.
- Set permissions with chmod using symbolic and octal notation.
- Transfer file ownership with chown.
Create test files and examine default permissions
Every file on a Linux system has a permission string that controls who can interact with it. Before you can fix permissions, you need to know how to read them.
- Navigate to your configs directory by running this command:
cd ~/server-admin/configs
- Create two test files by running these commands:
touch script.sh
touch secrets.conf
- Examine the default permissions on your new files by running:
ls -la
You should see output showing both files with a permission string like -rw-r--r-- next to each filename.
How do I read a permission string?
A permission string like -rw-r--r-- is split into four sections. The first character is the file type (- for file, d for directory).
The next nine characters are three groups of three: owner, group, and others. Each group can have r (read), w (write), and x (execute). A dash means that permission is denied.
So -rw-r--r-- means the owner can read and write, while group and others can only read.
Don't see your files?
Make sure you're in the right directory. Run pwd to confirm you're in ~/server-admin/configs. If not, run the cd command above again.
Help me debug my file permissions issue
Set permissions with chmod
The chmod command changes permissions on files. You can use two styles: symbolic notation (letters like +x) or octal notation (numbers like 755). Cloud engineers use both daily.
- Make your script executable using symbolic notation by running:
chmod +x script.sh
- Verify the permission change by running:
ls -la script.sh
You should see -rwxr-xr-x now. The x appears in all three groups because +x adds execute for everyone.
Why make scripts executable?
Linux won't run a script file unless it has the execute permission set. When you write a bash script (like the health-check script later in this project), you'll always need to chmod +x it before you can run it with ./script.sh.
- Now set the same permissions using octal notation by running:
chmod 755 script.sh
- Verify by running:
ls -la script.sh
You should see -rwxr-xr-x again. Both commands produce the same result.
How does octal notation work?
Each digit represents one group (owner, group, others). The number is a sum: read = 4, write = 2, execute = 1. So 7 means read+write+execute (4+2+1), and 5 means read+execute (4+1).
Common patterns you'll use constantly: 755 for scripts (owner full, others read+execute), 644 for regular files (owner read+write, others read only), and 600 for secrets (owner read+write only, nobody else gets anything).
Now let's lock down a file the way you would protect SSH keys or database credentials.
- Lock down secrets.conf so only the owner can read and write it by running:
chmod 600 secrets.conf
- Verify the restricted permissions by running:
ls -la secrets.conf
You should see -rw------- now. No permissions for group or others. This is the exact pattern SSH requires for private key files.
Why 600 for secrets?
SSH will refuse to use a private key if other users can read it. The chmod 600 pattern (owner read/write only) is a security requirement for SSH keys, database credentials, and any file containing secrets.
Permissions don't look right?
Double-check the number you typed. A common mistake is 660 (which gives the group read/write) instead of 600. Run chmod 600 secrets.conf again to fix it.
Help me understand why my file permissions look wrong
Transfer ownership with chown
Permissions control what actions are allowed. Ownership controls who those permissions apply to. The chown command changes which user and group own a file.
- Transfer ownership of secrets.conf to root by running:
sudo chown root:root ~/server-admin/configs/secrets.conf
- Verify the ownership change by running:
ls -la ~/server-admin/configs/secrets.conf
You should see root root in the owner and group columns instead of your username.
- Try to write to the file without sudo by running:
echo "test" >> ~/server-admin/configs/secrets.conf
You should see a "Permission denied" error. The file has 600 permissions (owner read/write only), and the owner is now root. You're no longer the owner, so you can't write to it.
Why does this matter on real servers?
On production servers, configuration files for services like nginx or MySQL are owned by root or a service account. Regular users can't accidentally (or maliciously) modify them. This is how Linux enforces the principle of least privilege.
- Reclaim ownership of the file by running:
sudo chown $USER:$USER ~/server-admin/configs/secrets.conf
- Verify you're the owner again by running:
ls -la ~/server-admin/configs/secrets.conf
You should see your username in both the owner and group columns.
- Confirm you can write to the file again by running:
echo "test" >> ~/server-admin/configs/secrets.conf
This time it succeeds with no error. You've proven that ownership and permissions work together to control file access.
Getting "Operation not permitted" on chown?
You need sudo before chown. Only root can change file ownership. Make sure your command starts with sudo chown.
Help me fix my chown command
Extra for Experts: What's the difference between sudo and being root?
The sudo command lets you run a single command with root privileges. You stay as your normal user between commands. Logging in as root directly (which is disabled on most cloud servers) gives permanent elevated access with no audit trail. Cloud providers enforce sudo-based access for security and accountability.
You now have the two core skills for securing files on any Linux server: setting permissions with chmod and managing ownership with chown. Next up, you'll learn to manage the processes and services that keep a server alive.
Manage Processes and Services
You've investigated logs and locked down file permissions. Now it's time to learn how to see what's actually happening on a running server.
When a server is slow or unresponsive, the first thing a cloud engineer does is check processes, memory, and disk usage. In this step, you'll use the exact commands that answer "what's eating my resources?" and learn to control services and processes directly.
In this step, get ready to:
- Inspect running processes and system resources with ps, df, free, and top.
- Manage services using systemctl to stop, start, and verify service status.
- Control background processes by launching, finding, and terminating them.
Inspect running processes and system resources
When a server feels slow, you need to quickly identify what's consuming CPU, memory, and disk. These are the diagnostic commands every cloud engineer runs first.
- List every running process on the system by running this command:
ps aux
You should see a table of every process running on your system. Each row shows the user, PID (process ID), CPU and memory usage, and the command that started it.
What does ps aux mean?
The ps command shows process status. The flags a (all users), u (user-oriented format), and x (include processes without a terminal) give you the complete picture of what's running.
- That's a lot of output. Filter it down to just the cron service by piping to grep:
ps aux | grep cron
You should see one or two lines. One is the actual cron process, and the other is your grep command itself (which contains the word "cron").
- Check your disk usage by running:
df -h
You should see a table showing each mounted filesystem, its total size, how much is used, and the percentage full. The -h flag makes sizes human-readable (GB, MB instead of raw bytes).
- Check your memory usage by running:
free -m
You should see total, used, and available memory in megabytes. The -m flag displays values in MB for easy reading.
- Open the interactive process monitor by running:
top
You should see a live, auto-updating view of processes sorted by CPU usage. This is the "task manager" of Linux.
- Press q to exit top and return to your normal terminal.
When do you use these commands?
These are the first commands you run when a server is "slow." df -h tells you if the disk is full. free -m tells you if memory is exhausted. top shows you which process is consuming everything.
Manage services with systemctl
Services are long-running background programs (web servers, databases, schedulers). On Linux, systemd manages them all, and systemctl is the command you use to control them.
- Check the current status of the cron service by running:
systemctl status cron
You should see output showing the service is active (running) in green text, along with how long it's been running and recent log entries.
What is cron?
Cron is the Linux task scheduler. It runs commands on a schedule (like "every night at 2am, back up the database"). It's a service that's almost always running on production servers, making it a safe target for practice.
- Stop the cron service by running:
sudo systemctl stop cron
- Verify that it actually stopped by running:
systemctl is-active cron
You should see the word inactive printed. This confirms the service is down.
- Restart the cron service by running:
sudo systemctl start cron
- Verify it's running again by running:
systemctl is-active cron
You should see active printed this time. You just stopped and restarted a service the same way you would on a production server.
- List all currently running services on your system by running:
systemctl list-units --type=service --state=running
You should see a table listing every active service. On a real server, this list helps you understand what's installed and running.
Seeing "Failed to connect to bus"?
This means systemd is not running as your init system. Go back to Step 1 and verify that ps -p 1 -o comm= outputs systemd. If it shows init, you need to enable systemd in /etc/wsl.conf and restart WSL.
Help me troubleshoot systemd not running in WSL 2
Control background processes
Sometimes a process gets stuck or you need to terminate something that's consuming too many resources. This is the workflow for finding and killing a specific process by its PID.
- Start a background process that simulates a long-running task by running:
sleep 300 &
You should see output like [1] 12345 where the number after the space is the PID (process ID). The & at the end sends the process to the background so you keep your terminal.
- Find the process and confirm its PID by running:
ps aux | grep sleep
You should see a line containing sleep 300 with its PID in the second column. Note that PID number.
- Terminate the sleep process using its PID by running:
kill [[PID="the PID number from the output above"]]
- Verify the process is gone by running:
ps aux | grep sleep
You should only see the grep command itself in the output. The sleep 300 process is gone.
Why is this important on real servers?
On production servers, runaway processes can consume all CPU or memory and bring down applications. Knowing how to find a process by name and terminate it by PID is a core troubleshooting skill you'll use regularly.
Still seeing the sleep process?
Make sure you used the correct PID from the ps aux | grep sleep output. The PID is the number in the second column. If kill didn't work, try kill -9 followed by the PID to force termination.
Help me find and kill a background process in Linux
You've now practiced the full lifecycle of process management. You can inspect what's running, check resource usage, control services, and terminate runaway processes. Next up, you'll bring all these skills together into an automated health-check script.
Write Your Health Check Script
You've investigated logs, locked down permissions, and managed processes and services. Those are all reactive skills. Now you'll build something proactive.
In this step, you'll write a bash script that automatically checks your system's health and logs a report. This is the kind of automation cloud engineers write on day one of a new server.
In this step, get ready to:
- Create a health-check script with configuration variables and a disk usage check.
- Add memory, CPU, and service monitoring sections to the script.
- Run the script and verify it generates a log report.
Create the script with configuration and disk check
Every production bash script starts with two things: a shebang line that tells the system which interpreter to use, and error-handling settings that stop the script if something goes wrong. After that, you define configuration variables so the script is easy to modify later.
- Create and open your script file by running:
nano ~/server-admin/scripts/health_check.sh
- Paste the following code into the editor:
#!/bin/bash
# health_check.sh - Server Health Check Script
# A cloud engineer's automated health monitoring tool
set -euo pipefail
# Configuration
LOG_DIR="$HOME/server-admin/logs"
LOG_FILE="$LOG_DIR/health_report.log"
DISK_THRESHOLD=80
# Ensure log directory exists
mkdir -p "$LOG_DIR"
# Timestamp
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
# Start report
echo "========================================" | tee -a "$LOG_FILE"
echo " Health Check Report" | tee -a "$LOG_FILE"
echo " $TIMESTAMP" | tee -a "$LOG_FILE"
echo "========================================" | tee -a "$LOG_FILE"
# 1. Disk Usage Check
echo "" | tee -a "$LOG_FILE"
echo "[Disk Usage]" | tee -a "$LOG_FILE"
DISK_USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
df -h / | tail -1 | awk '{print " Root: " $5 " used (" $3 " of " $2 ")"}' | tee -a "$LOG_FILE"
if [ "$DISK_USAGE" -gt "$DISK_THRESHOLD" ]; then
echo " WARNING: Disk usage exceeds ${DISK_THRESHOLD}%!" | tee -a "$LOG_FILE"
else
echo " Status: Healthy" | tee -a "$LOG_FILE"
fi
What does this code do?
- set -euo pipefail makes the script exit immediately if any command fails, if an undefined variable is used, or if a pipe fails. This is production best practice for error handling.
- LOG_DIR, LOG_FILE, and DISK_THRESHOLD are configuration variables. Putting them at the top means you can tweak the script without hunting through the logic.
- tee -a both displays output on screen AND appends it to the log file. The -a flag means "append" so previous reports aren't overwritten.
- The disk check uses df to get usage, awk to extract the percentage column, and sed to strip the % symbol so it can be compared as a number in the if conditional.
- Save the file without exiting by pressing Ctrl+O, then press Enter to confirm the filename.
Keep nano open. You'll add the remaining sections next.
Add memory, CPU, and service checks
The disk check tells you about storage. Now you'll add sections that cover the other two things you check when a server is slow: memory usage and CPU-hungry processes. You'll also add a service check that loops over critical services to confirm they're running.
- In nano, position your cursor below the fi line (the end of the disk check).
- Paste the following code below it:
# 2. Memory Usage Check
echo "" | tee -a "$LOG_FILE"
echo "[Memory Usage]" | tee -a "$LOG_FILE"
free -m | grep Mem | awk '{printf " Used: %sMB / %sMB (%.1f%%)\n", $3, $2, $3/$2*100}' | tee -a "$LOG_FILE"
# 3. Top CPU Processes
echo "" | tee -a "$LOG_FILE"
echo "[Top 5 Processes by CPU]" | tee -a "$LOG_FILE"
ps aux --sort=-%cpu | head -6 | tail -5 | awk '{printf " %-12s %5s%% CPU %s\n", $1, $3, $11}' | tee -a "$LOG_FILE"
# 4. Service Status Check
echo "" | tee -a "$LOG_FILE"
echo "[Service Status]" | tee -a "$LOG_FILE"
SERVICES=("cron")
for SERVICE in "${SERVICES[@]}"; do
if systemctl is-active --quiet "$SERVICE" 2>/dev/null; then
echo " [OK] $SERVICE is running" | tee -a "$LOG_FILE"
else
echo " [FAIL] $SERVICE is NOT running" | tee -a "$LOG_FILE"
fi
done
# Summary
echo "" | tee -a "$LOG_FILE"
echo "----------------------------------------" | tee -a "$LOG_FILE"
echo "Report saved to: $LOG_FILE" | tee -a "$LOG_FILE"
echo "========================================" | tee -a "$LOG_FILE"
What does this code do?
- free -m | grep Mem | awk gets memory stats in megabytes, filters to the main memory line, then formats it into a readable percentage.
- ps aux --sort=-%cpu | head -6 | tail -5 lists all processes sorted by CPU usage (highest first), takes the top 6 lines (header + 5 processes), then removes the header with tail -5.
- SERVICES=("cron") defines a bash array. The for loop iterates over each service name and uses systemctl is-active --quiet to silently check if the service is running (exit code 0 means active).
- 2>/dev/null after the systemctl command suppresses any error messages. This prevents noisy warnings for services that don't exist.
- Save and exit nano by pressing Ctrl+O, then Enter, then Ctrl+X.
Why tee instead of just echo?
Every echo in this script pipes through tee -a. This lets you see output in real time on your terminal AND write it to a log file simultaneously.
On a real server, you'd often run health checks via cron (scheduled) where nobody is watching the terminal. The log file is your record of what happened.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete health_check.sh file. Compare yours against this:
#!/bin/bash
# health_check.sh - Server Health Check Script
# A cloud engineer's automated health monitoring tool
set -euo pipefail
# Configuration
LOG_DIR="$HOME/server-admin/logs"
LOG_FILE="$LOG_DIR/health_report.log"
DISK_THRESHOLD=80
# Ensure log directory exists
mkdir -p "$LOG_DIR"
# Timestamp
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
# Start report
echo "========================================" | tee -a "$LOG_FILE"
echo " Health Check Report" | tee -a "$LOG_FILE"
echo " $TIMESTAMP" | tee -a "$LOG_FILE"
echo "========================================" | tee -a "$LOG_FILE"
# 1. Disk Usage Check
echo "" | tee -a "$LOG_FILE"
echo "[Disk Usage]" | tee -a "$LOG_FILE"
DISK_USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
df -h / | tail -1 | awk '{print " Root: " $5 " used (" $3 " of " $2 ")"}' | tee -a "$LOG_FILE"
if [ "$DISK_USAGE" -gt "$DISK_THRESHOLD" ]; then
echo " WARNING: Disk usage exceeds ${DISK_THRESHOLD}%!" | tee -a "$LOG_FILE"
else
echo " Status: Healthy" | tee -a "$LOG_FILE"
fi
# 2. Memory Usage Check
echo "" | tee -a "$LOG_FILE"
echo "[Memory Usage]" | tee -a "$LOG_FILE"
free -m | grep Mem | awk '{printf " Used: %sMB / %sMB (%.1f%%)\n", $3, $2, $3/$2*100}' | tee -a "$LOG_FILE"
# 3. Top CPU Processes
echo "" | tee -a "$LOG_FILE"
echo "[Top 5 Processes by CPU]" | tee -a "$LOG_FILE"
ps aux --sort=-%cpu | head -6 | tail -5 | awk '{printf " %-12s %5s%% CPU %s\n", $1, $3, $11}' | tee -a "$LOG_FILE"
# 4. Service Status Check
echo "" | tee -a "$LOG_FILE"
echo "[Service Status]" | tee -a "$LOG_FILE"
SERVICES=("cron")
for SERVICE in "${SERVICES[@]}"; do
if systemctl is-active --quiet "$SERVICE" 2>/dev/null; then
echo " [OK] $SERVICE is running" | tee -a "$LOG_FILE"
else
echo " [FAIL] $SERVICE is NOT running" | tee -a "$LOG_FILE"
fi
done
# Summary
echo "" | tee -a "$LOG_FILE"
echo "----------------------------------------" | tee -a "$LOG_FILE"
echo "Report saved to: $LOG_FILE" | tee -a "$LOG_FILE"
echo "========================================" | tee -a "$LOG_FILE"
Make the script executable and run it
A script file is just a text file until you give it execute permission. This is the same chmod skill you practiced in the permissions step.
- Make the script executable by running:
chmod 755 ~/server-admin/scripts/health_check.sh
- Move into the scripts directory and run the script:
cd ~/server-admin/scripts
./health_check.sh
You should see a formatted health report printed to your terminal with sections for Disk Usage, Memory Usage, Top 5 Processes by CPU, and Service Status showing cron as [OK].
Seeing a permission denied or syntax error?
- If you see "Permission denied", confirm you ran the chmod 755 command. Verify with ls -la ~/server-admin/scripts/health_check.sh and check for rwxr-xr-x in the output.
- If you see a syntax error, open the file with nano ~/server-admin/scripts/health_check.sh and check that the quotes, brackets, and semicolons match the code above exactly.
- If you see "unbound variable", make sure you didn't accidentally delete a variable definition like DISK_THRESHOLD or LOG_FILE from the Configuration section at the top.
Help me debug my health_check.sh script
- Verify the log file was created by running:
cat ~/server-admin/logs/health_report.log
You should see the same report that printed to your terminal, now saved as a persistent file.
- Run the script a second time to see the append behavior:
./health_check.sh
- Check the log file again by running:
cat ~/server-admin/logs/health_report.log
You should see two complete reports, one after the other. Each run appends a new report because the script uses tee -a (append mode). If you used > instead of >>, each run would overwrite the previous report.
Why append matters on real servers
On a production server, health checks typically run every few minutes via a scheduled job. Appending means you build a history of system health over time. If something breaks at 3am, you can scroll back through the log to see when things started going wrong.
Your health-check script is working and logging reports. You've combined every skill from this project into one automated tool: pipes, redirection, variables, conditionals, loops, and service management. Next up, the Secret Mission challenges you to extend this script with even more monitoring power.
Secret mission
Extend Your Health Check With Warning Tracking
Your health check script works, but it reports each section independently without tracking the overall system state. Can you extend it to count warnings across all checks, monitor multiple services, scan system errors with journalctl, and deliver a single pass/fail verdict?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally inside WSL 2, so there are no ongoing costs or cloud resources to worry about.
Decide whether to keep your project files for future reference, or delete them to free up disk space.
Resources you used:
- The ~/server-admin/ directory (contains logs, scripts, and configs subdirectories).
- Generated log files including app.log, errors_only.log, and health_report.log.
- The health_check.sh script you wrote.
Keep everything running
No action needed. Choose this if you want to keep practicing Linux commands or plan to extend the health-check script further.
- Your ~/server-admin/ directory stays in place with all your scripts and logs.
- You can re-run health_check.sh anytime to see updated system status.
- The project uses minimal disk space (a few kilobytes total).
Pause - I'll come back to this later
Since this project has no running processes or services, there is nothing to pause. Your files sit idle until you return.
- Your WSL instance can be shut down to free up memory. In PowerShell, run:
wsl.exe --shutdown
- When you're ready to come back, relaunch Ubuntu from Windows Terminal. Your ~/server-admin/ directory will be exactly as you left it.
Delete - I don't want to use this again
Remove all project files to start fresh. This deletes everything you created during the project.
- Delete the entire project directory by running this in your Ubuntu terminal:
rm -rf ~/server-admin
- Confirm the directory is gone by running:
ls ~/server-admin
- You should see a message saying the directory cannot be found. This confirms the cleanup is complete.
Nice Work!
Nice Work!
Congratulations! You just completed a cloud engineering simulation that mirrors real on-the-job tasks.
You built an automated server health-check script while practicing the Linux fundamentals that cloud engineers use every single day.
You learned how to:
- Investigated application logs using grep, awk, sort, uniq, and pipes to extract error patterns and generate filtered reports.
- Managed file permissions with chmod and chown, and controlled processes and services with ps, kill, and systemctl.
- Wrote a production-style Bash script with variables, conditionals, loops, redirection, and tee for automated server health monitoring.
- Extended your health-check script with journalctl, multi-service monitoring, and warning aggregation in the Secret Mission.
These are the exact skills tested in cloud engineering interviews and used on Day 1 of any cloud role. Every EC2 instance, GCP VM, or Azure server you will ever manage speaks the same language you just practiced.
Ready to quiz yourself?