A Simple Bash Script for Multi-Server MySQL Backups
A Bash script that automates MySQL backups across multiple servers using mysqldump, with daily and monthly snapshots, JSON configuration, retention cleanup, and simple restore workflows.
MySQL Backup Script
A Bash script that automates MySQL database backups across multiple servers using mysqldump. It reads server credentials from a JSON config file, backs up all non-system databases, organizes backups into daily and monthly directories, and enforces a configurable retention policy.
Requirements
jq- JSON processormysql-client- MySQL client tools (mysqlandmysqldump)
Setup
1. Configure the Script
Copy the example config and fill in your server details:
1cp config.example.json config.json1cp config.example.json config.json
Edit config.json with your servers:
1{2 "backup_dir": "/backups/mysql",3 "retention_days": 7,4 "servers": [5 {6 "host": "db1.example.internal",7 "login_path": "db1-backup"8 }9 ]10}1{2 "backup_dir": "/backups/mysql",3 "retention_days": 7,4 "servers": [5 {6 "host": "db1.example.internal",7 "login_path": "db1-backup"8 }9 ]10}
Credentials are not stored in
config.json. They live in an encrypted~/.mylogin.cnffile managed bymysql_config_editor(see step 2).
login_pathis the name of the credential entry in.mylogin.cnf. If omitted, the script falls back to usinghostas the login-path name.
2. The script
create mysql_backup.sh
1#!/bin/bash23set -e45# Check if jq is installed6if ! command -v jq &> /dev/null; then7 echo "jq is required but not installed. Please install jq to use this script."8 exit 19fi1011# Check if mysqldump is installed12if ! command -v mysqldump &> /dev/null; then13 echo "mysqldump is required but not installed. Please install mysql-client to use this script."14 exit 115fi1617# Get the directory of the script18SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"1920# Set the config file path relative to the script directory21CONFIG_FILE="${SCRIPT_DIR}/config.json"2223# Function to read configuration and check backup directory24read_config_and_check_dir() {25 if [ ! -f "$CONFIG_FILE" ]; then26 echo "Configuration file not found: $CONFIG_FILE"27 exit 128 fi2930 BACKUP_DIR=$(jq -r '.backup_dir' "$CONFIG_FILE")31 RETENTION_DAYS=$(jq -r '.retention_days' "$CONFIG_FILE")3233 # Check if backup directory exists, create if it doesn't34 if [ ! -d "$BACKUP_DIR" ]; then35 echo "Backup directory does not exist. Creating: $BACKUP_DIR"36 mkdir -p "$BACKUP_DIR"37 if [ $? -ne 0 ]; then38 echo "Failed to create backup directory. Please check permissions and path."39 exit 140 fi41 fi4243 echo "Using backup directory: $BACKUP_DIR"44}4546# Function to create backup47create_backup() {48 local HOST=$149 local LOGIN_PATH=$250 local DB=$351 local BACKUP_PATH="$BACKUP_DIR/$HOST/$DB"5253 mkdir -p "$BACKUP_PATH/daily"54 mkdir -p "$BACKUP_PATH/monthly"5556 echo "Backing up database: $DB"57 mysqldump --login-path="$LOGIN_PATH" --single-transaction --routines --triggers "$DB" > "$BACKUP_PATH/daily/$DB-$DATE.sql"5859 # Create monthly backup on the 1st of each month60 if [ "$(date +%d)" = "01" ]; then61 cp "$BACKUP_PATH/daily/$DB-$DATE.sql" "$BACKUP_PATH/monthly/$DB-$MONTH.sql"62 fi63}6465# Function to remove old backups66cleanup_old_backups() {67 local HOST=$168 local DB=$269 local BACKUP_PATH="$BACKUP_DIR/$HOST/$DB/daily"7071 find "$BACKUP_PATH" -name "*.sql" -type f -mtime +$RETENTION_DAYS -delete72}7374# Function to list databases75list_databases() {76 local LOGIN_PATH=$17778 echo "Attempting to list databases using login-path $LOGIN_PATH" >&279 mysql --login-path="$LOGIN_PATH" -N -e "SHOW DATABASES;" 2>/dev/null | grep -Ev "^(information_schema|performance_schema|mysql|sys)$"80}8182# Main backup process83main() {84 read_config_and_check_dir8586 DATE=$(date +"%Y-%m-%d")87 MONTH=$(date +"%Y-%m")8889 echo "Starting backup process..."9091 jq -c '.servers[]' "$CONFIG_FILE" | while read -r server; do92 HOST=$(echo $server | jq -r '.host')93 LOGIN_PATH=$(echo $server | jq -r '.login_path // .host')9495 echo "Backing up server: $HOST (login-path: $LOGIN_PATH)"9697 # Get list of databases98 echo "Listing databases..."99 DATABASES=$(list_databases "$LOGIN_PATH")100101 if [ -z "$DATABASES" ]; then102 echo "No databases found or unable to list databases on $HOST. Check your login-path and permissions."103 continue104 fi105106 echo "Databases found:"107 echo "$DATABASES"108109 echo "$DATABASES" | while read -r DB; do110 create_backup "$HOST" "$LOGIN_PATH" "$DB"111 cleanup_old_backups "$HOST" "$DB"112 done113 done114115 echo "Backup process completed."116}117118main1#!/bin/bash23set -e45# Check if jq is installed6if ! command -v jq &> /dev/null; then7 echo "jq is required but not installed. Please install jq to use this script."8 exit 19fi1011# Check if mysqldump is installed12if ! command -v mysqldump &> /dev/null; then13 echo "mysqldump is required but not installed. Please install mysql-client to use this script."14 exit 115fi1617# Get the directory of the script18SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"1920# Set the config file path relative to the script directory21CONFIG_FILE="${SCRIPT_DIR}/config.json"2223# Function to read configuration and check backup directory24read_config_and_check_dir() {25 if [ ! -f "$CONFIG_FILE" ]; then26 echo "Configuration file not found: $CONFIG_FILE"27 exit 128 fi2930 BACKUP_DIR=$(jq -r '.backup_dir' "$CONFIG_FILE")31 RETENTION_DAYS=$(jq -r '.retention_days' "$CONFIG_FILE")3233 # Check if backup directory exists, create if it doesn't34 if [ ! -d "$BACKUP_DIR" ]; then35 echo "Backup directory does not exist. Creating: $BACKUP_DIR"36 mkdir -p "$BACKUP_DIR"37 if [ $? -ne 0 ]; then38 echo "Failed to create backup directory. Please check permissions and path."39 exit 140 fi41 fi4243 echo "Using backup directory: $BACKUP_DIR"44}4546# Function to create backup47create_backup() {48 local HOST=$149 local LOGIN_PATH=$250 local DB=$351 local BACKUP_PATH="$BACKUP_DIR/$HOST/$DB"5253 mkdir -p "$BACKUP_PATH/daily"54 mkdir -p "$BACKUP_PATH/monthly"5556 echo "Backing up database: $DB"57 mysqldump --login-path="$LOGIN_PATH" --single-transaction --routines --triggers "$DB" > "$BACKUP_PATH/daily/$DB-$DATE.sql"5859 # Create monthly backup on the 1st of each month60 if [ "$(date +%d)" = "01" ]; then61 cp "$BACKUP_PATH/daily/$DB-$DATE.sql" "$BACKUP_PATH/monthly/$DB-$MONTH.sql"62 fi63}6465# Function to remove old backups66cleanup_old_backups() {67 local HOST=$168 local DB=$269 local BACKUP_PATH="$BACKUP_DIR/$HOST/$DB/daily"7071 find "$BACKUP_PATH" -name "*.sql" -type f -mtime +$RETENTION_DAYS -delete72}7374# Function to list databases75list_databases() {76 local LOGIN_PATH=$17778 echo "Attempting to list databases using login-path $LOGIN_PATH" >&279 mysql --login-path="$LOGIN_PATH" -N -e "SHOW DATABASES;" 2>/dev/null | grep -Ev "^(information_schema|performance_schema|mysql|sys)$"80}8182# Main backup process83main() {84 read_config_and_check_dir8586 DATE=$(date +"%Y-%m-%d")87 MONTH=$(date +"%Y-%m")8889 echo "Starting backup process..."9091 jq -c '.servers[]' "$CONFIG_FILE" | while read -r server; do92 HOST=$(echo $server | jq -r '.host')93 LOGIN_PATH=$(echo $server | jq -r '.login_path // .host')9495 echo "Backing up server: $HOST (login-path: $LOGIN_PATH)"9697 # Get list of databases98 echo "Listing databases..."99 DATABASES=$(list_databases "$LOGIN_PATH")100101 if [ -z "$DATABASES" ]; then102 echo "No databases found or unable to list databases on $HOST. Check your login-path and permissions."103 continue104 fi105106 echo "Databases found:"107 echo "$DATABASES"108109 echo "$DATABASES" | while read -r DB; do110 create_backup "$HOST" "$LOGIN_PATH" "$DB"111 cleanup_old_backups "$HOST" "$DB"112 done113 done114115 echo "Backup process completed."116}117118main
3. Store Credentials with mysql_config_editor
Since the cron job runs as root, credentials must live in root's home (/root/.mylogin.cnf). For each server in config.json, create a matching login-path:
1sudo mysql_config_editor set --login-path=db1-backup --host=db1.example.internal --user=backups --password1sudo mysql_config_editor set --login-path=db1-backup --host=db1.example.internal --user=backups --password
You'll be prompted for the password, which is then stored in /root/.mylogin.cnf (obfuscated, mode 0600).
Verify entries:
1sudo mysql_config_editor print --all1sudo mysql_config_editor print --all
Remove an entry if needed:
1sudo mysql_config_editor remove --login-path=db1-backup1sudo mysql_config_editor remove --login-path=db1-backup
Test a connection:
1sudo mysql --login-path=db1-backup -e "SHOW DATABASES;"1sudo mysql --login-path=db1-backup -e "SHOW DATABASES;"
4. Create a MySQL Backup User on the Remote Server
Connect to MySQL on the database server:
1mysql -u root -p1mysql -u root -p
Create the backup user. Replace 198.51.100.25 with the IP of the server running this script, or use % to allow connections from any host:
1CREATE USER 'backups'@'198.51.100.25' IDENTIFIED BY 'strong_password_here';23GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER, PROCESS ON *.* TO 'backups'@'198.51.100.25';45FLUSH PRIVILEGES;1CREATE USER 'backups'@'198.51.100.25' IDENTIFIED BY 'strong_password_here';23GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER, PROCESS ON *.* TO 'backups'@'198.51.100.25';45FLUSH PRIVILEGES;
PROCESSmust be granted globally (*.*) and is required to avoid tablespace errors duringmysqldump.
Verify the user was created:
1SELECT user, host FROM mysql.user WHERE user = 'backups';1SELECT user, host FROM mysql.user WHERE user = 'backups';
If you need to fix the host IP after creation:
1RENAME USER 'backups'@'wrong_host' TO 'backups'@'198.51.100.25';2FLUSH PRIVILEGES;1RENAME USER 'backups'@'wrong_host' TO 'backups'@'198.51.100.25';2FLUSH PRIVILEGES;
5. Ensure MySQL Accepts Remote Connections
On the database server, check the bind address:
1sudo grep -E "bind.address|bind-address" /etc/mysql/mysql.conf.d/mysqld.cnf1sudo grep -E "bind.address|bind-address" /etc/mysql/mysql.conf.d/mysqld.cnf
If it shows 127.0.0.1, change it to listen on the network:
1bind-address = 0.0.0.01bind-address = 0.0.0.0
Restart MySQL:
1sudo systemctl restart mysql1sudo systemctl restart mysql
6. Open the Firewall
Allow the backup server to reach MySQL:
1sudo ufw allow from 198.51.100.25 to any port 33061sudo ufw allow from 198.51.100.25 to any port 3306
Avoid opening port 3306 to the world. Lock it to specific source IPs.
7. Make the Script Executable and Run
1chmod +x mysql_backup.sh2sudo ./mysql_backup.sh1chmod +x mysql_backup.sh2sudo ./mysql_backup.sh
Must run as root —
/root/.mylogin.cnfis only readable by root, and cron runs backups as root too.
Cron Job
Run backups daily at 2 AM:
1sudo crontab -e1sudo crontab -e
10 2 * * * /path/to/mysqlbackup/mysql_backup.sh10 2 * * * /path/to/mysqlbackup/mysql_backup.sh
Backup Structure
1backup_dir/2└── hostname/3 └── database_name/4 ├── daily/5 │ └── database_name-YYYY-MM-DD.sql6 └── monthly/7 └── database_name-YYYY-MM.sql1backup_dir/2└── hostname/3 └── database_name/4 ├── daily/5 │ └── database_name-YYYY-MM-DD.sql6 └── monthly/7 └── database_name-YYYY-MM.sql
- Daily backups are created on every run.
- Monthly backups are created on the 1st of each month.
- Daily backups older than the configured
retention_daysare automatically deleted.
Excluded Databases
The following system databases are automatically excluded:
information_schemaperformance_schemamysqlsys
Required MySQL Privileges
| Privilege | Scope | Purpose |
|---|---|---|
SELECT |
*.* |
Read table data |
LOCK TABLES |
*.* |
Consistent backups |
SHOW VIEW |
*.* |
Back up views |
EVENT |
*.* |
Back up events |
TRIGGER |
*.* |
Back up triggers |
PROCESS |
*.* |
Dump tablespace info |
Restoring a Backup
To restore a dump using a configured login-path:
1mysql --login-path=db1-backup < /path/to/backup.sql1mysql --login-path=db1-backup < /path/to/backup.sql
Because the dump is created with --databases, it includes the CREATE DATABASE and USE statements needed to recreate and select the database during restore.
To restore with a different host or user:
1mysql -h db1.example.internal -u your-user -p < /path/to/backup.sql1mysql -h db1.example.internal -u your-user -p < /path/to/backup.sql
Notes
A few practical details worth calling out:
--single-transactionworks best for InnoDB tables and avoids locking during most backups.- This script writes plain
.sqldumps, which keeps restores simple. - If storage becomes a concern, piping the output through
gzipis an easy next step.
It’s not fancy, but it’s easy to understand, easy to schedule, and easy to extend.