Docker 开发镜像跨主机迁移:从 Ubuntu 20.04 到 24.04

摘要:说明如何把已验证的 Ubuntu 20.04 开发镜像迁移到 Ubuntu 24.04,并逐层验证 Docker、镜像、源码挂载和真实构建。

在这里插入图片描述

@[toc]
旧开发机长期使用 Ubuntu 20.04,交叉编译 SDK、CMake 和各种依赖已经调通;新机器升级到 Ubuntu 24.04 后,最稳妥的做法往往不是重装整套依赖,而是继续运行已经验证过的开发镜像。

1. 宿主机升级,不等于容器也升级

假设我们已经有一张镜像:

1
cross-dev:20.04

它内部包含:

1
2
3
4
5
6
Ubuntu 20.04 用户空间
Yocto SDK
交叉编译器
CMake / Make / Python
代码生成工具
entrypoint

把它迁移到 Ubuntu 24.04 后,结构是:

1
2
3
4
5
6
flowchart TB
Host["Ubuntu 24.04 宿主机"] --> Engine["Docker Engine"]
Engine --> Container["cross-dev:20.04 容器"]
Container --> Userspace["Ubuntu 20.04 用户空间"]
Userspace --> SDK["SDK / Toolchain / Build Tools"]
Container -->|"共享"| Kernel["Ubuntu 24.04 宿主机 Linux Kernel"]

容器不是完整虚拟机。它使用宿主机 Linux 内核,但镜像提供自己的用户空间文件系统。

所以在同一 CPU 架构下:

1
2
3
Ubuntu 24.04 宿主机
+
Ubuntu 20.04 容器用户空间

完全是正常组合。

这正是旧项目需要的效果:宿主机更新了,但编译环境不跟着改变。

需要注意的是,容器并不能封装所有东西。USB、CAN、串口、GPU 等宿主机设备仍然依赖目标机器的驱动和 Docker 设备映射;如果源机器和目标机器 CPU 架构不同,也需要额外考虑多架构镜像或模拟。

2. 迁移时其实有三份数据

真正迁移之前,要先把下面三类内容分开:

1
2
3
4
5
6
7
8
A. Docker Image
Ubuntu / SDK / 编译工具 / entrypoint

B. 项目源码
Git 仓库 / 当前分支 / 本地修改

C. 运行辅助文件
compose.yaml / dev.sh / import-image.sh

其中 docker save 只负责 A。

如果以前运行容器时使用:

1
docker run -v /home/devuser/work/demo-app:/workspace ...

那么源码真正存储在宿主机:

1
/home/devuser/work/demo-app

容器里的 /workspace 只是 bind mount 入口。源码不会自动进入镜像归档。

因此迁移关系应该理解为:

1
2
3
4
flowchart LR
Image["Docker Image"] -->|"save / load"| NewImage["新主机 Image"]
Source["Git 源码"] -->|"Git / rsync"| NewSource["新主机源码"]
Runtime["compose / dev.sh"] -->|"复制"| NewRuntime["新主机运行配置"]

把这三个对象分开,后面很多问题会简单得多。

3. 先确认要迁移的镜像

旧机器上执行:

1
docker image ls cross-dev:20.04

进一步确认:

1
2
docker image inspect cross-dev:20.04 >/dev/null \
&& echo "image exists"

如果机器里同时存在:

1
2
3
cross-dev:20.04
cross-dev:test
cross-dev:latest

就不要只凭记忆操作。

4. 镜像迁移要用 save/load

导出:

1
docker save -o cross-dev_20.04.tar cross-dev:20.04

导入:

1
docker load -i cross-dev_20.04.tar

这是镜像迁移的标准组合。

不要和下面这组混淆:

1
docker export / docker import

docker export 面向的是某个容器的文件系统快照,而 docker save 面向完整 Docker image,并保留镜像层和 tag 等信息。

对于开发环境迁移,应该记住:

1
2
Image:save <-> load
Container filesystem:export <-> import

5. 大镜像导出前先看磁盘

交叉编译镜像如果包含完整 Yocto SDK,很容易达到二十多 GiB。

导出前检查当前目录:

1
df -h .

同时看 Docker 本身:

1
docker system df

这里存在两个不同的空间需求:

1
2
3
Docker 保存镜像的数据
+
即将生成的离线归档

如果两者都在根分区,大镜像导出很容易把系统盘写满。

完成后查看实际文件:

1
ls -lh cross-dev_20.04.tar

不要直接用 docker image ls 的 SIZE 推断 tar 必然同样大;层共享和统计口径不同,最终以实际归档大小为准。

6. 大文件迁移最好带 SHA256

生成:

1
sha256sum cross-dev_20.04.tar > cross-dev_20.04.tar.sha256

目标主机收到后:

1
sha256sum -c cross-dev_20.04.tar.sha256

预期:

1
cross-dev_20.04.tar: OK

对于二三十 GiB 的文件,先做完整性校验可以避免 docker load 到后面才发现归档已经损坏。

7. 如何把归档传到新机器

小文件用 scp 很方便:

1
2
scp cross-dev_20.04.tar \
devuser@192.168.1.100:~/

大文件更推荐:

1
2
rsync -avP cross-dev_20.04.tar \
devuser@192.168.1.100:~/

rsync -P 可以显示进度,也更适合中断后的继续传输。使用移动硬盘时还要避开 FAT32 的单文件大小限制,大型归档更适合 exFAT、NTFS 或 ext4。

8. Ubuntu 24.04 上先验证 Docker Engine

新机器不要一上来就导入 20 多 GiB 镜像。

先执行:

1
docker --version

然后:

1
docker run --rm hello-world

当前 Docker 官方文档把 Ubuntu 24.04 LTS 列为 Docker Engine 支持系统。

这一阶段只确认新宿主机的 Docker 自己是否正常。如果 hello-world 都失败,就先处理安装、权限或 daemon 问题,不要把旧镜像引入排查范围。

9. 校验归档后再 docker load

确认 SHA256:

1
sha256sum -c cross-dev_20.04.tar.sha256

然后:

1
docker load -i cross-dev_20.04.tar

正常会看到类似:

1
Loaded image: cross-dev:20.04

检查:

1
docker image ls cross-dev:20.04

如果归档已经使用 gzip、bzip2、xz 或 zstd 压缩,当前 Docker load 也支持直接读取压缩归档,不必先手动解成完整 tar。这个问题会在下一篇“大镜像导出导入工程化”中单独展开。

10. 导入以后先裸启动镜像

第一次测试不要挂项目:

1
docker run --rm -it cross-dev:20.04 bash

进入后:

1
cat /etc/os-release

应该仍然看到 Ubuntu 20.04。

然后检查核心工具:

1
2
3
4
cmake --version
make --version
git --version
thrift --version

如果镜像内置环境检查脚本,再执行:

1
verify-dev-env

这一步是在隔离变量:

1
2
3
不挂源码
不跑项目 CMake
只确认镜像本身能否正常启动

如果这里通过,说明“镜像从旧主机迁到新主机”这个动作已经基本成功。

11. 源码要单独迁移

源码应该按照源码自己的方式迁移。

如果有完整远程仓库,最好重新 clone;如果有尚未推送的分支或本地修改,可以先检查:

1
2
cd ~/work/demo-app
git status

再通过 Git 或 rsync 搬迁。

例如:

1
2
rsync -avP ~/work/demo-app/ \
devuser@192.168.1.100:~/work/demo-app/

目标机器确认:

1
2
cd ~/work/demo-app
git status

这里不要把“镜像成功导入”和“源码已经迁移”当成同一件事。

12. 新宿主机路径不必和旧主机完全相同

bind mount 的左边是宿主机路径,右边是容器路径。新主机的源码即使换了目录,只要仍挂到固定的 /workspace,容器内部看到的开发路径就可以保持不变:

1
2
3
4
docker run --rm -it \
-v ~/projects/demo-app:/workspace \
-w /workspace \
cross-dev:20.04 bash

只有工程脚本硬编码了旧宿主机绝对路径时,才需要额外兼容。

13. 第一次编译不要复用旧 CMakeCache

跨主机迁移后,不建议直接把旧 build/ 继续拿来编译。

CMakeCache.txt 中可能缓存:

1
2
3
4
5
6
旧源码路径
旧 build 路径
编译器路径
sysroot
toolchain file
第三方工具路径

更稳妥的是重新配置:

1
2
3
4
5
6
7
rm -rf /workspace/build

cmake -S /workspace \
-B /workspace/build \
-DP=DemoTarget

cmake --build /workspace/build -j"$(nproc)"

这样验证的是:

1
新宿主机 + 已迁移镜像 + 同一份源码

能否从零恢复构建,而不是旧缓存能否侥幸继续工作。

14. 最适合第一次迁移的验收顺序

建议固定成四层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
第一层:Docker Engine
docker run hello-world

第二层:Docker Image
docker load
docker run cross-dev:20.04

第三层:Bind Mount
挂源码
检查 pwd / ls / 权限

第四层:真实工程
clean configure
compile / link

如果第二层失败,不查 CMake;如果第三层是权限问题,不急着改编译参数;只有前三层都正常,第四层才进入工程依赖排查。

这个顺序看起来保守,但它能让迁移故障非常容易定位。

15. 最终迁移模型

1
2
3
4
5
6
7
flowchart LR
Old["旧 Ubuntu 主机"] -->|"docker save"| Archive["镜像归档 + SHA256"]
Archive -->|"rsync / 移动硬盘"| New["Ubuntu 24.04 主机"]
New -->|"docker load"| Image["Ubuntu 20.04 开发镜像"]
Source["新主机 Git 源码"] -->|"bind mount"| Container["开发容器"]
Image --> Container
Container -->|"clean build"| Artifact["构建产物"]

整个过程真正被保存下来的是“已验证的开发环境”,而不是旧宿主机本身。

这也是容器化旧项目最实际的收益:宿主机可以更新,项目的用户空间、SDK 和编译工具不必同步升级。

参考资料