Build a Containerized Health Monitor

Write a Bash health monitor, containerize it with Docker, and track it with Git.

Introduction

30 Second Summary

Every server you have ever used is quietly juggling disk space, memory, and CPU load. When one of those resources maxes out, the server crashes and takes whatever it was running down with it. DevOps engineers prevent this by running monitoring tools that watch those numbers and shout the moment something looks wrong.

In this project, you will write a Bash script that monitors your system's health, containerize it with Docker, and track every change with Git. By the end, you will have a container that runs continuously, reports warnings when thresholds are breached, and proves its own liveness.

What You'll Build

You will run a Docker container and watch timestamped health reports stream to your terminal every 30 seconds, with clear WARNING lines appearing the moment disk or memory usage crosses a threshold you set.

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

  • A running container that prints timestamped health reports for disk, memory, and CPU every 30 seconds so you can watch your system's vitals in real time.
  • Threshold-based WARNING alerts that surface immediately when disk or memory usage is too high, so problems never hide in noise.
  • A Docker HEALTHCHECK that marks your container as "healthy" in docker ps, proving the monitor itself is alive and writing output.
  • Secret Mission: Add a JSON output mode that makes your logs machine-readable for monitoring pipelines like Prometheus or Datadog.

Are there any prerequisites?

This project assumes basic familiarity with terminal commands (cd, ls, cat). You should already have Git, Docker Desktop, and WSL with Ubuntu installed on your machine.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. This step is your chance to commit to the goal before any hands-on work begins.

Set Up the Project

Every DevOps project starts the same way: a clean directory with version control from the first commit. This ensures every change you make is tracked, reversible, and visible in your project history.

In this step, you will create your project folder, initialize a Git repository, and confirm that Docker is accessible from your WSL terminal.

In this step, get ready to:
  • Create a project directory with Git version control and an initial commit.
  • Confirm Docker Desktop is accessible from your Ubuntu terminal.
Create the project directory and initialize Git

You need a dedicated folder on your Desktop so you can find it easily later.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir health-monitor
cd health-monitor
  • Confirm you are in the right directory by running:
pwd

You should see a path ending in health-monitor.

  • Initialize a Git repository by running:
git init

You will see a message confirming that an empty Git repository was initialized.

  • Create your .gitignore file by running this command:
cat > .gitignore << 'EOF'
*.log
.DS_Store
Thumbs.db
EOF

What does this file do?

A .gitignore file tells Git which files to never track.

  • *.log ignores log files your monitor will generate.
  • .DS_Store and Thumbs.db ignore hidden OS files that add noise to your repo.
  • Verify the file was created correctly by running:
cat .gitignore

You should see the three ignore patterns you just wrote.

File looks empty or missing?

  • Make sure you typed EOF on its own line at the end of the command. The heredoc only closes when it finds a line containing exactly EOF.
  • If you see an error about the file not existing, re-run the cat > .gitignore << 'EOF' block carefully.

Help me create a .gitignore file using a heredoc in Bash

✔️ Awesome, I've got everything!

Great. Make sure the file is saved and move on to the commit below.

ⓧ I'd like to double check the full code

Your .gitignore should contain exactly this:

*.log
.DS_Store
Thumbs.db
  • Stage all files and make your initial commit by running:
git add .
git commit -m "Initial commit"

You will see output confirming one file was changed and created.

Before you run the next command, how many commits do you think your repository has right now?

  • Check your Git history by running:
git log --oneline

You will see a single line showing your initial commit hash followed by the message Initial commit.

Seeing 'Author identity unknown'?

Git needs your name and email for commits. Run the two commands below, replacing the placeholder values with your own details:

git config --global user.email "you@example.com"
git config --global user.name "Your Name"

Then re-run the git commit command.

Help me fix 'Author identity unknown' when running git commit

Verify Docker is accessible from WSL

Docker Desktop runs on Windows, but your project runs inside WSL. You need to confirm that the two can communicate.

  • Check that the Docker CLI is available by running:
docker --version

You should see a version number printed to the terminal.

Now for the real test. Do you think Docker can pull and run an image from within your WSL session?

  • Run a test container to confirm full Docker connectivity:
docker run --rm hello-world

You should see a message starting with Hello from Docker! confirming that Docker pulled the image and ran the container successfully.

Seeing 'Cannot connect to the Docker daemon'?

  • Make sure Docker Desktop is running on Windows. Look for the Docker whale icon in your system tray.
  • In Docker Desktop, go to Settings, then Resources, then WSL Integration. Confirm that your Ubuntu distro is enabled.
  • After enabling the integration, restart your WSL terminal and try again.

Help me connect Docker Desktop to my WSL Ubuntu terminal

Your project foundation is in place. You have a tracked repository and Docker ready to build containers. Next up, you will write your first health monitoring script and see real system data printed to your terminal.

Write a Naive Health Script

Your project directory is set up and Git is tracking changes. Now it is time to write actual code.

The fastest way to understand system monitoring is to write a script that reports real data from your machine right now. You will write a simple Bash script that reads disk usage, memory usage, and CPU load. This version is intentionally naive so you can see the gaps that production scripts need to fill.

In this step, get ready to:
  • Write a Bash script that reports disk, memory, and CPU stats.
  • Run the script and observe what a naive version is missing.
  • Commit this first version to Git.
Create health_monitor.sh with basic checks

Your monitoring script needs to answer three questions about the system: how full is the disk, how much memory is in use, and how busy is the CPU. Each answer comes from a different Linux tool.

  • Create health_monitor.sh by running this command in your terminal:
cat << 'EOF' > health_monitor.sh
#!/bin/bash

echo "=== Disk Usage ==="
df -h /

echo "=== Memory Usage ==="
free -h

echo "=== CPU Load ==="
cat /proc/loadavg
EOF

What does this command do?

The cat << 'EOF' > health_monitor.sh syntax is called a heredoc. It writes everything between the two EOF markers into a new file called health_monitor.sh.

  • df -h / prints disk space usage for the root filesystem in human-readable format.
  • free -h prints memory usage in human-readable format.
  • cat /proc/loadavg reads the CPU load averages from the /proc filesystem, a special Linux interface that exposes kernel data as files.
  • Confirm the file was created by running:
cat health_monitor.sh

You should see the full script contents printed to your terminal, starting with #!/bin/bash and ending with cat /proc/loadavg.

Don't see the file contents?

  • Make sure you pasted the entire command, including both EOF lines. The closing EOF must be on its own line with no spaces before it.
  • Confirm you are inside the health-monitor directory by running pwd. You should see a path ending in health-monitor.

Help me create a file using a heredoc in Bash

Make it executable and run it

Before Linux will let you run a script directly, you need to mark it as executable. Then you can see what your monitoring script actually reports.

  • Make the script executable by running:
chmod +x health_monitor.sh

Before you run the script, think about what you expect to see. What information will each command print?

  • Run the script by executing:
bash health_monitor.sh

You should see three blocks of output. Disk usage shows a table with your filesystem size and percentage used. Memory usage shows total, used, and available RAM. CPU load shows three numbers representing load averages over 1, 5, and 15 minutes.

What is missing from this output?

Look at the output and notice what a production monitoring script would need:

  • No timestamps. You cannot tell when this check ran.
  • No threshold warnings. The script shows numbers but never tells you if something is wrong.
  • No error handling. If a command fails, the script continues silently without alerting you.

These gaps are exactly what you will fix in the next step.

Seeing 'command not found' for free?

The free command requires the procps package. On most Ubuntu installations it is already present. If missing, install it by running sudo apt-get install -y procps.

Help me fix 'command not found' when running my health monitor script

  • Commit this naive version to Git by running:
git add health_monitor.sh
git commit -m "Add naive health monitoring script"

Why commit a script that has gaps?

Committing the naive version captures a snapshot of your starting point. When you improve the script later, Git will show exactly what changed between the simple version and the production version. This is how professional developers document their thought process.

  • Verify your Git history now has two commits by running:
