Skip to content

Instantly share code, notes, and snippets.

@ProMasoud
Last active April 30, 2025 11:53
Show Gist options
  • Save ProMasoud/956df11973f0bfb153fd4b8ae31108ca to your computer and use it in GitHub Desktop.
Save ProMasoud/956df11973f0bfb153fd4b8ae31108ca to your computer and use it in GitHub Desktop.
This Bash script automates setting up a MySQL replica running in Docker by connecting to a Dockerized master (potentially remote via SSH), creating a replication user, performing a consistent backup with binlog coordinates, transferring the backup, generating and launching a `docker-compose` stack on the replica host, importing the data, configu…
#!/usr/bin/env bash
#
# replica_bootstrap.sh – quick (re)bootstrap of a MySQL replica in Docker
# Author: [email protected]
# ----------------------------------------------------------------------
set -euo pipefail
IFS=$'\n\t'
##############################################################################
# CONFIGURATION – change what you need and/or export as env‑vars beforehand
##############################################################################
MASTER_CTN="mysql"
MASTER_ROOT_PW="password"
SLAVE_HOST="10.0.0.1"
SLAVE_SSH="root@$SLAVE_HOST"
SLAVE_STACK_DIR="/opt/mysql-replica"
SLAVE_CTN="replica-db"
SLAVE_EXPORTER_CTN="mysqld-exporter"
REPL_USER="replicator"
REPL_PW="$(openssl rand -base64 24)" # $(openssl rand -base64 24) auto‑generate unless REPL_PW is preset
DUMP_FILE="/tmp/mysql_replica_$(date +%Y%m%d_%H%M%S).sql.gz"
##############################################################################
# HELPER FUNCTIONS
##############################################################################
banner() { printf '\n\033[1;36m▶ %s\033[0m\n' "$*"; }
die() { printf '\033[1;31m✖ %s\033[0m\n' "$*" >&2; exit 1; }
mysql_exec() {
docker exec -i "$MASTER_CTN" mysql -uroot -p"$MASTER_ROOT_PW" "$@" 2>/dev/null
}
health_check() {
banner "Health‑checking $1 ..."
if [[ $1 == "master" ]]; then
docker exec -e MYSQL_PWD="$MASTER_ROOT_PW" "$MASTER_CTN" \
mysqladmin ping -uroot >/dev/null \
|| die "Master MySQL is not responding"
else
ssh "$SLAVE_SSH" "docker ps --format '{{.Names}}' | grep -q '^$SLAVE_CTN$'" \
&& ssh "$SLAVE_SSH" "docker exec $SLAVE_CTN mysqladmin ping -uroot -p'$MASTER_ROOT_PW' >/dev/null" \
|| echo "(slave not up yet – will come up later)"
fi
printf '✔ OK\n'
}
##############################################################################
# 1. HEALTH CHECK
##############################################################################
health_check master
##############################################################################
# 2. CREATE/RESET REPLICATION USER
##############################################################################
banner "Creating replication user on master (if absent) ..."
mysql_exec -e "CREATE USER IF NOT EXISTS '$REPL_USER'@'%' IDENTIFIED WITH mysql_native_password BY '$REPL_PW';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO '$REPL_USER'@'%';
FLUSH PRIVILEGES;"
##############################################################################
# 3. CONSISTENT BACKUP WITH LIGHT LOCK
##############################################################################
banner "Dumping master (with --single-transaction, quick) ..."
# We still FLUSH TABLES WITH READ LOCK very briefly to freeze binlog position
mysql_exec -e "FLUSH TABLES WITH READ LOCK;"
LOCK_ACTIVE=1
trap '[[ ${LOCK_ACTIVE:-0} == 1 ]] && mysql_exec -e "UNLOCK TABLES;"' EXIT
# Grab current coordinates
CURRENT_STATUS=$(mysql_exec -e "SHOW MASTER STATUS\G")
BINLOG_FILE=$(printf '%s\n' "$CURRENT_STATUS" | awk '/File:/ {print $2}')
BINLOG_POS=$(printf '%s\n' "$CURRENT_STATUS" | awk '/Position:/ {print $2}')
docker exec -e MYSQL_PWD="$MASTER_ROOT_PW" "$MASTER_CTN" mysqldump \
-uroot --single-transaction --quick --source-data=2 \
--set-gtid-purged=OFF --all-databases --triggers --routines --events \
| gzip > "$DUMP_FILE"
mysql_exec -e "UNLOCK TABLES;"
LOCK_ACTIVE=0
trap - EXIT
##############################################################################
# 4. COPY DUMP AND BUILD SLAVE STACK
##############################################################################
banner "Copying dump to slave ..."
scp "$DUMP_FILE" "$SLAVE_SSH:/tmp/" >/dev/null
banner "Generating docker‑compose on slave ..."
cat <<EOF | ssh "$SLAVE_SSH" "mkdir -p $SLAVE_STACK_DIR && cat > $SLAVE_STACK_DIR/docker-compose.yml"
version: "3.9"
services:
$SLAVE_CTN:
image: mysql:8.1
container_name: $SLAVE_CTN
environment:
MYSQL_ROOT_PASSWORD: "$MASTER_ROOT_PW"
MYSQL_DATABASE: fanus
command:
- --server-id=2
- --log-bin=mysqld-bin
- --binlog_format=ROW
- --relay_log=mysqld-relay-bin
- --read_only=1
volumes:
- db-data:/var/lib/mysql
restart: unless-stopped
ports:
- "$SLAVE_HOST:3306:3306"
$SLAVE_EXPORTER_CTN:
image: prom/mysqld-exporter:latest
container_name: $SLAVE_EXPORTER_CTN
environment:
MYSQLD_EXPORTER_PASSWORD: "$MASTER_ROOT_PW"
command:
- "--mysqld.username=root"
- "--mysqld.address=127.0.0.1:3306"
depends_on: [$SLAVE_CTN]
restart: unless-stopped
ports:
- "9104:9104"
volumes:
db-data:
EOF
banner "Starting slave stack ..."
ssh "$SLAVE_SSH" "cd $SLAVE_STACK_DIR && docker compose up -d --wait"
banner "Waiting for MySQL inside slave to accept connections ..."
ssh "$SLAVE_SSH" '
until docker exec '"$SLAVE_CTN"' \
mysqladmin -h127.0.0.1 -uroot -p"'"$MASTER_ROOT_PW"'" ping --silent
do
sleep 2
done
'
##############################################################################
# 5. IMPORT DUMP AND CONFIGURE REPLICA
##############################################################################
banner "Importing dump into slave (this might take a few minutes) ..."
ssh "$SLAVE_SSH" "
gunzip -c /tmp/$(basename "$DUMP_FILE") |
docker exec -i $SLAVE_CTN \
mysql -h127.0.0.1 -uroot -p'$MASTER_ROOT_PW'
"
banner "Setting up CHANGE MASTER TO ..."
MASTER_IP=$(hostname -I | awk '{print $2}') # crude but works in single‑IP env
MASTER_PORT=12121
ssh "$SLAVE_SSH" "docker exec $SLAVE_CTN mysql -uroot -p'$MASTER_ROOT_PW' -e \"
STOP SLAVE;
RESET SLAVE ALL;
CHANGE MASTER TO
MASTER_HOST='$MASTER_IP',
MASTER_PORT='$MASTER_PORT',
MASTER_USER='$REPL_USER',
MASTER_PASSWORD='$REPL_PW',
MASTER_LOG_FILE='$BINLOG_FILE',
MASTER_LOG_POS=$BINLOG_POS,
GET_MASTER_PUBLIC_KEY=1;
START SLAVE;
\""
##############################################################################
# 6. VERIFY REPLICATION
##############################################################################
banner "Waiting for slave to catch up ..."
for i in {1..30}; do
SECS_BEHIND=$(ssh "$SLAVE_SSH" "docker exec $SLAVE_CTN mysql -N -uroot -p'$MASTER_ROOT_PW' -e 'SHOW SLAVE STATUS\G' | awk '/Seconds_Behind_Master:/ {print \$2+0}'")
if [[ $SECS_BEHIND -eq 0 ]]; then
printf '\033[1;32m✓ Replication healthy. Done.\033[0m\n'
exit 0
fi
sleep 2
done
die "Replication did not converge in time; check the slave's log."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment