
隔离网络环境下做 Docker 部署最核心的痛点不是不会敲 docker run而是“怎么把东西搬进去”。我在政企项目、工业现场、园区机房反复折腾过好多次 Docker 内网离线部署踩过不少坑也沉淀了一套相对稳定的流程。这篇文稿把我实际用下来的方案、命令、避坑点都整理出来给需要在内网环境交付容器的同学一个可以直接照抄的路径。开头先花 200 字介绍一下全流程逻辑。离线部署不是从零开始而是在一台“能联网的打包机”上完成镜像获取、导出、压缩然后通过摆渡介质或内网传输工具送到目标服务器再完成引擎安装、镜像导入和运行时配置。整条链路里最容易出错的是镜像依赖不完整、架构不匹配、引擎与镜像版本兼容性。提前规划好打包、传输、导入三个阶段的操作规范和校验方式部署成功率会大幅提高。我默认读者已经对 Docker 基本命令有了解知道什么是镜像、容器、docker-compose。如果完全没接触过先从“把应用装进容器”这个概念入手再回来看离线的部分会更顺。1. 离线部署的核心逻辑与规划思路1.1 先想清楚离线环境到底“离线”到什么程度很多朋友一听到离线部署就直接把目标机器断网处理。但实际项目里的隔离网络分好几种情况有的是物理断网只能靠 U 盘、移动硬盘摆渡有的是逻辑隔离内网机器有独立的域名解析和内部源但无法访问公网还有的是半隔离部分白名单域名可以访问比如只开放了某些仓库源或系统更新源。这三种场景的应对策略完全不同。物理断网时所有镜像、安装包、依赖文件必须一次带全逻辑隔离时可以在内网架一台私有仓库或者通过内网 HTTP 服务分发文件半隔离时甚至可以在目标机器上用代理配置把公网镜像源换成内网镜像源实现半自动拉取。我建议在项目启动前先画一张“网络拓扑与数据流图”明确打包机、摆渡介质、目标服务器、私有仓库这几个节点的位置。不需要多精细但要把“镜像从哪来、经谁中转、存到哪去”标清楚。很多离线部署翻车都是因为在中转环节把镜像压缩包弄丢、弄错或者弄半截。另外要明确一个原则离线部署的目标不是“一次安装成功”而是“可重复交付”。部署脚本、镜像版本清单、校验信息、升级说明这些都要随包一起交付方便后续运维团队在不同机器上重复落地。我见过太多人只带了一堆 tar 包过去不带版本记录结果出了问题完全无法回溯。1.2 打包机与目标机的环境对齐检查打包机和目标机的对齐是离线部署最重要也是最容易被忽略的一步。这里有几个维度第一是操作系统发行版和内核版本。Docker 引擎对内核版本、iptables 版本、cgroup 版本都有要求如果在 CentOS 7 上打包的安装步骤拿到 Ubuntu 22.04 上执行依赖包名称完全对不上。建议打包机和目标机尽量用相同发行版、相同小版本至少也要保持同系列。第二是 CPU 架构。x86_64 和 arm64 是两套完全不同的镜像体系从打包机导出的 amd64 镜像无法在 arm64 机器上直接运行除非镜像本身做了多架构合并。所以先确认目标机是 Intel/AMD 还是鲲鹏、飞腾、麒麟这类 ARM 平台。架构不一致后面所有工作都白费。第三是 Docker 版本。镜像的 manifest 格式、存储驱动、网络模式都跟引擎版本相关。导出的镜像 tar 包里带有元数据目标机 Docker 版本太老可能无法识别新格式版本太新可能改变默认的 iptables 策略。建议打包机和目标机使用相同的 Docker 版本哪怕旧一点全链路统一就是稳定。第四是磁盘空间和文件系统。镜像文件占空间往往比想象中大一个包含训练模型或大型依赖的镜像轻松超过 5GB。建议目标机的数据目录预留至少两倍镜像总大小空间而且优先使用 xfs 或 ext4 文件系统避免使用某些精简文件系统导致 overlay2 驱动创建失败。我把这些检查项整理成一张核对表每次交付前逐项打钩目标机 OS、内核版本、架构目标机是否已装 Docker版本是否与打包机一致数据盘挂载路径、剩余空间是否足够目标机可用的网络端口、是否需要自建私有仓库目标机是否允许 systemd 服务自启动安全策略是否放行容器网桥所需的 iptables 规则1.3 三层交付物模型镜像包、引擎包、编排包离线部署不是只带镜像就完事。我的标准交付物分成三层第一层是镜像包即通过 docker save 导出的 tar 文件每个镜像单独导出或者按服务分组导出附带 sha256 校验文件。第二层是引擎安装包包括 Docker 引擎的 rpm/deb 文件、依赖库、containerd 组件以及需要的 systemd 配置。第三层是编排包包括 docker-compose.yml、.env 环境变量文件、配置目录、启动脚本、健康检查脚本。把这三层分开的好处是升级时可以只替换某一层。比如只需要更新业务镜像就不需要动引擎安装包需要改端口映射或环境变量就只替换编排包。三层都齐了离线部署就相当于“一次复制、三步执行”。实际交付时我习惯用一个目录结构来组织这些东西delivery/ ├── images/ # docker save 出来的 tar 包 │ ├── app-api.tar │ ├── app-web.tar │ └── checksum.sha256 ├── runtime/ # Docker 引擎离线安装包 │ ├── docker-ce/ │ └── install.sh └── compose/ # 编排文件 ├── docker-compose.yml ├── .env ├── conf/ └── scripts/这个结构不是银弹但能保证交付团队、运维团队、实施团队之间沟通时不会遗漏东西。2. 镜像获取在联网机器上做预打包2.1 拉取镜像时的标签与平台坑在联网打包机上第一件事是确认要拉哪些镜像。运维同学经常凭记忆写 docker pull xxx结果拿到的镜像版本和线上不一致。正确做法是先查清楚目标服务需要的镜像仓库地址、镜像名、标签最好从官方文档或原有 docker-compose.yml 里提取。拉取时强烈建议使用完整标签不要只写 latest。latest 标签在离线环境里是灾难——你永远不知道它对应的是哪个具体版本。举个例子nginx:1.25.3 和 nginx:latest 在导出文件上可能相差几十 MB运行时行为也可能不同。离线环境一旦拉错排查代价极高。然后是平台问题。在 x86_64 打包机上docker pull 默认拉取 amd64 版本镜像。如果目标机是 ARM 架构必须显式拉取 arm64 变体。Docker 支持多架构镜像可以用以下命令检查docker buildx imagetools inspect nginx:1.25.3如果目标机是 ARM 平台建议直接在打包机上设置平台变量后重新拉取docker pull --platform linux/arm64 nginx:1.25.3拉取完成后建议立刻给镜像打上符合内网规范的新标签。很多离线环境要求镜像名不能带公网域名比如统一改成 registry.local/project/app:version方便后面往私有仓库推或直接 load 后使用。改标签时我用组织名称加服务名加版本的格式一眼能看出归属和版本docker tag nginx:1.25.3 registry.local/base/nginx:1.25.3 docker tag mysql:8.0.36 registry.local/base/mysql:8.0.362.2 docker save 与 docker load 的底层逻辑保存镜像最常用的命令是 docker save。很多新手会拿 docker export 来替代这是两个完全不同的东西。docker save 保存的是镜像的完整结构包括所有历史层、元数据、标签信息导出的 tar 可以被 docker load 恢复成镜像docker export 导出的是容器文件系统快照丢掉了镜像层和历史信息只能用 docker import 导入成一个新镜像很多原始配置会丢失。离线部署必须用 docker save docker load 这对组合否则镜像的 CMD、ENTRYPOINT、环境变量、健康检查指令都可能受影响。基础导出命令docker save -o images/app-api.tar registry.local/app/api:1.2.0如果要批量导出多个镜像建议逐个导出并记录而不是用一个 tar 包含多镜像。虽然 docker save 支持同时包含多个镜像但内网传输时一旦整包损坏全部镜像都受影响分文件导出后传输哪个失败就重传哪个代价更小。导出操作本身不压缩镜像文件多大 tar 就多大。为了节省摆渡空间我会在导出后做一次 gzip 压缩gzip -9 images/app-api.tar注意压缩和解压两端都要有 gzip 工具实际使用中 gzip 压缩率大约能减少 30%-50%。在目标机上 load 前记得先解压。Docker 的 load 命令本身不支持直接读 gz 包必须先解压或者用管道方式处理gzip -dc images/app-api.tar.gz | docker load这两种方式我都试过管道方式省一次磁盘占用但若机器配置低或镜像巨大管道解压会拖慢速度因此大包我建议先解压再 load。2.3 多镜像批量拉取与版本记录脚本如果项目包含十几个甚至几十个镜像手敲命令非常容易漏。我习惯在打包机上写一个拉取和导出一体的脚本。脚本大致思路是定义一个镜像清单文件里面每行是“原始镜像:标签 目标标签”然后循环执行拉取、打标签、导出、校验。#!/bin/bash # save-images.sh while read -r src dst; do echo 处理 $src - $dst docker pull $src docker tag $src $dst docker save -o $(echo $dst | tr / _).tar $dst done image-list.txt脚本跑完后我在每个 tar 文件旁生成一个 md5 或 sha256 校验文件方便目标机校验传输完整性sha256sum images/*.tar images/checksum.sha256这会花一点点时间但能防止 U 盘拷过去之后 load 时才发现文件损坏。我踩过一次一个 3GB 的镜像包在移动硬盘上拷到一半中断重新拷贝后没校验load 到一半报错整个交付节奏被打乱。从那以后校验文件成了标配。版本记录方面我建议在交付目录里放一个 registry-image-list.txt内容包含镜像原始地址、导出文件名、校验值、对应服务名。这样后续查问题、回退版本都有依据。3. 离线安装 Docker 引擎3.1 下载并转移引擎安装包目标机上如果还没有 Docker那么离线部署还需要解决 Docker 引擎本身的安装。在联网机器上下载 Docker 引擎离线包的方式根据系统不同有所区别。对于 CentOS/RHEL 系列可以直接下载 rpm 包。我的做法是先在联网机器上配置 Docker 官方仓库然后使用 yum 的下载插件或 dnf download 命令把需要的包全部下载到本地dnf install --downloadonly --downloaddir./docker-ce docker-ce docker-ce-cli containerd.io docker-compose-plugin这样会把 docker-ce、cli、containerd 以及所有依赖包一并下到指定目录。比手工在网页里一个个找 rpm 靠谱得多依赖关系也基本完整。下载完之后把整个 docker-ce 目录拷到目标机用 rpm 或 yum 本地安装rpm -Uvh docker-ce/*.rpm或者使用 yum localinstall自动识别本地目录中的依赖关系。两种方式在实际项目里都有用过rpm 对依赖的处理更直接yum localinstall 会从本地目录里找依赖不容易出现“某个依赖被跳过”的问题。对于 Ubuntu/Debian 系统思路类似在联网机器上下载 deb 包。使用 apt 的 download 命令逐个获取或者用 apt-get update 之后配合 deb 包缓存目录整体拷贝。实际过程中 Ubuntu 的依赖关系更复杂我建议用 apt-offline 这类工具生成依赖清单再离线安装或者干脆在打包机上装一个同样版本的 Ubuntu 虚拟机把安装过程产生的所有 deb 包缓存复制出来。这个方法笨但可靠。3.2 静态二进制包方案不依赖系统的备选方式有些场景下目标机系统太杂根本无法用 rpm/deb 统一处理。这时候我会选择 Docker 官方提供的静态二进制包。Docker 官网上有 docker-xx.x.x.tgz 这样的压缩包里面包含 dockerd、containerd、docker CLI 等二进制文件不依赖系统包管理器只需要系统有 glibc 和 iptables 就能跑。部署方式是把压缩包解压到 /usr/local/bin 或自定义目录然后手动创建 systemd service 文件。官方二进制包本身不强绑定 /var/lib/docker 路径但建议保持一致。启动核心服务时至少需要启动 containerd 和 dockerd 进程使用 systemd 管理比较稳妥。静态包方案最大的优势是不需要处理依赖关系拷贝体积小尤其适合“不知道目标机的包管理器是否被阉割”的场景。缺点是系统服务文件要自己写升级也要自己维护。如果团队熟悉 systemd 配置这个方案反而更可控。我给的 systemd 参考配置如下[Unit] DescriptionDocker Application Container Engine Afternetwork-online.target firewalld.service containerd.service Wantsnetwork-online.target [Service] Typenotify ExecStart/usr/local/bin/dockerd --data-root /data/docker ExecReload/bin/kill -s HUP $MAINPID LimitNOFILE1048576 LimitNPROCinfinity LimitCOREinfinity [Install] WantedBymulti-user.target注意我把数据目录指向了 /data/docker这在大数据量场景下很有必要避免系统盘写满拖垮宿主机。自定义数据目录后镜像导入、容器运行都默认走这个路径。3.3 存储驱动、防火墙与网段规划引擎装好后第一件事是确认存储驱动。现在 Docker 默认优先使用 overlay2需要宿主机内核模块支持 overlay。绝大多数现代 Linux 发行版都自带但某些精简内核或容器化宿主机可能没有加载。在隔离环境里无法随便装内核模块所以提前确认lsmod | grep overlay如果没输出尝试手动加载modprobe overlay加载失败的话可能要退回 vfs 或 fuse-overlayfs但性能会打折。建议在项目早期就确认这一点否则交付当天发现驱动不支持临时改变部署方案就麻烦了。防火墙和 iptables 环境也很关键。Docker 启动时会自动修改 iptables 规则默认使用桥接网络时容器和宿主机之间靠 NAT 通信。在政企内网环境防火墙策略比较严格要提前找安全团队确认哪些端口需要放行并把目标机的防火墙配置导出来检查。很多时候离线部署不成功不是镜像或引擎的问题而是防火墙拦截了容器流量。网段规划也要提前做。Docker 默认的 172.17.0.0/16 会跟某些内网网段冲突。如果目标服务器自身 IP 就在 172.17.x.x 网段容器网络会乱掉。这时候需要在 daemon.json 里显式配置{ bip: 10.88.0.1/24, data-root: /data/docker, log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 } }配置文件写好之后重启 Docker 再检查 ip addr 里 docker0 口的地址是否换成了新网段。4. 传输与导入离线部署的核心实操4.1 用 U 盘还是用内网传输服务镜像包准备好之后面临传输选择。小规模的部署U 盘或移动硬盘最直接拷贝时注意文件系统格式FAT32 不支持超过 4GB 的单文件镜像包经常超过这个尺寸因此建议使用 exFAT 或 NTFS 介质。如果你在 Mac 上制作好镜像包再拷到 exFAT 盘Windows 服务器上识别一般没有大问题。大规模的部署或者目标机在多栋楼、多个机房可以考虑搭一个内网文件分发服务。最简单的做法是用 Python 起一个 HTTP 服务python3 -m http.server 8080 --directory /data/delivery目标机上就可以用 wget 或 curl 拉取文件。注意在内网搭 HTTP 服务之前要确认安全策略允许目标机器通过 8080 端口访问服务端。如果安全要求高我会用 rsync 配合 SSH 做增量同步断点续传比 HTTP 更稳。rsync 的好处是文件传一半断了可以继续适合大包rsync -avzP --partial delivery/ usertarget:/data/delivery/不管用哪种方式到了目标机都要跑一遍 sha256sum 校验确保与交付端的 checksum.sha256 完全一致再进入下一步。4.2 镜像导入与批量 load镜像包到达目标机后导入过程本身并不复杂逐个解压并 load 即可tar -xzf app-api.tar.gz docker load -i app-api.tarload 完成后用 docker images 确认镜像已经出现。批量处理时写一个脚本遍历整个 images 目录中的 tar 或 tar.gz 文件并标记每个镜像是否导入成功#!/bin/bash set -euo pipefail LOG_FILEload.log for f in images/*.tar.gz images/*.tar; do echo 正在导入 $f ... $LOG_FILE gzip -dc $f 2/dev/null || cat $f | docker load -q $LOG_FILE done这个脚本里我同时处理了 .gz 和非 .gz 两种文件用 gzip -dc 尝试解压失败则降级到 cat实际使用中少了不少麻烦。需要注意的一点是docker load 只会导入镜像不会自动修复镜像名里的域名。如果镜像原本打的是 registry.local/xxx 这种标签load 后 docker images 里会显示这个名称但目标机本地并没有 registry.local 域名解析没有关系只要镜像已经在本机存在docker-compose 里使用这个镜像名就能正常启动。若你的 compose 文件引用的是某个仓库地址而导入镜像标签不带仓库地址需要在目标机上重新 tag或者在 compose 中统一调整镜像名。4.3 本地私有仓库在内网中的价值当目标机器数量达到三台以上逐台 load 镜像的体力活就会非常不划算。我更推荐在内网架一台 Docker 私有仓库比如 Harbor 或纯 registry然后把镜像推到私有仓库目标机器只需要配置 docker 客户端的 insecure-registries 指向仓库地址再像平时一样 docker pull。搭建私有仓库的核心步骤是先获取 registry 镜像并导入到仓库服务器然后启动容器docker run -d -p 5000:5000 --name registry \ -v /data/registry:/var/lib/registry \ --restartalways \ registry:2.8.3使用 HTTPS 证书的情况比较复杂内网环境通常先使用 HTTP需要在目标机 Docker 的 daemon.json 里加入 insecure-registries{ insecure-registries: [registry.local:5000] }配置完成后所有目标机统一从私有仓库拉镜像版本控制也更方便。仓库服务器意外损坏时可以把镜像 tar 包再导入一次重建所以离线包仍然要保留。对于 Harbor功能更强支持镜像复制、权限控制、审计日志适合规章制度比较严的环境。但 Harbor 自身也是容器应用它的离线部署反而是一个典型的“Docker 内网离线部署”实践先在有网机器上获取 Harbor 安装包和镜像 tar再带到内网导入启动。我给客户交付时经常先部署 Harbor再基于 Harbor 分发业务镜像形成一套可持续的离线供给体系。5. 编排与配置让离线环境跑得更稳5.1 docker-compose 离线编排的注意事项镜像导入完成后通常用 docker-compose 拉起整个应用栈。离线环境下compose 文件里不要写 build 指令因为镜像已经提前打包好了build 过程需要从网络拉基础镜像和依赖离线环境必挂。compose 文件里全部使用 image 关键字并且镜像名要和 load 进来的标签一一对应。在 .env 文件里维护端口、密码、路径等环境变量。我建议把 compose 文件、.env、配置目录一起打包成 compose 交付目录。需要保证的是目标机器上有 docker compose 插件离线安装 Docker 引擎时要把 docker-compose-plugin 这个 rpm/deb 一起装上否则 compose 命令会提示找不到。启动前先做一次配置检查docker compose config这条命令不会启动服务只是校验 compose 文件语法和引用关系。配置无误后再执行docker compose up -d启动后无论结果如何第一件事看日志和健康状态docker compose ps docker compose logs --tail200 -f离线环境的排查通道少日志是最重要的信息源。5.2 数据持久化与备份策略容器部署的一个老生常谈是数据卷挂载。离线部署时尤其要确认数据卷挂载点存在且权限正确。很多镜像容器内进程以非 root 用户运行如果挂载目录权限不对容器会启动失败或者写入失败。我一般先把目录权限设成 755 或按需 777实测有效后再按最小权限收紧。数据备份不能依赖容器层。默认的容器可写层随时可能因为容器重建而丢失所以必须把所有需要持久化的目录都挂到宿主机。建议 compose 里统一用 /data/appname/ 下的子目录管理备份时整体打包即可。离线环境出问题时的恢复思路也要提前设计如果某台目标机彻底损坏操作系统重装后拿出交付包重新安装引擎、load 镜像、恢复数据卷就能在较短时间内恢复业务。这就是“可重复交付”的价值不能等到灾难来了才手忙脚乱。5.3 日志、资源限制与健康检查给每个容器配置资源限制和日志策略是运维成熟的标志。compose 里的资源限制可以防止某个容器把宿主机内存吃爆。实际项目中内存泄漏、日志暴涨是内网应用最常见的稳定性问题所以日志大小限制和轮转非常必要。健康检查也建议在 compose 里内置。默认容器启动后只代表进程活着不代表业务可用。通过健康检查可以提前发现依赖服务没就绪的问题比如应用起来但连不上数据库。配置合理的 healthcheck 后docker compose ps 里会显示 healthy 或 unhealthy能快速定位。健康检查的写法参考services: app: image: registry.local/app/api:1.2.0 healthcheck: test: [CMD, curl, -f, http://localhost:8080/healthz] interval: 30s timeout: 5s retries: 3 start_period: 20s5.4 离线环境的系统初始化脚本封装交付过程中我已经习惯把整个部署过程封装成一个初始化脚本包括安装引擎、加载镜像、同步配置文件、启动 compose、拉起监控。脚本内部保证幂等重复执行同一部署不会产生副作用。这个脚本放在交付包根目录执行时输出详细日志。脚本里除了核心部署逻辑还会在导出一份环境检查报告包括操作系统版本、内核模块状态、磁盘空间、Docker 版本、镜像列表。这份报告会成为项目验收资料的一部分也能为后续升级提供参考。6. 常见问题与排查技巧6.1 离线部署高频问题速查表我把实际项目中遇到的高频问题和排查思路整理成一个表格遇到问题时先对照表格快速定位。问题表现常见原因排查命令/建议docker pull 报无法解析域名引擎未配置内网仓库地址检查 /etc/docker/daemon.json 的 insecure-registriesdocker load 报 EOF 或损坏tar 文件传输不完整重新校验 sha256重新拷贝或传输overlay2 挂载失败内核缺少 overlay 模块执行 modprobe overlay检查内核版本容器启动后端口不通防火墙拦截或网段冲突检查 iptables、firewalld、docker0 网段镜像导入成功但 compose 拉取报错镜像名不匹配docker images 对比 compose 里 image 字段启动后容器秒退应用配置错误或数据卷权限不对docker logs 查看退出原因检查挂载权限内存不足导致容器被杀资源限制配置缺失sysctl vm.overcommit_memory、调整 compose mem_limit磁盘写入缓慢数据目录落在性能差磁盘将 docker>