#!/usr/bin/env bash # Pull WordPress CT101 (pve-hetzner) → CT600 wp-mirror (pve-mu-3) # CT101 impact: read-only mysqldump (--single-transaction) + read-only tar. No restarts. # Logs: Host /var/log/wp-mirror-sync.log + CT600 /var/log/wp-mirror-sync.log → Loki (Promtail) set -euo pipefail export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" HETZNER_HOST="${HETZNER_HOST:-root@100.88.230.59}" HETZNER_CT="${HETZNER_CT:-101}" MIRROR_CT="${MIRROR_CT:-600}" STAGING="${STAGING:-/opt/wp-mirror-sync/staging}" LOG="${LOG:-/var/log/wp-mirror-sync.log}" LOG_CT="/var/log/wp-mirror-sync.log" LOCK_FILE="/var/run/wp-mirror-sync.lock" FAILOVER_FLAG="/opt/wp-mirror-sync/FAILOVER_ACTIVE" MIRROR_SITEURL="http://192.168.178.142" MYSQL_USER="wordpress" MYSQL_PASS="astral66" MYSQL_DB="wordpress" WP_VOL="/var/lib/docker/volumes/wordpress_wp_data/_data" MIRROR_RUNNING=0 log() { local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*" echo "$msg" | tee -a "$LOG" if [[ "$MIRROR_RUNNING" == "1" ]]; then echo "$msg" | pct exec "$MIRROR_CT" -- tee -a "$LOG_CT" >/dev/null 2>&1 || true fi } ensure_mirror_ct() { if [[ "$(pct status "$MIRROR_CT" 2>/dev/null | awk '{print $2}')" != "running" ]]; then pct start "$MIRROR_CT" fi for _ in $(seq 1 30); do if pct exec "$MIRROR_CT" -- test -d /var/log 2>/dev/null; then MIRROR_RUNNING=1 pct exec "$MIRROR_CT" -- touch "$LOG_CT" 2>/dev/null || true return 0 fi sleep 2 done return 1 } ct101_http_ok() { local code code="$(ssh -o BatchMode=yes -o ConnectTimeout=15 "$HETZNER_HOST" \ "pct exec ${HETZNER_CT} -- curl -sS -o /dev/null -w '%{http_code}' --max-time 15 http://127.0.0.1/" 2>/dev/null || echo 000)" [[ "$code" == "200" || "$code" == "301" ]] } tar_sync_files() { rm -rf "${STAGING}/wpfiles" mkdir -p "${STAGING}/wpfiles" ssh -o BatchMode=yes -o ConnectTimeout=30 "$HETZNER_HOST" \ "ionice -c3 nice -n 19 pct exec ${HETZNER_CT} -- tar c -C ${WP_VOL} \ --exclude=wp-content/cache \ --exclude=wp-content/debug.log \ --exclude=wp-content/upgrade \ . 2>/dev/null" | tar x -C "${STAGING}/wpfiles" } exec 9>"$LOCK_FILE" if ! flock -n 9; then log "SKIP: another sync is running" exit 0 fi if [[ -f "$FAILOVER_FLAG" ]]; then log "SKIP: ${FAILOVER_FLAG} present (manual failover)" exit 0 fi if ! command -v pct >/dev/null 2>&1; then log "ABORT: must run on Proxmox host with pct (pve-mu-3)" exit 1 fi mkdir -p "$STAGING" "$(dirname "$LOG")" log "Checking CT101 health..." if ! ct101_http_ok; then log "ABORT: CT101 does not respond on http://127.0.0.1/ (expected HTTP 200/301)" exit 1 fi log "Starting CT600 (Promtail/Loki)..." if ! ensure_mirror_ct; then log "ABORT: CT600 did not become ready" exit 1 fi log "START sync CT101 → CT600" DB_FILE="${STAGING}/wordpress.sql.gz" log "DB dump (single-transaction, read-only)..." if ! ssh -o BatchMode=yes -o ConnectTimeout=30 "$HETZNER_HOST" \ "ionice -c3 nice -n 19 pct exec ${HETZNER_CT} -- docker exec wordpress-mysql mysqldump \ -u${MYSQL_USER} -p${MYSQL_PASS} \ --single-transaction --quick --skip-lock-tables \ --add-drop-table ${MYSQL_DB} 2>/dev/null" | gzip > "${DB_FILE}.tmp"; then log "ABORT: mysqldump failed" rm -f "${DB_FILE}.tmp" exit 1 fi mv "${DB_FILE}.tmp" "$DB_FILE" log "DB dump size: $(du -h "$DB_FILE" | awk '{print $1}')" log "File sync via tar (read-only, ~5–8 min)..." if ! tar_sync_files; then log "ABORT: file sync via tar failed" exit 1 fi log "Files staged: $(du -sh "${STAGING}/wpfiles" | awk '{print $1}')" for _ in $(seq 1 45); do if pct exec "$MIRROR_CT" -- docker info >/dev/null 2>&1; then break fi sleep 2 done pct exec "$MIRROR_CT" -- docker stop wordpress-app 2>/dev/null || true log "Import DB into CT600..." gunzip -c "$DB_FILE" | pct exec "$MIRROR_CT" -- docker exec -i wordpress-mysql \ mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" "$MYSQL_DB" log "Import files into CT600..." tar c -C "${STAGING}/wpfiles" . 2>/dev/null | pct exec "$MIRROR_CT" -- tar x -C "$WP_VOL" log "Reset mirror idle siteurl/home..." pct exec "$MIRROR_CT" -- docker exec wordpress-mysql mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" "$MYSQL_DB" -e \ "UPDATE wp_options SET option_value='${MIRROR_SITEURL}' WHERE option_name IN ('siteurl','home');" pct exec "$MIRROR_CT" -- docker start wordpress-app sleep 5 POSTS="$(pct exec "$MIRROR_CT" -- docker exec wordpress-mysql mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" -N -e \ "SELECT COUNT(*) FROM wp_posts WHERE post_status='publish' AND post_type='post';" "$MYSQL_DB" 2>/dev/null)" LAST="$(pct exec "$MIRROR_CT" -- docker exec wordpress-mysql mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" -N -e \ "SELECT MAX(post_date) FROM wp_posts WHERE post_type='post';" "$MYSQL_DB" 2>/dev/null)" log "Mirror verified: ${POSTS} published posts, latest ${LAST}" if ! ct101_http_ok; then log "WARN: CT101 health check failed AFTER sync — investigate immediately" exit 1 fi log "Stopping CT600 (standby)..." pct stop "$MIRROR_CT" MIRROR_RUNNING=0 log "DONE"