Docker:迁移工具箱📦

#!/bin/bash
# Docker 容器迁移工具箱 (防冲突版)

LOGFILE="migrate.log"

log() {
  echo -e "[$(date '+%F %T')] $*" | tee -a $LOGFILE
}

# -------------------------
# 自动安装依赖
# -------------------------
check_dependencies() {
  if command -v apt >/dev/null 2>&1; then
    PKG_INSTALL="sudo apt update && sudo apt install -y"
  elif command -v yum >/dev/null 2>&1; then
    PKG_INSTALL="sudo yum install -y"
  else
    PKG_INSTALL=""
  fi

  if ! command -v pv >/dev/null 2>&1; then
    log "⚠️ 未检测到 pv,正在尝试安装..."
    if [ -n "$PKG_INSTALL" ]; then
      eval "$PKG_INSTALL pv" || { log "❌ 安装 pv 失败,请手动安装"; exit 1; }
    else
      log "❌ 未检测到包管理器,请手动安装 pv"; exit 1
    fi
  fi

  if ! command -v docker-compose >/dev/null 2>&1; then
    log "⚠️ 未检测到 docker-compose,正在尝试安装..."
    if [ -n "$PKG_INSTALL" ]; then
      if command -v apt >/dev/null 2>&1; then
        eval "$PKG_INSTALL docker-compose-plugin" || true
        if ! command -v docker-compose >/dev/null 2>&1 && [ -f /usr/libexec/docker/cli-plugins/docker-compose ]; then
          sudo ln -s /usr/libexec/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose || true
        fi
      else
        eval "$PKG_INSTALL docker-compose" || true
      fi
    fi

    if ! command -v docker-compose >/dev/null 2>&1; then
      sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" \
        -o /usr/local/bin/docker-compose
      sudo chmod +x /usr/local/bin/docker-compose
    fi

    if ! command -v docker-compose >/dev/null 2>&1; then
      log "❌ 安装 docker-compose 失败,请手动安装"; exit 1
    fi
  fi

  log "✅ 所有依赖已满足 (pv, docker-compose)"
}

# -------------------------
# 磁盘空间检查
# -------------------------
check_disk_space() {
  local target_dir=$1
  local required_size=$2
  local available=$(df -P "$target_dir" | awk 'NR==2 {print $4}')   # KB
  local available_bytes=$((available * 1024))

  if [ "$available_bytes" -lt "$required_size" ]; then
    log "❌ 磁盘空间不足: 需要 $(numfmt --to=iec $required_size),可用 $(numfmt --to=iec $available_bytes)"
    exit 1
  else
    log "✅ 磁盘空间检查通过: 需要 $(numfmt --to=iec $required_size),可用 $(numfmt --to=iec $available_bytes)"
  fi
}

# -------------------------
# UI 菜单
# -------------------------
show_menu() {
  echo "==========================================="
  echo " 🐳 Docker 容器迁移工具箱"
  echo "==========================================="
  echo "1) 迁移容器 (旧服务器执行)"
  echo "2) 恢复容器 (新服务器执行)"
  echo "3) 生成 docker-compose.yml (新服务器执行)"
  echo "4) 启动容器 (新服务器执行)"
  echo "5) 检查容器状态 (新服务器执行)"
  echo "6) 回滚迁移 (新服务器执行)"
  echo "7) 查看日志"
  echo "8) 退出"
  echo "==========================================="
}

check_port() {
  port=$1
  if lsof -i :$port >/dev/null 2>&1; then
    log "⚠️ 端口 $port 已被占用"
    return 1
  fi
  return 0
}

