Docker离线迁移实战:save/load与配置数据完整还原指南
发布时间:2026/10/26 20:08:42 锦皓数字建站

1. 为什么需要离线迁移容器场景与方案选型1.1 真实场景拆解没有外网的机房怎么搬容器干运维这行十几年最怕听到的一句话就是这台机器不能连外网。偏偏很多生产环境就是这样某金融机构的内网机房、某制造企业的车间边缘服务器、某实验室的隔离测试集群物理隔离做得死死的连个软件源都访问不了。这时候你要把一台宿主机上跑着的 Docker 容器搬到另一台宿主机上docker pull用不了docker save和docker load就成了唯一靠谱的路子。这个项目的核心需求说白了就一句话在没有网络连通性的前提下把 A 宿主机上的容器完整地搬到 B 宿主机上跑起来。听起来简单但完整两个字里藏着不少坑——镜像本身要搬容器的配置参数要搬挂载的数据卷要搬网络设置要搬甚至连容器里跑的服务依赖的环境变量都不能丢。我见过太多人以为docker export一个容器就完事了结果到新机器上一跑数据全没了配置全丢了服务起不来。适合看这篇内容的人大概分三类一是刚接触 Docker、被安排做环境迁移的新手二是有一定基础但没做过离线迁移、想搞清楚完整流程的开发者三是需要定期做容器备份和灾备演练的运维人员。不管你是哪一类下面的内容都是从实际踩坑里总结出来的能直接抄作业。1.2 三种迁移方案对比save/load、export/import、commit 怎么选动手之前先把方案选清楚选错了后面全是返工。Docker 生态里跟搬容器相关的命令主要有三组很多人搞混我用一张表把它们摆清楚方案命令组合搬的是什么保留镜像层历史保留容器配置适用场景镜像级迁移docker save/docker load镜像含所有层是否需重新 run迁移标准服务、可重建容器容器级迁移docker export/docker import容器文件系统快照否压成单层否迁移一次性容器、做精简镜像提交式迁移docker commitsave/load容器当前状态转镜像部分部分启动命令等保留容器内改动、快速固化我的建议很明确绝大多数场景走镜像级迁移save/load。原因是它保留了完整的镜像层结构和元数据包括 ENTRYPOINT、CMD、ENV、EXPOSE 这些到新机器上docker run时行为跟原来一致。export/import会把所有层压成一层镜像历史全丢而且导入后没有原来的启动命令你得手动指定稍不注意就跑不起来。commit适合你在容器里手动改了一堆东西、想把这些改动固化下来的场景但它本质上也是生成一个新镜像配置还是得靠run参数补。注意docker save保存的是镜像不是运行中的容器。如果你要迁移的是一个正在跑的容器正确做法是先确认它的镜像来源保存镜像再记录容器的启动参数到新机器上重新run。容器的可写层数据比如容器内直接写的文件不在镜像里这部分要么通过数据卷挂载出来单独搬要么用commit固化。1.3 迁移前必须盘点的四类资产我每次做迁移前都会列一张清单把要搬的东西分成四类缺一类都可能出问题镜像资产容器依赖的基础镜像、构建出来的业务镜像用docker images列出来记下 REPOSITORY、TAG、IMAGE ID。配置资产容器的启动命令、端口映射、环境变量、挂载点、网络模式、重启策略。这些用docker inspect能全部导出来。数据资产通过-v或--mount挂载到宿主机的目录或数据卷这部分是容器之外的数据必须单独打包。依赖资产如果容器依赖宿主机的特定配置比如 hosts 文件、时区、特定内核参数也要一并记录。这四类里最容易漏的是配置资产和数据资产。镜像搬过去了run的时候端口忘了映射、环境变量忘了传、数据目录没挂上服务照样起不来。所以下面我会重点讲怎么把这几类资产完整地提取和还原。2. 镜像打包与传输的核心操作细节2.1 用 docker save 打包镜像参数选择与体积优化docker save的基本用法不复杂但参数选对了能省不少事。最基础的写法是docker save -o myapp_v1.tar myapp:v1这里-o指定输出文件后面跟镜像名和标签。如果你要一次打包多个镜像可以这样docker save -o bundle.tar myapp:v1 nginx:1.25 redis:7.2多个镜像打进一个 tar 包到对面docker load一次就能全部导入省得来回传文件。关于体积优化有几个实操点值得说。第一save出来的包大小约等于镜像所有层的总和如果你镜像里有很多中间层残留比如构建时没清理的缓存包会很大。打包前可以先docker image prune清一下悬空镜像再用docker history myapp:v1看看各层大小心里有数。第二如果镜像本身很大几个 G建议打包时顺便压缩docker save myapp:v1 | gzip myapp_v1.tar.gz到对面用gunzip -c myapp_v1.tar.gz | docker load解压导入。gzip 对镜像层的压缩率通常不错文本类依赖多的镜像能压到原来的一半甚至更少。第三如果镜像有多个标签指向同一个 IMAGE IDsave时把标签都写上导入后标签才不会丢。实操心得docker save默认输出到 stdout 的话是二进制流直接重定向到文件容易出问题比如终端编码干扰所以养成用-o指定文件名的习惯。另外打包前确认磁盘空间一个 5G 的镜像打包过程会临时占用额外空间别把磁盘撑爆了。2.2 镜像命名规范与版本标签管理离线环境没有 registry 帮你管理版本标签乱起后面就是灾难。我的习惯是遵循一套固定的命名规则业务名:版本号-构建日期比如order-service:v2.3.1-20240115。这样一眼能看出是什么服务、哪个版本、什么时候构建的。为什么要这么讲究因为离线迁移经常是批量操作一次搬十几个镜像如果标签都是latest你根本分不清哪个是哪个。而且latest标签在save/load过程中容易出歧义——如果源机器上latest指向 A 镜像目标机器上已经有个latest指向 B 镜像load之后可能覆盖掉导致原本依赖 B 的容器起不来。具体操作上打包前先给镜像打一个明确的标签docker tag myapp:v1 myapp:v1.0.0-20240115 docker save -o myapp_v1.0.0-20240115.tar myapp:v1.0.0-20240115到目标机器load之后用docker images核对标签和 IMAGE ID 是否跟源端一致。如果源端有多个标签指向同一镜像建议全部打上再打包避免丢失。2.3 大镜像分卷打包与校验传输完整性的保障离线传输最怕的就是文件损坏。一个 8G 的镜像包通过移动硬盘拷来拷去中间出个错位到对面load时报 unexpected EOF你就得从头再来。所以校验这一步绝对不能省。打包完成后立刻生成校验值sha256sum myapp_v1.tar myapp_v1.tar.sha256到目标机器上先校验再导入sha256sum -c myapp_v1.tar.sha256只有输出OK才继续往下走。这一步花不了几秒钟但能帮你排除掉大部分传输损坏的问题。如果镜像特别大单个文件不方便传输比如某些介质对单文件大小有限制可以分卷split -b 2G myapp_v1.tar myapp_v1.tar.part_到对面合并cat myapp_v1.tar.part_* myapp_v1.tar合并后再做 sha256 校验。分卷的块大小根据你的传输介质来定U 盘拷的话 2G 一块比较稳妥网络共享盘可以大一些。注意分卷合并的顺序很重要cat的时候通配符展开是按字典序的part_aa、part_ab这种命名能保证顺序正确。如果你用数字后缀part_1、part_2、part_10字典序会乱合并出来就是坏的。所以分卷命名一定要用字母序列或者补零的数字。2.4 docker load 导入与镜像完整性验证到目标机器上导入镜像docker load -i myapp_v1.tar或者压缩包的情况gunzip -c myapp_v1.tar.gz | docker load导入完成后docker load会输出每个导入镜像的Loaded image: xxx信息。这时候别急着跑先做三件事验证第一docker images看标签和 IMAGE ID 是否跟源端一致。第二docker inspect myapp:v1看镜像的配置Entrypoint、Cmd、Env、ExposedPorts是否完整。第三如果镜像有健康检查或者启动脚本可以先docker run --rm myapp:v1 某个轻量命令试跑一下确认镜像本身没问题。我遇到过一种情况load显示成功但docker images里 IMAGE ID 跟源端对不上。排查下来是源端镜像有多个标签打包时只写了一个导入后 Docker 重新计算了镜像 ID因为标签信息变了。这种情况不影响使用但如果你有脚本依赖固定 IMAGE ID就得注意。解决办法是打包时把所有标签都带上或者导入后手动docker tag补上。3. 容器配置与数据资产的完整还原3.1 用 docker inspect 提取容器全部配置镜像搬过去了只是第一步容器怎么跑起来才是关键。最可靠的办法是在源机器上把容器的完整配置导出来docker inspect mycontainer mycontainer_inspect.json这个 JSON 里包含了容器的所有信息启动命令、环境变量、端口映射、挂载点、网络配置、重启策略、资源限制等等。但直接看这个 JSON 很痛苦几百行嵌套结构关键信息埋得很深。我一般用jq提取关键字段docker inspect mycontainer | jq .[0] | {Name, Config, HostConfig, Mounts, NetworkSettings}重点看这几块Config.Env环境变量run时用-e逐个还原。Config.Cmd和Config.Entrypoint启动命令决定容器跑什么。HostConfig.PortBindings端口映射对应-p参数。HostConfig.Binds和Mounts挂载配置对应-v参数。HostConfig.RestartPolicy重启策略对应--restart。HostConfig.NetworkMode网络模式对应--network。把这些提取出来整理成一条完整的docker run命令到新机器上直接执行。我习惯把这条命令写进一个run.sh脚本里连同镜像包一起归档下次迁移或者重建的时候直接跑脚本不用再翻记录。3.2 数据卷与绑定挂载的迁移策略数据资产是最容易被忽略的部分。容器的可写层在容器删除后就没了真正重要的数据都在挂载出来的目录或数据卷里。迁移前先搞清楚容器用了哪些挂载docker inspect mycontainer | jq .[0].Mounts输出里会列出每个挂载的Typebind 或 volume、Source宿主机路径或卷名、Destination容器内路径。分两种情况处理绑定挂载bind mount宿主机上的一个目录直接挂进容器。这种直接把源目录打包搬过去就行tar -czf data_volume.tar.gz -C /host/path/to/data .到目标机器解压到对应路径mkdir -p /host/path/to/data tar -xzf data_volume.tar.gz -C /host/path/to/data注意权限问题。容器里的进程往往以特定 UID 运行如果解压后文件属主变了容器可能读写不了。打包时用tar -czf ... --numeric-owner保留数字属主解压时也用--numeric-owner能减少权限错乱。数据卷volumeDocker 管理的卷存在/var/lib/docker/volumes/下面。迁移时先找到卷的实际路径docker volume inspect myvolume | jq .[0].Mountpoint然后打包这个目录的内容。到目标机器上先创建同名卷再把数据解压进去docker volume create myvolume tar -xzf volume_data.tar.gz -C $(docker volume inspect myvolume | jq -r .[0].Mountpoint)实操心得数据迁移前一定要停掉容器或者至少确保没有写入操作。我见过有人在容器跑着的时候直接 tar 数据目录结果拷出来的是个半截状态数据库文件损坏到对面根本起不来。正确做法是docker stop之后再打包或者用数据库自带的备份工具做一致性快照。3.3 网络模式与端口映射的还原网络配置这块离线迁移最常见的坑是端口冲突和网络模式不匹配。源机器上容器用的是host网络模式到目标机器上如果也这么配端口可能已经被别的服务占了。所以迁移前先确认目标机器的端口占用情况ss -tlnp | grep 端口号如果端口被占要么改映射端口要么停掉冲突的服务。改端口的话docker run时把-p 宿主机端口:容器端口里的宿主机端口换掉就行容器内部端口不用动。如果容器用的是自定义 bridge 网络目标机器上得先创建同名网络docker network create --driver bridge mynet然后再run的时候用--network mynet。如果容器之间有依赖比如 app 容器要连 db 容器网络名和容器名都得保持一致否则 DNS 解析会失败。这一点在微服务场景下特别重要我见过迁移后 app 连不上 db排查半天发现是网络名不一样容器名解析不到。3.4 环境变量与启动参数的逐项核对环境变量看着不起眼但少一个就可能让服务起不来。比如数据库连接串、密钥、时区设置这些通常都通过-e传进去。从docker inspect的Config.Env里能拿到全部环境变量但要注意里面有一部分是 Docker 自动注入的比如PATH、HOSTNAME这些不用手动传。我的做法是把Config.Env导出来过滤掉 Docker 自动注入的剩下的整理成-e KEYVALUE的形式docker inspect mycontainer | jq -r .[0].Config.Env[] | grep -v -E ^(PATH|HOSTNAME|HOME)然后逐条拼进run命令。对于敏感信息密码、密钥不建议直接写在脚本里明文保存可以用--env-file从文件读取文件权限设成 600。启动参数里还有几个容易漏的--restart重启策略生产环境一般是always或unless-stopped、--log-driver日志驱动、--memory和--cpus资源限制。这些在HostConfig里都能查到逐项还原就行。4. 迁移后的验证与常见问题排查4.1 容器启动后的五项健康检查容器run起来不等于迁移成功得做验证。我一般按这五项逐一检查第一docker ps看容器状态是不是Up有没有反复重启。如果状态是Restarting说明启动就失败了直接docker logs看报错。第二docker logs看应用日志有没有异常。重点关注启动阶段的错误比如连不上数据库、配置文件找不到、端口绑定失败。第三端口连通性测试。从宿主机curl或者telnet一下映射的端口确认服务真的在监听。容器内监听和宿主机映射是两回事有时候容器里服务起来了但端口没映射对外面照样访问不了。第四数据读写验证。进容器或者通过接口触发一次写操作确认挂载的数据目录能正常读写权限没问题。第五依赖服务连通性。如果容器依赖其他服务数据库、缓存、消息队列逐一确认网络能通、认证能过。这五项里任何一项不过都说明迁移有问题得回头排查。我习惯把这五项做成一个检查脚本每次迁移后跑一遍输出结果一目了然。4.2 常见报错速查表与排查思路离线迁移的报错翻来覆去就那么几类整理成表方便对照报错现象可能原因排查与解决docker load报 unexpected EOF镜像包传输损坏重新校验 sha256损坏则重新传输容器启动即退出日志无输出Entrypoint/Cmd 丢失或错误docker inspect核对启动命令手动指定容器反复重启依赖服务连不上或配置错误看日志定位检查网络和环境变量端口访问不通端口未映射或宿主机防火墙拦截docker port确认映射检查防火墙规则数据目录权限拒绝文件属主与容器内 UID 不匹配用--numeric-owner重新打包解压或chown调整容器间 DNS 解析失败自定义网络未创建或名称不一致创建同名网络确认容器名一致镜像 ID 与源端不一致标签信息变化导致重新计算不影响使用必要时手动docker tag补齐这张表覆盖了我实际遇到过的八成问题。剩下两成通常是环境差异导致的比如目标机器内核版本太低不支持某些特性、SELinux 或 AppArmor 策略拦截、cgroup 版本不一致等。这类问题排查起来费劲建议迁移前先确认两台机器的 Docker 版本和内核版本大致相当能省掉很多麻烦。4.3 迁移脚本化把重复劳动变成一键操作如果迁移是常态化的比如定期灾备、多环境同步手动敲命令迟早出错。我的做法是把整个流程脚本化分成源端导出脚本和目标端导入脚本两部分。源端脚本大概长这样#!/bin/bash IMAGE$1 CONTAINER$2 OUTDIR$3 mkdir -p $OUTDIR docker save -o $OUTDIR/${IMAGE//\//_}.tar $IMAGE docker inspect $CONTAINER $OUTDIR/${CONTAINER}_inspect.json docker inspect $CONTAINER | jq -r .[0].Mounts[] | select(.Typebind) | .Source $OUTDIR/bind_paths.txt sha256sum $OUTDIR/*.tar $OUTDIR/checksums.sha256目标端脚本负责校验、导入、还原配置。脚本化的好处不只是省事更重要的是可重复、可审计。每次迁移都有脚本记录出了问题能回溯是哪一步不对。注意脚本里涉及路径和容器名的地方尽量用变量别写死。我见过有人把容器名硬编码在脚本里换个环境就得改脚本改着改着就改出 bug 了。另外脚本执行前加个set -e任何一步失败就停别让错误往下传。4.4 几个踩过的坑和独家经验最后分享几个文档里不会写、但实际会遇到的坑。坑一镜像里的时区不对。源机器容器用的是东八区迁移到目标机器后日志时间差了 8 小时。原因是时区依赖宿主机的/etc/localtime或者镜像里的TZ环境变量。解决办法是run时挂载宿主机的时区文件-v /etc/localtime:/etc/localtime:ro或者显式传-e TZAsia/Shanghai。坑二容器内 DNS 解析慢。迁移后容器访问外部服务特别慢排查发现是 DNS 配置问题。Docker 默认会用宿主机的 DNS但某些环境下需要显式指定--dns。这个坑很隐蔽因为服务最终能通只是慢容易被当成性能问题。坑三日志驱动不一致导致磁盘爆满。源机器容器用的是json-file驱动并配了日志轮转目标机器默认没配跑几天日志把磁盘写满了。迁移时一定要核对HostConfig.LogConfig把日志驱动和轮转参数一并还原。坑四docker save打包时镜像正在被使用。这个一般没问题但如果镜像层正在被写入比如有容器在跑并且往可写层写数据打包出来的镜像可能不一致。稳妥做法是打包前确认没有容器在用这个镜像或者至少确认没有写操作。坑五目标机器 Docker 版本比源机器低。高版本 Docker 构建的镜像低版本可能不认某些配置比如新的镜像格式特性。迁移前用docker version对比两端版本差距大就先升级目标机器的 Docker。这些坑的共同点是它们都不会在迁移当下报错而是在运行一段时间后才暴露。所以迁移后的观察期很重要头几天多看看日志和监控别以为docker ps显示Up就万事大吉了。我个人在实际操作中的体会是离线迁移这件事技术难度不高难在细致。镜像、配置、数据、网络四样东西每一样都得核对到位少一样就是给自己埋雷。把流程脚本化、把检查清单化比什么技巧都管用。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。