Shell ScriptingReal-World ProjectAdvancedBackupOperationsMay 2026

Shell Scripting Real-World Projects: Backup Orchestrator

Coordinate all server backups in one script — MySQL dumps, file archives, config backups, integrity verification, rsync transfer to remote storage, retention pruning, and Slack reporting.

A backup orchestrator coordinates all the backup jobs for a server — files, databases, configs — into a single script with consistent logging, retention management, remote transfer, and notification. It's the single source of truth for what is backed up and where.

BASH
#!/usr/bin/env bash
# backup_orchestrator.sh — Coordinated backup of all server assets

set -euo pipefail

# ── Configuration ─────────────────────────────────────────
BACKUP_ROOT="/backups"
REMOTE_DEST="backup-server:/archives/$(hostname)"
KEEP_LOCAL_DAYS=7
KEEP_REMOTE_DAYS=30
SLACK_URL="${SLACK_WEBHOOK_URL:-}"
LOG="/var/log/backup-orchestrator.log"

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="${BACKUP_ROOT}/${TIMESTAMP}"
ERRORS=0
START_TIME=$(date +%s)

# ── Helpers ───────────────────────────────────────────────
log()   { echo "[$(date +%H:%M:%S)] $*" | tee -a "${LOG}"; }
slack() { [[ -n "${SLACK_URL}" ]] && curl -s -X POST "${SLACK_URL}" \
            -d "{\"text\":\"$*\"}" &>/dev/null; }

backup_item() {
  local name="$1" cmd="$2" dest="$3"
  log "  Backing up: ${name}"
  if eval "${cmd}" > "${dest}"; then
    local size; size=$(du -sh "${dest}" | cut -f1)
    log "  ✔ ${name}: ${dest} (${size})"
  else
    log "  ✘ ${name}: FAILED"
    (( ERRORS++ ))
  fi
}

mkdir -p "${BACKUP_DIR}"/{db,files,config}
log "=== Backup Orchestrator: ${TIMESTAMP} ==="
slack "🗄 Backup started on $(hostname)"

# ── 1. MySQL databases ────────────────────────────────────
log "── Database backups"
for db in myapp analytics; do
  backup_item "MySQL:${db}" \
    "mysqldump --defaults-file=/etc/myapp/mysql.conf \
     --single-transaction --routines ${db} | gzip -9" \
    "${BACKUP_DIR}/db/${db}_${TIMESTAMP}.sql.gz"
done

# ── 2. Application files ───────────────────────────────────
log "── File backups"
backup_item "uploads" \
  "tar -czf - /opt/myapp/shared/uploads" \
  "${BACKUP_DIR}/files/uploads_${TIMESTAMP}.tar.gz"

backup_item "logs (last 7 days)" \
  "find /var/log/myapp -name '*.log' -mtime -7 | tar -czf - -T -" \
  "${BACKUP_DIR}/files/logs_${TIMESTAMP}.tar.gz"

# ── 3. Configuration ──────────────────────────────────────
log "── Config backups"
backup_item "nginx config" \
  "tar -czf - /etc/nginx" \
  "${BACKUP_DIR}/config/nginx_${TIMESTAMP}.tar.gz"

backup_item "app config" \
  "tar -czf - /etc/myapp" \
  "${BACKUP_DIR}/config/appconfig_${TIMESTAMP}.tar.gz"

# ── 4. Verify all backups ─────────────────────────────────
log "── Verifying backups"
find "${BACKUP_DIR}" -name "*.gz" | while read -r f; do
  if file "${f}" | grep -qE "(gzip|tar)"; then
    log "  ✔ OK: $(basename "${f}")"
  else
    log "  ✘ CORRUPT: $(basename "${f}")"
    (( ERRORS++ ))
  fi
done

# ── 5. Transfer to remote ─────────────────────────────────
log "── Remote transfer"
if rsync -azq --delete "${BACKUP_DIR}/" "${REMOTE_DEST}/${TIMESTAMP}/"; then
  REMOTE_SIZE=$(du -sh "${BACKUP_DIR}" | cut -f1)
  log "  ✔ Transferred ${REMOTE_SIZE} to ${REMOTE_DEST}"
else
  log "  ✘ Remote transfer FAILED"
  (( ERRORS++ ))
fi

# ── 6. Prune old backups ──────────────────────────────────
log "── Pruning"
find "${BACKUP_ROOT}" -maxdepth 1 -type d -mtime "+${KEEP_LOCAL_DAYS}" -exec rm -rf {} +
log "  ✔ Local: kept last ${KEEP_LOCAL_DAYS} days"

# ── Summary ───────────────────────────────────────────────
ELAPSED=$(( $(date +%s) - START_TIME ))
TOTAL_SIZE=$(du -sh "${BACKUP_DIR}" | cut -f1)

log ""
log "=== Backup complete: ${TIMESTAMP} ==="
log "    Size: ${TOTAL_SIZE}  Time: ${ELAPSED}s  Errors: ${ERRORS}"

if (( ERRORS == 0 )); then
  slack "✅ Backup complete on $(hostname): ${TOTAL_SIZE} in ${ELAPSED}s"
else
  slack "⚠ Backup completed WITH ERRORS on $(hostname): ${ERRORS} failures — check ${LOG}"
  exit 1
fi
backup orchestrator
vriddh@prod-01:~/scripts$sudo ./backup_orchestrator.sh
[02:00:01] === Backup Orchestrator: 20260501_020001 ===
[02:00:01] ── Database backups
[02:00:38] ✔ MySQL:myapp: db/myapp_20260501_020001.sql.gz (347M)
[02:00:51] ✔ MySQL:analytics: db/analytics_20260501_020001.sql.gz (128M)
[02:00:51] ── File backups
[02:01:14] ✔ uploads: files/uploads_20260501_020001.tar.gz (2.1G)
[02:01:14] ── Config backups
[02:01:15] ✔ nginx config: config/nginx_20260501_020001.tar.gz (48K)
[02:01:20] ── Remote transfer
[02:01:58] ✔ Transferred 2.6G to backup-server:/archives/prod-01
[02:01:58] === Backup complete: 20260501_020001 ===
[02:01:58] Size: 2.6G Time: 117s Errors: 0
✔ Backup orchestrator rules — Never assume a backup succeeded — always verify with gzip -t or file. Transfer to a remote server over rsync with -az (archive + compress). Prune both local and remote. Send Slack success AND failure notifications — silence from a backup job is not success. Log every step with timestamps to a persistent log file. Run with 0 2 * * * /opt/scripts/backup_orchestrator.sh.