先交代一个背景我在某项目的内网环境里折腾过好几轮容器基础设施其中最关键的一步就是在没有任何外网连接的 SUSE 12 服务器上把 Docker 跑起来再搭一个能用的私有镜像仓库。这个事听起来简单但真正操作过的人都知道离线环境的坑全藏在依赖关系、仓库配置和证书校验这些细节里。这篇文章把我实测通过的一条完整路径写出来从离线 rpm 包准备到 Registry 认证和推送验证照着做基本能一次走通。如果你手头正好是 SLES 12 / openSUSE 12 系列的机器又装了 Docker那这篇就是冲你来的。1. 离线安装的整体思路与物料准备1.1 为什么离线安装不能“拿到安装包就装”在线环境下装 Docker一条命令的事包管理器会把依赖全部拉齐。离线环境下最大的变数就是依赖docker-ce主包依赖containerd.io、docker-ce-cli、docker-buildx-plugin而这些包在不同系统版本上的依赖树完全不一样。SUSE 12 用的是zypper它的依赖解析依赖网络仓库一旦断网zypper只会告诉你“无法解析依赖”不会帮你自动把本地 rpm 包排好序。我刚开始也试图走“下载一个 rpm 然后rpm -Uvh硬装”的捷径结果被libseccomp版本过老、containerd和runc互相要求特定版本这些问题反复摩擦。所以第一条经验就是离线安装必须先把完整的依赖包清单列出来在一台有网络的同版本机器上把所有 rpm 下载好整体搬到内网再装。1.2 离线安装的两条可行路径对比针对 SUSE 12我实际对比过两条路方案获取方式优点缺点方案 ASUSE 仓库自带 Docker 包有网机器上zypper install --download-only docker docker-client依赖与系统完全匹配安装冲突最少版本通常偏旧新特性比如 BuildKit可能缺方案 BDocker 官方 SUSE 仓库 rpm从 Docker 官方linux/suse仓库下载 docker-ce 系列 rpm版本较新功能完整依赖libseccomp版本可能不满足需要额外下载如果只是“把 Docker 跑起来 搭 Registry”方案 A 就够用。但如果你后续要跑新版镜像、用docker compose或 BuildKit方案 B 更合适。我最终采用的是方案 B因为某项目里要跑一个要求 Bundle 版本较高的工具老版本 Docker 撑不住。后面我把方案 B 的完整依赖准备过程拉通讲。1.3 离线物料清单无论走哪条路径最后都要在内网机器上准备这么几类东西docker 主程序 rpm 包包括 docker-ce、docker-ce-cli、containerd.io、docker-buildx-plugin、docker-compose-plugin。系统依赖包libseccomp 及其它被依赖的动态库以 SUSE 12 仓库中的版本为准。Registry 镜像 tar 包在一台有网的机器上docker pull registry:2然后docker save成 tar 文件拷到内网。客户端工具有podman或skopeo可以辅助检查镜像 manifest不是必须但调试时有用。这里有个容易被忽略的点docker save出来的 tar 包在拷贝过程中不要用tar再套一层直接把.tar当普通文件传就行。我之前犯过错用 FTP 传到一半断线最后docker load时直接报“corrupt image”。传完后一定要sha256sum跟源文件比对一遍不要迷信传输工具的成功提示。2. SUSE 12 环境准备与依赖处理2.1 先摸清系统版本与内核前提离线安装之前必须确认两件事系统版本和内核。SUSE 12 是一个大版本统称实际有 SP1 到 SP5不同小版本的内核和软件库差异不大但libseccomp的版本会有差别。用下边命令确认cat /etc/os-release | grep VERSION_ID uname -r zypper --versionDocker 对内核有硬性要求3.10 以上且必须开启overlay或overlay2文件系统支持。SUSE 12 默认内核通常满足这个条件但某些最小化安装可能没加载overlay模块需要提前确认lsmod | grep overlay如果输出为空先执行modprobe overlay并写入/etc/modules-load.d/overlay.conf保证重启自动加载。这一步不做后面dockerd启动时大概率直接报 kernel 不支持 overlay2非常容易误判成“docker 文件损坏”。2.2 在有网机器上批量下载 rpm 包这一步是整个离线安装的核心工序。先说结论不要手动一个个点链接下载 rpm太容易漏依赖。正确做法是先把 Docker 官方源或 SUSE 源配置到一台有网络的同版本机器上然后让包管理器解析依赖并下载全部 rpm。SUSE 12 下配置 Docker 官方 SUSE 仓库并下载依赖包我用的命令是zypper addrepo https://download.docker.com/linux/suse/docker-ce.repo zypper refresh接着先模拟安装看依赖能否解析zypper install --dry-run docker-ce--dry-run只会输出依赖分析结果不实际安装。这一步能提前发现依赖问题比如libseccomp版本不足有问题就在有网机器上先解决别等到了内网才发现。确认无冲突后执行下载zypper install --download-only docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin--download-only参数的作用是只把 rpm 包下载到本地缓存不安装。下载完成后缓存目录在/var/cache/zypp/packages下需要把所有下载的 rpm 集中到一个目录mkdir -p /tmp/docker-offline find /var/cache/zypp/packages -name *.rpm -exec cp {} /tmp/docker-offline/ \;然后把这个目录整体往内网搬。传输我用过scp、U 盘、内网共享目录都行但一定要记住校验完整性。2.3 内网机器上的安装顺序与依赖处理到了内网机器上不建议一上来就直接rpm -Uvh *.rpm。虽然 rpm 支持一次传多个包但依赖顺序错了照样失败。我踩过的坑是这样的containerd.io和docker-ce之间存在版本匹配关系如果先装 docker-ce 再装 containerd.iorpm 会报“need containerd.io xxx”必须要加--nodeps绕过而--nodeps装出来的环境后续跑容器时经常会莫名崩溃。正确做法是分两步第一步先把系统依赖libseccomp、libcgroup等装上rpm -Uvh /tmp/docker-offline/libseccomp*.rpm rpm -Uvh /tmp/docker-offline/libcgroup*.rpm第二步按依赖层级从底层往上装rpm -Uvh /tmp/docker-offline/containerd.io-*.rpm rpm -Uvh /tmp/docker-offline/docker-ce-cli-*.rpm rpm -Uvh /tmp/docker-offline/docker-buildx-plugin-*.rpm rpm -Uvh /tmp/docker-offline/docker-compose-plugin-*.rpm rpm -Uvh /tmp/docker-offline/docker-ce-*.rpm装完验证命令是否存在docker --version containerd --version如果docker --version正常输出说明主程序装好了。如果提示缺某个共享库用ldd /usr/bin/dockerd | grep not found查缺的是哪个库再从离线目录找对应 rpm 补装。这一步是慢功夫但比重新找包快得多。注意如果libseccomp版本太老导致runc启动容器时直接报 “seccomp: operation not permitted”不要急着--nodeps优先从网络机器上找更高版本的libseccomprpm 一起带回来。3. Docker 配置与启动验证3.1 daemon.json 关键配置项Docker 装完后先别急着systemctl start docker把/etc/docker/daemon.json配好再启动省得后面改配置还要重启服务甚至迁移数据目录。我在离线环境里用的配置文件如下{ exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 }, storage-driver: overlay2, data-root: /data/docker, registry-mirrors: [], insecure-registries: [registry.internal:5000] }逐个说一下关键项exec-optsSUSE 12 默认的 init 系统是 systemdcgroup 驱动必须跟 systemd 对齐否则容器内系统资源限制会失效。storage-driveroverlay2性能比devicemapper好得多SUSE 12 内核默认支持。>systemctl enable docker systemctl start docker systemctl status docker -l如果状态是active (running)就基本稳了。然后跑一个 hello-world 验证闭环docker pull hello-world docker run --rm hello-world离线环境没有外网docker pull hello-world会失败是正常的不用慌。验证 docker 是否能正常工作更直接的手段是创建一个本地容器测试docker run --rm busybox echo docker works前提是你的离线目录里有 busybox 镜像的 tar 包。如果连这点镜像都没准备也可以直接看docker info的输出能正常显示容器和镜像数量就说明 daemon 是健康的。3.3 用户组与 socket 权限非 root 用户执行 docker 命令时如果报permission denied while trying to connect to the Docker daemon socket说明当前用户没进docker组。SUSE 12 安装 docker-ce 后会自动创建 docker 组把目标用户加进去即可usermod -aG docker $(whoami) newgrp docker这里有个容易混淆的点加完组后必须重新登录一次或者用newgrp激活否则当前会话仍然沿用旧组权限你还会以为 docker 没配好。SUSE 12 默认启用了 AppArmorDocker 容器默认受限。如果容器内部某些服务异常比如写/proc或操作设备节点被拒排查顺序要先看 AppArmorjournalctl -u docker | grep -i apparmor必要的话在 daemon.json 里给特定容器加security-opt: [apparmorunconfined]绕过限制但生产环境建议还是精确配置 AppArmor profile而不是一刀切放开。4. Docker Registry 私有仓库搭建4.1 Registry 镜像的离线导入Registry 本身是一个容器镜像离线环境下需要先在一台有网的机器上把镜像导出再导入到内网。命令很简单有网机器上执行docker pull registry:2 docker save registry:2 -o registry2.tar把registry2.tar传到内网机器后执行docker load -i registry2.tardocker load完成后用docker images | grep registry确认镜像已经存在。这时你可能注意到镜像名是registry:2这样的标准名tag 是2。4.2 启动 Registry 容器启动 Registry 容器我建议把数据目录挂到宿主机持久化否则容器一删仓库里的镜像全部消失。实际用的启动命令mkdir -p /data/registry/{auth,data,certs} docker run -d \ --name registry \ --restartalways \ -p 5000:5000 \ -v /data/registry/data:/var/lib/registry \ -v /data/registry/certs:/certs \ -v /data/registry/auth:/auth \ -e REGISTRY_AUTHhtpasswd \ -e REGISTRY_AUTH_HTPASSWD_REALMRegistry Realm \ -e REGISTRY_AUTH_HTPASSWD_PATH/auth/htpasswd \ -e REGISTRY_HTTP_TLS_CERTIFICATE/certs/domain.crt \ -e REGISTRY_HTTP_TLS_KEY/certs/domain.key \ registry:2这个命令把认证和 TLS 都配置上了是生产可用的形态。如果只是内网快速验证可以先跳过 TLS 和认证用最简命令docker run -d --name registry -p 5000:5000 -v /data/registry/data:/var/lib/registry registry:2最简形态适合验证网络链路通不通但一旦暴露在多人协作环境里没有认证会非常危险任何一个能访问 5000 端口的人都能推删镜像。我的建议是验证阶段用最简命令正式使用带上认证和证书。4.3 配置 htpasswd 认证Registry 的认证用的是 Apache htpasswd 格式需要提前生成密码文件。在有apache2-utils的机器上执行htpasswd -nbB admin YourStrongPassword /data/registry/auth/htpasswd参数说明-n表示输出到标准输出而不是创建文件-b表示密码在命令行直接提供-B表示使用 bcrypt 加密算法比默认的 apr1 更安全但要求 Registry 2.7 以上版本如果你导入的registry:2是老版本2.6 或更早-B生成的密码可能不被识别用htpasswd -nb admin YourStrongPassword生成的 apr1 格式更保险。添加第二个用户时注意不能用-c参数否则会覆盖已有文件htpasswd -bB /data/registry/auth/htpasswd devuser DevPss4.4 TLS 证书配置内网环境没有正规 CA只能自签证书。openssl自签证书的生成命令如下openssl req -newkey rsa:2048 -nodes -keyout /data/registry/certs/domain.key -x509 -days 365 -out /data/registry/certs/domain.crt -subj /CNregistry.internal这里的 CN 一定要写对。客户端 Docker 访问 registry 时会拿访问的主机名去校验证书 CN 是否匹配。如果你内网机器通过 IP 访问CN 要写 IP通过域名访问CN 写域名。写错的话即使你把 CA 配到客户端TLS 握手依然失败。要让内网所有 Docker 客户端信任这个自签证书有两种方式方式一将domain.crt拷贝到所有客户端机器的/etc/docker/certs.d/registry.internal:5000/ca.crtDocker 会自动识别。这里的目录名必须包含端口号。方式二在/etc/docker/daemon.json里把registry.internal:5000加入insecure-registries。这种方式跳过证书校验省事但不适合多环境共用。4.5 验证 Registry 是否可用配置完认证和证书后在 Registry 机器本机验证curl -u admin:YourStrongPassword https://registry.internal:5000/v2/返回{}代表协议正常。有认证时还需要在 Docker 客户端先登录docker login registry.internal:5000 -u admin -p YourStrongPassword登录成功后~/.docker/config.json里会保存认证信息。这一步如果报http: server gave HTTP response to HTTPS client说明客户端没有走 TLS要么是证书配置没生效要么是insecure-registries没配。5. 镜像导入导出与批量推送流程5.1 为什么不建议用 docker pull 直接分发镜像离线环境里镜像分发主要靠docker save和docker load。有人图省事把 Registry 搭起来后就想在有网机器上docker pull再docker push到内网仓库但这条路走不通因为 push 操作本身要访问外网镜像源而离线机器根本没有外网。正确的流程是在有网机器上docker pull需要的镜像。docker save导出成 tar 包。传到内网机器docker load导入。docker tag打上 Registry 地址的标签。docker push推送到私有仓库。5.2 批量 save 与 load 脚本当镜像数量一多手动敲命令容易漏我写了个简单脚本把镜像列表存在文件里批量处理。保存镜像的脚本save_images.sh#!/bin/bash IMAGES_FILEimages.txt TAR_DIR./image-tars mkdir -p $TAR_DIR while read image; do [ -z $image ] continue name$(echo $image | tr /: _) docker pull $image docker save $image -o $TAR_DIR/${name}.tar echo saved: $image - $TAR_DIR/${name}.tar done $IMAGES_FILE导入镜像的脚本load_images.sh#!/bin/bash TAR_DIR./image-tars for tar in $TAR_DIR/*.tar; do docker load -i $tar echo loaded: $tar done导入完成后给镜像打标签并推送可以在load_images.sh之后追加一步TARGET_REGISTRYregistry.internal:5000 docker images --format {{.Repository}}:{{.Tag}} | grep -v registry.internal | grep -v none | while read img; do docker tag $img $TARGET_REGISTRY/$img docker push $TARGET_REGISTRY/$img done这个批量推送有个隐含前提Registry 里已经建立了对应项目的命名空间。Registry 本身不区分命名空间library/xxx和project/xxx都允许所以直接推就行。5.3 验证推送结果与镜像完整性推送成功后在 Registry 机器上确认镜像确实存在curl -u admin:YourStrongPassword https://registry.internal:5000/v2/_catalog输出的 json 里会列出所有仓库名。再查某个具体的 tagcurl -u admin:YourStrongPassword https://registry.internal:5000/v2/busybox/tags/list内网其他机器想从私有仓库拉镜像直接docker pull registry.internal:5000/busybox:latest如果拉取时提示证书问题按照前面说的把domain.crt放到对应目录或者在 daemon.json 里加insecure-registries然后重启 docker。6. 常见问题与排查技巧实录6.1 安装阶段的依赖冲突问题现象rpm -Uvh装 docker-ce 时提示require containerd.io 1.6.0但 containerd.io 明明已经在前面装过了。排查思路用rpm -qa | grep containerd查看实际装的版本号如果版本确实低于要求说明离线目录里的 containerd rpm 太旧需要回到有网机器重新下载新版。如果版本满足但 rpm 还报冲突可能是包架构不对比如装了 i686 的 containerd而 docker-ce 要求 x86_64用rpm -qa --qf %{ARCH} containerd.io确认。解决删掉错误架构的包装对应架构的版本。rpm -e --nodeps慎用卸载 containerd 可能导致已经装好的 docker-ce 也挂掉。6.2 Docker 服务启动失败日志不明确问题现象systemctl start docker后服务一直是activating一段时间然后 failedjournalctl -u docker里只有Failed to start Docker Application Container Engine没有具体报错。排查思路这在 SUSE 12 上很典型多是 dockerd 二进制本身缺动态库但 systemd 日志没有显示细节。手动执行一下排除 daemon 启动问题dockerd --debug如果输出里能看到error while loading shared libraries说明缺依赖库。用ldd找到缺的库后再补 rpm。另外检查/etc/docker/daemon.json的 JSON 格式用docker run --rm之前系统如果版本过旧不能识别>{ insecure-registries: [registry.internal:5000] }重启 docker 后再试。如果用了自签证书但想保留 HTTPS 校验按 4.4 节的方式把 CA 放到客户端对应目录不要走 insecure 路线。6.4 Registry 容器反复重启问题现象docker ps看到 registry 容器状态一直Restartingdocker logs registry里提示权限不足或目录不存在。原因宿主机挂载的数据目录权限不对。Registry 容器内进程以uid 1000运行如果宿主机目录属主不是 1000容器内无法写入。解决chown -R 1000:1000 /data/registry或者干脆在启动命令里加--user root强制容器以 root 运行不推荐生产用。拉取镜像列表时若返回 401先去检查 htpasswd 文件是否生成成功cat /data/registry/auth/htpasswd如果文件是空的说明htpasswd -nbB输出被重定向失败重新生成并把权限改成 0600 即可。6.5 容器内 DNS 解析异常问题现象容器能启动但内部域名解析不通ping外部域名无响应docker exec进容器后/etc/resolv.conf里的 nameserver 指向家里路由器的 192.168.x.x。原因dockerd 启动时读取了宿主机/etc/resolv.conf内网环境的 DNS 配置可能指向了不可达地址。解决在 daemon.json 里显式指定 DNS{ dns: [10.20.0.2, 10.20.0.3] }重启 docker 后新容器就会使用指定 DNS。对已有的容器需要重建。6.6 离线环境 docker pull 一直超时现象在离线机器上执行docker pull直接卡住或报dial tcp ... i/o timeout。原因docker 客户端默认从 Docker Hub 拉取离线环境访问不到。解决思路这不是故障而是使用方式不对。离线环境一律通过docker load导入了的镜像运行或从私有 Registry 拉取。如果非要让docker pull不超时可以配置registry-mirrors指向内网 Registry但 Registry 里没有的镜像照样拉不到。这套流程下正确姿势就是“有网机器提前备货”。7. 个人实操中的几点体会最后聊几句实在话。第一离线安装问题的本质不是“装不上”而是“依赖关系处理不彻底”。我见过不少人在内网上硬装 docker遇到依赖报错就--nodeps、--force一路绕过最后 Docker 能起来但跑容器时各种诡异问题根本没法定位。依赖问题一定要在源头解决有网机器上把依赖树理清楚离线安装才能一遍过。第二Registry 的认证和证书不要偷懒。内网环境总觉得“反正没外网不怕”但一个团队里几百个人都能访问到 Registry 时没有认证就意味着任何人都能推 tag 覆盖生产镜像版本。我在这上边吃过亏——有人在测试时顺手把某个镜像 push 成了latest生产发布时拉到的直接是坏版本排查了整整一个下午。第三镜像数据的备份策略要提前想好。Registry 数据目录一旦损坏仓库里的所有镜像全部丢失你是没法从外网重新 pull 回来的。至少每周把/data/registry/data打包存一份。恢复的时候直接把目录解压回去重启容器即可操作不复杂但真正坚持做的人不多。这套流程我已经在多个 SUSE 12 环境上跑通了从 rpm 准备到 Registry 认证推送全套下来大概半天时间。如果你也是刚接手这类任务照着文中的步骤走重点留意依赖版本和证书配置应该能少走不少弯路。