Series: Linux Mastery Road to Cloud

I want to be honest with you. I am not a programmer. I have no CS degree. I study Linux every day after work. So when I say I spent two hours building one bash script — I am not embarrassed. I am proud.

Let me tell you what happened.


Why I Started Learning Bash Scripting

After completing two rounds across all 12 Linux domains, my mentor told me the next step is bash scripting. Not just running commands — writing scripts that run commands for you automatically.

I had avoided it honestly. It looked like code. I am not a developer. But then I realized something important — every junior cloud and DevOps job posting expects automation. Terraform and Ansible are great tools, but bash is what runs before and around everything else. If I cannot write a basic script, I cannot call myself a cloud engineer.

So I started from zero.


The Basics First

Before writing any real script I had to understand three things:

The shebang line. Every script starts with #!/bin/bash. This tells the kernel which interpreter to use. Without it, your file is just text. The kernel does not know what to do with it.

Execute permission. When you run ./script.sh the kernel checks permissions first. No execute bit means permission denied. When you run bash script.sh you are calling the interpreter directly — it reads the file as text, no execute permission needed. This is not magic. This is how the kernel works.

Subshell vs current shell. Running ./script.sh creates a clone of your shell — a subshell. Everything that happens inside stays inside. When it finishes, the clone is destroyed. Variables set inside are gone. Running source script.sh runs directly in your current shell. Variables survive. This is why you always source ~/.bashrc when you want your config changes to take effect immediately.


Variables, Loops, Functions — Building Blocks

I will not walk through every topic in detail here. But I want to say something important that clicked for me:

90% of real production scripts are just four things:

  • if — make decisions
  • for and while — repeat things
  • $1, $2, read — accept input
  • set -e, exit codes — handle failures

That is it. Everything else is just these four things arranged differently. The complexity comes from the problem you are solving, not from exotic syntax you need to memorize.

One thing that confused me for a long time was $1 inside functions. Then it clicked — it works exactly like script arguments. When you call:

log "Nginx is OK"

Inside the log function, $1 becomes "Nginx is OK". Whatever you pass when calling the function travels in as $1. Same rule, different context.


The Health Check Script

After learning the basics I built a real script that runs on my actual Hetzner VPS. Not a tutorial exercise. A real script checking my real server.

Here is what it does:

  1. Checks disk usage — warns if above 80%
  2. Reports free memory
  3. Checks if nginx is running
  4. Checks if MySQL is running
  5. Logs everything with timestamps to a log file
  6. Exits with code 1 if anything failed

Here is the final script:

#!/bin/bash
set -u

LOGFILE="/tmp/system_check.log"
EXIT_CODE=0

log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a $LOGFILE
}

log "=== Health Check Started ==="

DISK=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
if [ $DISK -gt 80 ]; then
    log "WARNING: Disk usage is ${DISK}%"
    EXIT_CODE=1
else
    log "Disk OK: ${DISK}%"
fi

MEMORY=$(free -m | awk 'NR==2 {print $NF}')
log "Available memory: ${MEMORY}MB"

if systemctl is-active --quiet nginx; then
    log "Nginx is ok"
else
    log "Nginx is down"
    EXIT_CODE=1
fi

if systemctl is-active --quiet mysql; then
    log "MySQL is ok"
else
    log "MySQL is down"
    EXIT_CODE=1
fi

log "=== Health Check Complete ==="
exit $EXIT_CODE

Three Things I Learned That No Tutorial Told Me

1. Every command has two outputs — not one.

What it prints to your screen is stdout. What it returns silently is the exit code. These are completely separate. if in bash never looks at what a command prints. It only reads the exit code. This one mental model changed how I read scripts.

2. set -e is not always the right choice.

In a health check script you do NOT want set -e. If nginx is down and the script stops immediately, you never check MySQL. You want the script to detect the problem, log it, keep going, and report the final result at the end. Defensive scripting is not always about stopping early. Sometimes it is about surviving to the end.

3. tee -a is how you log properly.

echo only prints to terminal. When the terminal closes the output is gone. tee -a sends output to terminal AND appends to a file simultaneously. One pipe changes a print statement into a log entry. Simple but powerful.


What The Output Looks Like

When I run this on my server I see:

2026-03-27 14:35:22 - === Health Check Started ===
2026-03-27 14:35:22 - Disk OK: 45%
2026-03-27 14:35:22 - Available memory: 1243MB
2026-03-27 14:35:22 - Nginx is ok
2026-03-27 14:35:22 - MySQL is ok
2026-03-27 14:35:22 - === Health Check Complete ===

And the same lines are saved to /tmp/system_check.log. Every run appends. History preserved.


Why Two Hours Is Not Too Long

Someone reading this might think — two hours for one script? That is slow.

But I did not just copy a script. I built it line by line, understood every single line, and can now explain why each decision was made. The next script I build will take 45 minutes. The one after that — 20 minutes.

More importantly — I now have a real script running on a real server that goes to GitHub as a real portfolio piece. Not a tutorial screenshot. Real work.

That is the whole point.


What Is Next

Next I will wire this script into a cron job so it runs automatically every hour. Then build a backup script. Eventually integrate with Prometheus and Grafana for proper monitoring on limonlab.online.

One step at a time.


This post is part of my Linux Mastery Road to Cloud series. I document real learning, real servers, real mistakes. No fake environments, no sanitized tutorials. If you are on a similar path — self-taught, no degree, changing careers — feel free to follow along.

GitHub: billal4232 Server: limonlab.online running on Hetzner VPS, Helsinki

Leave a Reply

Your email address will not be published. Required fields are marked *