ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

离线镜像包处理指南:区分docker load与registry存储目录

离线镜像包处理指南:区分docker load与registry存储目录

简介:面向无法访问外网的内网环境运维与开发人员,这份资源提供可直接导入的 Docker Registry 镜像文件,用于在内网快速搭建私有镜像仓库,解决离线部署时镜像拉取困难、仓库搭建缺少基础镜像的问题。压缩包共 18 个文件,约 25.15MB,以 json 元数据、tar 层数据、version 版本标识及 repositories 仓库索引为主,完整保留了镜像的分层结构与清单信息,便于通过 docker load 等方式还原使用。已有 436 人学习下载,说明其在离线私有仓库场景中具有一定参考价值。读者可据此获得一套结构完整的 Registry 镜像素材,理解镜像分层与清单组织方式,并直接用于内网仓库的搭建与验证,减少从零准备镜像的时间成本,适合需要离线部署容器基础设施的中级运维人员参考。

1. 拿到 registry.tar.gz 先别急着 docker load:一个离线镜像包的版本坑

上周帮同事在一台内网机器上补 Docker 环境,对方甩过来一个registry.tar.gz,说“解压完docker load一下就行”。结果docker load报invalid tar header,折腾半小时才发现这包根本不是docker save出来的镜像归档,而是私有 registry 的存储目录打包——里面是docker/registry/v2/那套 blobs 和 repositories 结构。这类离线包在隔离环境里特别常见,装错方式不会报“你搞错了”,只会给你一个看不懂的 tar 错误。这篇记录的就是我处理这类 registry 离线包的标准流程:先认清包的类型,再决定是docker load还是挂存储目录起 registry 容器,最后验证 push/pull 能跑通。适合正在内网搭镜像仓库、被离线包格式绕晕的开发者。

2. 认清 registry.tar.gz 的两种形态:镜像归档还是存储目录

2.1 先解压看结构,别上来就 docker load

拿到任何.tar.gz,我第一件事不是执行命令,而是先看里面长什么样。这一步花三十秒,能省掉后面半小时的报错排查。

# 不解压,先列出归档内的一级目录结构 tar -tzf registry.tar.gz | head -30 # 统计条目数量,判断是单镜像还是整仓 tar -tzf registry.tar.gz | wc -l

逻辑说明:-t是列出内容不解压,-z处理 gzip,-f指定文件。head -30只看前 30 行足够判断形态。如果输出里出现manifest.json、repositories、一长串<sha256>.json,那是docker save的镜像归档;如果出现docker/registry/v2/blobs/、docker/registry/v2/repositories/,那是 registry 的存储目录。条目数量也有参考价值:单镜像归档通常几十到几百条,整仓存储目录动辄上千条。

判断结果直接决定后续路线,两条路不能混:

归档内特征包的类型处理方式
manifest.json+repositoriesdocker save 镜像归档docker load -i
docker/registry/v2/blobs/registry 存储目录挂载目录起 registry 容器
顶层就是v2/目录registry 存储目录(相对路径版)同上,注意挂载点对齐

提示:如果tar -tzf直接报not in gzip format,说明文件后缀骗人,实际是未压缩 tar,把-z去掉再试。

2.2 镜像归档路线:docker load 的版本兼容性

确认是docker save产物后,docker load本身不复杂,坑在版本。Docker 19.03 之前docker save默认输出旧格式,19.03 之后支持 OCI 格式,跨大版本搬运时容易出问题。

# 查看本机 Docker 版本,决定是否需要 --platform 参数 docker version --format '{{.Server.Version}}' # 标准加载,-i 指定输入文件 docker load -i registry.tar.gz # 加载后确认镜像进了本地 docker images | grep -i registry

逻辑说明:docker version --format只取服务端版本,比看完整输出干净。docker load -i的-i是 input 的缩写,等价于docker load < registry.tar.gz,但用-i更直观也不受 shell 重定向影响。加载完成后docker images过滤确认,注意看 TAG 列——有些归档加载出来是<none>,那是打包时没打 tag,需要手动docker tag补上才能用。

常见做法是:如果服务端版本低于 20.10,而归档是在新版本机器上打的,加载可能报unsupported manifest media type。这时要么在打包端用docker save --format docker强制旧格式重打,要么升级加载端。我一般倾向后者,因为旧格式会丢一些元数据。

2.3 存储目录路线:挂载起 registry 容器

如果解压出来是docker/registry/v2/结构,说明这是某个 registry 实例的数据目录直接打包。正确做法是把它还原成目录,挂给官方 registry 镜像。

