Lesson 11: Real-World Scripts Collection

Goal: Complete, production-ready scripts you can adapt for your own projects.


Table of Contents

  1. Automated Backup Script
  2. Deployment Script
  3. Log Rotation Script
  4. System Monitoring & Alerts
  5. Docker Compose Helper
  6. CI/CD Pipeline Script
  7. Database Migration Runner
  8. SSL Certificate Checker

Automated Backup Script

#!/bin/bash
# backup.sh — Automated backup with rotation
set -euo pipefail

# --- Configuration ---
BACKUP_DIR="/backup"
SOURCE_DIRS=("/var/www" "/etc/nginx" "/home")
RETENTION_DAYS=30
DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_FILE="$BACKUP_DIR/backup-$DATE.tar.gz"

# --- Functions ---
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }

# --- Main ---
log "Starting backup..."

# Ensure backup directory exists
mkdir -p "$BACKUP_DIR"

# Create the backup
log "Archiving: ${SOURCE_DIRS[*]}"
tar -czf "$BACKUP_FILE" "${SOURCE_DIRS[@]}" 2>/dev/null || {
    log "Warning: some files could not be read (permission denied)"
}

# Show backup size
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log "Created: $BACKUP_FILE ($SIZE)"

# Remove old backups
log "Removing backups older than $RETENTION_DAYS days..."
DELETED=$(find "$BACKUP_DIR" -name "backup-*.tar.gz" -mtime +$RETENTION_DAYS -delete -print | wc -l)
log "Deleted $DELETED old backups"

# Summary
TOTAL=$(find "$BACKUP_DIR" -name "backup-*.tar.gz" | wc -l)
TOTAL_SIZE=$(du -sh "$BACKUP_DIR" | cut -f1)
log "Done. Total: $TOTAL backups ($TOTAL_SIZE)"

Deployment Script

#!/bin/bash
# deploy.sh — Deploy application with rollback support
set -euo pipefail

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly APP_NAME="myapp"
readonly DEPLOY_DIR="/var/www/$APP_NAME"
readonly RELEASES_DIR="$DEPLOY_DIR/releases"
readonly CURRENT_LINK="$DEPLOY_DIR/current"
readonly MAX_RELEASES=5

BRANCH="${1:-main}"
DRY_RUN=false

log() { echo -e "\033[32m[DEPLOY]\033[0m $*"; }
error() { echo -e "\033[31m[ERROR]\033[0m $*" >&2; }

# --- Pre-flight checks ---
command -v git &>/dev/null || { error "git is required"; exit 1; }
command -v rsync &>/dev/null || { error "rsync is required"; exit 1; }

# --- Deploy ---
RELEASE_DIR="$RELEASES_DIR/$(date +%Y%m%d-%H%M%S)"

log "Deploying branch '$BRANCH' to $RELEASE_DIR"

# Build
log "Building application..."
git checkout "$BRANCH"
git pull origin "$BRANCH"
npm ci
npm run build

# Copy to release directory
log "Copying build artifacts..."
mkdir -p "$RELEASE_DIR"
rsync -a ./dist/ "$RELEASE_DIR/"

# Switch symlink (atomic deployment)
log "Switching to new release..."
ln -sfn "$RELEASE_DIR" "$CURRENT_LINK"

# Restart service
log "Restarting service..."
sudo systemctl restart "$APP_NAME" || {
    error "Restart failed — rolling back!"
    # Rollback: point to previous release
    PREV=$(ls -t "$RELEASES_DIR" | sed -n '2p')
    if [[ -n "$PREV" ]]; then
        ln -sfn "$RELEASES_DIR/$PREV" "$CURRENT_LINK"
        sudo systemctl restart "$APP_NAME"
        log "Rolled back to $PREV"
    fi
    exit 1
}

# Cleanup old releases
log "Cleaning old releases (keeping $MAX_RELEASES)..."
ls -t "$RELEASES_DIR" | tail -n +$((MAX_RELEASES + 1)) | while read -r old; do
    rm -rf "$RELEASES_DIR/$old"
    log "  Removed: $old"
done

log "Deployment complete!"
log "Current: $(readlink "$CURRENT_LINK")"

Log Rotation Script

#!/bin/bash
# rotate-logs.sh — Compress and rotate log files
set -euo pipefail

LOG_DIR="${1:-/var/log/myapp}"
MAX_AGE_DAYS=30
COMPRESS_AFTER_DAYS=1

log() { echo "[$(date '+%H:%M:%S')] $*"; }

if [[ ! -d "$LOG_DIR" ]]; then
    echo "Directory not found: $LOG_DIR" >&2
    exit 1
fi

# Compress logs older than 1 day
log "Compressing logs older than $COMPRESS_AFTER_DAYS day(s)..."
find "$LOG_DIR" -name "*.log" -mtime +$COMPRESS_AFTER_DAYS -exec gzip -v {} \;