git log --oneline

You should see two commits listed. The most recent one is your naive health script.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file and both commits appear in git log --oneline.

ⓧ I'd like to double check the full code

Your health_monitor.sh should look exactly like this:

#!/bin/bash

echo "=== Disk Usage ==="
df -h /

echo "=== Memory Usage ==="
free -h

echo "=== CPU Load ==="
cat /proc/loadavg

You have a working script that reads real system data. The problem is clear: raw numbers with no context are not useful for monitoring. Next up, you will transform this naive script into a production-quality tool with structured logging and threshold-based alerting.

Add Production Bash Patterns

In the previous step, you ran your health script and saw raw disk, memory, and CPU data dumped to the terminal.

The output works, but it has real problems. There are no timestamps, no error handling, and no way to tell if something is actually wrong. Real DevOps scripts need strict mode so failures surface immediately, structured logging with timestamps, and threshold-based alerting so problems stand out instead of hiding in noise.

In this step, get ready to:
  • Add strict mode and a structured logging function.
  • Add threshold-based warnings for disk and memory usage.
  • Commit the production-quality script to Git.
Add strict mode and structured logging

Strict mode makes your script fail loudly on errors instead of silently continuing. A logging function gives every line a timestamp and severity level, so you can tell exactly when something happened and how serious it is.

  • Open health_monitor.sh in your editor and replace all of its content with the following:
#!/bin/bash
set -euo pipefail

LOG_FILE="${LOG_FILE:-/var/log/health_monitor.log}"

log() {
    local level="$1"
    local message="$2"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" | tee -a "$LOG_FILE"
}

What does this code do?

  • set -euo pipefail enables strict mode. -e exits on any error, -u treats unset variables as errors, and -o pipefail fails the whole pipeline if any command in it fails.
  • LOG_FILE uses a default value (/var/log/health_monitor.log) but can be overridden via an environment variable.
  • The log() function takes a level and message, then prints a timestamped line like [2026-07-19 10:30:00] [INFO] message. It also appends to the log file using tee.
  • Now add the check functions and a main() function below the log() function:
check_disk() {
    local usage
    usage=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
    log "INFO" "Disk usage: ${usage}%"
}

check_memory() {
    local usage
    usage=$(free | awk '/Mem:/ {printf "%.0f", $3/$2 * 100}')
    log "INFO" "Memory usage: ${usage}%"
}

check_cpu() {
    local load_avg
    load_avg=$(awk '{print $1}' /proc/loadavg)
    log "INFO" "CPU load average (1 min): ${load_avg}"
}

main() {
    log "INFO" "===== Health Check Started ====="
    check_disk
    check_memory
    check_cpu
    log "INFO" "===== Health Check Complete ====="
}

main

What does this code do?

  • check_disk() parses the usage percentage from df output using awk and strips the % sign.
  • check_memory() calculates memory usage as a percentage from free output.
  • check_cpu() reads the 1-minute load average from /proc/loadavg.
  • main() calls all three checks between start and complete log lines, keeping the execution flow clear.
  • Save health_monitor.sh.
  • Run the script by executing this command:
LOG_FILE=/tmp/health_monitor.log bash health_monitor.sh

You should see timestamped log lines with [INFO] level tags printed to your terminal, along with actual disk, memory, and CPU values from your system.

Why set LOG_FILE here?

The default log path /var/log/health_monitor.log requires root permissions to write. Overriding LOG_FILE on the command line lets you test locally without sudo. Later, the Docker container will create this file with the right permissions.

Seeing 'permission denied' or script exiting immediately?

  • Make sure you included LOG_FILE=/tmp/health_monitor.log before bash health_monitor.sh in the command. Without it, the script tries to write to /var/log/ which your user cannot access.
  • If you see a syntax error, double-check that your log() function has the closing brace } on its own line.

Still stuck? Help me debug my health_monitor.sh script that's failing on the log function

Add threshold-based warnings

