Complete documentation suite for KiteStacks covering all 11 services across 2-host active-active architecture. Includes beginner track (with AI, 8 files) and advanced track (without AI, 7 files) with time estimates, real troubleshooting cases, and command-by-command explanations. Updates certifications roadmap to reflect July 7 2026 A+ Core 2 exam goal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.9 KiB
Without AI — Part 2: Bash Scripting
Track: Advanced (No AI)
Time for this section: 1–2 weeks
Bash is the language of the Linux shell. Almost every automation script in this homelab is a Bash script. You do not need to master it — you need to be able to read it, write simple scripts, and understand what a script does before you run it.
What Is a Script?
A script is a text file containing a sequence of shell commands. Instead of typing commands one by one, you put them in a file and run the file.
#!/usr/bin/env bash
# This is a comment
echo "Hello from my script"
The first line (#!/usr/bin/env bash) is called the shebang. It tells Linux
which interpreter to use to run this file. Without it, Linux may use the wrong shell.
To run a script:
chmod +x myscript.sh # Make it executable
./myscript.sh # Run it
Or without making it executable:
bash myscript.sh
Variables
Variables store values you want to reuse:
name="kenpat"
port=3000
greeting="Hello, $name"
echo $name # prints: kenpat
echo $port # prints: 3000
echo $greeting # prints: Hello, kenpat
echo "${name}s" # prints: kenpats (braces needed when appending)
Special variables:
$0 # The script's own filename
$1 $2 $3 # Command-line arguments (first, second, third)
$# # Number of arguments passed
$? # Exit code of the last command (0 = success, non-zero = error)
$$ # Current process ID (PID)
$HOME # Your home directory path
$USER # Your username
Read-only environment variables:
export MY_VAR="value" # Make available to child processes
printenv # List all environment variables
printenv MY_VAR # Print one variable
Conditionals (if/else)
if [[ condition ]]; then
# commands if true
elif [[ other_condition ]]; then
# commands if second condition is true
else
# commands if nothing was true
fi
Common conditions:
[[ -f /path/to/file ]] # True if file exists and is a regular file
[[ -d /path/to/dir ]] # True if directory exists
[[ -s /path/to/file ]] # True if file exists and is non-empty
[[ -z "$var" ]] # True if variable is empty
[[ -n "$var" ]] # True if variable is NOT empty
[[ "$a" == "$b" ]] # True if strings are equal
[[ "$a" != "$b" ]] # True if strings are NOT equal
[[ $n -eq 5 ]] # True if number equals 5
[[ $n -gt 5 ]] # True if number is greater than 5
[[ $n -lt 5 ]] # True if number is less than 5
Real example from the homelab:
if [[ $# -ne 1 ]]; then
echo "Usage: $0 '<cloudflare_tunnel_token>'" >&2
exit 2
fi
This checks that exactly one argument was provided ($# -ne 1 means "number of args
is not equal to 1"). If not, it prints usage instructions and exits with code 2 (error).
The >&2 sends the message to stderr (error output) instead of stdout (normal output).
Loops
For loop — iterate over a list:
for item in one two three; do
echo "Item: $item"
done
# Iterate over files
for file in *.yml; do
echo "Found compose file: $file"
done
# Iterate over a range of numbers
for i in {1..10}; do
echo "Number: $i"
done
While loop — repeat while a condition is true:
count=0
while [[ $count -lt 5 ]]; do
echo "Count: $count"
count=$(( count + 1 ))
done
# Wait until a container is healthy
while [[ "$(docker inspect --format '{{.State.Health.Status}}' authentik)" != "healthy" ]]; do
echo "Waiting for authentik..."
sleep 5
done
echo "Authentik is healthy"
Functions
greet() {
local name="$1" # local = only exists inside this function
echo "Hello, $name"
}
greet "kenpat" # prints: Hello, kenpat
greet "world" # prints: Hello, world
Why local variables matter: Without local, variables are global and can
accidentally overwrite values from other parts of the script.
Error Handling
set -euo pipefail
Put this near the top of every script you write. It sets three behaviors:
-e— exit immediately if any command fails (returns non-zero exit code)-u— exit if you use an undefined variable-o pipefail— if any command in a pipeline fails, the whole pipeline fails
Without this, a script can silently continue after an error, potentially causing damage downstream (like deleting data after a failed backup).
Checking a command's result:
if curl -s https://example.com > /dev/null; then
echo "Site is up"
else
echo "Site is down"
fi
Exit codes:
exit 0 # Success
exit 1 # Generic error
exit 2 # Misuse (bad arguments)
String Manipulation
var="TUNNEL_TOKEN=abc123"
# Split by delimiter, take field 2
echo "$var" | cut -d= -f2 # prints: abc123
# But what if the value itself contains = signs?
echo "$var" | cut -d= -f2- # prints: abc123 (f2- = from field 2 to end)
# Remove trailing newline
echo "hello" | tr -d '\n'
# Convert to lowercase
echo "HELLO" | tr '[:upper:]' '[:lower:]'
# Replace text
echo "hello world" | sed 's/world/there/' # prints: hello there
echo "aabbcc" | sed 's/b/B/g' # prints: aaBBcc (g = all occurrences)
# Extract with grep
echo "addr: 192.168.1.1" | grep -oP '\d+\.\d+\.\d+\.\d+' # prints: 192.168.1.1
Here Documents (heredoc)
A heredoc lets you write multi-line strings inline:
cat <<'EOF'
This is line one
This is line two
Variables like $HOME are NOT expanded (because of the quotes around EOF)
EOF
cat <<EOF
This is line one
HOME is: $HOME (expanded because no quotes)
EOF
Used in this homelab to write multi-line content to files:
cat > /tmp/fix.sql <<'EOF'
BEGIN;
UPDATE ServerSetting SET Value='{"enabled":true}' WHERE "Key"=40;
COMMIT;
EOF
Real Scripts in This Homelab
The Token Rotation Script
~/kitestacks-homelab/scripts/rollout-cloudflared-token.sh:
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "Usage: $0 '<cloudflare_tunnel_token>'" >&2
exit 2
fi
token="$1"
monk_dir="${MONK_CLOUDFLARED_DIR:-$HOME/kitestacks-live/docker/cloudflared}"
kscloud1_host="${KSCLOUD1_HOST:?set KSCLOUD1_HOST, for example user@host}"
kscloud1_key="${KSCLOUD1_KEY:-$HOME/.ssh/id_ed25519_kscloud1}"
kscloud1_dir="${KSCLOUD1_CLOUDFLARED_DIR:-/opt/kitestacks/docker/cloudflared}"
Walking through each line:
set -euo pipefail— fail fast and safely$# -ne 1— check exactly one argument was given${MONK_CLOUDFLARED_DIR:-$HOME/...}— use environment variable if set, otherwise use default${KSCLOUD1_HOST:?...}— ifKSCLOUD1_HOSTis not set, exit with that error message
This is a real production script. Read it in full at that path.
Writing Your Own Scripts
Template for any script:
#!/usr/bin/env bash
set -euo pipefail
# --- Configuration (change these) ---
MY_VAR="${MY_ENV_VAR:-default_value}"
TARGET_HOST="${1:?Usage: $0 <hostname>}"
# --- Functions ---
log() {
echo "[$(date '+%H:%M:%S')] $*"
}
die() {
echo "ERROR: $*" >&2
exit 1
}
# --- Main ---
log "Starting..."
if [[ ! -d "$TARGET_HOST" ]]; then
die "Directory does not exist: $TARGET_HOST"
fi
log "Done."
Practice Exercises
- Write a script that checks if Docker is running and prints "Docker is up" or "Docker is down"
- Write a script that takes a service name as an argument and shows its logs:
./show-logs.sh forgejo - Write a script that loops through all directories in
~/kitestacks-live/docker/and prints the service name and whether it has a.envfile - Write a script that checks if a URL returns 200 OK and prints "UP" or "DOWN":
./check-url.sh https://gitforge.kitestacks.com - Read and understand every line of
scripts/rollout-cloudflared-token.sh
Next: Part 3 — Python Basics