# -------------------------
# 迁移容器
# -------------------------
migrate_containers() {
  read -p "请输入目标服务器用户: " USER
  read -p "请输入目标服务器IP: " HOST
  read -p "请输入目标目录(默认:/opt/docker-migrate): " DEST
  DEST=${DEST:-/opt/docker-migrate}

  mkdir -p migrate_tmp/images migrate_tmp/config migrate_tmp/volumes
  log ">>> 收集容器信息 (旧服务器)"
  echo ">>> 当前运行的容器:"
  docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"

  echo
  read -p "是否迁移所有容器?(y/n): " all_choice
  if [[ "$all_choice" =~ ^[Yy]$ ]]; then
    selected=$(docker ps -q)
  else
    echo "请输入要迁移的容器名 (多个用空格分隔):"
    read cname_list
    selected=""
    for cname in $cname_list; do
      cid=$(docker ps -q -f "name=^/${cname}$")
      if [ -n "$cid" ]; then
        selected="$selected $cid"
      else
        log "⚠️ 容器 $cname 未找到,跳过"
      fi
    done
  fi

  > migrate_tmp/containers.list
  for cid in $selected; do
    cname=$(docker inspect --format '{{.Name}}' $cid | sed 's#/##')
    echo "$cname" >> migrate_tmp/containers.list
    img=$(docker inspect --format '{{.Config.Image}}' $cid)
    log ">>> 处理容器: $cname (镜像: $img)"

    docker inspect $cid > migrate_tmp/config/${cname}.json

    if [[ "$img" == *"spg-registry"* || "$img" == *"gitlab"* ]]; then
      log "  ⏳ 保存私有镜像: $img"
      docker save -o migrate_tmp/images/${cname}.tar $img
    else
      echo "$img" >> migrate_tmp/images/public_images.txt
    fi

    for vol in $(docker inspect --format '{{range .Mounts}}{{.Source}} {{end}}' $cid); do
      if [ -d "$vol" ]; then
        vname=${cname}_$(basename $vol)
        log "  打包数据卷: $vol"
        tar czf migrate_tmp/volumes/${vname}.tgz -C $(dirname $vol) $(basename $vol)
      fi
    done
  done

  size=$(du -sb migrate_tmp | awk '{print $1}')
  check_disk_space "/opt" "$size"
  log ">>> 打包迁移文件 (大小: $(du -sh migrate_tmp | awk '{print $1}'))"
  tar cf - migrate_tmp | pv -s $size | gzip > docker_migrate_bundle.tgz

  fsize=$(du -sb docker_migrate_bundle.tgz | awk '{print $1}')
  log ">>> 传输到目标服务器 $HOST:$DEST (大小: $(du -sh docker_migrate_bundle.tgz | awk '{print $1}'))"
  pv -s $fsize docker_migrate_bundle.tgz | ssh $USER@$HOST "mkdir -p $DEST && cat > $DEST/docker_migrate_bundle.tgz"

  rm -rf migrate_tmp
  log "✅ 迁移完成"
}

# -------------------------
# 恢复容器
# -------------------------
restore_containers() {
  DEFAULT_SRC="/opt/docker-migrate/migrate_tmp"
  read -p "请输入迁移目录(默认:$DEFAULT_SRC): " SRC
  SRC=${SRC:-$DEFAULT_SRC}

  if [ ! -d "$SRC" ] && [ -f /opt/docker-migrate/docker_migrate_bundle.tgz ]; then
    bundle_size=$(du -sb /opt/docker-migrate/docker_migrate_bundle.tgz | awk '{print $1}')
    check_disk_space "/opt/docker-migrate" "$bundle_size"
    log ">>> 检测到迁移包,正在解压..."
    mkdir -p /opt/docker-migrate
    tar xzf /opt/docker-migrate/docker_migrate_bundle.tgz -C /opt/docker-migrate
  fi

  if [ ! -d "$SRC/config" ]; then
    log "❌ 未找到配置目录 $SRC/config,请确认迁移包是否存在"
    return 1
  fi

  echo ">>> 可用的容器配置文件:"
  ls -1 $SRC/config | sed 's/\.json$//'

  echo
  read -p "是否恢复所有容器?(y/n): " all_choice
  if [[ "$all_choice" =~ ^[Yy]$ ]]; then
    selected=$(ls -1 $SRC/config | sed 's/\.json$//')
  else
    echo "请输入要恢复的容器名 (多个用空格分隔):"
    read cname_list
    selected=$cname_list
  fi

  for cname in $selected; do
    # 检查同名容器冲突
    if docker ps -a --format '{{.Names}}' | grep -qw "$cname"; then
      echo "⚠️ 检测到新服务器已有容器 [$cname]"
      read -p "选择操作: [s=跳过, n=新名字启动, o=覆盖]: " action
      case $action in
        s|S) log "👉 跳过容器 $cname"; continue ;;
        n|N) new_name="${cname}_new"; log "👉 使用新容器名: $new_name"; cname=$new_name ;;
        o|O) log "👉 覆盖已有容器 $cname"; docker rm -f $cname >/dev/null 2>&1 ;;
        *) log "❌ 无效输入,跳过容器 $cname"; continue ;;
      esac
    fi

    img_file="$SRC/images/${cname}.tar"
    if [ -f "$img_file" ]; then
      size=$(du -sb $img_file | awk '{print $1}')
      check_disk_space "/" "$size"
      log "  加载镜像: $img_file"
      pv -s $size $img_file | docker load
    fi

    for vol in $SRC/volumes/${cname}_*.tgz; do
      [ -f "$vol" ] || continue
      vol_size=$(du -sb "$vol" | awk '{print $1}')
      check_disk_space "/" "$vol_size"
      log "  解压数据卷: $vol"
      pv -s $vol_size $vol | tar xzf - -C /
    done
  done

  if [ -f $SRC/images/public_images.txt ]; then
    log ">>> 拉取公共镜像"
    while read img; do
      docker pull $img
    done < $SRC/images/public_images.txt
  fi

  log "✅ 恢复完成 (容器: $selected)"
}

