Automated Server Health Monitor

Build a Python script that monitors CPU, memory, and disk, then auto-alerts via email.

Introduction

30 Second Summary

When a server runs out of disk space or memory at 2 AM, you want to know about it before your users do. Real-world DevOps teams solve this with automated monitoring that watches system health around the clock and alerts them the moment something crosses a danger threshold.

In this project, you will build a Python script that automatically monitors your system's CPU, memory, and disk usage. It logs timestamped reports, emails you when something is wrong, and pushes all results to GitHub on a schedule.

What You'll Build

A fully automated server health monitoring pipeline running inside WSL Ubuntu that collects system metrics, writes timestamped logs, sends email alerts when thresholds are exceeded, and pushes everything to GitHub via cron.

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

  • A Python health check script using psutil that monitors CPU load, memory usage, disk usage, and running services.
  • An automated pipeline with a shell script and cron job that runs your monitor on a schedule and pushes logs to GitHub.
  • Gmail email alerts that fire automatically when disk usage crosses 80%.
  • Secret Mission: Add multi-metric alerting for CPU and memory alongside disk, plus a daily historical summary report.

Are there any prerequisites?

This project assumes you have some Python experience and a GitHub account. You also need a Gmail account with 2-Step Verification enabled so you can generate an App Password for the email alerts.

Before We Start

Before We Start...

Set Up WSL and Project Environment

Your health monitoring script needs a Linux environment to run cron jobs, shell scripts, and system-level commands. WSL (Windows Subsystem for Linux) gives you a full Ubuntu terminal inside Windows without needing a separate machine or virtual server.

In this step, you will install WSL with Ubuntu, set up Python and its dependencies, configure Git for automatic pushes to GitHub, and create the project files that store your credentials and configuration.

In this step, get ready to:
  • Install WSL with Ubuntu and verify Python 3 is available.
  • Install psutil and configure Git for passwordless GitHub pushes.
  • Create a GitHub repository and configure the project with credentials.
Install WSL with Ubuntu

WSL lets you run a full Linux distribution alongside Windows. The wsl --install command installs Ubuntu by default with WSL 2.

  • Press the Windows key to open your search bar.
  • Type PowerShell, then right-click Windows PowerShell and select Run as administrator.
  • Install WSL by running this command:
wsl --install

What does this command do?

This single command enables the WSL feature, downloads and installs the Ubuntu distribution, and configures WSL 2 as the default version. Your machine will need to restart after installation completes.

  • When the installation finishes, restart your computer.
  • After restarting, Ubuntu will launch automatically and ask you to create a Linux username and password. Enter a username and password you will remember.

Remember your Linux password

Your Linux password is separate from your Windows password. You will need it any time you run sudo commands. Write it down somewhere safe.

Now that Ubuntu is running, update the system packages to make sure everything is current.

  • Update and upgrade your system packages by running this command:
sudo apt update && sudo apt upgrade -y

What does this command do?

The apt update part refreshes the list of available packages. The apt upgrade -y part installs all available updates without asking for confirmation on each one.

  • Verify Python 3 is available by running:
python3 --version

✔️ I see a Python 3 version number

You should see something like Python 3.12.3 (the exact version depends on your Ubuntu release). Python is ready.

ⓧ Command not found

This is rare on Ubuntu but can happen. Install Python 3 by running:

sudo apt install python3 -y

Run python3 --version again to confirm it is installed.

WSL not installing?

Make sure you are running Windows 10 version 2004 or higher, or Windows 11. If wsl --install just shows help text instead of installing, WSL may already be partially installed. Try running wsl --install -d Ubuntu instead.

help me troubleshoot my WSL installation

Install pip, psutil, and configure Git

