#!/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 个赞
感谢分享
这是个好东西
兄弟,你的头像换个有颜色的吧,啥也看不到,另外你的硬件版开了
感谢分享!另外点赞必回~
2 个赞
不错,已赞
点了
1 个赞
工具不错,已赞