# -------------------------
# 生成 compose
# -------------------------
gen_compose() {
  read -p "请输入 config 目录(默认:/opt/docker-migrate/migrate_tmp/config): " CFG
  CFG=${CFG:-/opt/docker-migrate/migrate_tmp/config}

  log ">>> 生成 docker-compose.yml"
  echo "version: '3.8'" > docker-compose.yml
  echo "services:" >> docker-compose.yml

  for cfg in $CFG/*.json; do
    cname=$(basename $cfg .json)
    image=$(jq -r '.[0].Config.Image' $cfg)
    cmd=$(jq -r '.[0].Path + " " + (.[0].Args|join(" "))' $cfg)

    echo "  $cname:" >> docker-compose.yml
    echo "    container_name: $cname" >> docker-compose.yml
    echo "    image: $image" >> docker-compose.yml

    ports=$(jq -r '.[0].HostConfig.PortBindings | to_entries[]? | "\(.value[0].HostPort):\(.key)"' $cfg)
    if [ -n "$ports" ]; then
      echo "    ports:" >> docker-compose.yml
      for p in $ports; do
        host_port=$(echo $p | cut -d: -f1)
        cont_port=$(echo $p | cut -d: -f2)
        if lsof -i :$host_port >/dev/null 2>&1; then
          new_port=$((host_port + 1000))
          log "⚠️ 端口 $host_port 已被占用,改用 $new_port"
          echo "      - \"$new_port:$cont_port\"" >> docker-compose.yml
        else
          echo "      - \"$host_port:$cont_port\"" >> docker-compose.yml
        fi
      done
    fi

    mounts=$(jq -r '.[0].Mounts[]? | "- \(.Source):\(.Destination)"' $cfg)
    if [ -n "$mounts" ]; then
      echo "    volumes:" >> docker-compose.yml
      echo "$mounts" | sed 's/^/      /' >> docker-compose.yml
    fi

    envs=$(jq -r '.[0].Config.Env[]? | "- \(. )"' $cfg)
    if [ -n "$envs" ]; then
      echo "    environment:" >> docker-compose.yml
      echo "$envs" | sed 's/^/      /' >> docker-compose.yml
    fi

    if [ "$cmd" != " " ]; then
      echo "    command: $cmd" >> docker-compose.yml
    fi

    echo >> docker-compose.yml
  done

  log "✅ 已生成 docker-compose.yml (冲突端口已自动处理)"
}

# -------------------------
# 启动容器
# -------------------------
start_containers() {
  log ">>> 启动容器"
  docker-compose up -d | tee -a $LOGFILE
}

# -------------------------
# 检查容器状态
# -------------------------
check_containers() {
  log ">>> 检查容器状态"
  docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" | tee -a $LOGFILE
}

# -------------------------
# 回滚
# -------------------------
rollback() {
  log "⚠️ 执行回滚:只清理迁移环境 (不会影响已有容器)"
  docker-compose down
  rm -rf /opt/docker-migrate
  log "✅ 已回滚,旧服务器容器依旧运行"
}

# -------------------------
# 查看日志
# -------------------------
view_logs() {
  less +F $LOGFILE
}

# -------------------------
# 主循环
# -------------------------
check_dependencies
while true; do
  show_menu
  read -p "请选择操作(1-8): " choice
  case $choice in
    1) migrate_containers ;;
    2) restore_containers ;;
    3) gen_compose ;;
    4) start_containers ;;
    5) check_containers ;;
    6) rollback ;;
    7) view_logs ;;
    8) log "👋 退出工具箱"; exit 0 ;;
    *) echo "❌ 无效选择,请重新输入";;
  esac
done


5 个赞

:hj031:感谢分享

这是个好东西

兄弟,你的头像换个有颜色的吧,啥也看不到,另外你的硬件版开了

感谢分享!另外点赞必回~

2 个赞

不错,已赞

点了

1 个赞

工具不错,已赞