Lesson 11: Real-World Scripts Collection
Goal: Complete, production-ready scripts you can adapt for your own projects.
Table of Contents
- Automated Backup Script
- Deployment Script
- Log Rotation Script
- System Monitoring & Alerts
- Docker Compose Helper
- CI/CD Pipeline Script
- Database Migration Runner
- 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!