docker hub推送问题
问题开始
我自己的项目有一套自动构建+推送到 dockerHub 的工作流。
现在我将原本的项目前后端拆分为单独的仓库,发现工作流运行出现了问题
这是我原本的工作流
1 | name: Publish frontend Docker image |
大概的步骤是:
先检查版本号,比如v1.0.0这样的三段式,v*.*.*的格式(Semantic Versioning,MAJOR.MINOR.PATCH语义化版本号),不符合格式的规范成这个格式
然后开始构建前端。先通过 docker/setup-buildx-action@v3 安装docker cli,然后使用docker/login-action@v3通过 github secret中的信息登录 docker hub。最后通过 docker/build-push-action@v6 构建并push镜像。
但是测试运行的时候,发现推送时报错
1 | ERROR: failed to build: failed to solve: failed to fetch oauth token: unexpected status from GET request to https://auth.docker.io/token?scope=repository%3A***%2Fvideobackup-frontend%3Apull%2Cpush&service=registry.docker.io: 401 Unauthorized: access token has insufficient scopes |
排查
一般来说这个的意思是 PAT 的权限不足以 push 上 dockerhub 。但是我很明确是授予了 read&write 权限的
而且更诡异的是,这一个发布的工作流是从另一个已经正常跑了 5 个月的仓库直接扒下来的,理论上配置什么的都相同,不应该出问题才对。
我的第一反应是 github secret 填错了,因为填入之后就看不到填入的内容,于是我尝试各种替换 github secrets 测试,无论怎样都是 401 错误。
于是我开始尝试根据流程排查。首先可以确认的是docker/login-action@v3是没有问题的,因为该步骤提示Login Succeeded!,说明REGISTRY_USERNAME和REGISTRY_PASSWORD是可以正常登录的,但是最后仍然提示没有授权。
已知:
提示令牌权限不足
确认令牌生成时给足了权限
确认填入的是我的令牌
那么可能的原因只剩下了
docker官方权限管理出问题了
工作流没有使用我的PAT
docker hub官方我没办法,且社区没有看到有明确的帖子讨论这件事情,因此不太可能是官方的问题。那就只能往工作流中没有使用我的 PAT 上面靠。
于是尝试让工作流直接将最终的config.json打印在控制台,发现了问题:config.json中存在两个密钥
1 | { |
按道理来说只应该存在registry-1.docker.io才对,多出了一个https://index.docker.io/v1/。
为此单独开了一个仓库测试:
在仓库传入registry host为registry-1.docker.io时,登录前和登录后的config.json分别为
1 | { |
1 | { |
在 registry host为docker.io时,登录前和登录后的config.json分别为
1 | { |
1 | { |
如实验结果所示,在登录工作流运行之前,Action Runner容器中就已经存在了一份config.json文件,并且config.json文件中存在一个PAT。查找资料后,感觉可能是因为这个的原因

这里有一个小插曲,在 Github Action 日志中,会自动隐藏掉 Github Secret 的值。我之前猜想这个隐藏是通过监控变量传值实现的。
但实际上,在日志中输出已经写入到文件中的 PAT 的值的时候,发现仍然不能输出,会被 github 拦截掉,才知道,最高等的食材往往采用最朴素的烹饪方式——直接从输出内容中匹配到相同的字符串,然后隐藏。
现在已经知道了存在一个多余的PAT,但是没有解释一件事情:在测试的时候,为什么当registry host为docker.io的时候,登录写入的 K 是https://index.docker.io/v1/,并且当registry host为registry-1.docker.io的时候推送存在问题
借助 GPT 对整体流程的相关代码进行扫描:
查阅了三个action脚本的代码没有发现明显问题
这里docker/login-action@v3对于 register host 的处理只有一个当不存在时自动设置为docker.io,无伤大雅
1 | export function getAuthList(inputs: Inputs): Array<Auth> { |
那么怀疑会不会是 docker 本身的问题。在本机 docker 环境完成复现后,我开始借助 gpt 对 docker 官方的代码库进行扫描,着重处理 docker 官方域名的处理逻辑:
docker cli
cli/command/registry/login.go line 161
1 | if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace { |
此处:
1 | registry.DefaultNamespace == docker.io |
在docker login流程中,传入的serverAddress不为空且不为docker.io时,设置为传入值,否则设置为默认https://index.docker.io/v1/
cli/config/configfile/file.go line 44
1 | func getAuthConfigKey(domainName string) string { |
而authConfigKey = "``https://index.docker.io/v1/``", 也就是说,当DomainName为docker.io的时候,会直接返回https://index.docker.io/v1/。
这两串代码一起,会导致docker login docker.io时在本地config.json文件写入的domian为https://index.docker.io/v1/而不是docker.io
docker buildKit
util/resolver/resolver.go line 146
1 | if host == "docker.io" { |
docker buildx
util/dockerutil/dockerconfig/configprovider.go line 122~125
1 | hostKey := host |
其中
1 | DockerHubConfigfileKey = "https://index.docker.io/v1/" |
在 docker 中, buildx用于管理BuildKit的各种控制器,此处代码用于读取本地 config.json 以为 BuildKit 提供仓库认证信息
此处会检查hostKey是否为registry-1.docker.io,如果是则将其替换为https://index.docker.io/v1/
container
在 container 中检索到代码
core/remotes/docker/config/hosts.go line 102
1 | if host == "docker.io" { |
在传入是docker.io时,container会将其映射回registry-1.docker.io
core/remotes/docker/registry.go line 200
1 | else if host == "docker.io" { |
串联
也就是说,在docker login阶段,如果:
传入的
register host为docker.io,那么会被规范化为https://index.docker.io/v1/并保存传入的
register host为registry-1.docker.io,不会做规范处理直接以registry-1.docker.io原样保存
在docker/build-push-action@v6阶段,如果:
传入的
register host为docker.io,会被规范化为https://index.docker.io/v1/并尝试从config.json获取传入的
register host为registry-1.docker.io,也会被规范化为https://index.docker.io/v1/并尝试从config.json获取
这样,
当传入
register host为docker.io时,写入和读取的都是https://index.docker.io/v1/,即使原本github runner中存在,也会被覆盖传入的
register host为registry-1.docker.io时,写入registry-1.docker.io,读取https://index.docker.io/v1/,本应该读取失败。但是因为github runner自带一个用于加速拉取的PAT,会读取成功,但是该PAT不具备访问个人仓库权限
后记
想要把上面的改改提个 pr 上去,看 cli 调用流程的时候又发现个小问题¯\(°_o)/¯
在docker login的时候,docker cli会将docker.io解析为index.docker.io并且写入config.json。但是docker logout的时候没有解析流程
也就是说,
登陆时:
docker login docker.io成功,写入https://index.docker.io/v1/登出时:
docker logout docker.io成功(实际失败),尝试删除docker.io,但不存在

该问题已提交pr,会被合并吗?
后记的后记
又双发现个问题,域名前方可以带有scheme,也就是http://和https://。在docker login中确实对其进行了处理,但是docker login的时候判断域名是否是docker.io是在 URL 处理之前的,也就是说:
处理官方域名兼容---->处理传入域名scheme
所以如果docker login ``https://docker.io最终会绕过解析化流程最终成功在config.json中写入docker.io......
1 | func runLogin(ctx context.Context, dockerCLI command.Cli, opts loginOptions) error { |
在login.go文件中会调用register.go文件的authConfig, err := command.GetDefaultAuthConfig()方法,该方法会在register.go文件中进行scheme前缀的处理,但这一个流程是在将docker.io解析化为index.docker.io流程之后,因此如果输入的是https://docker.io的话,会跳过前方的解析流程,在后方的register.go文件中才被scheme前缀处理,最终落库到config.json中的键就是docker.io而不是之前的index.docker.io
于是:
docker login ``https://docker.io,写入config.json为docker.iobuildKit推送失败,因为镜像引用不支持scheme,因此无论如何都不会读取docker.io
buildx 不会接受外部或者环境变量传入的 host,而是通过镜像引用解析出来的,比如镜像引用为ghcr.io/yin2hao/test:latest,buildx就会自动解析registry为ghcr.io。
但是,如果镜像引用没有指定的话,就会默认是docker.io,也就是会被解析为index.docker.io。在尝试获取凭证的时候,就会发现:config.json中只有docker.io的key没有index.docker.io的key,因此推送就会失败