11#! /bin/bash
2- # This is a sample backup script for your MySQL database.
3- # There's probably a hundred better ways to do it, but this + a simple cronjob does the job most of the time.
2+ # Enhanced MySQL backup script (multi-instance, env-driven)
3+ # Works great with Dockerized MySQL in a shared network (e.g. game_node_app)
44
5- # Current date in YYYY-MM-DD-HHMMSS format for unique backup filenames
5+ set -euo pipefail
6+ IFS=$' \n\t '
7+
8+ # === CONFIGURATION ===
9+
10+ # Current date for unique backup filenames
611DATE=$( date +%F-%H%M%S)
712
8- # Backup directory on the host
9- BACKUP_DIR=" ~/backups/sql"
13+ # Backup directory on host
14+ BACKUP_DIR=" ${BACKUP_DIR:-/ var/ local/ backups/ sql} "
15+ mkdir -p " $BACKUP_DIR "
16+
17+ # Docker network shared by both DB containers
18+ NETWORK=" ${NETWORK:- game_node_app} "
19+
20+ # Docker MySQL image used for dumping
21+ MYSQL_IMAGE=" ${MYSQL_IMAGE:- mysql: 8.3} "
22+
23+ # Rclone config (optional)
24+ RCLONE_CONFIG_NAME=" ${RCLONE_CONFIG_NAME:- cloudflare} "
25+ RCLONE_BUCKET_NAME=" ${RCLONE_BUCKET_NAME:- gamenode-sql-backup} "
26+
27+ # === DATABASES TO BACKUP ===
28+ # These must be passed via environment variables in crontab, e.g.:
29+ # DB1_NAME=gamenode DB1_USER=root DB1_PASS=pass DB1_HOST=db
30+ # DB2_NAME=supertokens DB2_USER=root DB2_PASS=pass DB2_HOST=supertokens_db
31+ # and so on.
32+
33+ declare -A DBS=(
34+ [" $DB1_NAME " ]=" $DB1_USER :$DB1_PASS @$DB1_HOST "
35+ )
36+
37+ # Add second database if defined
38+ if [[ -n " ${DB2_NAME:- } " && -n " ${DB2_USER:- } " && -n " ${DB2_PASS:- } " && -n " ${DB2_HOST:- } " ]]; then
39+ DBS[" $DB2_NAME " ]=" $DB2_USER :$DB2_PASS @$DB2_HOST "
40+ fi
41+
42+ # === BACKUP LOOP ===
43+
44+ for DB_NAME in " ${! DBS[@]} " ; do
45+ echo " 🔹 Backing up database: $DB_NAME "
46+
47+ CREDENTIALS=" ${DBS[$DB_NAME]} "
48+ DB_USER=" ${CREDENTIALS%%:* } "
49+ REST=" ${CREDENTIALS#*: } "
50+ DB_PASS=" ${REST%%@* } "
51+ DB_HOST=" ${REST#*@ } "
52+
53+ BACKUP_FILENAME=" $BACKUP_DIR /${DB_NAME} -${DATE} .sql"
54+ COMPRESSED_BACKUP_FILENAME=" ${BACKUP_FILENAME} .zst"
55+
56+ # Maximum number of retries
57+ MAX_RETRIES=3
58+ RETRY_DELAY=5 # seconds between attempts
1059
11- # Database credentials and details
12- DB_HOST=" localhost" # hostname of the mysql container
13- DB_USER=" root"
14- DB_PASSWORD=" root"
15- DB_NAME=" gamenode"
16- NETWORK=" your_network" # name of the network where mysql container is running. You can check the list of the docker neworks using doocker network ls
60+ # Dump DB using Dockerized MySQL client with retries
61+ SUCCESS=false
62+ for (( i= 1 ; i<= MAX_RETRIES; i++ )) ; do
63+ echo " 🔹 Attempt $i : Backing up database $DB_NAME ..."
64+ if docker run --rm --network " $NETWORK " " $MYSQL_IMAGE " \
65+ mysqldump --compact --single-transaction --quick --lock-tables=false \
66+ -h " $DB_HOST " -u " $DB_USER " -p" $DB_PASS " " $DB_NAME " > " $BACKUP_FILENAME " ; then
67+ SUCCESS=true
68+ break
69+ else
70+ echo " ⚠️ Backup attempt $i failed."
71+ if [[ $i -lt $MAX_RETRIES ]]; then
72+ echo " ⏳ Retrying in $RETRY_DELAY seconds..."
73+ sleep $RETRY_DELAY
74+ fi
75+ fi
76+ done
1777
18- # Docker image version of MySQL
19- MYSQL_IMAGE=" mysql:8.3"
78+ if [[ " $SUCCESS " != true ]]; then
79+ echo " ❌ Failed to backup database $DB_NAME after $MAX_RETRIES attempts. Exiting."
80+ exit 1
81+ fi
2082
21- # Backup filename
22- BACKUP_FILENAME=" $BACKUP_DIR /$DB_NAME -$DATE .sql"
23- COMPRESSED_BACKUP_FILENAME=" $BACKUP_FILENAME .zst"
83+ # Compress with 2 threads
84+ zstd -9 -T2 " $BACKUP_FILENAME " -o " $COMPRESSED_BACKUP_FILENAME "
2485
25- # S3 bucket name
26- BUCKET_NAME=" my-sql-backup"
27- RCLONE_CONFIG_NAME=" sql-backup"
86+ # Remove uncompressed dump
87+ rm -f " $BACKUP_FILENAME "
2888
29- # Run mysqldump within a new Docker container
30- docker run --rm --network $NETWORK $MYSQL_IMAGE mysqldump --compact -h $DB_HOST -u $DB_USER -p$DB_PASSWORD $DB_NAME > $BACKUP_FILENAME
89+ echo " ✅ Backup complete: $COMPRESSED_BACKUP_FILENAME "
3190
32- # Compress the backup file
33- zstd -19 " $BACKUP_FILENAME " -o " $COMPRESSED_BACKUP_FILENAME "
91+ # Upload to R2 via rclone if enabled
92+ if [[ " ${UPLOAD_TO_RCLONE:- false} " == " true" ]]; then
93+ echo " ☁️ Uploading $COMPRESSED_BACKUP_FILENAME to R2..."
94+ rclone copy " $COMPRESSED_BACKUP_FILENAME " " ${RCLONE_CONFIG_NAME} :${RCLONE_BUCKET_NAME} " \
95+ --quiet --s3-no-check-bucket
96+ fi
3497
35- # Removes SQL backup file after compression
36- rm $BACKUP_FILENAME
98+ # Optionally remove local copy after upload
99+ if [[ " ${CLEANUP_LOCAL:- false} " == " true" ]]; then
100+ echo " 🧹 Removing local backup $COMPRESSED_BACKUP_FILENAME "
101+ rm -f " $COMPRESSED_BACKUP_FILENAME "
102+ fi
103+ done
37104
38- rclone copy $COMPRESSED_BACKUP_FILENAME $RCLONE_CONFIG_NAME :$BUCKET_NAME
105+ # === Cleanup old backups (older than 14 days) ===
106+ # From local files
107+ echo " 🗑️ Removing local backups older than 14 days in $BACKUP_DIR ..."
108+ find " $BACKUP_DIR " -type f -name " *.zst" -mtime +14 -exec rm -f {} \;
109+ # From RCLONE
110+ echo " 🗑️ Removing bucket backups older than 14 days in ${RCLONE_CONFIG_NAME} :${RCLONE_BUCKET_NAME} ..."
111+ rclone delete --min-age 14d " ${RCLONE_CONFIG_NAME} :${RCLONE_BUCKET_NAME} "
39112
40- rm $COMPRESSED_BACKUP_FILENAME
113+ echo " 🎉 All backups completed successfully. "
0 commit comments