#!/bin/bash # Usage: curl -sL https://url/migrate.sh | bash -s -- [SSH_KEY_PATH] LOCAL_SEARCH="$1" DEST_CONN="$2" REMOTE_SEARCH="$3" SSH_KEY_PATH="$4" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # --- 1. CONFIGURE SSH --- # Base options to avoid "Are you sure?" prompts and timeouts SSH_BASE_OPTS="-o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no" if [ -n "$SSH_KEY_PATH" ]; then echo -e "šŸ”‘ Using Identity File: ${YELLOW}$SSH_KEY_PATH${NC}" if [ ! -f "$SSH_KEY_PATH" ]; then echo -e "${RED}Error: Key file not found at $SSH_KEY_PATH${NC}" exit 1 fi # Create the SSH command with the key SSH_CMD="ssh -i $SSH_KEY_PATH $SSH_BASE_OPTS" # Create the RSYNC generic shell command RSYNC_SH="ssh -i $SSH_KEY_PATH $SSH_BASE_OPTS" else SSH_CMD="ssh $SSH_BASE_OPTS" RSYNC_SH="ssh $SSH_BASE_OPTS" fi # --- 2. PRE-FLIGHT CHECKS --- if [ -z "$REMOTE_SEARCH" ]; then echo -e "${RED}Usage: $0 [Key_Path]${NC}" exit 1 fi echo -e "šŸ“” Connecting to $DEST_CONN..." if ! $SSH_CMD "$DEST_CONN" echo "SSH Connection OK" >/dev/null 2>&1; then echo -e "${RED}Error: Cannot connect to $DEST_CONN.${NC}" echo -e " Check your IP, User, or SSH Key permissions." exit 1 fi # --- 3. IDENTIFY CONTAINERS --- echo -e "\nšŸ” Identifying Containers..." # Local L_ID=$(docker ps -aq --filter "name=${LOCAL_SEARCH}" | head -n 1) L_NAME=$(docker ps -a --filter "name=${LOCAL_SEARCH}" --format "{{.Names}}" | head -n 1) # Remote (via SSH) R_ID=$($SSH_CMD "$DEST_CONN" "docker ps -aq --filter 'name=${REMOTE_SEARCH}' | head -n 1") R_NAME=$($SSH_CMD "$DEST_CONN" "docker ps -a --filter 'name=${REMOTE_SEARCH}' --format '{{.Names}}' | head -n 1") if [ -z "$L_ID" ]; then echo -e "${RED}Local container '${LOCAL_SEARCH}' not found.${NC}"; exit 1; fi if [ -z "$R_ID" ]; then echo -e "${RED}Remote container '${REMOTE_SEARCH}' not found on Server B.${NC}"; exit 1; fi echo -e " Source: ${GREEN}$L_NAME${NC} ($L_ID)" echo -e " Target: ${GREEN}$R_NAME${NC} ($R_ID)" # --- 4. STOP CONTAINERS --- echo -e "\nšŸ›‘ ${RED}Stopping containers to freeze state...${NC}" docker stop "$L_ID" $SSH_CMD "$DEST_CONN" "docker stop $R_ID" # --- 5. MIGRATE VOLUMES --- echo -e "\nšŸ“¦ Mapping & Syncing Volumes..." # Get Mounts: Type|SourcePath|InternalPath L_MOUNTS=$(docker inspect -f '{{range .Mounts}}{{.Type}}|{{.Source}}|{{.Destination}} {{end}}' "$L_ID") for MOUNT in $L_MOUNTS; do TYPE=$(echo "$MOUNT" | cut -d'|' -f1) L_PATH=$(echo "$MOUNT" | cut -d'|' -f2) INTERNAL_TARGET=$(echo "$MOUNT" | cut -d'|' -f3) # Skip system mounts if [[ "$INTERNAL_TARGET" == *".sock" ]] || [[ "$INTERNAL_TARGET" == *".conf" ]]; then continue; fi echo -e "\n šŸ”„ Volume: ${YELLOW}$INTERNAL_TARGET${NC}" # Find corresponding path on Remote R_PATH_RAW=$($SSH_CMD "$DEST_CONN" "docker inspect -f '{{range .Mounts}}{{if eq .Destination \"$INTERNAL_TARGET\"}}{{.Source}}{{end}}{{end}}' $R_ID") if [ -z "$R_PATH_RAW" ]; then echo -e " ${RED}Skipping: Target has no matching volume.${NC}" continue fi # Resolve Real Paths if [ "$TYPE" == "volume" ]; then REAL_L_PATH=$(docker volume inspect --format '{{.Mountpoint}}' "$L_PATH") REAL_R_PATH=$($SSH_CMD "$DEST_CONN" "docker volume inspect --format '{{.Mountpoint}}' $R_PATH_RAW 2>/dev/null || echo $R_PATH_RAW") else REAL_L_PATH="$L_PATH" REAL_R_PATH="$R_PATH_RAW" fi # RSYNC EXECUTION echo " šŸš€ Transferring data..." rsync -az --info=progress2 -e "$RSYNC_SH" "${REAL_L_PATH}/" "$DEST_CONN:${REAL_R_PATH}/" done # --- 6. RESTART --- echo -e "\nāœ… Transfer Complete." echo -e " Restarting containers..." $SSH_CMD "$DEST_CONN" "docker start $R_ID" docker start "$L_ID" echo -e "\nšŸŽ‰ ${GREEN}Success!${NC}"