# 建工作目录并解压 mkdir -p /data/registry-restore tar -xzf registry.tar.gz -C /data/registry-restore # 确认解压后的层级,找到 v2 目录的实际位置 find /data/registry-restore -maxdepth 3 -type d -name v2 # 起 registry 容器,把存储目录挂到容器内的 /var/lib/registry docker run -d --name registry-restore \ -p 5000:5000 \ -v /data/registry-restore/docker/registry:/var/lib/registry \ registry:2

逻辑说明:-C指定解压目标目录,避免污染当前路径。find -maxdepth 3是为了确认v2到底在第几层——不同打包方式层级不一样,挂载点必须精确到包含v2的那一级父目录,挂错一层 registry 起来后就是空仓库。-v的宿主机路径要对应到容器内/var/lib/registry,这是官方 registry 镜像的默认存储根。registry:2是官方镜像标签,内网环境如果拉不到,得先想办法把 registry 镜像本身也导进去,这是另一个先有鸡还是先有蛋的问题,后面避坑章会讲。

起容器后别急着 push,先看日志确认存储加载正常:

docker logs registry-restore --tail 50

日志里出现listening on [::]:5000且没有blob unknown之类的报错,才算存储目录认对了。

3. 配置 registry:端口、存储驱动与访问控制

3.1 用配置文件替代一堆命令行参数

docker run直接堆参数能跑,但参数一多就难维护,而且有些配置项命令行根本传不进去。我习惯写一个config.yml挂进去。

version: 0.1 log: level: info formatter: text storage: filesystem: rootdirectory: /var/lib/registry delete: enabled: true http: addr: :5000 headers: X-Content-Type-Options: [nosniff]

逻辑说明:storage.filesystem.rootdirectory必须和容器挂载点一致,写错就是空仓库。delete.enabled: true是很多人漏掉的——不开这个,删镜像的 API 直接返回 405,仓库只进不出,磁盘迟早撑爆。http.headers加nosniff是安全习惯,防止浏览器把 blob 当别的类型解析。log.level调试时改debug,平时info就够,debug日志量大,长期开会影响性能。

挂配置文件起容器:

docker run -d --name registry \ -p 5000:5000 \ -v /data/registry-restore/docker/registry:/var/lib/registry \ -v /data/registry-config/config.yml:/etc/docker/registry/config.yml \ registry:2

-v挂配置文件时注意宿主机路径必须是绝对路径,相对路径 Docker 会当成命名卷处理,结果就是容器里读不到配置还静默用默认值,这个坑很隐蔽。

3.2 端口与 insecure-registry 的对应关系

registry 默认走 HTTP,而 Docker 客户端默认只信任 HTTPS 的 registry。内网用 IP:5000 访问时,必须在客户端配insecure-registries,否则 push 直接报http: server gave HTTP response to HTTPS client。

# 编辑 Docker 守护进程配置 sudo tee /etc/docker/daemon.json <<'EOF' { "insecure-registries": ["192.168.1.10:5000"] } EOF # 重载配置并重启 sudo systemctl daemon-reload sudo systemctl restart docker

逻辑说明:insecure-registries里写的是IP:端口,不要带http://前缀,带了反而不认。改完必须重启 Docker 守护进程,daemon-reload只是让 systemd 重读 unit 文件,配置生效靠restart。重启会停掉所有运行中的容器,生产机上要挑时间窗口,或者用systemctl reload docker试试能否热加载——不是所有版本都支持,别赌。

注意:daemon.json是整文件覆盖式读取,如果原来已有内容,用tee直接写会把旧配置冲掉。稳妥做法是先cat /etc/docker/daemon.json看一眼,用jq合并后再写回。

3.3 存储容量与垃圾回收的提前规划

registry 的存储只增不减,删了镜像标签,blob 还在磁盘上躺着。这是设计如此,不是 bug。上线前就得把 GC 流程想好。

# 只读模式起容器做 GC,避免写入冲突 docker run --rm \ -v /data/registry-restore/docker/registry:/var/lib/registry \ registry:2 garbage-collect /etc/docker/registry/config.yml

逻辑说明:garbage-collect是 registry 镜像内置的子命令,扫描所有 manifest 引用,把没被引用的 blob 清掉。执行期间 registry 最好停写或切只读,否则可能删掉正在被引用的层。我一般做法是:先docker stop registry,跑 GC,再docker start registry,宁可短暂停服也不冒数据损坏的险。GC 耗时和仓库大小成正比,几十 GB 的仓库跑十几分钟正常,别以为卡死了。

4. 避坑与排查:registry 离线部署的五个血泪记录

