百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

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

mhr18 2025-08-06 21:19 4 浏览 0 评论

前言

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

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/ 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/ 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/ 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/
fi

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

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_found; then
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

相关推荐

订单超时自动取消业务的 N 种实现方案,从原理到落地全解析

在分布式系统架构中,订单超时自动取消机制是保障业务一致性的关键组件。某电商平台曾因超时处理机制缺陷导致日均3000+订单库存锁定异常,直接损失超50万元/天。本文将从技术原理、实现细节、...

使用Spring Boot 3开发时,如何选择合适的分布式技术?

作为互联网大厂的后端开发人员,当你满怀期待地用上SpringBoot3,准备在项目中大显身手时,却发现一个棘手的问题摆在面前:面对众多分布式技术,究竟该如何选择,才能让SpringBoot...

数据库内存爆满怎么办?99%的程序员都踩过这个坑!

你的数据库是不是又双叒叕内存爆满了?!服务器监控一片红色警告,老板在群里@所有人,运维同事的电话打爆了手机...这种场景是不是特别熟悉?别慌!作为一个在数据库优化这条路上摸爬滚打了10年的老司机,今天...

springboot利用Redisson 实现缓存与数据库双写不一致问题

使用了Redisson来操作Redis分布式锁,主要功能是从缓存和数据库中获取商品信息,以下是针对并发时更新缓存和数据库带来不一致问题的解决方案1.基于读写锁和删除缓存策略在并发更新场景下,...

外贸独立站数据库炸了?对象缓存让你起死回生

上周黑五,一个客户眼睁睁看着服务器CPU飙到100%——每次页面加载要查87次数据库。这让我想起2024年Pantheon的测试:Redis缓存能把WooCommerce查询速度提升20倍。跨境电商最...

手把手教你在 Spring Boot3 里纯编码实现自定义分布式锁

为什么要自己实现分布式锁?你是不是早就受够了引入各种第三方依赖时的繁琐?尤其是分布式锁这块,每次集成Redisson或者Zookeeper,都得额外维护一堆配置,有时候还会因为版本兼容问题头疼半...

如何设计一个支持百万级实时数据推送的WebSocket集群架构?

面试解答:要设计一个支持百万级实时数据推送的WebSocket集群架构,需从**连接管理、负载均衡、水平扩展、容灾恢复**四个维度切入:连接层设计-**长连接优化**:采用Netty或Und...

Redis数据结构总结——面试最常问到的知识点

Redis作为主流的nosql存储,面试时经常会问到。其主要场景是用作缓存,分布式锁,分布式session,消息队列,发布订阅等等。其存储结构主要有String,List,Set,Hash,Sort...

skynet服务的缺陷 lua死循环

服务端高级架构—云风的skynet这边有一个关于云风skynet的视频推荐给大家观看点击就可以观看了!skynet是一套多人在线游戏的轻量级服务端框架,使用C+Lua开发。skynet的显著优点是,...

七年Java开发的一路辛酸史:分享面试京东、阿里、美团后的心得

前言我觉得有一个能够找一份大厂的offer的想法,这是很正常的,这并不是我们的饭后谈资而是每个技术人的追求。像阿里、腾讯、美团、字节跳动、京东等等的技术氛围与技术规范度还是要明显优于一些创业型公司...

mysql mogodb es redis数据库之间的区别

1.MySQL应用场景概念:关系型数据库,基于关系模型,使用表和行存储数据。优点:支持ACID事务,数据具有很高的一致性和完整性。缺点:垂直扩展能力有限,需要分库分表等方式扩展。对于复杂的查询和大量的...

redis,memcached,nginx网络组件

1.理解阻塞io,非阻塞io,同步io,异步io的区别2.理解BIO和AIO的区别io多路复用只负责io检测,不负责io操作阻塞io中的write,能写多少是多少,只要写成功就返回,譬如准备写500字...

SpringBoot+Vue+Redis实现验证码功能

一个小时只允许发三次验证码。一次验证码有效期二分钟。SpringBoot整合Redis...

AWS MemoryDB 可观测最佳实践

AWSMemoryDB介绍AmazonMemoryDB是一种完全托管的、内存中数据存储服务,专为需要极低延迟和高吞吐量的应用程序而设计。它与Redis和Memcached相似,但具有更...

从0构建大型AI推荐系统:实时化引擎从工具到生态的演进

在AI浪潮席卷各行各业的今天,推荐系统正从幕后走向前台,成为用户体验的核心驱动力。本文将带你深入探索一个大型AI推荐系统从零起步的全过程,揭示实时化引擎如何从单一工具演进为复杂生态的关键路径。无论你是...

取消回复欢迎 发表评论: