Set up automated MySQL database backups with compression and rotation

Intermediate 45 min Apr 16, 2026 747 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Configure automated MySQL database backups with compression, encryption, and rotation using systemd timers. Includes monitoring and alerting for backup failures.

Prerequisites

  • MySQL server running
  • Root or sudo access
  • At least 2GB free disk space

What this solves

MySQL databases need regular backups to protect against data loss from hardware failures, accidental deletions, or corruption. Manual backups are unreliable and time-consuming. This tutorial sets up automated MySQL backups with compression to save disk space, rotation to manage retention periods, and monitoring to alert you when backups fail. You'll create a robust backup system that runs automatically without manual intervention.

Step-by-step installation

Install MySQL backup tools and dependencies

Install the necessary tools for creating compressed MySQL backups and handling encryption. These packages provide mysqldump for database exports and compression utilities.

sudo apt update
sudo apt install -y mysql-client gzip pigz gnupg2 mailutils
sudo dnf update -y
sudo dnf install -y mysql gnupg2 pigz mailx

Create backup user and directories

Create a dedicated MySQL user for backups with minimal required privileges. This follows the principle of least privilege for security. Also create the backup directory structure with proper permissions.

sudo mysql -e "CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'SecureBackupPass123!';"
sudo mysql -e "GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON *.* TO 'backup_user'@'localhost';"
sudo mysql -e "FLUSH PRIVILEGES;"

sudo mkdir -p /var/backups/mysql
sudo mkdir -p /var/log/mysql-backup
sudo groupadd mysql-backup
sudo useradd -r -g mysql-backup -s /bin/false mysql-backup
sudo chown mysql-backup:mysql-backup /var/backups/mysql /var/log/mysql-backup
sudo chmod 750 /var/backups/mysql /var/log/mysql-backup
Warning: Replace 'SecureBackupPass123!' with a strong, unique password. Store this password securely as it provides access to all your database contents.

Generate GPG encryption key for backups

Create a GPG key pair for encrypting backup files. This ensures that even if backup files are compromised, the data remains protected without the private key.

sudo su - mysql-backup -s /bin/bash
gpg --batch --generate-key <

Create MySQL backup configuration file

Store MySQL credentials securely in a configuration file with restricted permissions. This prevents passwords from appearing in process lists or command history.

[client]
user=backup_user
password=SecureBackupPass123!
host=localhost
port=3306

[mysql]
default-character-set=utf8mb4

[mysqldump]
single-transaction=true
routines=true
triggers=true
events=true
opt=true
sudo chown mysql-backup:mysql-backup /etc/mysql/backup.cnf
sudo chmod 600 /etc/mysql/backup.cnf

Create comprehensive backup script

Create the main backup script that handles database dumps, compression, encryption, and rotation. This script includes error handling and logging for reliable operation.

#!/bin/bash

# MySQL Backup Script with Compression and Rotation
# Author: Infrastructure Team
# Version: 2.1

set -euo pipefail

# Configuration
BACKUP_DIR="/var/backups/mysql"
LOG_FILE="/var/log/mysql-backup/backup.log"
MYSQL_CONFIG="/etc/mysql/backup.cnf"
RETENTION_DAYS=30
COMPRESSION_LEVEL=6
GPG_RECIPIENT="backup@example.com"
ALERT_EMAIL="admin@example.com"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
HOSTNAME=$(hostname)

# Logging function
log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}

# Error handling
error_exit() {
    log "ERROR: $1"
    echo "MySQL backup failed on $HOSTNAME at $(date)" | mail -s "MySQL Backup Failed - $HOSTNAME" "$ALERT_EMAIL" 2>/dev/null || true
    exit 1
}

