Py学习  »  docker

真正的生产力来了!Docker迁移部署两步搞定!

dotNET跨平台 • 2 周前 • 29 次点击  

前言

最近遇到了需要部署一套比较复杂的应用场景,刚好这套应用我在其他服务器部署过,为了节省折腾的时间,我打算直接把服务器上已有的搬过去。

PS:没想到这个过程比从头开始来耗费时间😂

好在是把一键迁移的脚本也搞出来了,以后遇到类似的情况就比较舒服了。

Docker 的一个典型优势场景就是可移植性

只需要把原服务器上的 应用相关目录 和 docker-compose.yml 文件 打包复制过去,在目标服务器上解压、部署即可。

本文记录一下 docker 迁移部署的过程。

打包原服务器的应用目录

需要找到 docker-compose 项目目录,一般包含:

  • docker-compose.yml
  • .env(如果有)
  • 其他挂载卷的本地目录,如 ./data./config./db 等

然后执行:

tar czvf myapp.tar.gz myapp/

复制

建议使用 scp 命令复制

这个是最方便的

scp myapp.tar.gz user@目标IP:/路径/

当然用 rsync 也可以,这个效率更高。但我习惯 scp 够用了。

迁移数据卷

如果  docker-compose.yml 中定义的 volumes 是 命名卷(named volumes),而不是绑定到主机目录(bind mount)。

例如:

volumes:
  oradata:
  dify_es01_data:

docker 通常是 /var/lib/docker/volumes/ 管理这些数据

数据卷会麻烦一些,需要导出和导入

导出卷数据

docker run --rm -v oradata:/data -v $(pwd):/backup alpine tar czf /backup/oradata.tar.gz -C /data .
docker run --rm -v dify_es01_data:/data -v $(pwd):/backup alpine tar czf /backup/dify_es01_data.tar.gz -C /data .

复制

继续使用 scp 导出




    
scp oradata.tar.gz dify_es01_data.tar.gz user@remote:/your/path/

创建空卷

在目标服务器创建空卷

docker volume create oradata
docker volume create dify_es01_data

导入数据

导入数据到卷

docker run --rm -v oradata:/data -v $(pwd):/backup alpine sh -c "cd /data && tar xzf /backup/oradata.tar.gz"
docker run --rm -v dify_es01_data:/data -v $(pwd):/backup alpine sh -c "cd /data && tar xzf /backup/dify_es01_data.tar.gz"

解压&启动

tar xzvf myapp.tar.gz
cd myapp/
docker-compose up -d

一键迁移脚本

这么多步骤执行下来还是太麻烦

我让大模型爷爷帮忙设计了一个一键迁移脚本

在反复打磨之下,这个脚本体验还是非常不错的,一百多行的代码可以实现自动识别数据卷,自动打包成大文件夹并复制到目标服务器

有需要的同学可以试试

将以下文件保存为 docker-app-pack.sh

#!/bin/bash

# Docker Compose 应用打包脚本
set -e

# 简化日志函数
log() { echo"⏰ [$(date +'%H:%M:%S')$1" >&2; }
error() { echo"❌ [ERROR] $1" >&2; exit 1; }

# 检查 Docker
! docker info >/dev/null 2>&1 && error  "Docker 未运行"

# 发现项目相关的数据卷
find_project_volumes() {
    local app_dir="$1"
    local project_name=$(basename "$app_dir")
    
    log"🔍 搜索项目相关数据卷 (前缀: $project_name)"
    docker volume ls --format "{{.Name}}" | grep "^${project_name}[_-]" || true
}

# 导出数据卷
export_volume() {
    local volume="$1" backup_dir="$2"
    log"💾 导出数据卷: $volume"
    docker run --rm -v "$volume:/data" -v "$backup_dir:/backup" alpine \
        sh -c "cd /data && tar czf /backup/${volume}.tar.gz ."
}

# 打包应用目录
package_app() {
    local app_dir="$1" backup_dir="$2" app_name="$3"
    log"📦 打包应用目录: $app_dir"
    tar czf "${backup_dir}/${app_name}_app.tar.gz" -C "$(dirname "$app_dir")""$(basename "$app_dir")"
}

# 创建最终压缩包
create_package() {
    local backup_dir="$1" app_name="$2" output_dir="$3"
    log"🗜️ 创建最终压缩包"
    tar czf "${output_dir}/${app_name}.tar.gz" -C "$backup_dir" .
}