Your monitoring script uses psutil to read system metrics like CPU and memory usage. You also need Git configured so your automation can push logs to GitHub without manual login.

  • Install pip (Python's package manager) by running:
sudo apt install python3-pip -y
  • Install psutil by running:
pip3 install psutil
  • Verify psutil installed correctly by running:
python3 -c "import psutil; print(psutil.__version__)"

You should see a version number like 7.2.2 printed. This confirms psutil is ready.

Seeing a ModuleNotFoundError?

If Python cannot find psutil, try installing with python3 -m pip install psutil instead. This ensures pip installs to the correct Python version.

help me fix psutil installation in WSL Ubuntu

Next, configure Git with your identity so commits from the automation script are properly attributed.

  • Set your Git username by running:
git config --global user.name "[[GIT_USERNAME="Your Name"]]"
  • Set your Git email by running:
git config --global user.email "[[GIT_EMAIL="your-email@example.com"]]"

For the automation script to push logs without you typing a password every time, you need a GitHub Personal Access Token (PAT). This token acts as a password substitute for HTTPS Git operations.

  • Open your browser and go to github.com.
  • Click your profile picture in the upper-right corner, then click Settings.
  • In the left sidebar, click Developer settings.
  • Under Personal access tokens, click Tokens (classic).
  • Click Generate new token, then click Generate new token (classic).
  • In the Note field, enter server-health-monitor.
  • Set Expiration to 90 days (or your preferred duration).
  • Under scopes, check the repo checkbox. This grants full access to your repositories.
  • Click Generate token.
  • Copy the token immediately. You will not be able to see it again after leaving this page.

Keep your token safe

Save the token in a password manager or a secure note. You will use it as your password when Git prompts for credentials during your first push.

  • Back in your WSL terminal, configure Git to store credentials so you only enter the token once by running:
git config --global credential.helper store

What does credential.helper store do?

This tells Git to save your username and token in a plain text file after the first time you enter them. For a personal learning project in WSL, this is the simplest approach. The first time you push, Git will ask for your GitHub username and your PAT as the password.

Create the GitHub repository and project files

Your automation script will push log files to GitHub every time it runs. You need a repository to receive those pushes, plus configuration files for credentials and Git ignore rules.

  • Open your browser and go to github.com.
  • Click the + icon in the upper-right corner, then click New repository.
  • Enter server-health-monitor as the repository name.
  • Set visibility to Public.
  • Check Add a README file.
  • Click Create repository.

Now clone the repository into your WSL home directory.

  • In your WSL terminal, clone the repository by running:
cd ~
git clone https://github.com/[[GITHUB_USERNAME="your-github-username"]]/server-health-monitor.git
cd server-health-monitor

What happens during your first clone?

Git will prompt you for your GitHub username and password. Enter your GitHub username, and when asked for the password, paste the Personal Access Token you generated earlier. The credential.helper store setting saves these so you will not be prompted again.

Now you need a Gmail App Password for the email alerting feature. This is a 16-character token that replaces your regular Gmail password for programmatic access.

  • Open your browser and go to myaccount.google.com/apppasswords.
  • Sign in to your Google account if prompted.
  • In the App name field, enter server-health-monitor.
  • Click Create.
  • Copy the 16-character app password shown. Save it somewhere secure.

Why a Gmail App Password?

Gmail does not allow third-party apps to use your regular password. An App Password is a one-time generated token specifically for apps that need to send email on your behalf. It requires 2-Step Verification to be enabled on your Google account.

If you do not see the App Passwords page, make sure 2-Step Verification is enabled in your Google account security settings first.

Now create the project configuration files.

  • Create the .env file by running:
cat > .env << 'EOF'
EMAIL_ADDRESS=[[EMAIL_ADDR="your-email@gmail.com"]]
EMAIL_PASSWORD=your-app-password-here
ALERT_RECIPIENT=[[ALERT_ADDR="your-email@gmail.com"]]
DISK_THRESHOLD=80
EOF

What does this file do?

The .env file stores your configuration as environment variables. EMAIL_ADDRESS is the Gmail that sends alerts. EMAIL_PASSWORD is the 16-character App Password you just generated. ALERT_RECIPIENT is where alerts get delivered (can be the same email). DISK_THRESHOLD is the percentage that triggers an alert.

  • Replace your-app-password-here with the 16-character App Password you copied from Google.
  • Open the file in nano to make your edits by running:
nano .env
  • After editing, press Ctrl+O then Enter to save, and Ctrl+X to exit nano.
  • Create the .gitignore file to keep secrets out of your repository by running:
cat > .gitignore << 'EOF'
.env
__pycache__/
*.pyc
EOF

Why do you need a .gitignore?

The .gitignore file tells Git which files to never track or push. Your .env contains your email password, so it must never be committed. The __pycache__/ and *.pyc entries exclude Python's compiled bytecode files, which are auto-generated and unnecessary in a repository.

  • Create the logs/ directory where your health check script will write its reports by running:
mkdir logs
  • Verify your project structure by running:
ls -la

You should see .env, .gitignore, logs/, and README.md listed in the output.

✔️ Awesome, I've got everything!

Great. Your project directory is set up with all the configuration files you need.

ⓧ I'd like to double check the full code

Here is what your .env file should look like (with your actual values filled in):

EMAIL_ADDRESS=your-email@gmail.com
EMAIL_PASSWORD=your-app-password-here
ALERT_RECIPIENT=your-email@gmail.com
DISK_THRESHOLD=80

And your .gitignore:

.env
__pycache__/
*.pyc

Git clone asking for credentials but failing?

Make sure you are entering your GitHub username (not your email) when prompted for the username. For the password, paste the Personal Access Token, not your GitHub account password.

If you get a 403 error, check that the repo scope was selected when you created your token.

help me fix Git authentication with my GitHub personal access token in WSL

Your environment is fully set up. You have WSL with Ubuntu, Python and psutil installed, Git configured for automatic pushes, and your project directory ready with credentials and ignore rules. Next up, you will write the health check script that collects system metrics.

Write the Health Check Script

Your environment is set up and your GitHub repo is ready to go. Now it is time to write the script that actually monitors your system.

The health check script uses psutil to read live system metrics like CPU load, memory consumption, and disk usage. It logs everything with timestamps and fires off an email alert when disk usage crosses a threshold you control.

In this step, get ready to:
  • Create the health check script with functions that collect CPU, memory, disk, and service data.
  • Add timestamped logging that writes readable reports to a log file.
  • Add an email alert function that triggers when disk usage exceeds 80%.
Create the script and collect system metrics

The script needs to know where to send alerts, what thresholds to check, and where to write logs. All of these values come from environment variables so the script stays portable and your credentials stay out of the code.

  • In your WSL terminal, make sure you are in your project directory and create the script file by running:
cd ~/server-health-monitor
touch health_check.py
  • Open health_check.py in your text editor and add the imports, configuration, and all four metric collection functions:
import psutil
import smtplib
import os
from email.message import EmailMessage
from datetime import datetime

# Load configuration from environment variables
SMTP_SERVER = "smtp.gmail.com"
SMTP_PORT = 465
EMAIL_ADDRESS = os.environ.get("EMAIL_ADDRESS")
EMAIL_PASSWORD = os.environ.get("EMAIL_PASSWORD")
ALERT_RECIPIENT = os.environ.get("ALERT_RECIPIENT")
DISK_THRESHOLD = int(os.environ.get("DISK_THRESHOLD", "80"))

LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
LOG_FILE = os.path.join(LOG_DIR, "health_log.txt")


def get_cpu_usage():
    return psutil.cpu_percent(interval=1)


def get_memory_usage():
    memory = psutil.virtual_memory()
    return memory.percent


def get_disk_usage():
    disk = psutil.disk_usage("/")
    return disk.percent


def get_running_services():
    services = []
    for proc in psutil.process_iter(["pid", "name", "status"]):
        try:
            if proc.info["status"] == psutil.STATUS_RUNNING:
                services.append(proc.info["name"])
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            continue
    return list(set(services))[:20]

What does this code do?

The top section loads all settings from environment variables so credentials never live in the script itself. DISK_THRESHOLD defaults to 80 if the variable is missing.

The four functions each collect one piece of system data:

  • get_cpu_usage() blocks for 1 second to measure CPU load as a percentage.
  • get_memory_usage() returns how much RAM is in use as a percentage.
  • get_disk_usage() checks the root filesystem's usage percentage.
  • get_running_services() loops through all processes, filters for running ones, removes duplicates, and returns up to 20 names.
  • Save health_check.py.
  • You will not see any output yet because the script has no main() function to call these. The next substep adds the logging function that ties them together.

Seeing a syntax error when you save?

Check that every def line ends with a colon and the body below it is indented with 4 spaces. Also confirm that the from email.message import EmailMessage line uses a lowercase email (it is a standard library module, not a third-party package).

Help me fix a syntax error in my health_check.py imports or function definitions.

Add timestamped logging

Collecting metrics is useless without a record of them over time. The logging function writes a human-readable report to logs/health_log.txt every time the script runs.

  • In health_check.py, add the write_log function below the get_running_services function:
def write_log(cpu, memory, disk, services):
    os.makedirs(LOG_DIR, exist_ok=True)

    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    log_entry = (
        f"\n{'='*50}\n"
        f"Health Check - {timestamp}\n"
        f"{'='*50}\n"
        f"CPU Usage:    {cpu}%\n"
        f"Memory Usage: {memory}%\n"
        f"Disk Usage:   {disk}%\n"
        f"Running Services ({len(services)}): {', '.join(services[:10])}\n"
        f"{'='*50}\n"
    )

    with open(LOG_FILE, "a") as f:
        f.write(log_entry)

    print(log_entry)

What does this code do?

  • os.makedirs(LOG_DIR, exist_ok=True) creates the logs/ directory if it does not already exist.
  • The timestamp format %Y-%m-%d %H:%M:%S produces something like 2026-07-06 14:30:00 so you can track exactly when each check ran.
  • The log entry uses a clear, bordered format with separator lines so entries are easy to read when the file grows.
  • Opening the file with "a" (append mode) means new entries are added to the end without overwriting previous ones.
  • Save health_check.py.
  • The script still cannot run on its own yet. The next substep adds the alert function and the main() that orchestrates everything.

Worried about the f-string formatting?

The parentheses around the log_entry assignment let you split one long string across multiple lines. Make sure each f"..." line is inside those parentheses and ends with \n for a newline.

Help me fix the write_log function in my health_check.py.

Add the email alert function and run the script

The monitoring script needs to notify you when something is wrong. The alert function uses Gmail SMTP to send an email whenever disk usage crosses your configured threshold.

  • In health_check.py, add the send_alert function, the main() function, and the entry point below the write_log function:
def send_alert(subject, body):
    if not all([EMAIL_ADDRESS, EMAIL_PASSWORD, ALERT_RECIPIENT]):
        print("Email configuration incomplete. Skipping alert.")
        return

    msg = EmailMessage()
    msg["Subject"] = subject
    msg["From"] = EMAIL_ADDRESS
    msg["To"] = ALERT_RECIPIENT
    msg.set_content(body)

    try:
        with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as smtp:
            smtp.login(EMAIL_ADDRESS, EMAIL_PASSWORD)
            smtp.send_message(msg)
        print(f"Alert sent to {ALERT_RECIPIENT}")
    except Exception as e:
        print(f"Failed to send alert: {e}")


def main():
    cpu = get_cpu_usage()
    memory = get_memory_usage()
    disk = get_disk_usage()
    services = get_running_services()

    write_log(cpu, memory, disk, services)

    if disk > DISK_THRESHOLD:
        subject = f"ALERT: Disk Usage at {disk}% (Threshold: {DISK_THRESHOLD}%)"
        body = (
            f"Disk usage has exceeded the configured threshold.\n\n"
            f"Current Disk Usage: {disk}%\n"
            f"Threshold: {DISK_THRESHOLD}%\n"
            f"CPU Usage: {cpu}%\n"
            f"Memory Usage: {memory}%\n"
            f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
        )
        send_alert(subject, body)


if __name__ == "__main__":
    main()

What does this code do?

send_alert builds an email using Python's built-in EmailMessage class and sends it through Gmail's SMTP_SSL connection on port 465. It first checks that all three email settings exist, and gracefully skips if any are missing.

main() is the orchestrator. It collects all four metrics, writes the log, then checks if disk usage exceeds DISK_THRESHOLD. If it does, it constructs a detailed alert email and sends it.

The if __name__ == "__main__" block ensures main() only runs when you execute the script directly, not when another script imports it.

  • Save health_check.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved health_check.py and let's test it below.

ⓧ I'd like to double check the full code

import psutil
import smtplib
import os
from email.message import EmailMessage
from datetime import datetime

# Load configuration from environment variables
SMTP_SERVER = "smtp.gmail.com"
SMTP_PORT = 465
EMAIL_ADDRESS = os.environ.get("EMAIL_ADDRESS")
EMAIL_PASSWORD = os.environ.get("EMAIL_PASSWORD")
ALERT_RECIPIENT = os.environ.get("ALERT_RECIPIENT")
DISK_THRESHOLD = int(os.environ.get("DISK_THRESHOLD", "80"))

LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
LOG_FILE = os.path.join(LOG_DIR, "health_log.txt")


def get_cpu_usage():
    return psutil.cpu_percent(interval=1)


def get_memory_usage():
    memory = psutil.virtual_memory()
    return memory.percent


def get_disk_usage():
    disk = psutil.disk_usage("/")
    return disk.percent


def get_running_services():
    services = []
    for proc in psutil.process_iter(["pid", "name", "status"]):
        try:
            if proc.info["status"] == psutil.STATUS_RUNNING:
                services.append(proc.info["name"])
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            continue
    return list(set(services))[:20]


def send_alert(subject, body):
    if not all([EMAIL_ADDRESS, EMAIL_PASSWORD, ALERT_RECIPIENT]):
        print("Email configuration incomplete. Skipping alert.")
        return

    msg = EmailMessage()
    msg["Subject"] = subject
    msg["From"] = EMAIL_ADDRESS
    msg["To"] = ALERT_RECIPIENT
    msg.set_content(body)

    try:
        with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as smtp:
            smtp.login(EMAIL_ADDRESS, EMAIL_PASSWORD)
            smtp.send_message(msg)
        print(f"Alert sent to {ALERT_RECIPIENT}")
    except Exception as e:
        print(f"Failed to send alert: {e}")


def write_log(cpu, memory, disk, services):
    os.makedirs(LOG_DIR, exist_ok=True)

    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    log_entry = (
        f"\n{'='*50}\n"
        f"Health Check - {timestamp}\n"
        f"{'='*50}\n"
        f"CPU Usage:    {cpu}%\n"
        f"Memory Usage: {memory}%\n"
        f"Disk Usage:   {disk}%\n"
        f"Running Services ({len(services)}): {', '.join(services[:10])}\n"
        f"{'='*50}\n"
    )

    with open(LOG_FILE, "a") as f:
        f.write(log_entry)

    print(log_entry)


def main():
    cpu = get_cpu_usage()
    memory = get_memory_usage()
    disk = get_disk_usage()
    services = get_running_services()

    write_log(cpu, memory, disk, services)

    if disk > DISK_THRESHOLD:
        subject = f"ALERT: Disk Usage at {disk}% (Threshold: {DISK_THRESHOLD}%)"
        body = (
            f"Disk usage has exceeded the configured threshold.\n\n"
            f"Current Disk Usage: {disk}%\n"
            f"Threshold: {DISK_THRESHOLD}%\n"
            f"CPU Usage: {cpu}%\n"
            f"Memory Usage: {memory}%\n"
            f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
        )
        send_alert(subject, body)


if __name__ == "__main__":
    main()

Now let's test the script to confirm it collects metrics and writes to the log file.

  • Load your environment variables and run the script by executing these commands:
cd ~/server-health-monitor
set -a
source .env
set +a
python3 health_check.py

Why load the .env file first?

The script reads configuration from environment variables using os.environ.get(). The set -a and source .env pattern exports all values from your .env file into the current shell session so Python can access them.

  • You should see a formatted health report printed to your terminal showing CPU, memory, and disk percentages along with a list of running services.
  • Verify the log file was created by running:
cat logs/health_log.txt
  • You should see the same health report stored in the file with a timestamp header.

Not seeing any output?

  • If you see ModuleNotFoundError: No module named 'psutil', run pip3 install psutil again and retry.
  • If you see KeyError or TypeError related to DISK_THRESHOLD, make sure you ran source .env before running the script. The .env file must contain DISK_THRESHOLD=80 on its own line.
  • If the script runs but you see "Email configuration incomplete. Skipping alert." that is expected when disk usage is below 80%. The alert only fires when the threshold is exceeded.

Help me debug my health_check.py script that is not producing output.

Your health check script is working and logging real system data. Next up, you will wrap it in a shell script that automates the Git push so every health check is backed up to your GitHub repo.

Create the Automation Shell Script

Your health check script collects system metrics, logs them, and can fire off email alerts. But running it still requires you to manually load environment variables and push logs to GitHub each time.

In this step, you'll create a shell script that ties everything together into a single command. One execution loads your config, runs the health check, and pushes the updated logs to your repo automatically.

In this step, get ready to:
  • Write run_healthcheck.sh to orchestrate the full health check pipeline.
  • Test the script and verify logs appear in your GitHub repo.
Write the automation shell script

The shell script acts as the single entry point for your monitoring pipeline. It handles loading credentials, running the Python script, and pushing logs to Git so that a scheduler can trigger it all with one command.

  • In your WSL terminal, make sure you're in the server-health-monitor directory by running:
cd ~/server-health-monitor
  • Create a new file called run_healthcheck.sh by opening it in nano:
nano run_healthcheck.sh
  • Paste the following code into the editor:
#!/bin/bash

# Navigate to the project directory
cd "$(dirname "$0")"

# Load environment variables from .env file
set -a
source .env
set +a

# Run the health check script
python3 health_check.py

# Push logs to GitHub
git add logs/health_log.txt
git commit -m "Health check log update: $(date '+%Y-%m-%d %H:%M:%S')"
git push origin main

What does this script do?

  • cd "$(dirname "$0")" navigates to the folder where the script lives. This ensures the script finds .env and health_check.py no matter where it's called from.
  • set -a tells Bash to export every variable that gets defined. source .env loads your credentials into the shell session. set +a turns off auto-export so nothing else leaks.
  • python3 health_check.py runs your monitoring script, which now picks up the environment variables for email alerts.
  • The git add, git commit, and git push commands push the updated log file to your GitHub repo with a timestamped commit message.

Why load .env in Bash instead of Python?

Using set -a and source .env in Bash means the Python script can read credentials with the standard os.environ.get() call. This avoids installing an extra library like python-dotenv, keeping your dependencies minimal.

  • Save and exit nano by pressing **Ctrl+O**, then **Enter**, then **Ctrl+X**.
  • Confirm the file was created by running:
cat run_healthcheck.sh

You should see the full script printed in your terminal, matching the code you just pasted.

Seeing a blank output or file not found?

  • Make sure you pressed **Ctrl+O** (the letter O, not zero) to save before exiting nano.
  • Verify you're in the ~/server-health-monitor directory by running pwd.

Help me troubleshoot why my run_healthcheck.sh file is empty or missing

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

#!/bin/bash

# Navigate to the project directory
cd "$(dirname "$0")"

# Load environment variables from .env file
set -a
source .env
set +a

# Run the health check script
python3 health_check.py

# Push logs to GitHub
git add logs/health_log.txt
git commit -m "Health check log update: $(date '+%Y-%m-%d %H:%M:%S')"
git push origin main
Make the script executable and test it

Linux doesn't let you run a script file unless it has execute permission. You need to mark run_healthcheck.sh as executable before it can be triggered directly.

  • Grant execute permission to the script by running:
chmod +x run_healthcheck.sh

What does chmod +x do?

chmod +x adds execute permission to a file. Without it, the system treats the file as plain text and refuses to run it. You only need to do this once per script.

  • Run the script manually to test the full pipeline:
./run_healthcheck.sh

You should see the health check log entry printed in your terminal, followed by Git output showing a commit and successful push.

  • Open your GitHub repository in a browser.
  • Navigate to the logs folder.
  • Click on health_log.txt and verify the latest timestamped entry appears.

Push failed or permission denied?

  • If you see Permission denied, you may have forgotten to run chmod +x run_healthcheck.sh. Run it now and try again.
  • If git push fails with an authentication error, confirm your Git credential helper is configured. Run git config --global credential.helper store and try pushing again with your personal access token as the password.
  • If you see an error about .env not found, make sure you're running the script from inside ~/server-health-monitor.

Help me fix my run_healthcheck.sh script errors

Your monitoring pipeline now runs end-to-end with a single command. Next up, you'll schedule it with cron so it runs automatically on a timer without any manual intervention.

Schedule with Cron and Deploy

Your health check script runs perfectly, logs results, and pushes to GitHub. But right now it only runs when you manually execute it.

In this step, you'll automate the entire pipeline with cron, Linux's built-in task scheduler. Once configured, your monitor runs every hour without you lifting a finger. You'll also add a README so anyone can clone and use your project.

In this step, get ready to:
  • Start the cron service and configure it to auto-start in WSL.
  • Schedule the health check to run every hour with crontab.
  • Add a README so others can clone and deploy your project.
Start the cron service and configure auto-start

Cron is the standard Linux job scheduler that runs commands on a repeating schedule. In WSL, cron does not start automatically when you open your terminal. You need to start it manually and configure it to launch on boot.

  • Start the cron service by running this command:
sudo service cron start

You should see a message like Starting periodic command scheduler: cron confirming it started.

What does this command do?

The service command manages background processes (daemons) on Linux. Here you're telling it to start the cron daemon so it can watch for scheduled jobs.

Now you need to make cron start automatically every time WSL launches. Without this, cron stops when you close your terminal and your scheduled jobs won't run.

  • Create the WSL boot configuration file by running this command:
printf '[boot]\ncommand=service cron start\n' | sudo tee /etc/wsl.conf

What does this command do?

This writes a configuration file at /etc/wsl.conf that tells WSL to run service cron start as root every time the WSL instance boots. The [boot] section with the command key is the official WSL mechanism for running startup commands.

This feature requires Windows 11 or Windows Server 2022. If you're on Windows 10, you'll need to start cron manually each session with sudo service cron start.

  • For the boot configuration to take effect, restart your WSL instance. Open PowerShell on Windows and run:
wsl --shutdown
  • Reopen your WSL Ubuntu terminal.
  • Verify cron is running by running this command:
service cron status

You should see output indicating cron is running.

Cron not starting automatically?

If cron is not running after restarting WSL, your Windows version may not support the [boot] section in wsl.conf. This feature requires Windows 11. On Windows 10, run sudo service cron start each time you open WSL.

Help me configure cron to start automatically in my WSL environment.

Add a crontab entry to run the health check every hour

Now that cron is running, you need to tell it what to run and when. You do this by adding an entry to your crontab, which is your personal schedule file.

  • Find the full path to your shell script by running:
realpath ~/server-health-monitor/run_healthcheck.sh

Copy the output path. You will need it in the next command.

  • Open your crontab for editing by running:
crontab -e

Seeing an editor selection prompt?

If this is your first time running crontab -e, Ubuntu will ask you to select a text editor. Select option 1 for nano (marked as "easiest"). You will see a file open with some commented-out instructions at the top.

  • Scroll to the bottom of the file and add this line, replacing the path with the output you copied earlier:
0 * * * * [[SCRIPT_PATH="/home/your-username/server-health-monitor/run_healthcheck.sh"]]

What does this cron schedule mean?

A crontab line has five time fields followed by the command to run: minute hour day month weekday command.

The entry 0 * * * * means "at minute 0 of every hour, every day." Your health check runs once per hour, on the hour.

  • Save and exit the editor. In nano, press Ctrl+X, then Y, then Enter.
  • Verify your crontab was saved by running:
crontab -l

You should see your 0 * * * * entry listed in the output.

To confirm the full pipeline still works (rather than waiting an hour for cron to trigger it), run the script one more time using its full path.

  • Simulate what cron will do by running the script with its absolute path:
[[SCRIPT_PATH="/home/your-username/server-health-monitor/run_healthcheck.sh"]]
  • Check that a new entry was appended to the log by running:
tail -20 ~/server-health-monitor/logs/health_log.txt

You should see a new timestamped health check entry at the bottom of the output.

  • Verify the log was pushed to GitHub by visiting your repository in a browser and checking that logs/health_log.txt has been updated with the latest entry.

Script not running or push failing?

Make sure the script has execute permissions. Run ls -l ~/server-health-monitor/run_healthcheck.sh and confirm you see -rwxr-xr-x at the start. If not, run chmod +x ~/server-health-monitor/run_healthcheck.sh.

If the git push fails, confirm your Git credentials are cached. Try a manual git push from inside the project directory to see the specific error.

Help me debug why my health check script is not running or pushing to GitHub.

Add a README for project documentation

A good README turns a personal script into a shareable project. Anyone who clones your repository should be able to set it up and run it on their own machine without asking you questions.

  • Switch back to your project directory by running:
cd ~/server-health-monitor
  • Create and open the README file by running:
nano README.md
  • Paste the following content into the file:
# Server Health Monitor

An automated server health monitoring script that checks CPU, memory, disk usage, and running services. Logs results with timestamps, sends email alerts when disk usage exceeds a threshold, and pushes logs to GitHub automatically.

## Features

- Monitors CPU load, memory usage, disk usage, and running services
- Timestamped logging to track system health over time
- Email alerts via Gmail when disk usage exceeds configurable threshold
- Automatic log push to GitHub for version-controlled history
- Runs on a schedule via cron

## Requirements

- Linux (or WSL on Windows)
- Python 3.6+
- psutil library
- Git
- Gmail account with App Password

## Setup

1. Clone this repository:
   ```bash
   git clone https://github.com/your-username/server-health-monitor.git
   cd server-health-monitor
   ```

2. Install psutil:
   ```bash
   pip3 install psutil
   ```

3. Create a `.env` file with your settings:
   ```
   EMAIL_ADDRESS=your-email@gmail.com
   EMAIL_PASSWORD=your-16-char-app-password
   ALERT_RECIPIENT=recipient@gmail.com
   DISK_THRESHOLD=80
   ```

4. Make the shell script executable:
   ```bash
   chmod +x run_healthcheck.sh
   ```

5. Add to crontab for automatic execution:
   ```bash
   crontab -e
   # Add this line to run every hour:
   0 * * * * /path/to/run_healthcheck.sh
   ```

## Configuration

| Variable | Description | Default |
|----------|-------------|---------|
| EMAIL_ADDRESS | Gmail address for sending alerts | - |
| EMAIL_PASSWORD | Gmail App Password (16 characters) | - |
| ALERT_RECIPIENT | Email address to receive alerts | - |
| DISK_THRESHOLD | Disk usage percentage to trigger alert | 80 |

## Generating a Gmail App Password

1. Go to your Google Account > Security
2. Enable 2-Step Verification if not already enabled
3. Go to App Passwords (search "app password" in account settings)
4. Generate a new app password and copy the 16-character code
5. Use this code as your EMAIL_PASSWORD

## License

MIT

What does this README include?

Your README covers everything a new user needs: what the project does, how to install it, how to configure the environment variables, and how to generate a Gmail App Password.

The configuration table makes it easy to scan the available settings at a glance. The setup steps follow the same order you used to build the project.

  • Save the file. Press Ctrl+X, then Y, then Enter.
  • Commit and push the README to GitHub by running these commands:
git add README.md
git commit -m "Add project README with setup instructions"
git push origin main
  • Visit your GitHub repository in a browser. You should see the README rendered on the main page with all the sections, configuration table, and setup instructions displayed.

README not showing on GitHub?

Make sure the file is named exactly README.md (case-sensitive). GitHub only auto-renders files with this exact name. If the push failed, check your Git credentials and try running git push origin main again.

Help me get my README showing correctly on GitHub.

✔️ Awesome, I've got everything!

Great. Double check you have saved all files and that your GitHub repo shows both the README and the latest log entries.

ⓧ I'd like to double check the full code

Here is the complete README.md file:

# Server Health Monitor

An automated server health monitoring script that checks CPU, memory, disk usage, and running services. Logs results with timestamps, sends email alerts when disk usage exceeds a threshold, and pushes logs to GitHub automatically.

## Features

- Monitors CPU load, memory usage, disk usage, and running services
- Timestamped logging to track system health over time
- Email alerts via Gmail when disk usage exceeds configurable threshold
- Automatic log push to GitHub for version-controlled history
- Runs on a schedule via cron

## Requirements

- Linux (or WSL on Windows)
- Python 3.6+
- psutil library
- Git
- Gmail account with App Password

## Setup

1. Clone this repository:
   ```bash
   git clone https://github.com/your-username/server-health-monitor.git
   cd server-health-monitor
   ```

2. Install psutil:
   ```bash
   pip3 install psutil
   ```

3. Create a `.env` file with your settings:
   ```
   EMAIL_ADDRESS=your-email@gmail.com
   EMAIL_PASSWORD=your-16-char-app-password
   ALERT_RECIPIENT=recipient@gmail.com
   DISK_THRESHOLD=80
   ```

4. Make the shell script executable:
   ```bash
   chmod +x run_healthcheck.sh
   ```

5. Add to crontab for automatic execution:
   ```bash
   crontab -e
   # Add this line to run every hour:
   0 * * * * /path/to/run_healthcheck.sh
   ```

## Configuration

| Variable | Description | Default |
|----------|-------------|---------|
| EMAIL_ADDRESS | Gmail address for sending alerts | - |
| EMAIL_PASSWORD | Gmail App Password (16 characters) | - |
| ALERT_RECIPIENT | Email address to receive alerts | - |
| DISK_THRESHOLD | Disk usage percentage to trigger alert | 80 |

## Generating a Gmail App Password

1. Go to your Google Account > Security
2. Enable 2-Step Verification if not already enabled
3. Go to App Passwords (search "app password" in account settings)
4. Generate a new app password and copy the 16-character code
5. Use this code as your EMAIL_PASSWORD

## License

MIT

Your monitoring pipeline is fully deployed. Cron runs the health check every hour, logs are pushed to GitHub automatically, and your README makes it easy for anyone to clone and use your project. In the Secret Mission, you'll extend this with multi-metric alerting and daily summary reports.

Secret mission

Multi-Metric Alerting and Summary Reports

Your monitor currently only alerts on disk usage. Real production systems need alerts for CPU spikes and memory exhaustion too. In this secret mission, you'll extend the alerting system to cover all three metrics and build a daily summary report that tracks trends over time.

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 on WSL with free Gmail SMTP, so there are no ongoing cloud costs.

Resources you used:

  • Two cron jobs (hourly health check and daily report).
  • Project files in the server-health-monitor directory inside WSL.
  • psutil Python package.
  • GitHub repository (server-health-monitor).
  • Gmail App Password.
  • WSL Ubuntu distribution (if installed specifically for this project).

Keep everything running

No action needed. Choose this if you want your health monitor to keep running on schedule.

  • Your cron jobs will continue to run every hour (health check) and once daily (report).
  • Logs will accumulate in logs/health_log.txt and get pushed to GitHub automatically.
  • Alerts will fire whenever thresholds are exceeded.
  • Note: WSL must be running for cron to execute. If you restart your computer, WSL starts automatically (thanks to your wsl.conf boot command), but only on Windows 11.

Pause - I'll come back to this later

Stop the scheduled jobs to free up resources, but keep all your files and configuration intact for later.

  • Open your WSL Ubuntu terminal.
  • Remove your cron jobs temporarily by running:
crontab -r
  • Shut down the WSL instance from a Windows PowerShell or Command Prompt by running:
wsl --shutdown

When you are ready to resume, open WSL again and re-add your crontab entries with crontab -e.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below.

  • Open your WSL Ubuntu terminal and remove all cron jobs by running:
crontab -r
  • Delete the project directory by running:
rm -rf ~/server-health-monitor
  • Uninstall the psutil package by running:
pip3 uninstall psutil
  • Delete your GitHub repository:
  • Go to your server-health-monitor repository on GitHub.
  • Click the Settings tab.
  • Scroll down to the Danger Zone section.
  • Click Delete this repository and follow the confirmation prompts.
  • Revoke your Gmail App Password:
  • Go to myaccount.google.com/apppasswords.
  • Find the app password you created for this project.
  • Click the trash icon next to it to revoke access.
  • (Optional) If you installed WSL specifically for this project and no longer need it, unregister the Ubuntu distribution from a Windows PowerShell or Command Prompt by running:
wsl --unregister Ubuntu

This removes everything inside WSL

Running wsl --unregister Ubuntu permanently deletes all files, packages, and configurations stored in your Ubuntu distribution. Only do this if you are certain you do not use WSL for anything else.

Nice Work!

Nice Work!

Nice work! You just built a fully automated server health monitoring system that runs hands-free inside WSL Ubuntu.

Here's what you accomplished:

You've learned how to:

  • Build a Python monitoring script using psutil that collects CPU, memory, disk, and running service data with timestamped logging.
  • Set up Gmail SMTP email alerts that fire automatically when system metrics exceed configurable thresholds.
  • Automate an entire DevOps pipeline with cron and Git so health reports push to GitHub on a schedule without manual intervention.
  • Secret Mission: Added multi-metric alerting for CPU, memory, and disk plus a daily summary report with averages, peaks, and disk trend analysis.

Ready to quiz yourself?