离线部署大体积 Docker 镜像:分盘刻录与还原完整指南

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. 核心思路与流程概览

整个操作流程可以概括为以下四个步骤:

  1. 保存与压缩 :在有网络的环境中,使用 docker save 将镜像导出为 .tar 文件,并使用压缩工具(如 gzip)减小其体积。
  2. 分割文件 :使用文件分割工具(如 split)将压缩后的单个大文件切割成多个指定大小(例如 4GB)的小文件。
  3. 分盘刻录:将每个分割后的小文件分别刻录到不同的物理光盘上。
  4. 离线还原 :在目标离线机器上,将所有光盘中的文件复制到同一目录,按顺序合并,解压缩,最后使用 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 或内部仓库的联网机器上操作。

  1. 查看镜像列表,确认需要导出的镜像名称和标签。

    bash 复制代码
    docker images
  2. 保存镜像为 .tar 文件docker save 命令会将镜像及其所有层导出为一个归档文件。

    bash 复制代码
    docker save -o my_large_image.tar myregistry.com/myapp:latest
    • -o: 指定输出文件名。
    • 请将 myregistry.com/myapp:latest 替换为你的实际镜像名。
  3. 压缩 .tar 文件 。为了减少总体积,便于分割和传输,我们使用 gzip 进行压缩。

    bash 复制代码
    gzip 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 : 可以使用如 ImgBurnCDBurnerXP 或系统自带的刻录功能,选择"刻录数据光盘",将单个 .part 文件拖入并刻录。
  • Linux/macOS : 可以使用 braseroK3b 或命令行工具 wodimgrowisofs

关键提示 :务必记录好文件的刻录顺序!建议在光盘封面或文件名中标记顺序(如 光盘1_of_3_part_aa),这对于后续正确合并至关重要。

3.4 步骤四:离线环境合并与加载

在目标离线服务器或工作站上操作。

  1. 复制所有分割文件 :将所有光盘中的 my_image_part_* 文件复制到服务器上的同一个目录中,例如 ~/offline_images/

  2. 合并文件 :使用 cat 命令按照字母顺序(即 aa, ab, ac... 的顺序)将所有部分合并成一个完整的 .tar.gz 文件。

    bash 复制代码
    cd ~/offline_images
    cat my_image_part_* > my_large_image_restored.tar.gz
  3. 解压文件 :将合并后的压缩包解压,还原为 Docker 可识别的 .tar 格式。

    bash 复制代码
    gzip -d my_large_image_restored.tar.gz

    解压后会得到 my_large_image_restored.tar 文件。

  4. 加载 Docker 镜像 :最后,使用 docker load 命令将镜像导入到本地 Docker 引擎。

    bash 复制代码
    docker load -i my_large_image_restored.tar
    • -i: 指定输入文件。
  5. 验证 :加载成功后,使用 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. 注意事项与最佳实践

  • 校验完整性 :在刻录前和合并后,可以使用 md5sumsha256sum 校验源文件和目标文件的一致性,确保传输过程没有出错。

    bash 复制代码
    # 在源机器计算原始压缩包的哈希
    md5sum my_large_image.tar.gz
    # 在目标机器计算合并后文件的哈希
    md5sum my_large_image_restored.tar.gz

    两个哈希值应该完全相同。

  • 处理更多分区 :如果分割后文件超过 26 个(aazz),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 savegzipsplitcatdocker load 这一系列标准 Linux 工具的组合,我们可以有效地解决大体积 Docker 镜像的离线物理传输问题。这种方法不依赖于特定商业软件,通用性强,是系统管理员和运维工程师在面对严格网络隔离环境时的必备技能。关键点在于保持文件顺序在关键步骤进行完整性校验,以确保整个流程万无一失。