# 上传到服务器
upload_file() {
    local file="$1" server="$2"
    [[ -z "$server" ]] && return 0
    log"🚀 上传到服务器: $server"
    scp " $file""$server"
}

# 主函数
main() {
    echo"🐳 === Docker Compose 应用打包工具 === 🐳"
    echo
    
    # 获取输入
    read -p "📁 应用目录路径: " app_dir
    [[ ! -d "$app_dir" ]] && error "目录不存在: $app_dir"
    
    app_name=$(basename "$app_dir")
    read -p "📦 应用名称 [$app_name]: " input_name
    [[ -n "$input_name" ]] && app_name="$input_name"
    
    # 自动发现数据卷
    auto_volumes=($(find_project_volumes "$app_dir"))
    echo"💾 发现数据卷:"
    if [[ ${#auto_volumes[@]} -eq 0 || (${#auto_volumes[@]} -eq 1 && -z "${auto_volumes[0]}") ]]; then
        echo"   └── 无"
    else
        for vol in"${auto_volumes[@]}"do
            [[ -n "$vol" ]] && echo"   └── $vol"
        done
    fi
    
    read -p "➕ 额外数据卷 (空格分隔): " extra_volumes
    volumes=(${auto_volumes[@]}$extra_volumes)
    
    read -p "🌐 上传服务器 (user@host:/path): " server
    
    # 显示摘要
    echo
    echo"📋 === 操作摘要 ==="
    echo"📂 应用目录: $app_dir"
    echo"📦 输出文件: ${app_name}.tar.gz"
    echo"💾 数据卷:"
    if [[ ${#volumes[@]} -eq 0 || (${#volumes[@]} -eq 1 && -z "${volumes[0]}") ]]; then
        echo"   └── 无"
    else
        for vol in" ${volumes[@]}"do
            [[ -n "$vol" ]] && echo"   └── $vol"
        done
    fi
    echo"🌐 上传服务器: ${server:-无}"
    echo
    
    read -p "✅ 确认执行? (y/N): " confirm
    [[ "$confirm" != [yY] ]] && exit 0
    
    # 执行备份
    backup_dir="/tmp/backup_$$"
    mkdir -p "$backup_dir"
    trap"rm -rf '$backup_dir'" EXIT
    
    # 打包应用
    package_app "$app_dir""$backup_dir""$app_name"
    
    # 导出数据卷
    for vol in"${volumes[@]}"do
        [[ -n "$vol" ]] && export_volume "$vol""$backup_dir"
    done
    
    # 创建最终包
    output_file="${app_name}.tar.gz"
    create_package "$backup_dir""$app_name""$(pwd)"
    
    # 上传
    upload_file "$output_file""$server"
    
    echo
    echo"🎉 === 备份完成! ==="
    echo"📁 文件: $output_file"
    echo"📏 大小: $(du -h "$output_file" | cut -f1)"
    echo"✨ 备份成功完成! ✨"
}

# 运行主函数
main "$@"

解包脚本

对应的有解包脚本,docker-app-unpack.sh

#!/bin/bash

set -e

# 简化日志函数
log() { echo"⏰ [$(date +'%H:%M:%S')$1" >&2; }
error() { echo"❌ [ERROR] $1" >&2; exit 1; }

# 检查 Docker
check_docker() {
    log"🔍 检查 Docker 服务..."
    ! docker info >/dev/null 2>&1 && error "Docker 未运行或无法访问"
    log"✅ Docker 服务运行正常。"
}

# 解压主包
unpack_package() {
    local package_file="$1" temp_dir="$2"
    log"📦 解压主包: $package_file 到 $temp_dir"
    tar xzf "$package_file" -C "$temp_dir"
}

# 导入应用目录
import_app() {
    local app_tar_file="$1" target_dir="$2"
    log"📂 导入应用目录: $app_tar_file 到 $target_dir"
    mkdir -p "$target_dir"
    tar xzf "$app_tar_file" -C "$target_dir " --strip-components=1
}

# 导入数据卷
import_volume() {
    local volume_tar_file="$1" volume_name="$2"
    log"💾 导入数据卷: $volume_name (来自 $volume_tar_file)"

    if docker volume inspect "$volume_name" >/dev/null 2>&1; then
        read -p "数据卷 '$volume_name' 已存在。是否覆盖? (y/N): " confirm_overwrite
        if [[ "$confirm_overwrite" != [yY] ]]; then
            log"跳过数据卷 '$volume_name' 的导入。"
            return 0
        fi
        log"删除现有数据卷 '$volume_name'..."
        docker volume rm "$volume_name" >/dev/null
    fi

    log"创建数据卷 '$volume_name'..."
    docker volume create "$volume_name" >/dev/null

    log"导入数据到数据卷 '$volume_name'..."
    docker run --rm -v "$volume_name:/data" -v "$(dirname "$volume_tar_file"):/backup" alpine \
        sh -c "tar xzf /backup/$(basename "$volume_tar_file") -C /data"
    log"✅ 数据卷 '$volume_name' 导入成功。"
}

# 主函数
main() {
    echo"🐳 === Docker Compose 应用解包工具 === 🐳"
    echo

    check_docker

    read -p "📦 请输入待解包的 .tar.gz 文件路径: " package_file
    [[ ! -f "$package_file" ]] && error "文件不存在: $package_file"

    # 创建临时目录
    local temp_dir
    temp_dir="$(mktemp -d -t docker-unpack-XXXXXX)"
    log"创建临时目录: $temp_dir"
    trap"log '清理临时目录: $temp_dir'; rm -rf '$temp_dir'" EXIT

    unpack_package "$package_file""$temp_dir"

    echo
    echo"📋 === 解包摘要 ==="
    echo"📦 源文件: $package_file"
    echo"📁 临时解压目录: $temp_dir"

    # 查找应用目录包
    local app_tar_found=false
    for f in"$temp_dir"/*_app.tar.gz; do
        if [[ -f "$f" ]]; then
            app_tar_file="$f"
            app_tar_found=true
            break
        fi
    done

    if ! $app_tar_foundthen
        error "在解压包中未找到应用目录文件 (*_app.tar.gz)。"
    fi

    local default_app_dir="$(pwd)/$(basename "${app_tar_file%_app.tar.gz}")"
    read -p "📂 请输入应用目录解压目标路径 [$default_app_dir]: " target_app_dir
    [[ -z "$target_app_dir" ]] && target_app_dir="$default_app_dir"

    echo"应用目录将解压到: $target_app_dir"

    # 查找数据卷包
    local volume_tar_files=("$temp_dir"/*.tar.gz)
    # 过滤掉应用目录包
    volume_tar_files=( "${volume_tar_files[@]/$app_tar_file}" )

    echo"💾 发现数据卷包:"
    if [[ ${#volume_tar_files[@]} -eq 0 || (${#volume_tar_files[@]} -eq 1 && -z "${volume_tar_files[0]}") ]]; then
        echo"   └── 无"
    else
        for vol_file in"${volume_tar_files[@]}"do
            [[ -n "$vol_file" ]] && echo"   └── $(basename "$vol_file")"
        done
    fi
    echo

    read -p "✅ 确认执行? (y/N): " confirm
    [[ "$confirm" != [yY] ]] &&  exit 0

    # 执行解包和导入
    import_app "$app_tar_file""$target_app_dir"

    for vol_file in"${volume_tar_files[@]}"do
        if [[ -f "$vol_file" ]]; then
            volume_name="$(basename "${vol_file%.tar.gz}")"
            import_volume "$vol_file""$volume_name"
        fi
    done

    echo
    echo"🎉 === 解包完成! ==="
    echo"✨ 应用和数据卷已成功导入! ✨"
}

# 运行主函数
main "$@"

运行后大概是这样:

ubuntu@VM-0-3-ubuntu:~/apps-docker$ ./docker-app-unpack.sh
🐳 === Docker Compose 应用解包工具 === 🐳

⏰ [17:18:17] 🔍 检查 Docker 服务...
⏰ [17:18:17] ✅ Docker 服务运行正常。
📦 请输入待解包的 .tar.gz 文件路径: /home/ubuntu/apps-docker/zammad-docker-compose.tar.gz
⏰ [17:18:23] 创建临时目录: /tmp/docker-unpack-q47OnW
⏰ [17:18:23] 📦 解压主包: /home/ubuntu/apps-docker/zammad-docker-compose.tar.gz 到 /tmp/docker-unpack-q47OnW

📋 === 解包摘要 ===
📦 源文件: /home/ubuntu/apps-docker/zammad-docker-compose.tar.gz
📁 临时解压目录: /tmp/docker-unpack-q47OnW
📂 请输入应用目录解压目标路径 [/home/ubuntu/apps-docker/zammad-docker-compose]:
应用目录将解压到: /home/ubuntu/apps-docker/zammad-docker-compose
💾 发现数据卷包:
   └── zammad-docker-compose_elasticsearch-data.tar.gz
   └── zammad-docker-compose_postgresql-data.tar.gz
   └── zammad-docker-compose_redis-data.tar.gz
   └── zammad-docker-compose_zammad-backup.tar.gz
   └── zammad-docker-compose_zammad-storage.tar.gz

✅ 确认执行? (y/N): y
⏰ [17:18:33] 📂 导入应用目录: /tmp/docker-unpack-q47OnW/zammad-docker-compose_app.tar.gz 到 /home/ubuntu/apps-docker/zammad-docker-compose
⏰ [17:18:33] 💾 导入数据卷: zammad-docker-compose_elasticsearch-data (来自 /tmp/docker-unpack-q47OnW/zammad-docker-compose_elasticsearch-data.tar.gz)
数据卷 'zammad-docker-compose_elasticsearch-data' 已存在。是否覆盖? (y/N): y
⏰ [17:18:37] 删除现有数据卷 'zammad-docker-compose_elasticsearch-data'...
⏰ [17:18:37] 创建数据卷 'zammad-docker-compose_elasticsearch-data'...
⏰ [17:18:37] 导入数据到数据卷 'zammad-docker-compose_elasticsearch-data'...
⏰ [17:18:37] ✅ 数据卷 'zammad-docker-compose_elasticsearch-data' 导入成功。
⏰ [17:18:37] 💾 导入数据卷: zammad-docker-compose_postgresql-data (来自 /tmp/docker-unpack-q47OnW/zammad-docker-compose_postgresql-data.tar.gz)
数据卷 'zammad-docker-compose_postgresql-data' 已存在。是否覆盖? (y/N): y
⏰ [17:18:38] 删除现有数据卷 'zammad-docker-compose_postgresql-data'...
⏰ [17:18:38] 创建数据卷 'zammad-docker-compose_postgresql-data'...
⏰ [17:18:38] 导入数据到数据卷 'zammad-docker-compose_postgresql-data'...
⏰ [17:18:41] ✅ 数据卷 'zammad-docker-compose_postgresql-data' 导入成功。
⏰ [17:18:41] 💾 导入数据卷: zammad-docker-compose_redis-data (来自 /tmp/docker-unpack-q47OnW/zammad-docker-compose_redis-data.tar.gz)
数据卷 'zammad-docker-compose_redis-data' 已存在。是否覆盖? (y/N): y
⏰ [17:18:41] 删除现有数据卷 'zammad-docker-compose_redis-data'...
⏰ [17:18:41] 创建数据卷 'zammad-docker-compose_redis-data'...
⏰ [17:18:41] 导入数据到数据卷 'zammad-docker-compose_redis-data'...
⏰ [17:18:42] ✅ 数据卷 'zammad-docker-compose_redis-data' 导入成功。
⏰ [17:18:42] 💾 导入数据卷: zammad-docker-compose_zammad-backup (来自 /tmp/docker-unpack-q47OnW/zammad-docker-compose_zammad-backup.tar.gz)
数据卷 'zammad-docker-compose_zammad-backup' 已存在。是否覆盖? (y/N): y
⏰ [17:18:47] 删除现有数据卷 'zammad-docker-compose_zammad-backup'...
⏰ [17:18:47] 创建数据卷 'zammad-docker-compose_zammad-backup'...
⏰ [17:18:47] 导入数据到数据卷 'zammad-docker-compose_zammad-backup'...
⏰ [17:18:48] ✅ 数据卷 'zammad-docker-compose_zammad-backup' 导入成功。
⏰ [17:18:48] 💾 导入数据卷: zammad-docker-compose_zammad-storage (来自 /tmp/docker-unpack-q47OnW/zammad-docker-compose_zammad-storage.tar.gz)
数据卷 'zammad-docker-compose_zammad-storage' 已存在。是否覆盖? (y/N): y
⏰ [17:18:49] 删除现有数据卷 'zammad-docker-compose_zammad-storage'...
⏰ [17:18:49] 创建数据卷 'zammad-docker-compose_zammad-storage'...
⏰ [17:18:49] 导入数据到数据卷 'zammad-docker-compose_zammad-storage'...
⏰ [17:18:50] ✅ 数据卷 'zammad-docker-compose_zammad-storage' 导入成功。

🎉 === 解包完成! ===
✨ 应用和数据卷已成功导入! ✨
⏰ [17:18:50] 清理临时目录: /tmp/docker-unpack-q47OnW

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/183875
 
29 次点击