Last active
April 30, 2025 11:53
-
-
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…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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