Right now every line says INFO regardless of the values. Threshold variables let the script compare actual usage against a limit and escalate to WARNING when something needs attention.

  • In health_monitor.sh, find this line:
LOG_FILE="${LOG_FILE:-/var/log/health_monitor.log}"
  • Add two threshold variables directly above it, so the three variable lines look like this:
DISK_THRESHOLD="${DISK_THRESHOLD:-80}"
MEMORY_THRESHOLD="${MEMORY_THRESHOLD:-80}"
LOG_FILE="${LOG_FILE:-/var/log/health_monitor.log}"
  • Now replace your check_disk() and check_memory() functions with these threshold-aware versions:
check_disk() {
    local usage
    usage=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
    if [ "$usage" -ge "$DISK_THRESHOLD" ]; then
        log "WARNING" "Disk usage: ${usage}% (threshold: ${DISK_THRESHOLD}%)"
    else
        log "INFO" "Disk usage: ${usage}% - OK"
    fi
}

check_memory() {
    local usage
    usage=$(free | awk '/Mem:/ {printf "%.0f", $3/$2 * 100}')
    if [ "$usage" -ge "$MEMORY_THRESHOLD" ]; then
        log "WARNING" "Memory usage: ${usage}% (threshold: ${MEMORY_THRESHOLD}%)"
    else
        log "INFO" "Memory usage: ${usage}% - OK"
    fi
}

What does this code do?

  • DISK_THRESHOLD and MEMORY_THRESHOLD default to 80 but can be overridden from the command line, just like LOG_FILE.
  • Each check function now compares the parsed usage value against its threshold using -ge (greater than or equal).
  • If usage meets or exceeds the threshold, the function logs a WARNING with both the value and the threshold. Otherwise it logs INFO with an OK status.
  • Save health_monitor.sh.
  • Run the script normally first:
LOG_FILE=/tmp/health_monitor.log bash health_monitor.sh

You should see INFO lines with - OK for disk and memory (assuming your system is under 80% usage).

Now, what do you think will happen if you set the disk threshold to 1%?

  • Test the warning output by running with a low threshold:
DISK_THRESHOLD=1 LOG_FILE=/tmp/health_monitor.log bash health_monitor.sh

You should see a WARNING line for disk usage, since your actual usage is almost certainly above 1%. The memory line still shows INFO because its threshold is still at 80%.

All lines still showing INFO?

  • Check that you placed DISK_THRESHOLD=1 before the LOG_FILE override on the same line. Environment variables must come before the command.
  • Verify the if [ "$usage" -ge "$DISK_THRESHOLD" ] line uses -ge (greater than or equal), not -gt (greater than).
  • Make sure you replaced the old simple check_disk() completely and did not accidentally leave the old version.

Need a hand? Help me figure out why my threshold warning is not appearing

✔️ Awesome, I've got everything!

Great. Double check you have saved health_monitor.sh with all changes.

ⓧ I'd like to double check the full code