4.1 现象:docker load 报 invalid tar header

原因:把 registry 存储目录包当成docker save归档去加载。两者都是 tar.gz,但内部结构完全不同,docker load只认manifest.json那套格式。

解决:按 2.1 的方法先tar -tzf看结构,确认是存储目录就走挂载路线。已经误操作的,本地不会留下半截镜像,直接换路线即可,不用清理。

4.2 现象:registry 容器起来了,push 报 405 Method Not Allowed

原因:config.yml里没开storage.delete.enabled,或者开了但配置文件没挂进去,容器用的还是默认配置。默认配置下删除相关 API 全部禁用。

解决:docker exec registry cat /etc/docker/registry/config.yml确认容器内实际读到的配置内容,和宿主机上的文件比对。不一致就是挂载路径写错,常见于用了相对路径被当成命名卷。

4.3 现象:push 报 http: server gave HTTP response to HTTPS client

原因:客户端没配insecure-registries,或者配了但没重启 Docker 守护进程,或者 IP 端口写错(带了http://前缀、端口漏写、用了localhost而实际访问的是 IP)。

解决:docker info | grep -A5 "Insecure Registries"看当前生效列表,确认目标地址在里面。不在就按 3.2 改daemon.json并重启。改完还是不行,检查是不是有多个daemon.json位置冲突(/etc/docker/和~/.docker/都可能存在)。

4.4 现象:registry 起来后仓库是空的,但存储目录明明有数据

原因:挂载点层级错了。打包时可能是docker/registry/v2/,解压后v2在第三层,而挂载时挂到了docker/registry的上一层或下一层,registry 在容器内找不到v2目录就初始化了个空的。

解决:docker exec registry ls /var/lib/registry看容器内实际看到什么。应该能看到v2目录。看不到就调整-v的宿主机路径,用find精确定位v2的父目录再挂。

4.5 现象:内网机器连 registry:2 镜像都拉不到,死循环

原因:离线环境里 registry 镜像本身也需要从外部获取,而机器又不能上网。这是典型的先有鸡还是先有蛋。

解决:找一台能上网的机器docker pull registry:2,然后docker save registry:2 -o registry-image.tar,把 tar 拷进内网docker load。这一步要在部署 registry 之前完成,别等起了容器才发现基础镜像没有。我现在的习惯是任何离线部署清单里,基础镜像永远排第一项。

5. 装完怎么验证:三步确认 registry 真的能用

环境起没起来,docker ps一看便知,但“能用”和“起来了”是两回事。我验证 registry 固定走三步,缺一步都不算完。

第一步,确认 API 可达。registry 有个/v2/健康检查端点,返回 200 说明 HTTP 层通了:

curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.10:5000/v2/

返回200才算过。返回401说明开了认证但没带凭据,返回404多半是端口或路径不对。

第二步,走一遍完整的 push/pull 闭环。光能访问 API 不代表存储可写,必须实际推一个镜像进去再拉出来:

# 给本地镜像打上 registry 标签 docker tag registry:2 192.168.1.10:5000/test/registry:verify # 推送 docker push 192.168.1.10:5000/test/registry:verify # 删掉本地标签,强制从 registry 拉 docker rmi 192.168.1.10:5000/test/registry:verify docker pull 192.168.1.10:5000/test/registry:verify

逻辑说明:docker tag的格式是registry地址/命名空间/镜像名:标签,地址部分必须和insecure-registries里配的完全一致,差一个字符都推不上去。docker rmi删本地是为了确保pull真的走网络而不是命中本地缓存——这一步很多人省掉,结果验证的是本地镜像不是 registry。pull成功且镜像 ID 和推送前一致,闭环才算通。

第三步,确认数据落盘。push 完去宿主机上看存储目录有没有新数据:

# 查看存储目录大小变化 du -sh /data/registry-restore/docker/registry/v2/ # 通过 catalog API 列出仓库里的镜像 curl -s http://192.168.1.10:5000/v2/_catalog

du有增长说明数据真写进去了,不是内存里的假象。_catalog返回的 JSON 里应该能看到test/registry,这是 registry 自带的仓库列表接口,比翻目录直观。两个都对上,这套离线 registry 才算真正交付。

三步里我最看重第二步,因为前两步都可能“看起来正常”但实际存储是坏的——API 通、目录有数据,但 push 进去的层读不出来,这种半坏状态只有完整闭环能暴露。从那以后我每次交付离线 registry,不管多赶时间,push/pull 闭环都强制走一遍,宁可多花五分钟,也不想第二天被人追着问“镜像拉不下来”。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进