# Check prerequisites
check_prerequisites() {
    log "Checking prerequisites..."
    
    # Check if MySQL is accessible
    if ! mysql --defaults-file="$MYSQL_CONFIG" -e "SELECT 1" >/dev/null 2>&1; then
        error_exit "Cannot connect to MySQL server"
    fi
    
    # Check backup directory
    if [[ ! -d "$BACKUP_DIR" ]]; then
        mkdir -p "$BACKUP_DIR" || error_exit "Cannot create backup directory"
    fi
    
    # Check disk space (require at least 2GB free)
    AVAILABLE_SPACE=$(df "$BACKUP_DIR" | awk 'NR==2 {print $4}')
    if [[ $AVAILABLE_SPACE -lt 2097152 ]]; then
        error_exit "Insufficient disk space. Available: ${AVAILABLE_SPACE}KB, Required: 2GB"
    fi
    
    log "Prerequisites check completed successfully"
}

# Get list of databases
get_databases() {
    mysql --defaults-file="$MYSQL_CONFIG" -e "SHOW DATABASES;" | grep -Ev '^(Database|information_schema|performance_schema|mysql|sys)$'
}

# Backup individual database
backup_database() {
    local db_name=$1
    local backup_file="${BACKUP_DIR}/${HOSTNAME}_${db_name}_${TIMESTAMP}.sql"
    local compressed_file="${backup_file}.gz"
    local encrypted_file="${compressed_file}.gpg"
    
    log "Starting backup for database: $db_name"
    
    # Create database dump
    if ! mysqldump --defaults-file="$MYSQL_CONFIG" \
        --single-transaction \
        --routines \
        --triggers \
        --events \
        --add-drop-database \
        --databases "$db_name" > "$backup_file"; then
        error_exit "Failed to dump database: $db_name"
    fi
    
    # Compress backup
    if ! pigz -"$COMPRESSION_LEVEL" "$backup_file"; then
        rm -f "$backup_file"
        error_exit "Failed to compress backup for database: $db_name"
    fi
    
    # Encrypt backup
    if ! gpg --trust-model always --batch --yes --cipher-algo AES256 \
        --compress-algo 1 --recipient "$GPG_RECIPIENT" \
        --encrypt "$compressed_file"; then
        rm -f "$compressed_file"
        error_exit "Failed to encrypt backup for database: $db_name"
    fi
    
    # Remove unencrypted compressed file
    rm -f "$compressed_file"
    
    # Calculate and log file size
    local file_size=$(stat -c%s "$encrypted_file")
    local file_size_mb=$((file_size / 1024 / 1024))
    
    log "Successfully backed up database: $db_name (${file_size_mb}MB)"
}

# Rotate old backups
rotate_backups() {
    log "Starting backup rotation (retention: $RETENTION_DAYS days)..."
    
    local deleted_count=0
    while IFS= read -r -d '' file; do
        rm -f "$file"
        ((deleted_count++))
    done < <(find "$BACKUP_DIR" -name "*.gpg" -type f -mtime +$RETENTION_DAYS -print0)
    
    log "Backup rotation completed. Deleted $deleted_count old backup files"
}

# Generate backup report
generate_report() {
    local total_size=$(find "$BACKUP_DIR" -name "*.gpg" -type f -exec stat -c%s {} + | awk '{sum+=$1} END {print sum/1024/1024}')
    local backup_count=$(find "$BACKUP_DIR" -name "*_${TIMESTAMP}.sql.gz.gpg" -type f | wc -l)
    
    log "Backup completed successfully:"
    log "  - Databases backed up: $backup_count"
    log "  - Total backup size: ${total_size}MB"
    log "  - Backup location: $BACKUP_DIR"
    log "  - Retention period: $RETENTION_DAYS days"
}

# Main execution
main() {
    log "=== MySQL Backup Started ==="
    
    check_prerequisites
    
    # Get databases and backup each one
    local databases
    databases=$(get_databases)
    
    if [[ -z "$databases" ]]; then
        error_exit "No databases found to backup"
    fi
    
    while IFS= read -r database; do
        backup_database "$database"
    done <<< "$databases"
    
    rotate_backups
    generate_report
    
    log "=== MySQL Backup Completed Successfully ==="
}

# Run main function
main "$@"

Set script permissions and test

Configure proper permissions for the backup script and test it manually to ensure everything works correctly before automating it.

sudo chown mysql-backup:mysql-backup /usr/local/bin/mysql-backup.sh
sudo chmod 750 /usr/local/bin/mysql-backup.sh

# Test the backup script
sudo -u mysql-backup /usr/local/bin/mysql-backup.sh

