Master complete backup automation for ViciDial production systems, including MariaDB databases, call recordings, configuration files, and verification workflows that run unattended on schedule.
Introduction
ViciDial is a mission-critical dialing platform. Losing your database, recordings, or configuration files can bring revenue-generating operations to a halt in minutes. This tutorial walks you through building a production-grade automated backup system that protects your entire ViciDial infrastructure—not just the database, but also recordings stored in /var/spool/asterisk/monitor/ and configuration files scattered across /etc/asterisk/.
You'll learn to create a robust vicidial backup script that runs daily, compresses efficiently, maintains version history, validates integrity, and alerts you to failures before they become disasters.
Prerequisites
- ViciDial 2.12+ (tested on 2.14.1, but compatible with 2.12 and above)
- Asterisk 11+ running on CentOS 7/8, Rocky Linux, or Ubuntu 18.04+
- Root or sudoer access on the ViciDial server
- MariaDB 5.5+ or MySQL 5.7+ with direct shell access
- Minimum 200GB free disk space for backup retention (calculate: 7 days × daily full backup size)
- Cron daemon running (verify:
systemctl status crondorsystemctl status cron) - Basic bash scripting knowledge (we'll provide complete scripts)
- Confirmation that
/etc/asterisk/sip-vicidial.confexists and is readable
Understanding ViciDial's Critical Data
Before backing up, know what you're protecting:
| Component | Location | Size | Criticality | Frequency |
|---|---|---|---|---|
| Asterisk DB | /var/lib/mysql/asterisk/ |
500MB–5GB | Critical | Every backup |
| Call Recordings | /var/spool/asterisk/monitor/ |
1–50GB+ | Critical | Every backup |
| SIP Config | /etc/asterisk/sip-vicidial.conf |
50KB–500KB | Critical | Every backup |
| Extensions | /etc/asterisk/extensions-vicidial.conf |
100KB–2MB | Critical | Every backup |
| Agent Configs | /usr/share/astguiclient/config.php |
5–50KB | Important | Every backup |
| Logs | /var/log/asterisk/ |
100MB–1GB | Optional | Weekly |
The asterisk database contains all agent accounts (vicidial_users), campaigns (vicidial_campaigns), call logs (vicidial_log), and dialing lists (vicidial_list). Losing this is unrecoverable without backups.
Section 1: Creating the Backup Directory Structure
Start by creating isolated storage for backups with proper permissions:
#!/bin/bash
# Setup backup storage directories
BACKUP_ROOT="/mnt/backups/vicidial"
MYSQL_BACKUPS="${BACKUP_ROOT}/mysql"
RECORDING_BACKUPS="${BACKUP_ROOT}/recordings"
CONFIG_BACKUPS="${BACKUP_ROOT}/configs"
LOG_BACKUPS="${BACKUP_ROOT}/logs"
ARCHIVE_BACKUPS="${BACKUP_ROOT}/archives"
# Create directory structure
mkdir -p ${MYSQL_BACKUPS}/{daily,weekly,monthly}
mkdir -p ${RECORDING_BACKUPS}/{daily,weekly}
mkdir -p ${CONFIG_BACKUPS}/{daily,weekly}
mkdir -p ${LOG_BACKUPS}/daily
mkdir -p ${ARCHIVE_BACKUPS}
# Set restrictive permissions (backup files contain sensitive data)
chmod 700 ${BACKUP_ROOT}
chmod 700 ${MYSQL_BACKUPS}
chmod 700 ${RECORDING_BACKUPS}
chmod 700 ${CONFIG_BACKUPS}
chmod 700 ${LOG_BACKUPS}
# Create backup user for cron (optional but recommended)
useradd -M -s /bin/false vicidial-backup 2>/dev/null || true
chown -R vicidial-backup:vicidial-backup ${BACKUP_ROOT}
echo "Backup directories initialized at ${BACKUP_ROOT}"
Run this once as root to initialize the backup structure. In production, use a dedicated partition (e.g., /mnt/backups on a separate mount) to prevent backup disk issues from affecting the ViciDial system.
Section 2: Building the Core vicidial backup script
The centerpiece of your backup strategy is a comprehensive bash script that handles all components. Here's a production-grade implementation:
#!/bin/bash
#
# ViciDial Complete Backup Script
# Backs up: MySQL database, recordings, configs, logs
# Usage: ./vicidial-backup.sh [full|quick]
# Full = all components, Quick = DB + configs only
#
set -o pipefail
trap 'echo "Backup interrupted at $(date)" | mail -s "ViciDial Backup Error" admin@example.com' EXIT
# ===== CONFIGURATION =====
BACKUP_ROOT="/mnt/backups/vicidial"
MYSQL_BACKUPS="${BACKUP_ROOT}/mysql"
RECORDING_BACKUPS="${BACKUP_ROOT}/recordings"
CONFIG_BACKUPS="${BACKUP_ROOT}/configs"
ARCHIVE_BACKUPS="${BACKUP_ROOT}/archives"
MYSQL_USER="root"
MYSQL_PASSWORD="" # Leave empty to use ~/.my.cnf
MYSQL_HOST="localhost"
MYSQL_DB="asterisk"
ASTERISK_USER="asterisk"
BACKUP_USER="vicidial-backup"
LOG_FILE="${BACKUP_ROOT}/backup.log"
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_TIMESTAMP=$(date +%Y-%m-%d\ %H:%M:%S)
RETENTION_DAYS=7
COMPRESSION="pigz" # faster than gzip; fallback to gzip
MAX_BACKUP_SIZE="50G"
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# ===== FUNCTION: Logging =====
log_info() {
echo "[${BACKUP_TIMESTAMP}] [INFO] $1" | tee -a ${LOG_FILE}
}
log_error() {
echo -e "${RED}[${BACKUP_TIMESTAMP}] [ERROR] $1${NC}" | tee -a ${LOG_FILE}
}
log_success() {
echo -e "${GREEN}[${BACKUP_TIMESTAMP}] [SUCCESS] $1${NC}" | tee -a ${LOG_FILE}
}
# ===== FUNCTION: Check Prerequisites =====
check_prerequisites() {
log_info "Checking prerequisites..."
# Check if running as root or sudoer
if [[ ${EUID} -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
# Check required commands
for cmd in mysqldump mysql tar ${COMPRESSION:-gzip} md5sum; do
if ! command -v ${cmd} &> /dev/null; then
log_error "Required command not found: ${cmd}"
exit 1
fi
done
# Check ViciDial directories
for dir in /etc/asterisk /var/spool/asterisk/monitor /usr/share/astguiclient; do
if [[ ! -d ${dir} ]]; then
log_error "ViciDial directory not found: ${dir}"
exit 1
fi
done
# Test MySQL connectivity
if ! mysql -h ${MYSQL_HOST} -u ${MYSQL_USER} -e "SELECT 1;" &> /dev/null; then
log_error "Cannot connect to MySQL at ${MYSQL_HOST}"
exit 1
fi
# Check backup storage space
AVAILABLE_SPACE=$(df ${BACKUP_ROOT} | awk 'NR==2 {print $4}')
REQUIRED_SPACE=$(($(du -sb ${RECORDING_BACKUPS} 2>/dev/null | awk '{print $1}') + 10737418240)) # +10GB buffer
if (( AVAILABLE_SPACE < REQUIRED_SPACE )); then
log_error "Insufficient disk space. Available: ${AVAILABLE_SPACE}KB, Required: ${REQUIRED_SPACE}KB"
exit 1
fi
log_success "All prerequisites met"
}
# ===== FUNCTION: Backup MySQL Database =====
backup_mysql() {
log_info "Starting MySQL backup of database: ${MYSQL_DB}"
local BACKUP_FILE="${MYSQL_BACKUPS}/daily/asterisk_${BACKUP_DATE}.sql"
local BACKUP_COMPRESSED="${BACKUP_FILE}.${COMPRESSION:-gz}"
# Use mysqldump with optimizations for large databases
if ! mysqldump \
-h ${MYSQL_HOST} \
-u ${MYSQL_USER} \
--single-transaction \
--lock-tables=false \
--routines \
--triggers \
--events \
--master-data=2 \
--flush-logs \
${MYSQL_DB} | \
${COMPRESSION:-gzip} > ${BACKUP_COMPRESSED}; then
log_error "MySQL backup failed"
rm -f ${BACKUP_COMPRESSED}
return 1
fi
# Verify backup integrity
local BACKUP_SIZE=$(du -h ${BACKUP_COMPRESSED} | awk '{print $1}')
local LINE_COUNT=$(${COMPRESSION:-gzip} -dc ${BACKUP_COMPRESSED} | wc -l)
if (( LINE_COUNT < 1000 )); then
log_error "MySQL backup appears incomplete (only ${LINE_COUNT} lines)"
rm -f ${BACKUP_COMPRESSED}
return 1
fi
# Calculate checksum
md5sum ${BACKUP_COMPRESSED} > "${BACKUP_COMPRESSED}.md5"
log_success "MySQL backup completed: ${BACKUP_SIZE} (${LINE_COUNT} lines)"
echo "${BACKUP_COMPRESSED}" >> ${LOG_FILE}
return 0
}
# ===== FUNCTION: Backup Call Recordings =====
backup_recordings() {
log_info "Starting call recordings backup"
local RECORDINGS_DIR="/var/spool/asterisk/monitor"
local BACKUP_FILE="${RECORDING_BACKUPS}/daily/recordings_${BACKUP_DATE}.tar.${COMPRESSION:-gz}"
# Find recordings modified in last 24 hours
local RECORDING_COUNT=$(find ${RECORDINGS_DIR} -type f -mtime -1 | wc -l)
log_info "Found ${RECORDING_COUNT} recordings from last 24 hours"
if (( RECORDING_COUNT == 0 )); then
log_info "No new recordings to backup"
return 0
fi
# Backup with tar + compression, excluding temp files
if ! tar \
--exclude='*.tmp' \
--exclude='*.lock' \
-C ${RECORDINGS_DIR} \
-cf - \
$(find ${RECORDINGS_DIR} -type f -mtime -1 -printf '%f\n' | head -c 100000) 2>/dev/null | \
${COMPRESSION:-gzip} > ${BACKUP_FILE}; then
log_error "Recordings backup failed"
rm -f ${BACKUP_FILE}
return 1
fi
local BACKUP_SIZE=$(du -h ${BACKUP_FILE} | awk '{print $1}')
md5sum ${BACKUP_FILE} > "${BACKUP_FILE}.md5"
log_success "Recordings backup completed: ${BACKUP_SIZE}"
echo "${BACKUP_FILE}" >> ${LOG_FILE}
return 0
}
# ===== FUNCTION: Backup Configuration Files =====
backup_configs() {
log_info "Starting configuration files backup"
local BACKUP_FILE="${CONFIG_BACKUPS}/daily/configs_${BACKUP_DATE}.tar.gz"
# Tar all critical config files
if ! tar \
--gzip \
-cf ${BACKUP_FILE} \
/etc/asterisk/sip-vicidial.conf \
/etc/asterisk/extensions-vicidial.conf \
/etc/asterisk/voicemail-vicidial.conf \
/usr/share/astguiclient/config.php \
/etc/asterisk/manager.conf \
/etc/asterisk/iax.conf \
2>/dev/null; then
log_error "Config backup failed"
rm -f ${BACKUP_FILE}
return 1
fi
local BACKUP_SIZE=$(du -h ${BACKUP_FILE} | awk '{print $1}')
md5sum ${BACKUP_FILE} > "${BACKUP_FILE}.md5"
log_success "Config backup completed: ${BACKUP_SIZE}"
echo "${BACKUP_FILE}" >> ${LOG_FILE}
return 0
}
# ===== FUNCTION: Cleanup Old Backups =====
cleanup_old_backups() {
log_info "Cleaning up backups older than ${RETENTION_DAYS} days"
local DELETED_COUNT=0
for backup_dir in ${MYSQL_BACKUPS}/daily ${RECORDING_BACKUPS}/daily ${CONFIG_BACKUPS}/daily; do
if [[ -d ${backup_dir} ]]; then
DELETED_COUNT=$(find ${backup_dir} -type f -mtime +${RETENTION_DAYS} -delete -print | wc -l)
log_info "Deleted ${DELETED_COUNT} backups from ${backup_dir}"
fi
done
log_success "Cleanup completed"
}
# ===== FUNCTION: Generate Backup Report =====
generate_report() {
log_info "Generating backup report"
local REPORT_FILE="${BACKUP_ROOT}/backup_report_${BACKUP_DATE}.txt"
cat > ${REPORT_FILE} << EOF
=====================================
ViciDial Backup Report
Generated: ${BACKUP_TIMESTAMP}
=====================================
BACKUP SUMMARY:
$(du -sh ${MYSQL_BACKUPS}/daily ${RECORDING_BACKUPS}/daily ${CONFIG_BACKUPS}/daily 2>/dev/null)
TOTAL BACKUP SIZE:
$(du -sh ${BACKUP_ROOT} | awk '{print $1}')
MYSQL BACKUPS (Last 7 Days):
$(ls -lh ${MYSQL_BACKUPS}/daily/ 2>/dev/null | tail -7)
RECORDING BACKUPS (Last 7 Days):
$(ls -lh ${RECORDING_BACKUPS}/daily/ 2>/dev/null | tail -7)
CONFIG BACKUPS (Last 7 Days):
$(ls -lh ${CONFIG_BACKUPS}/daily/ 2>/dev/null | tail -7)
BACKUP INTEGRITY CHECKS:
$(for f in ${MYSQL_BACKUPS}/daily/*.md5; do
if md5sum -c ${f} &>/dev/null; then
echo "✓ $(basename ${f%.*})"
else
echo "✗ FAILED: $(basename ${f%.*})"
fi
done)
EOF
log_success "Report generated: ${REPORT_FILE}"
cat ${REPORT_FILE} >> ${LOG_FILE}
}
# ===== FUNCTION: Send Alert Email =====
send_alert() {
local subject="$1"
local body="$2"
local recipient="admin@example.com"
echo "${body}" | mail -s "${subject}" ${recipient}
}
# ===== MAIN EXECUTION =====
main() {
local BACKUP_MODE="${1:-full}"
log_info "========== ViciDial Backup Started =========="
log_info "Backup Mode: ${BACKUP_MODE}"
check_prerequisites || exit 1
case ${BACKUP_MODE} in
full)
backup_mysql || exit 1
backup_recordings || exit 1
backup_configs || exit 1
cleanup_old_backups
generate_report
log_success "========== Full Backup Completed =========="
;;
quick)
backup_mysql || exit 1
backup_configs || exit 1
log_success "========== Quick Backup Completed =========="
;;
*)
log_error "Unknown backup mode: ${BACKUP_MODE}"
exit 1
;;
esac
}
main "$@"
Save this as /root/vicidial-backup.sh and make it executable:
chmod 750 /root/vicidial-backup.sh
Testing the Backup Script
Run the script manually first to ensure it works:
/root/vicidial-backup.sh full
Check the log file:
tail -50 /mnt/backups/vicidial/backup.log
Verify backup files were created:
ls -lh /mnt/backups/vicidial/mysql/daily/
ls -lh /mnt/backups/vicidial/configs/daily/
Section 3: Scheduling with Cron
Create a cron job to run the backup script automatically every day at 2 AM:
cat > /etc/cron.d/vicidial-backup << 'EOF'
# ViciDial Automated Backup
# Runs daily full backup at 2 AM
# Weekly archives on Sunday at 3 AM
0 2 * * * root /root/vicidial-backup.sh full >> /var/log/vicidial-backup.log 2>&1
0 3 * * 0 root /root/vicidial-backup.sh full && cd /mnt/backups/vicidial && tar -czf archives/weekly_$(date +\%Y\%m\%d).tar.gz mysql/daily configs/daily
EOF
chmod 644 /etc/cron.d/vicidial-backup
Verify the cron job was added:
crontab -l | grep vicidial
# or
cat /etc/cron.d/vicidial-backup
To test cron functionality without waiting 24 hours, manually run at a scheduled time:
# Run backup in 2 minutes
echo "0 $(date -d '+2 minutes' +%H:%M) * * * root /root/vicidial-backup.sh full" | at now
# Check scheduled jobs
atq
Section 4: Backup Verification & Integrity Checks
Always verify your backups can be restored. Here's a validation script:
#!/bin/bash
# Backup Verification Script - Run weekly
BACKUP_ROOT="/mnt/backups/vicidial"
VERIFY_LOG="${BACKUP_ROOT}/verify.log"
verify_mysql_backup() {
local backup_file="$1"
echo "[$(date)] Verifying MySQL backup: ${backup_file}" >> ${VERIFY_LOG}
# Check file integrity with md5
if [[ -f "${backup_file}.md5" ]]; then
if md5sum -c "${backup_file}.md5" &>> ${VERIFY_LOG}; then
echo "[$(date)] ✓ MD5 check passed" >> ${VERIFY_LOG}
else
echo "[$(date)] ✗ MD5 check failed!" >> ${VERIFY_LOG}
return 1
fi
fi
# Try to restore to a temporary database for verification
local temp_db="verify_test_$(date +%s)"
if gunzip -c "${backup_file}" | mysql -u root &>> ${VERIFY_LOG}; then
echo "[$(date)] ✓ Backup structure is valid" >> ${VERIFY_LOG}
else
echo "[$(date)] ✗ Backup restore test failed" >> ${VERIFY_LOG}
return 1
fi
return 0
}
verify_config_backup() {
local backup_file="$1"
local temp_dir=$(mktemp -d)
echo "[$(date)] Verifying config backup: ${backup_file}" >> ${VERIFY_LOG}
if tar -tzf "${backup_file}" &>> ${VERIFY_LOG}; then
echo "[$(date)] ✓ Config tar is valid" >> ${VERIFY_LOG}
rm -rf ${temp_dir}
return 0
else
echo "[$(date)] ✗ Config tar is corrupted" >> ${VERIFY_LOG}
return 1
fi
}
# Find most recent backups and verify
echo "[$(date)] ===== Backup Verification Run =====" >> ${VERIFY_LOG}
LATEST_MYSQL=$(ls -t ${BACKUP_ROOT}/mysql/daily/*.sql.gz 2>/dev/null | head -1)
LATEST_CONFIG=$(ls -t ${BACKUP_ROOT}/configs/daily/*.tar.gz 2>/dev/null | head -1)
if [[ -n ${LATEST_MYSQL} ]]; then
verify_mysql_backup ${LATEST_MYSQL}
fi
if [[ -n ${LATEST_CONFIG} ]]; then
verify_config_backup ${LATEST_CONFIG}
fi
# Check backup age (warn if > 24 hours old)
LATEST_BACKUP_TIME=$(ls -t ${BACKUP_ROOT}/mysql/daily/ | head -1 | xargs -I {} stat -c %Y ${BACKUP_ROOT}/mysql/daily/{})
CURRENT_TIME=$(date +%s)
BACKUP_AGE=$(( (CURRENT_TIME - LATEST_BACKUP_TIME) / 3600 ))
if (( BACKUP_AGE > 24 )); then
echo "[$(date)] ⚠ WARNING: Last backup is ${BACKUP_AGE} hours old!" >> ${VERIFY_LOG}
fi
Save as /root/verify-vicidial-backups.sh and run weekly:
chmod 750 /root/verify-vicidial-backups.sh
# Add to cron (Sunday at 4 AM)
echo "0 4 * * 0 root /root/verify-vicidial-backups.sh" >> /etc/cron.d/vicidial-backup
Section 5: Restoring from Backup
When disaster strikes, know how to recover. Here's the restoration procedure:
Restore MySQL Database
#!/bin/bash
# Restore MySQL from backup
# Usage: ./restore-mysql.sh /path/to/backup.sql.gz
BACKUP_FILE="$1"
if [[ ! -f ${BACKUP_FILE} ]]; then
echo "Backup file not found: ${BACKUP_FILE}"
exit 1
fi
echo "Preparing to restore from: ${BACKUP_FILE}"
echo "This will DROP the current 'asterisk' database. Continue? [y/N]"
read -r response
if [[ ! "${response}" =~ ^[yY]$ ]]; then
echo "Restore cancelled"
exit 0
fi
# Stop ViciDial services to prevent connection conflicts
systemctl stop vicidial asterisk 2>/dev/null
# Create backup of current database before restore
mysqldump asterisk > /tmp/asterisk_backup_$(date +%s).sql
# Drop and restore
mysql -u root << EOF
DROP DATABASE IF EXISTS asterisk;
CREATE DATABASE asterisk DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
EOF
# Restore from backup
gunzip -c ${BACKUP_FILE} | mysql -u root asterisk
# Verify restoration
if mysql -u root -e "SELECT COUNT(*) FROM asterisk.vicidial_users LIMIT 1;" &>/dev/null; then
echo "✓ Database restored successfully"
systemctl start asterisk vicidial
else
echo "✗ Restore verification failed"
exit 1
fi
Restore Configuration Files
# Extract config from backup
tar -tzf /mnt/backups/vicidial/configs/daily/configs_YYYYMMDD_HHMMSS.tar.gz
# Restore specific config
tar -xzf /mnt/backups/vicidial/configs/daily/configs_YYYYMMDD_HHMMSS.tar.gz \
-C / \
--strip-components=1 \
etc/asterisk/sip-vicidial.conf
# Or restore all configs
tar -xzf /mnt/backups/vicidial/configs/daily/configs_YYYYMMDD_HHMMSS.tar.gz -C /
# Reload Asterisk configuration
asterisk -rx "reload"
Restore Call Recordings
# List recordings in backup
tar -tzf /mnt/backups/vicidial/recordings/daily/recordings_YYYYMMDD_HHMMSS.tar.gz | head -20
# Extract all recordings
tar -xzf /mnt/backups/vicidial/recordings/daily/recordings_YYYYMMDD_HHMMSS.tar.gz \
-C /var/spool/asterisk/monitor/
# Fix ownership
chown -R asterisk:asterisk /var/spool/asterisk/monitor/
Section 6: Monitoring & Alerting
Add health checks that email alerts if backups fail:
#!/bin/bash
# Backup Health Monitor - Run hourly
BACKUP_ROOT="/mnt/backups/vicidial"
ALERT_EMAIL="admin@example.com"
ALERT_THRESHOLD=86400 # 24 hours in seconds
check_backup_freshness() {
local backup_dir="$1"
local backup_name="$2"
if [[ ! -d ${backup_dir} ]]; then
send_alert "CRITICAL" "${backup_name} backup directory missing: ${backup_dir}"
return 1
fi
local latest_backup=$(find ${backup_dir} -type f -name "*.gz" -o -name "*.sql" | sort -r | head -1)
if [[ -z ${latest_backup} ]]; then
send_alert "CRITICAL" "No backups found in ${backup_name}"
return 1
fi
local backup_time=$(stat -c %Y "${latest_backup}")
local current_time=$(date +%s)
local backup_age=$((current_time - backup_time))
if (( backup_age > ALERT_THRESHOLD )); then
send_alert "WARNING" "${backup_name} backup is ${backup_age} seconds old"
return 1
fi
return 0
}
send_alert() {
local severity="$1"
local message="$2"
echo "${message}" | mail -s "[${severity}] ViciDial Backup Alert" ${ALERT_EMAIL}
echo "[$(date)] ${severity}: ${message}" >> ${BACKUP_ROOT}/health.log
}
# Run checks
check_backup_freshness "${BACKUP_ROOT}/mysql/daily" "MySQL"
check_backup_freshness "${BACKUP_ROOT}/configs/daily" "Config"
check_backup_freshness "${BACKUP_ROOT}/recordings/daily" "Recordings"
Add to cron (runs every hour):
echo "0 * * * * root /root/backup-health-monitor.sh" >> /etc/cron.d/vicidial-backup
Section 7: Offsite Backup Replication
For true disaster recovery, replicate backups to remote storage:
#!/bin/bash
# Replicate backups to remote server via SFTP
BACKUP_ROOT="/mnt/backups/vicidial"
REMOTE_HOST="backup-server.example.com"
REMOTE_USER="vicidial-backup"
REMOTE_PATH="/backups/vicidial/"
SFTP_KEY="/root/.ssh/id_rsa_vicidial_backup"
replicate_to_remote() {
local local_file="$1"
local remote_file="$2"
if ! scp -i ${SFTP_KEY} -q ${local_file} ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}${remote_file}; then
echo "[$(date)] Failed to replicate ${local_file}" >> ${BACKUP_ROOT}/replicate.log
return 1
fi
echo "[$(date)] Replicated ${local_file}" >> ${BACKUP_ROOT}/replicate.log
return 0
}
# Replicate today's backups
for backup in $(find ${BACKUP_ROOT}/mysql/daily -type f -mtime -1); do
replicate_to_remote "${backup}" "mysql/$(basename ${backup})"
done
for backup in $(find ${BACKUP_ROOT}/configs/daily -type f -mtime -1); do
replicate_to_remote "${backup}" "configs/$(basename ${backup})"
done
Add to cron (runs at 3 AM daily):
echo "0 3 * * * root /root/replicate-backups.sh" >> /etc/cron.d/vicidial-backup
Section 8: Troubleshooting
Issue: "mysqldump: command not found"
Solution: Install MySQL client tools:
# CentOS/Rocky
yum install -y mariadb mysql
# Ubuntu/Debian
apt-get install -y mariadb-client mysql-client
Issue: "Permission denied" when writing to backup directory
Solution: Check ownership and permissions:
ls -ld /mnt/backups/vicidial
chown -R vicidial-backup:vicidial-backup /mnt/backups/vicidial
chmod 750 /mnt/backups/vicidial
Issue: "Backup file is incomplete" or corrupted tar
Solution: The backup was interrupted. Check:
# Verify backup file size is reasonable
ls -lh /mnt/backups/vicidial/mysql/daily/
# Check if disk was full
df -h /mnt/backups
# Review backup log for errors
tail -100 /mnt/backups/vicidial/backup.log | grep -i error
Issue: Cron job not running
Solution: Verify cron is enabled and check logs:
# Check if crond is running
systemctl status crond # CentOS/Rocky
systemctl status cron # Ubuntu/Debian
# Check cron logs
tail -50 /var/log/cron # CentOS/Rocky
grep CRON /var/log/syslog | tail -50 # Ubuntu/Debian
# Test cron environment
env -i /bin/sh -c /root/vicidial-backup.sh
Issue: "Database already exists" error during test restore
Solution: Clean up test databases before each verification:
mysql -u root -e "DROP DATABASE IF EXISTS verify_test_*;"
Issue: Backups consuming too much disk space
Solution: Reduce retention or increase compression:
# Change RETENTION_DAYS in vicidial-backup.sh from 7 to 3
sed -i 's/RETENTION_DAYS=7/RETENTION_DAYS=3/' /root/vicidial-backup.sh
# Switch to xz compression for better ratio (slower)
sed -i 's/COMPRESSION="pigz"/COMPRESSION="xz"/' /root/vicidial-backup.sh