# Delete compressed logs older than max age
log "Deleting logs older than $MAX_AGE_DAYS days..."
find "$LOG_DIR" -name "*.log.gz" -mtime +$MAX_AGE_DAYS -delete -print | while read -r f; do
    log "  Deleted: $(basename "$f")"
done

# Report
TOTAL_SIZE=$(du -sh "$LOG_DIR" | cut -f1)
FILE_COUNT=$(find "$LOG_DIR" -type f | wc -l)
log "Done. $LOG_DIR: $FILE_COUNT files, $TOTAL_SIZE"

System Monitoring & Alerts

#!/bin/bash
# monitor.sh — System health check with alerts
set -euo pipefail

# Thresholds
CPU_THRESHOLD=80
MEM_THRESHOLD=85
DISK_THRESHOLD=80

ALERTS=()

check_cpu() {
    local usage
    usage=$(top -bn1 | grep "Cpu(s)" | awk '{printf "%.0f", $2 + $4}')
    if (( usage > CPU_THRESHOLD )); then
        ALERTS+=("CPU usage: ${usage}% (threshold: ${CPU_THRESHOLD}%)")
    fi
    echo "CPU:    ${usage}%"
}

check_memory() {
    local usage
    usage=$(free | awk '/Mem:/ {printf "%.0f", $3/$2 * 100}')
    if (( usage > MEM_THRESHOLD )); then
        ALERTS+=("Memory usage: ${usage}% (threshold: ${MEM_THRESHOLD}%)")
    fi
    echo "Memory: ${usage}%"
}

check_disk() {
    while read -r fs size used avail pct mount; do
        local usage="${pct%\%}"
        if (( usage > DISK_THRESHOLD )); then
            ALERTS+=("Disk $mount: ${usage}% (threshold: ${DISK_THRESHOLD}%)")
        fi
        echo "Disk:   ${usage}% ($mount)"
    done < <(df -h | awk 'NR>1 && /^\/dev/ {print}')
}

check_services() {
    local services=("nginx" "postgresql" "redis-server")
    for svc in "${services[@]}"; do
        if systemctl is-active --quiet "$svc" 2>/dev/null; then
            echo "Service: $svc [OK]"
        else
            ALERTS+=("Service $svc is NOT running")
            echo "Service: $svc [DOWN]"
        fi
    done
}

echo "=== System Health Check — $(date) ==="
check_cpu
check_memory
check_disk
check_services

echo ""
if [[ ${#ALERTS[@]} -gt 0 ]]; then
    echo "=== ALERTS (${#ALERTS[@]}) ==="
    for alert in "${ALERTS[@]}"; do
        echo "  [!] $alert"
    done
    exit 1
else
    echo "All checks passed."
    exit 0
fi

Docker Compose Helper

#!/bin/bash
# dc.sh — Docker Compose helper with common workflows
set -euo pipefail

readonly COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"

log() { echo -e "\033[36m[dc]\033[0m $*"; }

cmd_up() {
    log "Starting services..."
    docker compose -f "$COMPOSE_FILE" up -d --build "$@"
    docker compose -f "$COMPOSE_FILE" ps
}

cmd_down() {
    log "Stopping services..."
    docker compose -f "$COMPOSE_FILE" down "$@"
}

cmd_logs() {
    docker compose -f "$COMPOSE_FILE" logs -f --tail=100 "$@"
}

cmd_restart() {
    log "Restarting: ${*:-all services}"
    docker compose -f "$COMPOSE_FILE" restart "$@"
}

cmd_status() {
    docker compose -f "$COMPOSE_FILE" ps
    echo ""
    docker compose -f "$COMPOSE_FILE" top
}

cmd_shell() {
    local service="${1:?Specify a service}"
    shift
    local shell="${1:-sh}"
    log "Opening $shell in $service..."
    docker compose -f "$COMPOSE_FILE" exec "$service" "$shell"
}

cmd_clean() {
    log "Stopping and removing everything..."
    docker compose -f "$COMPOSE_FILE" down -v --rmi local --remove-orphans
    log "Pruning system..."
    docker system prune -f
}

cmd_backup_db() {
    local service="${1:-db}"
    local output="backup-$(date +%Y%m%d-%H%M%S).sql.gz"
    log "Backing up $service → $output"
    docker compose -f "$COMPOSE_FILE" exec -T "$service" \
        pg_dump -U "${POSTGRES_USER:-postgres}" "${POSTGRES_DB:-app}" \
        | gzip > "$output"
    log "Done: $output ($(du -h "$output" | cut -f1))"
}

# --- Main ---
case "${1:-help}" in
    up)       shift; cmd_up "$@" ;;
    down)     shift; cmd_down "$@" ;;
    logs)     shift; cmd_logs "$@" ;;
    restart)  shift; cmd_restart "$@" ;;
    status)   cmd_status ;;
    shell)    shift; cmd_shell "$@" ;;
    clean)    cmd_clean ;;
    backup)   shift; cmd_backup_db "$@" ;;
    *)
        echo "Usage: $0 {up|down|logs|restart|status|shell|clean|backup}"
        echo ""
        echo "Commands:"
        echo "  up [services]     Build and start services"
        echo "  down [flags]      Stop services"
        echo "  logs [service]    Follow logs"
        echo "  restart [svc]     Restart services"
        echo "  status            Show running services"
        echo "  shell <svc> [sh]  Open shell in service"
        echo "  clean             Remove everything"
        echo "  backup [svc]      Backup PostgreSQL database"
        ;;
