前阵子在调整 CI 流水线的时候发现只要执行到docker pull openjdk:8-jre这一行就会直接 fail报错信息是Error response from daemon: manifest for openjdk:8-jre not found: manifest unknown。更让我纳闷的是上个月同样的命令还好好的怎么突然就 not found 了后来排查了半天发现这里面的水还挺深官方 OpenJDK 镜像的标签策略、架构匹配、许可变更全都搅在一起。这篇文章就完整还原一下我当时的排查链路以及最后采用的替换方案给还在跟openjdk:8-jre较劲的同学一个参考。1. 拿到报错后先冷静做这三步检查很多人看到manifest not found第一反应就是网络问题或镜像仓库挂了其实大部分时候不是。我建议按下面的顺序做一遍快速体检能省很多时间。1.1 先确认 Docker Hub 连通性这一步非常简单但往往被忽略。直接在命令行里执行curl -I https://registry-1.docker.io/v2/如果返回HTTP/2 200说明你的机器到 Docker Hub 的路径是通的。如果出现TLS handshake timeout、Could not resolve host之类的错误那问题大概率出在 Docker daemon 的网络配置上。我遇到过一种情况公司内网的 Docker daemon 没配HTTP_PROXY导致每次拉取大镜像时都会卡在中间层小镜像偶尔能成功大镜像必挂。这种时候就算换什么镜像标签都没用先解决代理# 在 /etc/docker/daemon.json 里配置代理 { proxies: { http-proxy: http://proxy.example.internal:8080, https-proxy: http://proxy.example.internal:8080 } }然后重启 Docker。注意这里的代理配置跟容器的--env不是一回事它是给 Docker daemon 本身用的。如果你是在 rootless 模式下跑的 Docker配置路径会略有不同但原理一致。另外很多云厂商的机器会配置/etc/docker/daemon.json的registry-mirrors如果你用了某个第三方加速器也可能会因为加速器没有正确同步 manifest list造成奇怪的 not found。我建议用curl直连验证后再决定要不要怀疑加速器。1.2 区分三种典型报错同样是拉取失败报错信息五花八门但含义完全不同。先把三张常见的“脸”认清楚报错信息含义manifest for openjdk:8-jre not found: manifest unknown仓库存在但这个标签在 manifest 列表里不存在pull access denied for openjdk, repository does not exist or may require docker login仓库不存在或者没有拉取权限no matching manifest for linux/arm64/v8 in the manifest list entries镜像存在但不支持当前 CPU 架构我那个报错就是第一类仓库openjdk在 Docker Hub 上存在但8-jre这个标签在清单里查不到。这听起来像是最容易理解的一种但背后的原因最复杂。第三类在新买的 Apple Silicon 笔记本上很常见尤其是想直接拉旧镜像的时候。第二类通常不会发生在openjdk这种公共仓库上除非你输错了名字比如openjdk8或者不小心拉了一个私有仓库的地址。1.3 检查本地缓存和磁盘空间还有一种看起来很像是“镜像不存在”的情况其实是本地环境在捣乱。先用docker image inspect openjdk:8-jre看看本地是不是已经有一个同名但损坏的镜像。我有一次在 CI 机器上反复拉取失败最后发现是之前某个中断的docker pull留下了一个不完整的 manifest 缓存导致 Docker 每次都要去跟本地缓存比对最后校验失败。这属于比较偏门的情况但确实存在。再检查磁盘。Docker 的镜像层下载是流式的如果/var/lib/docker所在的磁盘已经满了虽然不会立刻爆出 no space left on device但也可能出现一些看起来很奇怪的 manifest 错误。建议执行docker system df df -h /var/lib/docker如果docker system df显示有大量悬空镜像跑一下docker image prune清理掉。磁盘空间清理完很多“灵异现象”会直接消失。2. 真相openjdk 官方镜像的标签机制正在慢慢消失当你确认网络、缓存、磁盘都没问题但openjdk:8-jre依旧拉不下来那就得从标签本身找原因了。2.1 官方仓库的发布历史与许可政策的拉扯早期 Docker 官方维护了一个叫openjdk的镜像仓库里面按 Java 大版本提供了jre和jdk两类标签比如openjdk:8-jre、openjdk:8-jdk。在 Docker 镜像生态早期这几乎是 Java 后端服务的基础镜像标配。但后来 Java 8 的许可政策发生了变化Oracle JDK 的商业授权开始收紧Docker 官方仓库不能再像以前那样随意分发某些构建版本。与此同时OpenJDK 社区本身也不再统一发布官方二进制包而是交给各发行版、各厂商自己构建。于是openjdk这个仓库的更新节奏就越来越慢最后基本停更了。更关键的是Docker Hub 偶尔会清理长期不更新的旧清单manifest。如果某个浮动标签已经长时间没有对应的镜像层被推送到索引里它就有可能在清理中被移除。openjdk:8-jre这种浮动标签恰恰是容易被清理的对象。2.2 为什么openjdk:8u232-jre可能还能拉而8-jre拉不到这里有个很容易混淆的点浮动标签 vs 具体版本标签。openjdk:8-jre是一个浮动标签它指向某个具体的 build但标签本身可以被重新指向、覆盖甚至删除。openjdk:8u232-jre这类带具体版本号的标签一旦推送通常不会再变。如果 Docker Hub 在做清理的时候只移除了浮动标签保留了具体版本标签那么你执行docker pull openjdk:8u232-jre是有可能成功的。如果这个具体版本标签也拉不到那就说明整个旧的openjdk:8系列清单都已经被清理得差不多了再纠缠下去没有意义。我当时试了一下8u232-jre确实能拉下来。但我不推荐你们在 CI 里用这种方式续命因为它无法保证 CVE 修复安全扫描会很难看它仍然是“不受官方持续维护”的旧镜像随时可能再次消失它和你原来的8-jre可能版本不一行为未必一致。所以这个验证只是为了帮助你确认问题原因不是让你把它当解决方案。2.3 不要顺手把openjdk:8或openjdk:8-jdk拿来顶替有人看到8-jre拉不下来就想当然地把基础镜像改成openjdk:8或者openjdk:8-jdk觉得反正是 Java 8 镜像差不多。这个做法看起来偷懒实际上坑很大。openjdk:8默认指向的是 JDK 镜像不是 JRE。它的体积通常比 JRE 镜像大不少而且里面包含了javac、jdb、javap等编译和调试工具。如果你的运行时镜像只是为了跑一个 Spring Boot 应用或 Java 进程这些工具不仅没有必要还会成为安全扫描器的重点标记对象。很多企业安全策略里明确不允许生产镜像包含 JDK 工具链。换个角度说你就算硬把openjdk:8拉下来了它也是和8-jre一样停更的旧镜像过不了多久又会踩到同样的问题。所以顶替方案治标不治本正确的做法是换一个持续维护的 OpenJDK 发行版镜像。3. 用工具让“标签是否存在”这件事无处遁形在决定替换方案之前我建议用工具把问题定位得再清楚一点避免以后遇到类似问题还是一头雾水。3.1docker manifest inspect是一个被低估的排查利器Docker 19.03 之后提供了docker manifest inspect可以直接查询远程仓库的 manifest而不需要真正拉取镜像。用法很简单docker manifest inspect openjdk:8-jre如果标签不存在会输出类似no such manifest: openjdk:8-jre如果标签存在但不支持当前架构则会输出一个 JSON 数组里面列出了所有可用的 platform。比如{ schemaVersion: 2, mediaType: application/vnd.docker.distribution.manifest.list.v2json, manifests: [ { digest: sha256:..., platform: { architecture: amd64, os: linux } } ] }如果列表里的 architecture 只有amd64而你的机器是arm64那就会报no matching manifest for linux/arm64/v8。这个工具在排查多架构问题时特别管用我后来在写基础镜像检查脚本的时候也直接调用了这个命令。3.2 用 Registry API 手动查询彻底摆脱 Docker CLI 的“翻译”如果你的 Docker 版本太老或者docker manifest命令被某种安全策略禁用了可以直接用 Registry HTTP API 查询。Docker Hub 的认证流程需要先拿 token再请求 manifest。完整命令如下TOKEN$(curl -s https://auth.docker.io/token?serviceregistry.docker.ioscoperepository:library/openjdk:pull | jq -r .token) curl -s \ -H Accept: application/vnd.docker.distribution.manifest.list.v2json \ -H Authorization: Bearer $TOKEN \ https://registry-1.docker.io/v2/library/openjdk/manifests/8-jre如果返回 404标签不存在如果返回 JSON你就可以看到完整的 platform 列表。这个方法比docker manifest inspect更底层输出的是原始数据不会被 CLI 额外包装。尤其适合写自动化脚本做标签可用性巡检。3.3 架构不匹配时的正确姿势假设你查到openjdk:8u232-jre存在但它只有linux/amd64的 manifest而你的本机是 Apple Silicon那你可以用--platform强制拉取 x86 镜像docker pull --platform linux/amd64 openjdk:8u232-jre拉下来之后Docker Desktop 会通过自带模拟层运行这个 amd64 容器。但注意这只是“能在本地跑起来”的权宜之计并不代表生产环境也适合这么做。如果你有一批 ARM 节点最好还是直接找支持linux/arm64/v8的镜像不要让所有东西都靠模拟跑。我在 CI 上踩过的另一个坑是docker compose 在构建镜像时不会自动使用你手动--platform拉下来的镜像还得在docker-compose.yml里面加platform: linux/amd64或者设置环境变量DOCKER_DEFAULT_PLATFORMlinux/amd64。这些细节不处理好本地能跑CI 照挂。4. 推荐的替代方案Temurin 与 Zulu 等持续维护的 JRE 镜像排查到这一步结论已经很明显了不要再跟openjdk:8-jre死磕换一个持续维护的发行版镜像才是正路。目前业界用得最多的几个替代品里我首选 Eclipse Temurin。4.1 为什么首选 Eclipse TemurinEclipse Temurin 是 Adoptium 社区主导的开源 OpenJDK 发行版前身是 AdoptOpenJDK。它的 Docker Hub 仓库地址是eclipse-temurin标签体系非常清晰8-jre、8-jre-alpine、11-jre等都有覆盖并且同时提供amd64和arm64架构还持续发布安全更新。和官方openjdk仓库对比eclipse-temurin的生命力明显强得多。很多主流的基础镜像、CI 镜像、甚至云厂商的托管服务都已经默认转向了 Temurin。如果你只有一个迁移名额先换它会是对性价比最高的。4.2 Dockerfile 迁移示例原来的 Dockerfile 可能是这样的FROM openjdk:8-jre改起来非常简单FROM eclipse-temurin:8-jre就这么一行基础镜像就换了。如果你的环境对镜像体积特别敏感可以用 Alpine 变体FROM eclipse-temurin:8-jre-alpine但要提醒一点Alpine 变体用的是 musl libc某些依赖了 glibc 特性的 Java 应用可能运行时会报奇怪的问题。如果你的项目没有特殊要求我更推荐先用标准的 Ubuntu 变体跑通再考虑是否要换 Alpine。另外你原来在openjdk:8-jre里执行apt-get install tzdata和fontconfig的那些命令在 Temurin 里仍然适用因为它们基于的都是 Debian/Ubuntu 体系。迁移时把时区和字体补上避免 Java 进程打印日志时区不对或者在生成验证码、图表时缺字体。FROM eclipse-temurin:8-jre RUN apt-get update \ apt-get install -y tzdata fontconfig \ rm -rf /var/lib/apt/lists/*4.3 为什么也可以考虑 Amazon Corretto / Azul Zulu如果你的团队已经在用某个云厂商的生态系统或者对上游厂商有指定要求那么也可以看下 Amazon Corretto 和 Azul Zulu。Amazon Corretto 提供了免费的长期支持镜像仓库为amazoncorretto标签有8、8-alpine等。需要注意它的8默认是一个完整的 JDK没有单独的jre变体镜像体积会偏大。如果你的公司安全策略严格要求“运行时镜像不能包含编译工具”Corretto 可能不是最优解。Azul Zulu 的仓库为azul/zulu-openjdk它有非常多的变体包括8-jre、8-jre-alpine架构支持也比较全。同样可以作为备选。我个人的经验是除非有明确的外部约束否则优先选 Temurin因为它的社区活跃度和文档完整度目前最均衡。下面这张表可以帮你快速做选型镜像标签示例支持架构备注eclipse-temurin:8-jre8-jreamd64 / arm64推荐持续更新eclipse-temurin:8-jre-alpine8-jre-alpineamd64 / arm64小体积需测试兼容性azul/zulu-openjdk:8-jre8-jreamd64 / arm64变体多灵活amazoncorretto:88amd64 / arm64默认是 JDK体积偏大openjdk:8-jre已失效不完整别用了4.4 迁移后一定要验证这几个点换完镜像不是改一行 Dockerfile 就完事至少要做一遍基础验证docker run --rm eclipse-temurin:8-jre java -version看到类似下面的输出才算成功openjdk version 1.8.0_XXX OpenJDK Runtime Environment (Temurin)(build 1.8.0_XXX-...) OpenJDK 64-Bit Server VM (Temurin)(build 1.8.0_XXX-..., mixed mode)然后把你项目的启动命令跑起来确认进程能正常拉起检查JAVA_HOME、classpath、字体、时区这些容易出幺蛾子的地方。我当时迁移完还跑了完整的 smoke test才敢把基础镜像切到 CI 上。5. 让 Dockerfile 不再踩“浮动标签”坑的实战建议问题解决了但如果只是把openjdk:8-jre改成eclipse-temurin:8-jre你仍然在踩“浮动标签”的坑只是时间点往后挪了一些。想要一劳永逸还得按下面的方式加固。5.1 将镜像锁定到摘要而不是只写标签浮动标签最大的问题在于“可变动”。即便eclipse-temurin:8-jre现在没问题也无法保证几个月后不会被重新指向新的 JRE build。如果你希望构建完全可复现应该把镜像锁定到 digest。获取 digest 的方式很简单docker pull eclipse-temurin:8-jre docker inspect eclipse-temurin:8-jre --format {{index .RepoDigests 0}}输出会类似eclipse-temurinsha256:6a2d39a1c83d08d8d1e2b8f43abcdd3f9e9a7e64f2e2b0a92a6f0a5e9aa8c3f1然后在 Dockerfile 中用 digest 锁定FROM eclipse-temurinsha256:6a2d39a1c83d08d8d1e2b8f43abcdd3f9e9a7e64f2e2b0a92a6f0a5e9aa8c3f1这样即使远端标签被删除、被覆盖只要你本地 cache 还在或者你构建时能访问到同一个 digest结果就是确定的。当然摘要锁定也有代价安全补丁更新后digest 会变你必须手动或通过自动化工具更新。很多团队会用 Dependabot 或 Renovate 来扫描 Dockerfile一旦上游基础镜像更新就自动提交 PR。这个模式比“写死浮动标签”靠谱得多。5.2 CI 里的拉取重试与缓存策略即使换到了 Temurin网络抖动依然是拉取失败的高频原因。在 CI 脚本里加一个简单的重试逻辑成本很低收益却很大for i in 1 2 3; do docker pull eclipse-temurin:8-jre break echo pull failed, retry $i sleep 5 done此外把基础镜像预推到你们内网的私有仓库可以大幅降低 CI 对 Docker Hub 的依赖。很多 CI 平台支持自定义缓存目录你可以把~/.docker或/var/lib/docker作为缓存挂载这样每次构建不用重新拉全部层速度会快很多。5.3 离线或内网环境怎么同步镜像如果你的构建环境物理隔离比如是纯内网的 Jenkins 集群那么可以先在一台能访问外网的机器上用 skopeo 把镜像同步到内网 registryskopeo copy --all \ docker://docker.io/eclipse-temurin:8-jre \ docker://registry.internal/eclipse-temurin:8-jre注意这里的--all参数会保留镜像的多架构 manifest这样内网节点拉取时可以自动匹配自己的架构。如果你要指定某个架构可以改用--platform linux/amd64。skopeo 还有一个好处是可以直接 copy 到 OCI 目录方便离线导入。同步完成之后Dockerfile 里只需要把基础镜像地址指向内网 registryFROM registry.internal/eclipse-temurin:8-jre这样既绕开了外网波动又能让镜像版本管控完全落位。换掉之后我之前那套流水线再也没有因为基础镜像拉不下来挂过。后来我把手头几个项目的 Dockerfile 都统一换成了eclipse-temurin:8-jre顺手把版本用 digest 锁定安全扫描的告警也少了一堆。如果你现在还写死openjdk:8-jre建议尽早改掉别等下周流水线突然红了再手忙脚乱。