← All Tutorials

Automated ViciDial Backups — Database, Recordings & Config

Infrastructure & DevOps Intermediate 14 min read #54

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

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

Issue: MySQL backup is much smaller

Need expert help with your setup?

VoIP infrastructure consulting, AI voice agent integration, monitoring stacks, scaling — I've done it all in production.

Get a Free Consultation