esac

CI/CD Pipeline Script

#!/bin/bash
# ci.sh — Simple CI/CD pipeline
set -euo pipefail

log() { echo -e "\n\033[1;34m=== $* ===\033[0m"; }

step_lint() {
    log "Linting"
    npm run lint
}

step_test() {
    log "Running Tests"
    npm test -- --coverage
}

step_build() {
    log "Building"
    npm run build
}

step_docker() {
    log "Building Docker Image"
    local tag="${1:-latest}"
    docker build -t "myapp:$tag" .
    echo "Built: myapp:$tag"
}

step_deploy() {
    log "Deploying"
    local env="${1:-staging}"
    echo "Deploying to $env..."
    # Add your deployment logic here
}

# --- Pipeline ---
STAGE="${1:-all}"

case "$STAGE" in
    lint)    step_lint ;;
    test)    step_test ;;
    build)   step_build ;;
    docker)  shift; step_docker "${1:-latest}" ;;
    deploy)  shift; step_deploy "${1:-staging}" ;;
    all)
        step_lint
        step_test
        step_build
        log "Pipeline Complete"
        ;;
    *)
        echo "Usage: $0 {lint|test|build|docker|deploy|all}"
        exit 1
        ;;
esac

Database Migration Runner

#!/bin/bash
# migrate.sh — Run SQL migrations in order
set -euo pipefail

MIGRATIONS_DIR="${1:-./migrations}"
DB_URL="${DATABASE_URL:?DATABASE_URL is required}"

log() { echo "[$(date '+%H:%M:%S')] $*"; }

if [[ ! -d "$MIGRATIONS_DIR" ]]; then
    echo "Migrations directory not found: $MIGRATIONS_DIR" >&2
    exit 1
fi

log "Running migrations from: $MIGRATIONS_DIR"

APPLIED=0
SKIPPED=0

for migration in "$MIGRATIONS_DIR"/*.sql; do
    [[ -f "$migration" ]] || continue
    filename=$(basename "$migration")

    # Check if already applied (using a migrations table)
    if psql "$DB_URL" -tAc \
        "SELECT 1 FROM schema_migrations WHERE filename='$filename'" 2>/dev/null | grep -q 1; then
        log "  Skip: $filename (already applied)"
        ((SKIPPED++))
        continue
    fi

    log "  Apply: $filename"
    psql "$DB_URL" -f "$migration" || {
        log "  FAILED: $filename"
        exit 1
    }

    # Record migration
    psql "$DB_URL" -c \
        "INSERT INTO schema_migrations (filename, applied_at) VALUES ('$filename', NOW())"
    ((APPLIED++))
done

log "Done. Applied: $APPLIED, Skipped: $SKIPPED"

SSL Certificate Checker

#!/bin/bash
# check-ssl.sh — Check SSL certificate expiry for domains
set -euo pipefail

WARN_DAYS=30
DOMAINS=("example.com" "api.example.com" "app.example.com")

check_cert() {
    local domain="$1"
    local expiry
    local days_left

    expiry=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null \
        | openssl x509 -noout -enddate 2>/dev/null \
        | cut -d= -f2)

    if [[ -z "$expiry" ]]; then
        echo "  [ERROR]   $domain — could not connect"
        return 1
    fi

    days_left=$(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 ))

    if (( days_left <= 0 )); then
        echo "  [EXPIRED] $domain — expired $expiry"
    elif (( days_left <= WARN_DAYS )); then
        echo "  [WARNING] $domain — expires in $days_left days ($expiry)"
    else
        echo "  [OK]      $domain — $days_left days remaining ($expiry)"
    fi
}

echo "=== SSL Certificate Check ==="
for domain in "${DOMAINS[@]}"; do
    check_cert "$domain"
done

Congratulations!

You've completed the Shell Scripting Course! You now know how to:

  • Write structured, strict-mode scripts
  • Handle variables, conditions, loops, and functions
  • Process text with grep, sed, and awk
  • Handle errors gracefully with trap and exit codes
  • Parse arguments and options professionally
  • Build Docker entrypoints with dependency waiting
  • Create real-world automation scripts

Keep scripting, keep automating!