Create systemd service for backups

Create a systemd service unit that runs the backup script. This provides better logging and management compared to running scripts directly from cron.

[Unit]
Description=MySQL Database Backup Service
Wants=mysql-backup.timer
After=mysql.service
Requires=mysql.service

[Service]
Type=oneshot
User=mysql-backup
Group=mysql-backup
ExecStart=/usr/local/bin/mysql-backup.sh
Environment="PATH=/usr/local/bin:/usr/bin:/bin"
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ReadWritePaths=/var/backups/mysql /var/log/mysql-backup
NoNewPrivileges=yes
UMask=0077

# Security hardening
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictNamespaces=yes
SystemCallFilter=@system-service
SystemCallFilter=~@debug @mount @cpu-emulation @obsolete @privileged

[Install]
WantedBy=multi-user.target

Create systemd timer for automated scheduling

Configure a systemd timer to run backups automatically. This example runs backups daily at 2:00 AM with some randomization to prevent multiple servers from backing up simultaneously.

[Unit]
Description=MySQL Database Backup Timer
Requires=mysql-backup.service

[Timer]
# Run daily at 2:00 AM with 30 minute randomization
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=1800
Persistent=true
AccuracySec=1m

# Prevent running if system was suspended
WakeSystem=false

[Install]
WantedBy=timers.target

Enable and start the backup timer

Enable the systemd timer and service so they start automatically on boot. The timer will handle scheduling the backup service.

sudo systemctl daemon-reload
sudo systemctl enable mysql-backup.service
sudo systemctl enable mysql-backup.timer
sudo systemctl start mysql-backup.timer

Create backup monitoring script

Create a monitoring script that checks backup status and sends alerts if backups fail or become too old. This provides proactive monitoring of your backup system.

#!/bin/bash

# MySQL Backup Monitor Script
# Checks backup status and sends alerts

set -euo pipefail

BACKUP_DIR="/var/backups/mysql"
LOG_FILE="/var/log/mysql-backup/backup.log"
ALERT_EMAIL="admin@example.com"
MAX_BACKUP_AGE_HOURS=26  # Alert if no backup in 26 hours
HOSTNAME=$(hostname)

# Check if recent backups exist
check_backup_freshness() {
    local latest_backup
    latest_backup=$(find "$BACKUP_DIR" -name "*.gpg" -type f -newermt "-$MAX_BACKUP_AGE_HOURS hours" | head -1)
    
    if [[ -z "$latest_backup" ]]; then
        echo "No recent MySQL backups found on $HOSTNAME. Last backup is older than $MAX_BACKUP_AGE_HOURS hours." | \
        mail -s "MySQL Backup Alert - No Recent Backups - $HOSTNAME" "$ALERT_EMAIL"
        exit 1
    fi
}

# Check for errors in log file
check_backup_errors() {
    if [[ -f "$LOG_FILE" ]]; then
        local recent_errors
        recent_errors=$(grep -i "error\|failed" "$LOG_FILE" | tail -5)
        
        if [[ -n "$recent_errors" ]]; then
            echo "Recent errors found in MySQL backup log on $HOSTNAME:\n\n$recent_errors" | \
            mail -s "MySQL Backup Errors - $HOSTNAME" "$ALERT_EMAIL"
        fi
    fi
}

# Check disk space
check_disk_space() {
    local available_space
    available_space=$(df "$BACKUP_DIR" | awk 'NR==2 {print $4}')
    local available_gb=$((available_space / 1024 / 1024))
    
    if [[ $available_gb -lt 1 ]]; then
        echo "Low disk space for MySQL backups on $HOSTNAME. Available: ${available_gb}GB" | \
        mail -s "MySQL Backup Disk Space Warning - $HOSTNAME" "$ALERT_EMAIL"
    fi
}

