ViciDial MySQL Backup and Restore Automation Guide

vicidial mysql backup automation - custom-vd-backup-featured.png

Our crashed
MySQL table repair guide
covers the emergency response when something has already gone wrong with
ViciDial’s database. This guide covers the preventive half of that same problem: a proper
automated backup and restore setup so a corrupted table, a bad migration, or a failed disk
doesn’t cost more than a few hours of call data. The vicidial_log, vicidial_list,
and campaign configuration tables in a live install are exactly the kind of data that’s expensive to lose
and cheap to back up, provided the backup job is actually running and actually tested.

This guide covers a mysqldump-based backup script, a systemd timer to run it nightly instead
of relying on cron, retention pruning so backups don’t quietly fill the disk, and a restore script with a
built-in confirmation guard so a restore never runs by accident against a live production database.

Write the Backup Script

Use mysqldump with --single-transaction so the backup doesn’t need to lock tables on a live system — this matters specifically for ViciDial since locking vicidial_log or vicidial_live_agents during a backup would stall live call logging for however long the dump takes. Compress the output as it’s written rather than compressing afterward, and prune anything older than a retention window so the backup directory doesn’t grow unbounded:

bashvicidial-mysql-backup.sh
#!/bin/bash
# vicidial-mysql-backup.sh -- dumps the asterisk DB and prunes old backups.
set -euo pipefail

BACKUP_DIR="/var/backups/vicidial"
DATE=$(date +%Y%m%d-%H%M)
RETENTION_DAYS=14

mkdir -p "$BACKUP_DIR"

mysqldump --single-transaction --quick --routines \
  -u backup_user -p"$(cat /etc/vicidial-backup.pass)" \
  asterisk | gzip > "$BACKUP_DIR/asterisk-$DATE.sql.gz"

find "$BACKUP_DIR" -name "asterisk-*.sql.gz" -mtime +$RETENTION_DAYS -delete

echo "Backup complete: asterisk-$DATE.sql.gz"

Store the backup MySQL user’s password in a root-only-readable file like /etc/vicidial-backup.pass rather than embedding it directly in the script, and create that backup user with only SELECT, LOCK TABLES, and SHOW VIEW privileges — it never needs write access to the database it’s backing up.

Diagram of mysqldump writing a compressed, retention-pruned backup
A single-transaction dump that doesn’t stall live logging

Schedule It With a Systemd Timer Instead of Cron

A systemd timer paired with a oneshot service gives you built-in logging through journalctl, automatic retry behavior, and a Persistent=true option that runs a missed backup on next boot if the server happened to be down at 2:30am — none of which plain cron gives you without extra scripting:

inivicidial-mysql-backup.timer
# /etc/systemd/system/vicidial-mysql-backup.timer
[Unit]
Description=Nightly ViciDial MySQL backup

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true

[Install]
WantedBy=timers.target
inivicidial-mysql-backup.service
# /etc/systemd/system/vicidial-mysql-backup.service
[Unit]
Description=ViciDial MySQL backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/vicidial-mysql-backup.sh

Enable and start the timer, then confirm it’s actually scheduled rather than assuming the unit files alone are enough:

bashterminal
systemctl daemon-reload
systemctl enable --now vicidial-mysql-backup.timer
systemctl list-timers | grep vicidial
Diagram of a systemd timer triggering the nightly backup service
Systemd timers give retry and persistent-run behavior cron doesn’t

Write a Restore Script That Refuses to Run by Accident

A restore script is the single most dangerous script on a ViciDial server — run it against the wrong environment and you’ve just overwritten live production data with an old backup. Require the backup filename as an explicit argument rather than defaulting to “latest,” and force a typed confirmation before anything destructive happens:

bashvicidial-mysql-restore.sh
#!/bin/bash
# vicidial-mysql-restore.sh -- restores a named backup file after confirmation.
set -euo pipefail

FILE="$1"
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
  echo "Usage: $0 /var/backups/vicidial/asterisk-YYYYMMDD-HHMM.sql.gz"
  exit 1
fi