Your health_monitor.sh should contain these sections in order. Scroll up and confirm each one is present:

  • Shebang (#!/bin/bash) followed by set -euo pipefail.
  • Three variables: DISK_THRESHOLD, MEMORY_THRESHOLD, LOG_FILE.
  • The log() function using tee -a.
  • check_disk() with the if/else threshold comparison.
  • check_memory() with the if/else threshold comparison.
  • check_cpu() reading /proc/loadavg.
  • main() calling all three checks between start and complete log lines.
  • A final main call at the bottom.
Commit with a meaningful message

Your script now has real production patterns. Lock this progress into your Git history so the improvement is visible as a distinct commit.

  • Stage and commit your changes by running these commands:
git add health_monitor.sh
git commit -m "Add production patterns: strict mode, logging, thresholds"
  • Confirm the commit landed by running:
git log --oneline

You should see three commits. The most recent one says Add production patterns: strict mode, logging, thresholds.

Commit failed or only two commits visible?

  • If Git says nothing to commit, make sure you saved the file before running git add.
  • Run git status to see if health_monitor.sh shows as modified. If it does, re-run the git add and git commit commands.

Still stuck? Help me figure out why my git commit is not working in the health-monitor project

Your script now catches errors, logs with timestamps, and raises warnings when thresholds are breached. Next up, you will containerize this monitor with Docker so it can run anywhere.

Containerize the Health Monitor

Your health monitor script now has proper error handling, structured logging, and threshold-based alerting. The next challenge is making it portable.

Right now, the script only runs on your machine with your specific packages and paths. Docker solves this by packaging your script along with its dependencies into a container image that anyone can run, on any machine, without installing anything else.

In this step, get ready to:
  • Write a Dockerfile for the health monitor.
  • Build and run the Docker container.
Write a basic Dockerfile

A Dockerfile is a recipe that tells Docker how to build your container image. Each instruction in the file adds a layer: pick a base OS, install packages, copy your files in, and declare what runs when the container starts.

  • Create a new file called Dockerfile in your health-monitor directory by running:
nano Dockerfile
  • Paste the following content into the file:
# Use Ubuntu 24.04 as the base image
FROM ubuntu:24.04

# Install procps (provides the 'free' command)
RUN apt-get update && \
    apt-get install -y --no-install-recommends procps && \
    rm -rf /var/lib/apt/lists/*

# Set the working directory inside the container
WORKDIR /app

# Copy the health monitor script into the container
COPY health_monitor.sh ./
RUN chmod +x health_monitor.sh

# Run the script when the container starts
CMD ["./health_monitor.sh"]

What does this Dockerfile do?

  • FROM ubuntu:24.04 starts from the same Ubuntu version you are running in WSL, so tools like apt work the same way.
  • RUN apt-get ... procps installs the procps package. The minimal Ubuntu image does not include the free command, so your check_memory() function would fail without it.
  • COPY health_monitor.sh ./ copies your script into the container's /app directory.
  • CMD ["./health_monitor.sh"] tells Docker to execute your script when the container starts.
  • Save the file in nano by pressing Ctrl+O, then Enter, then Ctrl+X to exit.
  • Confirm the file exists by running:
cat Dockerfile

You should see the full Dockerfile contents printed back to your terminal.

Seeing a blank file or command not found?

  • Make sure you saved before exiting nano. Press Ctrl+O to write, confirm with Enter, then Ctrl+X to exit.
  • Check you are in the health-monitor directory by running pwd.

Having trouble?

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

FROM ubuntu:24.04

RUN apt-get update && \
    apt-get install -y --no-install-recommends procps && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY health_monitor.sh ./
RUN chmod +x health_monitor.sh

CMD ["./health_monitor.sh"]
Build and run the container

With the Dockerfile written, you can now build an image and start a container from it. Building compiles your recipe into a runnable image that contains Ubuntu, procps, and your script.

  • Build the Docker image by running:
docker build -t health-monitor .

What does this command do?

docker build reads your Dockerfile and creates an image. The -t health-monitor flag tags the image with the name health-monitor so you can refer to it later. The . at the end tells Docker to use the current directory as the build context.

You should see Docker downloading the Ubuntu image, installing procps, and copying your script. The build finishes with a line showing the image was tagged successfully.

Build failing with a permission or connection error?

  • Make sure Docker Desktop is running on Windows. Check for the whale icon in your system tray.
  • If you see a permission denied error, confirm your WSL integration is enabled in Docker Desktop under Settings > Resources > WSL Integration.

Help me fix my Docker build error

Now the moment of truth. Before you run the container, think about this: what do you expect to happen when Docker starts your health monitor script inside a container?

  • Run the container by executing:
docker run health-monitor

You should see your familiar health check output with timestamps, INFO lines for disk, memory, and CPU. Then your terminal prompt returns immediately.

The container printed one health report and stopped. Confirm this by running:

docker ps -a

In the STATUS column, you will see Exited (0) next to your container. The container ran the script once, finished, and shut itself down.

Why did the container exit?

Docker containers live only as long as their main process runs. Your script executes the main function, prints the health report, and reaches the end of the file. With nothing left to do, the process exits and Docker shuts down the container.

For a monitoring tool, this is a problem. You need it running continuously, checking health every few seconds, not just once. That is the gap you will close in the next step.

  • Commit the Dockerfile to Git by running:
git add Dockerfile
git commit -m "Add basic Dockerfile with CMD"
  • Verify the commit was recorded by running:
git log --oneline

You should see four commits in your history, with the newest one at the top mentioning the Dockerfile.

Git commit rejected?

  • If you see a message about untracked files, make sure you ran git add Dockerfile before committing.
  • If Git asks for your name and email, set them with git config user.name and git config user.email.

Help me fix my Git commit

You have a containerized health monitor, but it only runs once and stops. Next up, you will make the container run continuously with a proper entrypoint, add a HEALTHCHECK so Docker knows the monitor is alive, and lock it down with a non-root user.

Make the Container Production-Ready

In the previous step, you saw the core problem with your containerized monitor: it runs once and exits. A monitoring tool that quits after one check is useless.

To make this container production-ready, you need four things: an entrypoint script that loops forever, a HEALTHCHECK so Docker can verify the monitor itself is alive, a non-root user for security, and a .dockerignore to keep the image lean.

In this step, get ready to:
  • Create an entrypoint script with an infinite monitoring loop.
  • Update the Dockerfile with a HEALTHCHECK, non-root user, and .dockerignore.
  • Build, run, and verify the production container using a Git feature branch workflow.
Create an entrypoint script with a monitoring loop

Docker containers exit when their main process finishes. Your health script runs once and returns, so the container dies immediately.

The fix is a separate entrypoint script that wraps your health check in an infinite loop, running it every 30 seconds.

  • Create a new file called entrypoint.sh by running:
cat > entrypoint.sh << 'EOF'
#!/bin/bash
set -euo pipefail

CHECK_INTERVAL="${CHECK_INTERVAL:-30}"

echo "Starting health monitor..."
echo "Check interval: ${CHECK_INTERVAL} seconds"
echo "Disk threshold: ${DISK_THRESHOLD:-80}%"
echo "Memory threshold: ${MEMORY_THRESHOLD:-80}%"
echo "---"

while true; do
    /app/health_monitor.sh
    sleep "$CHECK_INTERVAL"
done
EOF

What does this script do?

The CHECK_INTERVAL variable defaults to 30 seconds but can be overridden via an environment variable when running the container.

The echo statements print startup info so you can confirm the configuration when reading logs. The while true loop runs the health check, sleeps for the interval, and repeats forever.

  • Make the script executable and confirm it exists by running:
chmod +x entrypoint.sh
ls

You should see entrypoint.sh listed alongside health_monitor.sh and Dockerfile.

Don't see entrypoint.sh?

Make sure you copied the full cat > entrypoint.sh << 'EOF' command including the closing EOF on its own line. If the terminal is still waiting for input, type EOF and press Enter.

help me create entrypoint.sh for my health monitor Docker container

✔️ Awesome, I've got everything!

Great. Make sure you saved the file and that it's executable.

ⓧ I'd like to double check the full code

#!/bin/bash
set -euo pipefail

CHECK_INTERVAL="${CHECK_INTERVAL:-30}"

echo "Starting health monitor..."
echo "Check interval: ${CHECK_INTERVAL} seconds"
echo "Disk threshold: ${DISK_THRESHOLD:-80}%"
echo "Memory threshold: ${MEMORY_THRESHOLD:-80}%"
echo "---"

while true; do
    /app/health_monitor.sh
    sleep "$CHECK_INTERVAL"
done
Update the Dockerfile with HEALTHCHECK, non-root user, and .dockerignore

Your current Dockerfile uses CMD to run the health script directly. You need to replace it with the production version that adds a dedicated user, a health check, and points to your new entrypoint.

  • Replace the entire contents of your Dockerfile with this production version:
FROM ubuntu:24.04

RUN apt-get update && \
    apt-get install -y --no-install-recommends procps && \
    rm -rf /var/lib/apt/lists/*

RUN useradd -r -s /bin/false monitor && \
    mkdir -p /var/log && \
    touch /var/log/health_monitor.log && \
    chown monitor:monitor /var/log/health_monitor.log

WORKDIR /app

COPY health_monitor.sh entrypoint.sh ./
RUN chmod +x health_monitor.sh entrypoint.sh

HEALTHCHECK --interval=60s --timeout=10s --start-period=5s --retries=3 \
    CMD tail -1 /var/log/health_monitor.log | grep -q "Health Check Complete" || exit 1

USER monitor

ENTRYPOINT ["./entrypoint.sh"]

What changed in this Dockerfile?

  • useradd -r -s /bin/false monitor creates a system user with no login shell. Running as non-root is a security best practice.
  • The log file is created and owned by monitor before the USER switch so the non-root user can write to it.
  • HEALTHCHECK tells Docker to verify the monitor is alive every 60 seconds by checking that the log file's last line contains "Health Check Complete".
  • ENTRYPOINT replaces CMD. The entrypoint script loops forever, so the container stays running.

Now create a .dockerignore file to exclude unnecessary files from the Docker build context.

  • Create .dockerignore by running:
cat > .dockerignore << 'EOF'
.git
*.md
LICENSE
EOF

Why use a .dockerignore?

When you run docker build, Docker sends every file in the current directory to the build daemon. The .dockerignore file excludes the .git folder and markdown files, keeping the build context small and the image lean.

  • Confirm all your project files are in place by running:
ls -a

You should see .dockerignore, .git, .gitignore, Dockerfile, entrypoint.sh, and health_monitor.sh.

Missing .dockerignore?

Files starting with a dot are hidden by default. Make sure you used ls -a (with the -a flag) to show hidden files. If the file is missing, re-run the cat > .dockerignore command above.

help me create a .dockerignore file

✔️ Awesome, I've got everything!

Great. Double check you saved your Dockerfile changes.

ⓧ I'd like to double check the full code

Here is the complete Dockerfile:

FROM ubuntu:24.04

RUN apt-get update && \
    apt-get install -y --no-install-recommends procps && \
    rm -rf /var/lib/apt/lists/*

RUN useradd -r -s /bin/false monitor && \
    mkdir -p /var/log && \
    touch /var/log/health_monitor.log && \
    chown monitor:monitor /var/log/health_monitor.log

WORKDIR /app

COPY health_monitor.sh entrypoint.sh ./
RUN chmod +x health_monitor.sh entrypoint.sh

HEALTHCHECK --interval=60s --timeout=10s --start-period=5s --retries=3 \
    CMD tail -1 /var/log/health_monitor.log | grep -q "Health Check Complete" || exit 1

USER monitor

ENTRYPOINT ["./entrypoint.sh"]

And the .dockerignore:

.git
*.md
LICENSE
Build, run, and verify the production container

Everything is in place. Time to rebuild the image and launch the container in detached mode so it runs in the background.

  • Rebuild the Docker image by running:
docker build -t health-monitor .

You should see the build complete with each layer cached or rebuilt successfully.

Build failing?

If you see a permissions error on the COPY step, make sure entrypoint.sh exists in the same directory as your Dockerfile. Run ls to confirm.

If you see a syntax error in the HEALTHCHECK line, make sure the backslash \ at the end of the HEALTHCHECK line has no spaces after it.

help me fix my Docker build error

  • Run the container in detached mode by running:
docker run -d --name monitor health-monitor

The -d flag runs the container in the background. The --name monitor gives it a friendly name you can reference in later commands.

Before you check the container status, what do you expect docker ps to show? Think about whether it will say "healthy" immediately or need time first.

  • Check the container status by running:
docker ps

You will see the container listed as "Up" with a health status. Initially it shows "(health: starting)" because the HEALTHCHECK has a 5-second start period and a 60-second interval before the first check runs.

  • Wait about 60-70 seconds, then run docker ps again.
docker ps

Now the STATUS column shows "(healthy)" next to the uptime. Docker confirmed that your monitoring loop is alive and writing to the log file.

  • View the continuous health reports by running:
docker logs monitor

You should see the startup info followed by repeating health check blocks every 30 seconds, each with timestamped INFO lines for disk, memory, and CPU.

  • Inspect the running container from the inside by running:
docker exec monitor whoami

This prints monitor confirming the container runs as the non-root user you created.

Why does running as non-root matter?

If an attacker exploits a vulnerability in your container, running as root gives them full control of the container's filesystem and processes. A non-root user limits the damage to only what that user can access.

Now commit all these changes using a feature branch workflow to practice professional Git habits.

  • Create a feature branch and switch to it by running:
git checkout -b feature/production-container
  • Stage and commit all the new and updated files by running:
git add .
git commit -m "Add production container: entrypoint loop, HEALTHCHECK, non-root user, .dockerignore"
  • Switch back to main and merge the feature branch by running:
git checkout main
git merge feature/production-container
  • Verify your Git history shows the full progression by running:
git log --oneline

You should see your commits listed from the initial setup through to the production container work.

Why use a feature branch?

In professional workflows, you never commit directly to main. Feature branches let you develop in isolation and merge only when the work is complete and tested. This pattern scales to teams where multiple developers work on different features simultaneously.

Getting a 'name already in use' error from docker run?

If you previously ran a container named monitor, remove it first with docker rm -f monitor, then re-run the docker run command.

help me fix the Docker container name conflict

Your container is now production-ready. It loops forever, logs health reports every 30 seconds, proves its own liveness to Docker, and runs securely as a non-root user. Next up, you'll extend this tool with a machine-readable JSON output mode in the Secret Mission.

Secret mission

Add JSON Output Mode

Plain text logs are readable by humans, but monitoring pipelines like Prometheus, Datadog, and ELK stack need machine-readable output. In this secret mission, you will add a --json flag to your health monitor so it outputs structured JSON objects instead of plain text. This makes your tool pipeline-ready and teaches a pattern used in every production monitoring system.

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, so there are no ongoing costs.

Resources you used:

  • Docker container monitor (running the health monitor loop).
  • Docker image health-monitor.
  • Project directory ~/Desktop/health-monitor with all scripts, Dockerfile, and Git history.

Keep everything running

No action needed. Choose this if you are still actively building on the health monitor or want to experiment further with Docker and Bash scripting.

  • Your Docker container continues running in the background. You can check on it anytime with docker ps.
  • Your project files remain in ~/Desktop/health-monitor.
  • The Docker image stays cached locally, ready to rebuild or run again.

Pause - I'll come back to this later

Stop the running container to free up system resources. Your project files and Docker image stay on disk so you can resume later.

  • Stop the running container by running:
docker stop monitor

When you are ready to resume, start it again with:

docker start monitor
  • Your project files remain in ~/Desktop/health-monitor and the Docker image stays cached.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes the container, image, and all project files.

  • Stop and remove the Docker container by running:
docker stop monitor
docker rm monitor
  • Remove the Docker image by running:
docker rmi health-monitor
  • Delete the project directory by running:
cd ~/Desktop
rm -rf health-monitor
  • Confirm the directory is gone by running:
ls ~/Desktop

You should no longer see health-monitor in the output.

Nice Work!

Nice Work!

Nice work! You just built a containerized server health monitoring tool from scratch, using the same patterns that DevOps engineers rely on in production.

You've learned how to:

  • Built a Bash health monitoring script with production patterns: strict mode (set -euo pipefail), structured logging with timestamps and levels, threshold-based alerting, and modular functions.
  • Containerized the script with Docker best practices: ENTRYPOINT pattern for long-running processes, HEALTHCHECK for container liveness, non-root USER for security, and .dockerignore for lean builds.
  • Used Git professionally: initialized a repo, made meaningful commits, created feature branches, and merged.
  • Secret Mission: Added a machine-readable JSON output mode for integration with monitoring pipelines.

Ready to quiz yourself?