最近有个项目要发布内部演示镜像,我原来的习惯是直接推到 Docker Hub,但试过几次发现匿名拉取有层数限制,另一个问题是仓库一多,镜像和代码放两处,版本对应关系全靠自己记。后来索性把镜像托管搬到 GitHub Container Registry,也就是大家常说的 GHCR,统一用 GitHub 账号做权限,镜像和源码天然放在一起。这篇文章就把完整的使用流程整理出来:从为什么要迁、怎么配 token、镜像怎么打标签推送,到日常拉取、自动化集成和实际操作里最容易翻车的地方,都会按顺序讲清楚。如果你正在用 GitHub 管理代码,又不想同时维护两套镜像仓库权限体系,这篇能直接帮你省掉不少试错时间。
1. 为什么我把镜像托管从 Docker Hub 挪到了 GHCR
先说结论:GHCR 不是要完全替代 Docker Hub,而是对于"代码已经放在 GitHub 上"的项目,它更顺手。我把几个核心优势拆开讲,方便你判断自己用不用得上。
1.1 从"代码和镜像分家"到"一条链路管到底"
以前我用 Docker Hub 的典型流程是这样:代码推 GitHub,CI 构建镜像,然后 docker login 到 Docker Hub,再把镜像推到另一个平台的仓库。听起来不复杂,但实际维护时有两个痛点。第一,Docker Hub 的仓库权限和 GitHub 的组织成员权限是两套体系,新同事入职要分别配两个平台的访问权限;第二,镜像仓库里看不到这个镜像对应的源码版本,时间一长,镜像是从哪个 commit 构建出来的,只能靠 tag 命名去猜。
GHCR 的核心思路是把镜像注册表直接挂在 GitHub 账号体系下面,包的所有者和你的 GitHub 用户名或者组织名一一对应。权限模型基于仓库角色,读代码的权限和拉镜像的权限可以统一分配。比如某公司内部项目设置私有包,只有该组织的成员才能拉取,省掉一套独立的权限配置。
1.2 免费额度、拉取限制和可见性对比
GHCR 对公共镜像完全免费,存储和流量不额外计费,这对开源项目来说非常有吸引力。私有镜像会消耗账号的存储/流量配额,但个人账号和免费组织在合理使用范围内,通常够用。相比之下 Docker Hub 免费账号对匿名拉取有每小时按 IP 计算的层数上限,频繁构建或部署时容易踩到 rate limit。
还有一个很多人忽略的点:GHCR 的镜像可以直接在 GitHub 的 Release 页、README 或者 Issue 里引用,打开仓库页面就能看到关联的包版本。Docker Hub 则要切出去单独查,两个平台来回跳的感觉其实很割裂。对我这种长期泡在 GitHub 上的人来说,少一个平台就少一层维护成本。
1.3 GHCR 能存什么、不能存什么
GHCR 最开始主要支持 Docker 镜像和 OCI 制品,后来也支持 npm、Maven 等软件包格式,但最常用、链路最成熟的一定是容器镜像。它能很好地支持多架构镜像,比如 linux/amd64 和 linux/arm64 可以一起推送,这正好满足了现在笔记本上 Apple Silicon 和服务器 x86 并存的需求。
不能存什么也需要知道:GHCR 不是对象存储,不要试图把大体积二进制文件塞进去;它对单个镜像层大小虽然没有很苛刻的限制,但本质上还是为了分发容器镜像设计的,场景不合适硬用反而难受。另外它不是代码仓库,不能拿来做备份,只适合存"构建产物"。
2. 推送前必须搞清楚的认证体系:PAT 到底该怎么配
GHCR 的认证和 GitHub API 的认证走的是同一套体系:你在 GitHub 上设置一个 Personal Access Token(PAT),然后用它作为 docker login 的密码。这一步卡住的人特别多,很多报错其实不是因为命令写错,而是 token 的权限没勾够。
2.1 为什么需要 token 而不是用账号密码
GitHub 出于安全考虑,不允许用账号密码直接登录任何第三方工具。所以 GHCR 的登录命令表现为:
echo "你的_PAT" | docker login ghcr.io -u 你的GitHub用户名 --password-stdin这条命令把 PAT 通过标准输入传给 docker,而不是直接打在命令行里,避免 token 出现在 shell 历史记录中。注意用户名必须和 token 属于同一个 GitHub 账号,不能拿 A 用户名的 token 配 B 用户名的登录名。
2.2 经典 token 和细粒度 token 的作用域选择
在 GitHub 的 Developer settings -> Personal access tokens 里面,你可以创建两种 token:
- Tokens (classic):传统 PAT,按照 scope 控制权限
- Fine-grained tokens:细粒度 token,对资源级别和有效期控制得更细致
如果我主要是自己用,推荐直接选 classic token,勾选write:packages和read:packages即可。write:packages包含了推送和读取的能力,read:packages可以让你拉私有的包。如果涉及删除镜像版本或者在界面上管理包,需要额外勾delete:packages。
细粒度 token 更安全的是它可以把权限限制在指定的几个仓库而不是整个账号,但对新用户来说配置路径更长。你要在 Resource owner 里选择自己的账号或组织,然后给所有需要的仓库或指定仓库授予 Packages 的 Read/Write 权限。如果漏选了仓库范围,推送时会出现看似正确却被拒绝访问的情况。
我实际操作中给一个建议:token 只赋予需要的权限,不要图省事直接给 full scope。一旦泄露,影响范围会非常小。
2.3 组织场景下额外的一步开关
如果你要推送到组织命名空间下面,比如ghcr.io/某组织/镜像名,光有组织成员的 PAT 还不够,需要组织管理员先在组织设置里开启 Packages 权限,否则会看到类似“package creation failed”或 403 错误。个人账号默认可以直接建包,组织账号要去 Settings -> Packages 确认允许成员创建包。
这一步经常被忽略,特别是多人协作的场景,A 能推而 B 不能推,排查到最后发现是组织设置的问题。
3. 镜像命名和打标签:GHCR 最容易翻车的环节
很多人在完成了 token 配置之后,第一次推镜像依然失败,原因集中在命名空间规则上。GHCR 对镜像全名的格式要求非常严格,不符合要求直接拒绝。
3.1 命名空间规则:owner 必须和 GitHub 账号完全匹配
GHCR 的镜像完整名称是:
ghcr.io/OWNER/IMAGE_NAME:TAG这里的 OWNER 必须是你的 GitHub 用户名或组织名,不能随便起。比如你的 GitHub 用户名是 alice,那镜像就应该长成ghcr.io/alice/myapp:v1。如果你想把镜像放到某个组织下,比如ghcr.io/bigorg/backend/api:latest,那这个bigorg必须是你有权限推送的 GitHub 组织名,并且你已经按照上一节的步骤配置好了组织权限。
镜像名部分,官方要求全小写,只能包含字母、数字、短横线、下划线或点。很多人习惯用大写字母给镜像命名,但在 GHCR 这里会被拦下来。我习惯用开头短横线分隔的命名方式,比如backend-service、>docker tag demo-app:t1 ghcr.io/alice/demo-app:t1 docker push ghcr.io/alice/demo-app:t1
不要试图直接把 docker build 出来的本地镜像名改成 ghcr.io 开头的名字,虽然 docker tag 本质就是复制一个带新标签的引用,但更常见的做法是构建时直接指定完整名称:
docker build -t ghcr.io/alice/demo-app:t1 .这样构建出来的镜像就已经带上了目标仓库的 tag,省掉一次 tag 操作。不过如果你当前在服务器上已经有一个镜像,最稳妥的方式还是先 tag 再 push,tag 操作不会影响原有镜像。
3.3 镜像名和 GitHub 仓库名是否必须一致
很多人会纠结一个问题:是不是 GitHub 仓库叫demo-repo,镜像名也必须叫demo-repo?实际上并不强制。GHCR 的包归属根据推送者的权限决定,只要你对该命名空间有写权限,镜像名可以和仓库名不一致。但从可维护性角度,我强烈建议镜像名和仓库名保持对应关系。
比如代码仓库叫alice/demo-repo,镜像最好命名为ghcr.io/alice/demo-repo:v1,这样在 GitHub 的功能页面上能看到包,在仓库页面也能通过命名快速反查来源。如果随意起名,时间长了,你很可能在一个组织下面积累一堆来自不同仓库的镜像,却不知道哪个对应哪个。
4. 首次推送全流程:从构建到验证
到这里,认证配置、命名规则都已经清楚了,下面走一个完整流程。第一遍可以先按最小步骤跑通,再慢慢加入多架构、多标签等高级玩法。
4.1 一整套可复制的命令序列
我习惯按 4 步完成一次推送,每一步都能单独验证结果:
- 构建镜像。为了节省本地验证时间,可以先只构建当前平台的镜像:
docker build -t ghcr.io/alice/demo-repo:v1 . --no-cache--no-cache只在第一次用,确定构建无误后建议去掉,会快很多。
- 登录 GHCR:
echo "你的_PAT" | docker login ghcr.io -u alice --password-stdin登录成功后命令行会返回Login Succeeded。此时 docker 会把这个信息写入本机的配置文件中,具体路径一般是~/.docker/config.json,里面会记录 ghcr.io 对应的认证信息。
- 推送镜像:
docker push ghcr.io/alice/demo-repo:v1第一次推送时会提示The push refers to repository [ghcr.io/alice/demo-repo],然后逐层上传。如果层数多或者网络状态一般,会需要一点时间,这是正常的。
- 验证镜像是否存在。可以直接去 GitHub 页面顶部或者侧边栏找到 Packages,点击进入后能看到刚才推送的镜像版本。也可以先用 docker 拉取验证:
docker pull ghcr.io/alice/demo-repo:v1如果本地有相同镜像,docker 会显示已存在;如果本地没有,则会出现 Pulling 信息并成功拉到。
4.2 推送时报错的分类排查表
推送报错时,先看错误码再动手,能少走很多弯路。我整理了常见情况:
| 错误表现 | 常见原因 | 排查建议 |
|---|---|---|
| 没有权限:authentication required | 未登录 / token 权限不足 | 确认登录成功,检查 token 是否勾选了 write:packages |
| denied: permission_required_to_... | 组织未开启包创建权限 | 组织管理员去 Settings -> Packages 开启成员建包 |
| denied: installation_not_allow_... | 细粒度 token 未选择仓库范围 | 检查 token 的 Repository access 是否包含目标仓库 |
| image name has invalid characters | 镜像名含大写字母或非法字符 | 镜像全名和仓库部分都改成小写 |
| 409 / naming conflict | 镜像名与已有包冲突且归属不同 | 换一个镜像名,或者先在界面删除旧包 |
| unknown manifest / manifest unknown | 要拉取的标签不存在或已被删除 | 去页面确认 tag 名称、镜像名是否一致 |
其中 409 错误比较费解,通常是因为你的镜像名恰好和其他人已经创建过的包重名,而 GHCR 的命名空间存在全局唯一性。这时候改镜像名是最快的解决途径,不建议强行清理别人的包。
4.3 推送完成后别忘了设置包可见性
GHCR 的包默认继承仓库或账号的设置,但有时候创建出来的包是私有的,你自己能看,部署服务器上匿名拉取会被拒绝。所以推送成功之后,进入包设置页面,找到 "Change visibility" 选项:
- 如果你打算把镜像用于开源项目或者允许任何人拉取,选择 Public。
- 如果用于内部服务,保持 Private,但拉取时必须有 PAT 登录。
这里有一个容易踩坑的点:包可见性改变不会自动同步到同一个 repo 下多个镜像,你改了 A 包,B 包仍然是旧状态,所以每次批量推送新镜像后都要检查一遍可见性。
5. 拉取、分享与自动化集成:GHCR 的日常使用姿势
推送只是前半段,真正体现 GHCR 价值的是日常拉取和自动化流水线。这一段我把使用姿势按照不同场景拆开讲。
5.1 服务器上拉取私有镜像和公共镜像
拉取公共镜像很简单,任何一台有 docker 的机器都能直接执行:
docker pull ghcr.io/alice/demo-repo:v1不需要提前登录。拉取私有镜像则必须先进行 docker login,命令和推送时一样:
echo "你的_PAT" | docker login ghcr.io -u alice --password-stdin docker pull ghcr.io/alice/demo-repo:v1注意拉取私有镜像时使用的 token 至少要有read:packages权限。如果你在服务器上同时需要拉取多个私有镜像,用一个具有读取权限的 token 就行,不要拿写权限的 token 放到生产环境上,最小权限原则在服务器场景同样适用。
5.2 在 GitHub Actions 里推送镜像的推荐写法
GitHub Actions 本身和 GHCR 集成得很深,很多时候不用手动创建 PAT,直接使用自动生成的GITHUB_TOKEN就行。前提是工作流文件所在的仓库需要开启读写权限:仓库 Settings -> Actions -> General -> Workflow permissions,选成 Read and write permissions。
一个可以直接参考的工作流片段:
- name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: | ghcr.io/${{ github.repository }}:${{ github.sha }} ghcr.io/${{ github.repository }}:latest${{ github.repository }}会自动展开成类似alice/demo-repo的形式,正好作为镜像的 owner/name 部分,非常省心。加github.sha作为 tag,可以确保每个 commit 都能产生对应镜像,也方便回溯版本。
注意,如果工作的代码仓库本身不能开启 Actions 的写权限,或者你希望 Actions 推送的组织仓库跨出当前仓库权限范围,就需要单独配置 PAT 并存入仓库的 Secrets,再在 login-action 的 password 字段引用。
5.3 多架构镜像的推送方式
现在很多项目的运行环境是混合架构:开发机可能是 ARM,生产服务器是 x86_64。GHCR 天然支持多架构镜像,通过 docker buildx 一次性推送多个平台的 manifest 列表。
推荐的做法是用 BuildKit 的构建器:
docker buildx create --use docker buildx build --platform linux/amd64,linux/arm64 \ -t ghcr.io/alice/demo-repo:v1 \ --push .这样推送后,同一标签下会包含多个平台对应的 manifest,拉取时 docker 会按照当前系统的架构自动拉取对应层,不需要人为区分 tag。第一次使用 buildx 时,如果没有创建构建器,系统会提示你,记得先执行docker buildx create --use。
如果本地构建多架构会比较慢,也可以考虑在 GitHub Actions 里跑多平台构建,runner 上的 buildx 环境更干净,目标平台也会自动切换模拟执行,步骤和本地几乎一样,只是构建时依赖的缓存策略略有不同。
6. 我实际踩过的坑和最终养成的习惯
最后分享一些半年来实际使用中积累的心得。这些内容没有写在官方文档的醒目位置,但遇到一次就能记住很久。
6.1 镜像名全部小写这个坑,比想象中更容易踩
我第一次在项目里随手把服务名定成了BackendAPI,本地构建、本地运行都很正常,推到 GHCR 时直接被拒绝,提示镜像名称含非法字符。后来我把所有镜像名都改成了backend-api这种小写短横线风格。这个规则和 Docker Hub 默认宽容大写的习惯不太一样,从 Docker Hub 迁移过来的人最容易在这里卡住。我的建议是:从一开始就养成全小写命名的习惯,无论发到哪里都不会错。
6.2 不要在每台机器上都放一份长期 token
因为 docker login 会把认证信息写入本机配置,有些同事为了省事,在个人电脑、办公服务器、生产服务器上用同一个长期 PAT 登录。这其实是很大的安全隐患,一旦某台机器被攻破,token 就泄露了,影响范围等于所有机器和所有包。
我的做法是:开发机用自己有完整权限的 PAT,生产部署服务器单独创建一个只有read:packages权限的 token,专门用于拉取。Actions 里能走GITHUB_TOKEN就绝不手动放 PAT,减少 token 在不同系统间流转的频率。
6.3 版本 tag 策略:别只留 latest
很多项目习惯只推 latest 标签,图方便。但 GHCR 的界面会保留所有标签和层的版本记录。如果没有版本 tag,你在页面上看到一堆 latest,根本无法判断哪个对应哪个 commit,回滚时更是无从下手。
我建议在构建时至少打两个 tag:一个具体版本号(如v1.2.3),一个latest。再配合一两个固定环境 tag,比如staging或prod,既满足常规部署需求,又能保留完整的历史记录。这样即使后来镜像是从 CI 自动构建的,人工排查问题也能快速定位到具体版本。
6.4 清理策略:GHCR 不会自动帮你清垃圾
Docker Hub 对免费用户有仓库数量限制,GHCR 虽然宽松,但也不是无限空间。长期频繁构建会导致大量废弃镜像占用存储配额。GHCR 提供了删除包和保留策略的入口。
在包设置页面里,你可以手动删除不再使用的版本。如果你想按数量自动清理,可以选择设置保留策略,比如只保留最近 N 个版本的镜像。这类配置尽量在项目刚起步时就做好,等镜像攒了几百个再清理,操作成本会高很多。我自己的项目设定是:每个镜像名下最多保留 30 个版本,超过之后最老的会被自动清理,省心很多。
6.5 不要忽略网络因素
GHCR 域名对网络环境比较敏感,奇怪的超时或connection reset经常和设备所在网络有关,而不是 GHCR 本身不可用。遇到推送中断,先确认当前机器是否能正常访问ghcr.io域名,再做登录、重推。盲目重试有时候会浪费时间,因为问题可能出在网络链路上。
如果你在公司内网或者比较严格的防火墙后面,迁移到 GHCR 前最好先确认网络策略是否允许访问 registry 的域名和端口。考虑这一点比事后排查报错要轻松得多。
这些习惯看起来都是小事,但它们共同保证了一件事:镜像链路是稳定、可回溯、可清理的。目前我的绝大多数项目镜像都放在了 GHCR,和代码仓库在同一个生态里,权限、协作、回滚都变得顺畅不少。如果你也刚好有这个需求,按上面的流程先跑通一次推送,后面基本就顺了。