read -p "This will overwrite the live asterisk database. Type YES to continue: " CONFIRM
if [ "$CONFIRM" != "YES" ]; then
  echo "Aborted."
  exit 1
fi

systemctl stop asterisk
gunzip -c "$FILE" | mysql -u backup_user -p"$(cat /etc/vicidial-backup.pass)" asterisk
systemctl start asterisk

echo "Restore complete from $FILE"

Stopping Asterisk before the restore and starting it again afterward avoids a window where live calls are being logged against a database mid-restore, which would produce a corrupted mix of old and new data rather than a clean point-in-time recovery.

Flowchart of a guarded restore requiring explicit confirmation
A restore script that requires a filename and a typed confirmation

Store Backups Off the Server They Were Taken From

A backup sitting on the same disk as the database it backs up survives a bad DELETE statement but not a disk failure, so sync the backup directory to a separate host or object storage on a schedule — the same offsite-archiving habit our call recording storage guide recommends for recordings applies just as directly to database backups. A simple rsync or cloud-storage CLI call appended to the end of the backup script, run after the local dump succeeds, is enough for most single-cluster setups; a larger operation may want a dedicated backup server pulling from multiple sources instead.

Diagram of backups syncing from the DB server to offsite storage
Local backups protect against bad queries, not disk failure

Test the Restore Path, Not Just the Backup Job

A backup job that’s been running successfully every night for a year tells you nothing about whether the resulting file can actually be restored — test the restore script against a separate, non-production MySQL instance on a recurring schedule, confirming the restored database has the expected tables and a row count in the right ballpark for when the backup was taken. Treat a backup you’ve never successfully restored from as an untested one, no matter how long the backup job itself has been running without errors — the actual value of a backup strategy is proven at restore time, not at backup time.

Diagram of periodically test-restoring backups to a non-production instance
An untested restore path is not a real disaster-recovery plan

Coordinate Backup Timing With Cluster Maintenance

If running the multi-server cluster architecture from our companion guide, schedule the nightly backup during the same quiet dial-window hours used for planned maintenance, and confirm the backup job runs against the central DB server directly rather than being duplicated on every dial server pointed at the same database — a backup job accidentally scheduled on multiple servers against the same DB produces redundant dumps and wastes both database load and disk space for no additional safety.

Diagram of one backup job against the central DB server, not duplicated per dial server
One backup job per cluster, run against the DB server directly

Alert on Backup Failure, Don’t Assume Silence Means Success

Wire the backup service’s failure state into whatever alerting the rest of the infrastructure already uses — a simple systemd OnFailure= directive pointed at a notification script is enough for most setups — so a failed backup surfaces immediately rather than being discovered the day someone actually needs to restore from one. A backup job that fails silently for weeks is functionally identical to having no backup at all, just with a false sense of security attached to it.

Diagram of a failed backup triggering an alert instead of failing silently
Silence is not the same as success — alert on failure explicitly

Document the Recovery Procedure Alongside the Scripts

Keep a short, current runbook next to the backup and restore scripts covering exactly which server to run the restore from, where the backup files and offsite copies live, and who needs to approve a production restore before it runs — the systemd units and scripts in this guide are only half the plan without the human process wrapped around them. Review and re-test that runbook whenever the cluster architecture changes, since a runbook written for a single-server install quietly goes stale the moment the database moves to its own dedicated server in a cluster setup.

Checklist-style runbook documenting the recovery procedure
The scripts are only half the plan without a documented runbook

Backup Automation Checklist

Dump          mysqldump --single-transaction, compressed, retention-pruned
Schedule      systemd timer + oneshot service, not plain cron
Restore       requires explicit filename + typed confirmation, stops Asterisk first
Offsite       synced to a separate host/object storage, not left local-only
Test restore  run against a non-production instance on a recurring schedule
Alerting      OnFailure= wired to notifications, not silent failure
Runbook       documented and kept current alongside the scripts

Related tutorials

Image credits: All illustrations are original terminal/config mockups created for
Gnome IT Solutions — not screenshots from any third-party site. Tutorial text © Gnome IT Solutions.