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.
1Complete backup orchestrator
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
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.