Overview
This guide demonstrates how to implement automated MySQL database backups on Linux systems using cron jobs and bash scripting. The solution provides:
- Automated scheduling - Unattended backups at specified intervals
- Compression - Reduced storage footprint using gzip
- Retention policy - Automatic cleanup of old backups
- Secure authentication - Credentials stored in a protected MySQL options file
- Transaction safety - Consistent backups using
--single-transaction
Why Automated Backups Matter
Database backups are critical for:
- Disaster recovery - Protection against hardware failures, data corruption
- Human error mitigation - Recovery from accidental deletions or modifications
- Compliance requirements - Many regulations mandate regular backup procedures
- Development workflows - Safe testing environments with production data snapshots
Create bash script for backup
The backup script iterates through all user databases (excluding system databases) and creates compressed SQL dumps. Each backup is timestamped for easy identification and recovery.
1#!/bin/bash2
3BACKUP_DIR="/root/mysql-backup"4DATE=$(date +"%Y-%m-%d_%H-%M")5
6# Get list of databases (skip system DBs)7DATABASES=$(mysql --defaults-file=/root/.my.cnf -e "SHOW DATABASES;" | grep -Ev "Database|information_schema|performance_schema|mysql|sys")8
9# Create backup directory if missing10mkdir -p "$BACKUP_DIR"11
12for DB in $DATABASES; do13 FILE="$BACKUP_DIR/${DB}-${DATE}.sql"14
15 echo "Backing up $DB ..."16
17 mysqldump --defaults-file=/root/.my.cnf \18 --single-transaction \19 --quick \20 --routines \21 "$DB" > "$FILE"22
23 gzip "$FILE"24done25
26# Optional: remove backups older than 7 days27find "$BACKUP_DIR" -type f -name "*.gz" -mtime +7 -deleteScript Breakdown
Key mysqldump Options Explained:
--defaults-file=/root/.my.cnf- Reads MySQL credentials from a secure configuration file--single-transaction- Creates a consistent snapshot without locking tables (InnoDB only)--quick- Retrieves rows one at a time instead of buffering entire tables in memory--routines- Includes stored procedures and functions in the dump
Why Skip System Databases:
The script excludes:
information_schema- Virtual database containing metadata (read-only)performance_schema- Performance monitoring data (transient)mysql- System database (requires special handling)sys- System views and procedures (can be regenerated)
Alternative: Backup with Additional Options
For advanced scenarios, you might want to include triggers, events, and generate different formats:
1#!/bin/bash2
3BACKUP_DIR="/root/mysql-backup"4DATE=$(date +"%Y-%m-%d_%H-%M")5DATABASES=$(mysql --defaults-file=/root/.my.cnf -e "SHOW DATABASES;" | grep -Ev "Database|information_schema|performance_schema|mysql|sys")6
7mkdir -p "$BACKUP_DIR"8
9for DB in $DATABASES; do10 FILE="$BACKUP_DIR/${DB}-${DATE}.sql"11 12 echo "Backing up $DB with full options..."13 14 mysqldump --defaults-file=/root/.my.cnf \15 --single-transaction \16 --quick \17 --routines \18 --triggers \19 --events \20 --hex-blob \21 --add-drop-database \22 --add-drop-table \23 "$DB" > "$FILE"24 25 # Create both compressed and checksum26 gzip -c "$FILE" > "${FILE}.gz"27 sha256sum "${FILE}.gz" > "${FILE}.gz.sha256"28 rm "$FILE" # Remove uncompressed file29done30
31find "$BACKUP_DIR" -type f -name "*.gz" -mtime +7 -delete32find "$BACKUP_DIR" -type f -name "*.sha256" -mtime +7 -deleteAdditional options explained:
--triggers- Includes table triggers--events- Includes scheduled events--hex-blob- Dumps binary columns in hex format (more reliable)--add-drop-database- Adds DROP DATABASE IF EXISTS statements--add-drop-table- Adds DROP TABLE IF EXISTS statements
Logging and Error Handling
Enhanced version with logging:
1#!/bin/bash2
3BACKUP_DIR="/root/mysql-backup"4LOG_FILE="/var/log/mysql-backup.log"5DATE=$(date +"%Y-%m-%d_%H-%M")6TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")7
8# Redirect stdout and stderr to log file9exec 1>>"$LOG_FILE"10exec 2>&111
12echo "[$TIMESTAMP] Starting MySQL backup process"13
14DATABASES=$(mysql --defaults-file=/root/.my.cnf -e "SHOW DATABASES;" 2>/dev/null | grep -Ev "Database|information_schema|performance_schema|mysql|sys")15
16if [ -z "$DATABASES" ]; then17 echo "[$TIMESTAMP] ERROR: No databases found or MySQL connection failed"18 exit 119fi20
21mkdir -p "$BACKUP_DIR"22
23SUCCESS_COUNT=024FAIL_COUNT=025
26for DB in $DATABASES; do27 FILE="$BACKUP_DIR/${DB}-${DATE}.sql"28 29 echo "[$TIMESTAMP] Backing up database: $DB"30 31 if mysqldump --defaults-file=/root/.my.cnf \32 --single-transaction \33 --quick \34 --routines \35 "$DB" > "$FILE" 2>/dev/null; then36 37 gzip "$FILE"38 SIZE=$(du -h "${FILE}.gz" | cut -f1)39 echo "[$TIMESTAMP] SUCCESS: $DB backed up successfully (Size: $SIZE)"40 ((SUCCESS_COUNT++))41 else42 echo "[$TIMESTAMP] ERROR: Failed to backup $DB"43 ((FAIL_COUNT++))44 rm -f "$FILE" # Clean up partial backup45 fi46done47
48# Cleanup old backups49OLD_COUNT=$(find "$BACKUP_DIR" -type f -name "*.gz" -mtime +7 | wc -l)50find "$BACKUP_DIR" -type f -name "*.gz" -mtime +7 -delete51
52echo "[$TIMESTAMP] Backup summary: $SUCCESS_COUNT succeeded, $FAIL_COUNT failed, $OLD_COUNT old backups removed"53echo "[$TIMESTAMP] MySQL backup process completed"54echo "----------------------------------------"Create MySQL credentials file
1cat <<EOF > /root/.my.cnf2[client]3user=your_mysql_username4password=your_mysql_password5EOF6chmod 600 /root/.my.cnfSecurity Considerations
The .my.cnf file stores sensitive credentials. Important security practices:
- File permissions -
chmod 600ensures only the owner can read/write - Root ownership - Store in
/root/which is only accessible to root - Never commit to version control - Add
.my.cnfto.gitignore - Dedicated backup user - Create a MySQL user with only necessary privileges
Creating a Dedicated Backup User
For enhanced security, create a MySQL user specifically for backups with minimal privileges:
1-- Connect to MySQL as root2mysql -u root -p3
4-- Create backup user5CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'strong_password_here';6
7-- Grant necessary privileges for backups8GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON *.* TO 'backup_user'@'localhost';9
10-- For single-transaction backups (InnoDB)11GRANT RELOAD ON *.* TO 'backup_user'@'localhost';12
13-- Apply changes14FLUSH PRIVILEGES;15
16-- Verify privileges17SHOW GRANTS FOR 'backup_user'@'localhost';Then update your .my.cnf with these credentials:
1cat <<EOF > /root/.my.cnf2[client]3user=backup_user4password=strong_password_here5host=localhost6EOF7chmod 600 /root/.my.cnfMake the backup script executable
1chmod +x /root/mysql-backup.shThis command makes the script executable by the owner (root). Verify with:
1ls -lh /root/mysql-backup.sh2# Should show: -rwxr--r-- (executable bit set)Test the backup script manually before scheduling:
1# Run the script2/root/mysql-backup.sh3
4# Check if backups were created5ls -lh /root/mysql-backup/6
7# Verify a backup by checking its contents8zcat /root/mysql-backup/your_database-*.sql.gz | head -n 20Set up cron job for automated backups
1crontab -e20 20 * * * /root/mysql-backup.shUnderstanding Cron Syntax
The cron expression 0 20 * * * breaks down into five fields:
10 20 * * * /root/mysql-backup.sh2│ │ │ │ │3│ │ │ │ └─── Day of week (0-6, Sunday=0)4│ │ │ └───── Month (1-12)5│ │ └─────── Day of month (1-31)6│ └────────── Hour (0-23)7└──────────── Minute (0-59)Cron Field Reference:
| Field | Values | Special Characters |
|---|---|---|
| Minute | 0-59 | * , - / |
| Hour | 0-23 | * , - / |
| Day of Month | 1-31 | * , - / ? L W |
| Month | 1-12 or JAN-DEC | * , - / |
| Day of Week | 0-6 or SUN-SAT | * , - / ? L # |
Special Characters:
*= Any value (every minute, hour, day, etc.),= List of values (e.g.,1,15,30)-= Range of values (e.g.,1-5for Mon-Fri)/= Step values (e.g.,*/5for every 5 units)
This example runs the backup daily at 8:00 PM (20:00).
Common Cron Schedule Examples
1# Every day at 2:30 AM230 2 * * * /root/mysql-backup.sh3
4# Every 6 hours50 */6 * * * /root/mysql-backup.sh6
7# Every Sunday at midnight80 0 * * 0 /root/mysql-backup.sh9
10# Every weekday at 11 PM110 23 * * 1-5 /root/mysql-backup.sh12
13# First day of every month at 1 AM140 1 1 * * /root/mysql-backup.sh15
16# Every 12 hours (midnight and noon)170 0,12 * * * /root/mysql-backup.shVerify Cron Job Setup
1# List current crontab2crontab -l3
4# Check cron service status5systemctl status cron # Debian/Ubuntu6systemctl status crond # RHEL/CentOS7
8# View cron logs9grep CRON /var/log/syslog # Debian/Ubuntu10grep CRON /var/log/cron # RHEL/CentOSCron Best Practices
- Use absolute paths - Cron runs with limited PATH
- Add email notifications - Set
[email protected]at top of crontab - Redirect output - Capture stdout/stderr for debugging:
1# Crontab with email on errors only3SHELL=/bin/bash4PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin5
60 20 * * * /root/mysql-backup.sh >> /var/log/mysql-backup-cron.log 2>&1Download backups
10 20 * * * rsync -avz -e ssh root@IP-HOST:/root/mysql-backup /mnt/c/backup2025/sqlRemote Backup Transfer
Rsync is an efficient tool for transferring backups to a remote location for off-site storage.
Rsync Options Explained:
-a(archive mode) - Preserves permissions, timestamps, symbolic links-v(verbose) - Shows detailed progress-z(compress) - Compresses data during transfer (already gzipped files won't compress much more)-e ssh- Uses SSH for secure transfer--delete- Remove files in destination that no longer exist in source (use with caution)
Setting Up SSH Key Authentication
To avoid password prompts in cron jobs, use SSH key authentication:
1# On the local machine (where cron downloads backups)2ssh-keygen -t ed25519 -C "mysql-backup-key"3
4# Copy public key to remote server5ssh-copy-id -i ~/.ssh/id_ed25519.pub root@IP-HOST6
7# Test passwordless SSH8ssh root@IP-HOST "echo SSH connection successful"9
10# Add to crontab for automated downloads11crontab -eAdvanced Rsync Examples
Incremental backup with bandwidth limit:
1# Limit bandwidth to 5MB/s2rsync -avz --bwlimit=5000 -e ssh root@IP-HOST:/root/mysql-backup/ /mnt/c/backup2025/sql/With deletion of old files and logging:
1rsync -avz --delete -e ssh \2 --log-file=/var/log/rsync-mysql-backup.log \3 root@IP-HOST:/root/mysql-backup/ \4 /mnt/c/backup2025/sql/Copy only recent backups (last 24 hours):
1# On remote server, sync only recent files2ssh root@IP-HOST "find /root/mysql-backup -type f -name '*.gz' -mtime -1" | \3 while read file; do4 rsync -avz -e ssh "root@IP-HOST:$file" /mnt/c/backup2025/sql/5 doneAlternative: SCP for Simple Transfers
For simple one-time transfers:
1# Copy all .gz files from today2scp root@IP-HOST:/root/mysql-backup/*$(date +%Y-%m-%d)*.sql.gz /mnt/c/backup2025/sql/3
4# Copy specific database backup5scp root@IP-HOST:/root/mysql-backup/mydb-2026-01-14*.sql.gz ./Restoring from Backups
Restore a Single Database
1# Decompress and restore2gunzip < /root/mysql-backup/database_name-2026-01-14_20-00.sql.gz | \3 mysql --defaults-file=/root/.my.cnf database_name4
5# Or in two steps6gunzip /root/mysql-backup/database_name-2026-01-14_20-00.sql.gz7mysql --defaults-file=/root/.my.cnf database_name < database_name-2026-01-14_20-00.sqlRestore All Databases
1for backup in /root/mysql-backup/*.sql.gz; do2 DB=$(basename "$backup" | cut -d'-' -f1)3 echo "Restoring $DB..."4 gunzip < "$backup" | mysql --defaults-file=/root/.my.cnf "$DB"5doneVerify Database After Restore
1# Check tables in database2mysql --defaults-file=/root/.my.cnf -e "USE database_name; SHOW TABLES;"3
4# Check table row counts5mysql --defaults-file=/root/.my.cnf database_name -e "6 SELECT 7 TABLE_NAME, 8 TABLE_ROWS 9 FROM information_schema.TABLES 10 WHERE TABLE_SCHEMA = 'database_name';"11
12# Check database size13mysql --defaults-file=/root/.my.cnf -e "14 SELECT 15 table_schema AS 'Database',16 ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)'17 FROM information_schema.tables18 WHERE table_schema = 'database_name'19 GROUP BY table_schema;"Monitoring and Maintenance
Check Backup Disk Usage
1# Check backup directory size2du -sh /root/mysql-backup3
4# List backups sorted by size5du -h /root/mysql-backup/*.gz | sort -h6
7# Show oldest and newest backups8ls -lt /root/mysql-backup/*.gz | tail -n 5 # oldest9ls -lt /root/mysql-backup/*.gz | head -n 5 # newestAutomated Monitoring Script
Create a monitoring script to alert when backups fail:
1#!/bin/bash2# /root/check-mysql-backup.sh3
4BACKUP_DIR="/root/mysql-backup"5ALERT_EMAIL="[email protected]"6HOURS_THRESHOLD=25 # Alert if no backup in last 25 hours7
8# Find most recent backup9LATEST_BACKUP=$(find "$BACKUP_DIR" -type f -name "*.gz" -printf '%T@ %p\n' | sort -n | tail -1 | cut -d' ' -f2-)10
11if [ -z "$LATEST_BACKUP" ]; then12 echo "ERROR: No backups found in $BACKUP_DIR" | mail -s "MySQL Backup Alert: No backups found" "$ALERT_EMAIL"13 exit 114fi15
16# Check age of latest backup17LATEST_TIME=$(stat -c %Y "$LATEST_BACKUP")18CURRENT_TIME=$(date +%s)19AGE_HOURS=$(( (CURRENT_TIME - LATEST_TIME) / 3600 ))20
21if [ "$AGE_HOURS" -gt "$HOURS_THRESHOLD" ]; then22 echo "WARNING: Latest backup is $AGE_HOURS hours old (threshold: $HOURS_THRESHOLD hours)" | \23 mail -s "MySQL Backup Alert: Backup too old" "$ALERT_EMAIL"24 exit 125fi26
27echo "OK: Latest backup is $AGE_HOURS hours old"Schedule the monitoring script:
1# Run every 6 hours20 */6 * * * /root/check-mysql-backup.shTroubleshooting
Common Issues and Solutions
Issue: mysqldump fails with "Access denied"
1# Check MySQL credentials2mysql --defaults-file=/root/.my.cnf -e "SELECT 1"3
4# Verify user privileges5mysql -u root -p -e "SHOW GRANTS FOR 'backup_user'@'localhost';"Issue: Backups are too large
1# Check which databases are largest2mysql --defaults-file=/root/.my.cnf -e "3 SELECT 4 table_schema,5 ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)'6 FROM information_schema.tables7 GROUP BY table_schema8 ORDER BY SUM(data_length + index_length) DESC;"9
10# Consider using compression level for gzip (1=fast, 9=best compression)11gzip -9 "$FILE" # Maximum compressionIssue: Cron job doesn't run
1# Check if cron is running2systemctl status cron3
4# Check cron logs for errors5tail -f /var/log/syslog | grep CRON6
7# Test script directly8sudo -u root /root/mysql-backup.shIssue: Running out of disk space
1# Adjust retention period (reduce from 7 to 3 days)2find "$BACKUP_DIR" -type f -name "*.gz" -mtime +3 -delete3
4# Or keep only last N backups per database5for db in $(mysql --defaults-file=/root/.my.cnf -e "SHOW DATABASES;" | grep -Ev "Database|information_schema|performance_schema|mysql|sys"); do6 ls -t /root/mysql-backup/${db}-*.gz | tail -n +6 | xargs rm -f7doneBest Practices Summary
- 3-2-1 Backup Rule - 3 copies, 2 different media types, 1 off-site
- Test restores regularly - Verify backups can actually be restored
- Monitor backup status - Automated alerts for failures
- Secure credentials - Use dedicated users with minimal privileges
- Document procedures - Keep restoration steps accessible
- Consider replication - Use MySQL replication for high availability
- Encrypt sensitive backups - Use GPG for additional security layer
Optional: Encrypt Backups with GPG
1# Generate GPG key (one-time)2gpg --gen-key3
4# Encrypt backup5gzip "$FILE"6gpg --encrypt --recipient [email protected] "${FILE}.gz"7rm "${FILE}.gz" # Keep only encrypted version8
9# Decrypt when restoring10gpg --decrypt database-2026-01-14_20-00.sql.gz.gpg | gunzip | mysql database_name