真正的生产力来了!Docker迁移部署两步搞定!
前言
最近遇到了需要部署一套比较复杂的应用场景,刚好这套应用我在其他服务器部署过,为了节省折腾的时间,我打算直接把服务器上已有的搬过去。
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_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
真正的生产力来了!Docker迁移部署两步搞定!的更多相关文章
- Docker 一步搞定 ZooKeeper 集群的搭建
Docker 一步搞定 ZooKeeper 集群的搭建 背景 原来学习 ZK 时, 我是在本地搭建的伪集群, 虽然说使用起来没有什么问题, 但是总感觉部署起来有点麻烦. 刚好我发现了 ZK 已经有了 ...
- 一步搞定私有Git服务器部署(Gogs)
http://www.jianshu.com/p/424627516ef6 零.安装 Docker 和 Compsoe 首先安装 Docker: $ curl -sSL https://get.doc ...
- 使用 Docker 一步搞定 ZooKeeper 集群的搭建
背景 原来学习 ZK 时, 我是在本地搭建的伪集群, 虽然说使用起来没有什么问题, 但是总感觉部署起来有点麻烦. 刚好我发现了 ZK 已经有了 Docker 的镜像了, 于是就尝试了一下, 发现真是爽 ...
- 五步搞定Android开发环境部署
引言 在windows安装Android的开发环境不简单也说不上算复杂,本文写给第一次想在自己Windows上建立Android开发环境投入 Android浪潮的朋友们,为了确保大家能顺利完成开发 ...
- 五步搞定Android开发环境部署——非常详细的Android开发环境搭建教程
在windows安装Android的开发环境不简单也说不上算复杂,本文写给第一次想在自己Windows上建立Android开发环境投入Android浪潮的朋友们,为了确保大家能顺利完成开发环境的搭 ...
- 三步搞定Centos 7 上特定版本的 docker 安装
由于国内网络原因,使用centos的用户yum源常用国内的阿里云.现在把centos7上安装docker的详细过程记录如下: 一.配置centos7的yum源(阿里云) 1.cd /etc/yum. ...
- spring boot web项目在IDEA下热部署解决办法(四步搞定)
最近在用spring boot 做一个web站点,修改了类.html.js等,刷新页面,没有生效,非要手动去make一下或者重启,大大降低了开发效率. 什么是热部署? 应用启动后会把编译好的Class ...
- win 10 家庭中文版安装docker ,但是没有 Hyper-V , 这样一步搞定
本人要在 win 10 上安装docker,找了安装教程,按照安装教程,第一步开启Hyper-V 虚拟机,但是发现自己电脑上没有这个选项 然后找到了这位仁兄 http://www.win7999.c ...
- 三步搞定IDEA集成热部署
第一步.在你的SpringBoot项目中添加DevTools依赖 <!-- 热部署DevTools --> <dependency> <groupId>org.sp ...
- 使用Tapdata一步搞定关系型数据库到MongoDB的战略迁移
摘要:数据库作为最关键的基础设施,随着互联网时代的信息高速增长,关系型数据库因其高门槛.高成本以及扩展性差等原因导致的局限性逐渐浮出水面,如今更是面临诸多问题和挑战,Tapdata 专注新一代实时 ...
随机推荐
- 【HTML】步骤进度条组件
HTML步骤进度条 效果图 思路 分份: 有多少个步骤就可以分成多少分,每份宽度应该为100%除以步骤数,故以上效果图中的每份宽度应该为25%,每份用一个div. 每份: 每份中可以看成是三个元素,一 ...
- 小了 60,500 倍,但更强;AI 的“深度诅咒”
作者:Ignacio de Gregorio 图片来自 Unsplash 的 Bahnijit Barman 几周前,我们看到 Anthropic 尝试训练 Claude 去通关宝可梦.模型是有点进展 ...
- Electron35-DeepSeek桌面端AI系统|vue3.5+electron+arco客户端ai模板
2025跨平台ai实战electron35+vite6+arco仿DeepSeek/豆包ai流式打字聊天助手. electron-deepseek-chat:实战ai大模型对话,基于vue3.5+el ...
- JVM 的 TLAB(Thread-Local Allocation Buffer)是什么?
JVM 的 TLAB(Thread-Local Allocation Buffer)是什么? TLAB(Thread-Local Allocation Buffer)简介 TLAB(Thread-Lo ...
- MySQL的基本语法(增,删,改,查)
MySQL的基本语法(增,删,改,查) MySQL中的(增)操作 创建数据库 CREATE DATABASE 库名; 例如: CREATE DATABASE db; 创建一个名为db的数据库. 创建列 ...
- LangChain4j比SpringAI强在哪?一文读懂
LangChain4j 和 Spring AI 是 Java 生态中实现大模型应用开发的两个最重要的框架,但二者的区别是啥?生产级别又该使用哪种框架?令很多人犯了难,所以本文就来浅聊一下,希望给大家在 ...
- 物联网之对接MQTT最佳实践
小伙伴们,你们好呀,我是老寇,跟我一起学习对接MQTT 安装EMQX 采用docker-compose一键式启动!!! 还没有安装docker朋友,参考文章下面两篇文章 # Ubuntu20.04安装 ...
- 剖析 Vue:最适合小白入手的前端框架
@charset "UTF-8"; .markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; o ...
- Docker不装C盘
Docker默认安装在C盘,这未来随着docker使用必定会导致C盘空间吃紧.所以本文提前进行空间布局,将docker默认安装路径软链接到D盘.软链接D盘Docker默认安装路径为C:\Program ...
- wso2~对接外部认证系统keycloak
在 WSO2 Identity Server 或 WSO2 API Manager 中,Identity Providers (身份提供者) 功能允许您将外部身份管理系统(如 Keycloak.Azu ...