1. 引言
在离线或网络隔离的生产环境中部署 Docker 应用时,常常需要将预先构建好的镜像通过物理介质(如光盘、U盘)传输。然而,单个 Docker 镜像的体积可能远超常见存储介质的容量(例如 DVD 光盘的 4.7GB)。直接使用 docker save 命令保存的镜像文件(.tar)往往无法直接刻录到一张光盘上。
本文将详细介绍一种可靠的解决方案:将大型 Docker 镜像保存为压缩包,分割成适合光盘容量(如 4GB)的小块,分多张光盘刻录,最后在目标离线机器上重新合并并加载。这种方法完美解决了单介质容量限制的问题。
操作案例
例如:
vscode-ray-test:tensorflow 20.5GB
在有镜像的电脑上保存并压缩
bash
sudo docker save vscode-ray-test:tensorflow | gzip -9 > vscode-ray-test_tensorflow.tar.gz
查看大小:
bash
ls -lh vscode-ray-test_tensorflow.tar.gz
切成适合光盘的小块
光盘标称 4.5GB,实际可用空间通常不到 4.5GB,所以建议每块切成 3900MB:
bash
split -b 3900M -d -a 3 vscode-ray-test_tensorflow.tar.gz vscode-ray-test_tensorflow.tar.gz.part-
查看每块大小:
bash
ls -lh vscode-ray-test_tensorflow.tar.gz.part-*
生成校验文件
bash
sha256sum vscode-ray-test_tensorflow.tar.gz.part-* > SHA256SUMS.txt
刻录到多张光盘
在不联网电脑上恢复
把所有光盘里的 part 文件复制到同一个目录
先校验:
bash
sha256sum -c SHA256SUMS.txt
如果都显示 OK,再加载镜像:
bash
cat vscode-ray-test_tensorflow.tar.gz.part-* | gunzip -c | sudo docker load
查看镜像是否导入成功:
bash
sudo docker images | grep vscode-ray-test
注意:
ubuntu操作系统,光盘不能多次添加文件进行刻录,只能一次刻录,不然识别不出来,原因未知。
2. 核心思路与流程概览
整个操作流程可以概括为以下四个步骤:
- 保存与压缩 :在有网络的环境中,使用
docker save将镜像导出为.tar文件,并使用压缩工具(如gzip)减小其体积。 - 分割文件 :使用文件分割工具(如
split)将压缩后的单个大文件切割成多个指定大小(例如 4GB)的小文件。 - 分盘刻录:将每个分割后的小文件分别刻录到不同的物理光盘上。
- 离线还原 :在目标离线机器上,将所有光盘中的文件复制到同一目录,按顺序合并,解压缩,最后使用
docker load加载镜像。
下面的流程图清晰地展示了这一过程:
#mermaid-svg-YbPAFV0wGx9UHS26{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YbPAFV0wGx9UHS26 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YbPAFV0wGx9UHS26 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YbPAFV0wGx9UHS26 .error-icon{fill:#552222;}#mermaid-svg-YbPAFV0wGx9UHS26 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YbPAFV0wGx9UHS26 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YbPAFV0wGx9UHS26 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YbPAFV0wGx9UHS26 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YbPAFV0wGx9UHS26 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YbPAFV0wGx9UHS26 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YbPAFV0wGx9UHS26 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YbPAFV0wGx9UHS26 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YbPAFV0wGx9UHS26 .marker.cross{stroke:#333333;}#mermaid-svg-YbPAFV0wGx9UHS26 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YbPAFV0wGx9UHS26 p{margin:0;}#mermaid-svg-YbPAFV0wGx9UHS26 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YbPAFV0wGx9UHS26 .cluster-label text{fill:#333;}#mermaid-svg-YbPAFV0wGx9UHS26 .cluster-label span{color:#333;}#mermaid-svg-YbPAFV0wGx9UHS26 .cluster-label span p{background-color:transparent;}#mermaid-svg-YbPAFV0wGx9UHS26 .label text,#mermaid-svg-YbPAFV0wGx9UHS26 span{fill:#333;color:#333;}#mermaid-svg-YbPAFV0wGx9UHS26 .node rect,#mermaid-svg-YbPAFV0wGx9UHS26 .node circle,#mermaid-svg-YbPAFV0wGx9UHS26 .node ellipse,#mermaid-svg-YbPAFV0wGx9UHS26 .node polygon,#mermaid-svg-YbPAFV0wGx9UHS26 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YbPAFV0wGx9UHS26 .rough-node .label text,#mermaid-svg-YbPAFV0wGx9UHS26 .node .label text,#mermaid-svg-YbPAFV0wGx9UHS26 .image-shape .label,#mermaid-svg-YbPAFV0wGx9UHS26 .icon-shape .label{text-anchor:middle;}#mermaid-svg-YbPAFV0wGx9UHS26 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YbPAFV0wGx9UHS26 .rough-node .label,#mermaid-svg-YbPAFV0wGx9UHS26 .node .label,#mermaid-svg-YbPAFV0wGx9UHS26 .image-shape .label,#mermaid-svg-YbPAFV0wGx9UHS26 .icon-shape .label{text-align:center;}#mermaid-svg-YbPAFV0wGx9UHS26 .node.clickable{cursor:pointer;}#mermaid-svg-YbPAFV0wGx9UHS26 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YbPAFV0wGx9UHS26 .arrowheadPath{fill:#333333;}#mermaid-svg-YbPAFV0wGx9UHS26 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YbPAFV0wGx9UHS26 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YbPAFV0wGx9UHS26 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YbPAFV0wGx9UHS26 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YbPAFV0wGx9UHS26 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YbPAFV0wGx9UHS26 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YbPAFV0wGx9UHS26 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YbPAFV0wGx9UHS26 .cluster text{fill:#333;}#mermaid-svg-YbPAFV0wGx9UHS26 .cluster span{color:#333;}#mermaid-svg-YbPAFV0wGx9UHS26 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YbPAFV0wGx9UHS26 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YbPAFV0wGx9UHS26 rect.text{fill:none;stroke-width:0;}#mermaid-svg-YbPAFV0wGx9UHS26 .icon-shape,#mermaid-svg-YbPAFV0wGx9UHS26 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YbPAFV0wGx9UHS26 .icon-shape p,#mermaid-svg-YbPAFV0wGx9UHS26 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YbPAFV0wGx9UHS26 .icon-shape .label rect,#mermaid-svg-YbPAFV0wGx9UHS26 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YbPAFV0wGx9UHS26 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YbPAFV0wGx9UHS26 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YbPAFV0wGx9UHS26 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 有网络环境
大型 Docker 镜像
docker save -o image.tar
gzip image.tar
生成 image.tar.gz
split -b 4G image.tar.gz
image_part_
分割文件
image_part_aa, image_part_ab, ...
分盘刻录
Part_aa -> 光盘1, Part_ab -> 光盘2
离线环境
复制所有 part 文件至同一目录
cat image_part_* > image_restored.tar.gz
合并文件
gzip -d image_restored.tar.gz
解压还原为 .tar
docker load -i image_restored.tar
加载镜像
成功!
离线环境可用 docker images 查看
3. 操作步骤详解
3.1 步骤一:保存并压缩 Docker 镜像
首先,在可以访问 Docker Hub 或内部仓库的联网机器上操作。
-
查看镜像列表,确认需要导出的镜像名称和标签。
bashdocker images -
保存镜像为
.tar文件 。docker save命令会将镜像及其所有层导出为一个归档文件。bashdocker save -o my_large_image.tar myregistry.com/myapp:latest-o: 指定输出文件名。- 请将
myregistry.com/myapp:latest替换为你的实际镜像名。
-
压缩
.tar文件 。为了减少总体积,便于分割和传输,我们使用gzip进行压缩。bashgzip my_large_image.tar执行后,会生成
my_large_image.tar.gz文件,原始.tar文件会被删除。压缩率取决于镜像内容,通常可以显著减小体积。
3.2 步骤二:分割压缩文件
使用 split 命令将大文件分割成小块。这里以每块 4GB (即 4G)为例,以适应 DVD 光盘。
bash
split -b 4G my_large_image.tar.gz my_image_part_
-b 4G: 指定每个输出文件的大小为 4GB。单位可以是K(KB),M(MB),G(GB)。my_large_image.tar.gz: 需要分割的输入文件。my_image_part_: 输出文件的前缀。分割后的文件将被命名为my_image_part_aa,my_image_part_ab,my_image_part_ac... 依此类推。
执行后检查:
bash
ls -lh my_image_part_*
你应该能看到一系列大小约为 4GB 的文件(最后一个文件可能较小)。
3.3 步骤三:分盘刻录
现在,将每个分割后的文件(my_image_part_aa, my_image_part_ab, ...)分别刻录到不同的光盘上。
- Windows : 可以使用如
ImgBurn、CDBurnerXP或系统自带的刻录功能,选择"刻录数据光盘",将单个.part文件拖入并刻录。 - Linux/macOS : 可以使用
brasero、K3b或命令行工具wodim、growisofs。
关键提示 :务必记录好文件的刻录顺序!建议在光盘封面或文件名中标记顺序(如 光盘1_of_3_part_aa),这对于后续正确合并至关重要。
3.4 步骤四:离线环境合并与加载
在目标离线服务器或工作站上操作。
-
复制所有分割文件 :将所有光盘中的
my_image_part_*文件复制到服务器上的同一个目录中,例如~/offline_images/。 -
合并文件 :使用
cat命令按照字母顺序(即aa,ab,ac... 的顺序)将所有部分合并成一个完整的.tar.gz文件。bashcd ~/offline_images cat my_image_part_* > my_large_image_restored.tar.gz -
解压文件 :将合并后的压缩包解压,还原为 Docker 可识别的
.tar格式。bashgzip -d my_large_image_restored.tar.gz解压后会得到
my_large_image_restored.tar文件。 -
加载 Docker 镜像 :最后,使用
docker load命令将镜像导入到本地 Docker 引擎。bashdocker load -i my_large_image_restored.tar-i: 指定输入文件。
-
验证 :加载成功后,使用
docker images命令检查镜像是否已出现在列表中。
4. 完整命令示例与验证
假设我们有一个名为 bigapp:prod 的镜像,以下是在 源机器(联网) 和 目标机器(离线) 上执行的完整命令序列对比:
| 步骤 | 源机器(联网) | 目标机器(离线) |
|---|---|---|
| 1. 保存 | docker save -o bigapp.tar bigapp:prod |
|
| 2. 压缩 | gzip bigapp.tar |
|
| 3. 分割 | split -b 4G bigapp.tar.gz bigapp_part_ |
|
| 4. 刻录 | (将 bigapp_part_aa, ab, ac 分别刻盘) |
|
| 5. 复制 | (将所有 bigapp_part_* 文件从光盘复制到 ~/restore/) |
|
| 6. 合并 | cd ~/restore && cat bigapp_part_* > bigapp_restored.tar.gz |
|
| 7. 解压 | gzip -d bigapp_restored.tar.gz |
|
| 8. 加载 | docker load -i bigapp_restored.tar |
|
| 9. 验证 | `docker images |
5. 注意事项与最佳实践
-
校验完整性 :在刻录前和合并后,可以使用
md5sum或sha256sum校验源文件和目标文件的一致性,确保传输过程没有出错。bash# 在源机器计算原始压缩包的哈希 md5sum my_large_image.tar.gz # 在目标机器计算合并后文件的哈希 md5sum my_large_image_restored.tar.gz两个哈希值应该完全相同。
-
处理更多分区 :如果分割后文件超过 26 个(
aa到zz),split命令会自动使用更长的后缀(如aaa,aab),cat命令合并时能正确识别,无需担心。 -
使用更高效的压缩算法 :如果镜像压缩率不佳,可以考虑使用
pigz(并行 gzip)或xz算法以获得更高的压缩比,但解压时可能需要相应工具。bash# 使用 pigz 压缩 (更快) docker save myapp:latest | pigz > myapp.tar.gz # 使用 xz 压缩 (压缩比更高,但更慢) docker save myapp:latest | xz > myapp.tar.xz -
直接分割
.tar文件 :如果不进行压缩,也可以直接分割docker save产生的.tar文件。但分割后的总文件体积会更大,可能需要更多光盘。 -
备选方案:私有仓库中转:如果条件允许,可以在离线网络内部搭建一个私有的 Docker Registry,先将镜像推送到内网仓库,其他离线机器再从内网仓库拉取。这更适合需要频繁分发镜像的场景。
6. 总结
通过 docker save、gzip、split、cat 和 docker load 这一系列标准 Linux 工具的组合,我们可以有效地解决大体积 Docker 镜像的离线物理传输问题。这种方法不依赖于特定商业软件,通用性强,是系统管理员和运维工程师在面对严格网络隔离环境时的必备技能。关键点在于保持文件顺序 和在关键步骤进行完整性校验,以确保整个流程万无一失。