# Generate backup status report
generate_status_report() {
    local backup_count
    local total_size
    local latest_backup_time
    
    backup_count=$(find "$BACKUP_DIR" -name "*.gpg" -type f | wc -l)
    total_size=$(find "$BACKUP_DIR" -name "*.gpg" -type f -exec stat -c%s {} + | awk '{sum+=$1} END {print sum/1024/1024/1024}')
    latest_backup_time=$(find "$BACKUP_DIR" -name "*.gpg" -type f -printf '%T@ %p\n' | sort -n | tail -1 | awk '{print $2}' | xargs stat -c %y 2>/dev/null || echo "No backups found")
    
    echo "MySQL Backup Status Report for $HOSTNAME:"
    echo "  - Total backup files: $backup_count"
    echo "  - Total backup size: ${total_size}GB"
    echo "  - Latest backup: $latest_backup_time"
    echo "  - Backup directory: $BACKUP_DIR"
}

# Main monitoring function
main() {
    check_backup_freshness
    check_backup_errors
    check_disk_space
    
    # Optionally generate weekly status report
    if [[ $(date +%u) == "1" ]]; then
        generate_status_report | mail -s "Weekly MySQL Backup Status - $HOSTNAME" "$ALERT_EMAIL"
    fi
}

main "$@"

Set up monitoring timer

Create a systemd timer to run the monitoring script every 6 hours. This ensures you're alerted quickly if backups stop working.

sudo chown mysql-backup:mysql-backup /usr/local/bin/mysql-backup-monitor.sh
sudo chmod 750 /usr/local/bin/mysql-backup-monitor.sh
[Unit]
Description=MySQL Backup Monitor Service
After=network.target

[Service]
Type=oneshot
User=mysql-backup
Group=mysql-backup
ExecStart=/usr/local/bin/mysql-backup-monitor.sh
Environment="PATH=/usr/local/bin:/usr/bin:/bin"
[Unit]
Description=MySQL Backup Monitor Timer
Requires=mysql-backup-monitor.service

[Timer]
OnCalendar=*-*-* 00,06,12,18:15:00
Persistent=true
AccuracySec=1m

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable mysql-backup-monitor.timer
sudo systemctl start mysql-backup-monitor.timer

Configure log rotation

Set up logrotate to manage backup log files and prevent them from consuming excessive disk space over time.

/var/log/mysql-backup/*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 644 mysql-backup mysql-backup
    postrotate
        systemctl reload rsyslog > /dev/null 2>&1 || true
    endscript
}

Verify your setup

Test your automated backup system to ensure all components are working correctly.

# Check timer status
sudo systemctl status mysql-backup.timer
sudo systemctl status mysql-backup-monitor.timer

# List scheduled timers
sudo systemctl list-timers mysql-backup*

# Test backup manually
sudo systemctl start mysql-backup.service
sudo systemctl status mysql-backup.service

# Check backup files
sudo ls -la /var/backups/mysql/

# Check logs
sudo tail -f /var/log/mysql-backup/backup.log

# Test backup decryption
sudo -u mysql-backup gpg --decrypt /var/backups/mysql/$(ls /var/backups/mysql/ | head -1) | head -10

Common issues

SymptomCauseFix
Permission denied errorsIncorrect file ownership or permissionsCheck ownership: sudo chown -R mysql-backup:mysql-backup /var/backups/mysql /var/log/mysql-backup
MySQL access deniedWrong credentials in backup.cnfVerify MySQL user permissions: mysql -u backup_user -p -e "SHOW GRANTS"
GPG encryption failsGPG key not found or expiredCheck GPG keys: sudo -u mysql-backup gpg --list-keys
Backup timer not runningTimer not enabled or startedEnable timer: sudo systemctl enable --now mysql-backup.timer
Email alerts not workingMail system not configuredConfigure postfix: sudo dpkg-reconfigure postfix
Disk space fullBackup retention too long or large databasesReduce retention days or increase disk space
Backup files corruptCompression or encryption issuesTest pipeline manually: mysqldump | pigz | gpg --encrypt

Next steps

Automated install script

Run this to automate the entire setup

Non vuoi gestirlo da solo?

Gestiamo l'infrastruttura di aziende che dipendono dall'uptime. Completamente gestita, con un referente fisso che conosce il tuo ambiente.

Avete un referente fisso che conosce il vostro ambiente

Rotterdam 13:57 · raggiungibile con un messaggio, senza modulo ticket