
最近帮团队梳理内部镜像的存放方案正好把“把Docker镜像传到GitHub容器仓库GHCR”这条链路完整走了一遍。从创建令牌到构建镜像、打标签、推送、拉取验证再到用Actions自动发布整个过程踩了几个不算深但很烦的坑。这篇文章就把我实测过的完整流程写出来包括每一步为什么要这么做、参数怎么选、出问题了怎么排查尽量做到你照着操作就能跑通。1. 为什么选GHCR容器镜像仓库的选型思考1.1 常见镜像仓库方案对比在决定用GHCR之前先看市面上常用的几个镜像存储方案各有利弊。Docker Hub是默认选择公共镜像拉取方便但个人账号下的私有仓库数量有限而且如果你所在网络环境访问Docker Hub速度不稳定频繁pull/push确实让人头疼。国内云厂商的镜像仓库比如阿里云容器镜像服务、腾讯云TCR在国内访问体验很好还支持命名空间隔离和自动构建但如果你本身没有云资源、或者镜像不想绑定某一个云厂商那这份“便利”就变成了“锁定”。GHCR全称是GitHub Container Registry它和GitHub代码仓库天然集成。好处很直接代码和镜像放在同一个平台权限体系复用GitHub的账号体系不需要额外注册账号也不用单独配置复杂的权限策略。最关键的一点是GHCR对公共镜像完全免费私有镜像的免费额度对个人开发者和小团队来说通常也够用。1.2 GHCR的核心优势首先是权限模型清晰。你可以把镜像设为public公开或private私有公开镜像所有人都能拉取不需要认证私有镜像只有你或者被你加入协作的人能拉取。这和GitHub代码仓库的可见性概念完全一致心智负担很低。其次是和GitHub Actions的联动。代码推送后自动构建镜像并推送到GHCR整个CI/CD链路不需要额外配置外部服务全部在一个平台内完成。很多团队用GHCR就是看中这一点。再者是镜像命名规范。GHCR的镜像名格式是ghcr.io/{owner}/{image-name}:{tag}owner对应GitHub用户名或组织名。这个命名规则和docker.ioDocker Hub官方仓库、quay.io类似切换起来没有额外学习成本。1.3 适用场景和人群我个人的感受是GHCR特别适合这几类人个人开发者想把自己的项目镜像公开分享或者多个设备间同步镜像免费额度够了。中小团队代码本身就放在GitHub私有仓库镜像也放GHCR权限统一管理不用额外维护一套镜像仓库的账号体系。已经使用GitHub Actions的团队自动构建自动发布的链路短省事。如果你的团队大量依赖国内云资源、网络环境特殊或者对镜像仓库有合规审计、跨账号复杂权限的需求那GHCR未必是最合适的选项需要另外权衡。2. 准备工作账号、令牌和Docker环境2.1 前置条件清单在动手之前确保下面几件东西都就绪了一个GitHub账号最好已经启用了双因素认证2FA。没有2FA的话后面创建Personal Access Token时会有额外限制。本机装好了Docker并且Docker守护进程正常运行。我用的是Docker Engine命令行直接执行docker version能同时看到Client和Server信息就说明环境OK。确认你要推送的Docker镜像已经构建好或者至少有一个Dockerfile能构建出镜像。如果你要推送的镜像关联的是某个组织Organization而不是个人账号需要确认你在该组织内有写入权限。这点后面会细说。2.2 创建个人访问令牌PAT的流程GHCR目前推荐使用Personal Access TokenPAT来做认证。实际操作路径是登录GitHub → 点击头像 → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token。创建token时有几个细节容易踩坑我说一下我的选择Expiration建议选90天或者自定义一个较短的有效期不要选“No expiration”。令牌有效期短一点即使泄露了影响也可控。当然代价是过期后要重新生成并更新CI配置。Select scopes权限勾选必须要勾选write:packages权限这个权限包含对容器镜像仓库的读取和写入。如果你要通过Actions自动推送还需要勾选repo权限因为Actions读取代码仓库内容需要它。生成之后会得到一串ghp_开头的字符串只显示这一次记得复制保存。如果丢了只能重新生成。2.3 Docker登录GHCR两种认证方式有了token之后登录GHCR的命令很简单echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin这里的echo $CR_PAT是把token通过管道传给docker login-u后面填你的GitHub用户名。用--password-stdin而不是直接在命令行写密码是为了避免token出现在shell历史记录里。你可以先设置环境变量再执行export CR_PATghp_xxxxx echo $CR_PAT | docker login ghcr.io -u your-github-username --password-stdin输出结果出现Login Succeeded就表示认证成功。还有另一种方式是直接用用户名和token交互式登录但我个人不太建议token容易被历史记录暴露。如果你用了GitHub CLIgh命令也可以用gh auth login完成认证后再通过docker login ghcr.io间接使用凭证不过这要求本地装了gh并且已经登录。3. 镜像构建与本地验证上传前必须做的事3.1 从一个可用的Dockerfile开始很多人在镜像推送失败时才发现本地镜像本身就没构建好。这里用一个名为“模拟项目X”的示例项目来说明。假设这是个简单的Python Web服务Dockerfile大概长这样FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD [python, app.py]构建命令是docker build -t sim-project-x:0.1.0 .构建过程如果网络环境不太好基础镜像拉取可能会慢这是网络环境的问题重试几次通常能过。3.2 构建时的两个重要细节第一不要把敏感文件复制进镜像。比如.env、密钥、配置文件等要么通过环境变量注入要么在.dockerignore里排除掉。镜像一旦push到远程仓库里面如果有密钥等于把密钥公开了。第二优先使用多阶段构建来缩小镜像体积。以Python项目为例FROM python:3.11-slim AS builder WORKDIR /build COPY requirements.txt . RUN pip install --user --no-cache-dir -r requirements.txt FROM python:3.11-slim WORKDIR /app COPY --frombuilder /root/.local /root/.local COPY . . ENV PATH/root/.local/bin:$PATH EXPOSE 8000 CMD [python, app.py]这个写法把依赖安装和运行时环境分开了最终镜像只包含运行必需的文件体积能小非常多。对后面推送和拉取都有直接好处尤其是按流量计费的网络环境镜像小就是省钱。3.3 本地验证推之前先跑起来镜像构建好之后先别急着推送。先确认这个镜像真的能跑。docker run --rm -p 8000:8000 sim-project-x:0.1.0然后另开一个终端访问http://localhost:8000或者执行健康检查接口确认服务正常响应。确认没问题后用CtrlC停止容器。这一步的意义在于推送到GHCR的镜像如果本身有问题拉取下来也没用而且排查问题会隔着远程仓库这一层麻烦得多。本地验证通过推送的就是一个可用的产物。4. 上传GHCR的完整实操Tag、Push与Pull验证4.1 给镜像打标签命名规范是关键本地镜像名是sim-project-x:0.1.0要推到GHCR必须把它重命名成符合GHCR规范的格式。规范格式是ghcr.io/{owner}/{image-name}:{tag}拆开来看ghcr.ioGHCR仓库的固定域名前缀。{owner}你的GitHub用户名或组织名必须是小写。注意如果是用户名中包含大写字母需要全部转成小写。比如用户名叫Alice那这里要写ghcr.io/alice/...。{image-name}镜像名建议全小写可以用连字符连接多个单词。{tag}版本标签可以用latest、0.1.0等尽可能语义化。打标签的命令是docker tag sim-project-x:0.1.0 ghcr.io/your-github-username/sim-project-x:0.1.0这里有个很容易犯的错镜像名不能带下划线。如果你本地镜像叫sim_project_x推送时可能会报错。建议统一用连字符。4.2 推送命令与输出解读打标签完成后推送命令如下docker push ghcr.io/your-github-username/sim-project-x:0.1.0推送过程会显示分层上传进度每一层对应镜像文件系统的一层。如果之前已经推过同名的镜像只会有变化的层被上传没变化的层会显示Layer already exists这是Docker的层缓存机制在起作用。推送成功后登录GitHub进入你的主页左侧菜单会多出一个Packages入口点击进去就能看到刚才推送的镜像包。或者直接访问https://github.com/users/{your-username}/packages查看。4.3 Pull验证与可见性设置推完之后强烈建议做一次pull验证确认镜像从一个干净环境能正常拉取docker pull ghcr.io/your-github-username/sim-project-x:0.1.0如果镜像设置为public任何人都可以直接pull不需要登录。如果是private则需要先用PAT登录GHCR才能pull。设置可见性的路径是进入该镜像包的页面 →右上角Package settings→ 找到Danger Zone区域 → 调整可见性为Public或Private。改成Public时系统会弹窗确认确认后就变成公开镜像了。从我的经验来看公开的图不要携带任何敏感数据这个原则再怎么强调都不过分。代码仓库都会注意别泄露密钥镜像里更不能犯这种错。5. 进阶玩法用GitHub Actions自动构建并推送5.1 为什么建议自动推送手动推送适合临时用、偶尔用。如果你经常更新镜像手动操作会很繁琐而且还容易忘记打新标签。把推送过程交给GitHub Actions代码一推送镜像自动构建、自动打标签、自动推送到GHCR整个过程不需要本地Docker环境。这也能避免“本地构建的镜像和代码不一致”的经典问题——因为Actions是在代码仓库里构建的镜像内容和代码严格对应审计起来也很方便。5.2 一个可用的Workflow示例在项目根目录建一个workflow文件路径是.github/workflows/build-push.yml内容如下name: Build and Push Docker Image on: push: branches: [ main ] tags: [ v* ] jobs: build-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkoutv4 - name: Log in to GHCR uses: docker/login-actionv3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image uses: docker/build-push-actionv5 with: context: . push: true tags: ghcr.io/${{ github.actor }}/sim-project-x:${{ github.ref_name }}这里解释几个关键点permissions区块必须写packages: write否则Actions默认没有权限推送到GHCR。github.actor是触发工作流的用户通常就是仓库所有者github.ref_name在打tag时会取tag名比如v1.0.0这样镜像标签就和Git tag对应上了。这里用的是secrets.GITHUB_TOKEN是Actions运行时自动生成的临时token不需要自己创建PAT。如果出于特殊原因必须用你自己的PAT也可以在仓库Settings → Secrets and variables → Actions里配置一个名为CR_PAT的secret然后在workflow里引用它。5.3 多架构镜像的扩展思路如果你的服务要跑在多种架构上比如amd64和arm64可以用docker/build-push-action配合platforms参数构建多架构镜像列表manifest list。大致的写法是- name: Build and push multi-arch image uses: docker/build-push-actionv5 with: context: . push: true platforms: linux/amd64,linux/arm64 tags: ghcr.io/${{ github.actor }}/sim-project-x:latest需要提醒的是多架构构建要求Dockerfile里的基础镜像本身支持多架构。如果基础镜像只支持单一架构那多架构构建会失败。另外构建时间会明显变长因为要为每个架构分别构建一遍。6. 常见问题与排查技巧实录6.1 认证失败denied/unauthorized推送时如果出现denied: permission_denied或者unauthorized: authentication required通常原因有三个一是没登录或登录的token权限不够。确认你是否执行过docker login ghcr.io并且token是否勾选了write:packages权限。重新生成token并重新登录一般能解决。二是镜像名中的owner和你登录的账号不一致。比如你用自己的账号登录却往ghcr.io/某组织/镜像名推送如果这个组织没有对你授权就会denied。解决方法是把owner改成你的用户名或者让组织管理员给你授权。三是token过期。PAT设置了有效期过期后所有请求都会失败。重新生成token更新本地登录状态或者CI配置。6.2 推送失败名字或标签格式错误如果你看到类似repository name must be lowercase的报错说明镜像名或owner包含了大写字母。GitHub的用户名可能本身带大写但在GHCR的镜像名里必须全小写。处理方式是重命名镜像docker tag 旧的镜像名 ghcr.io/你的用户名小写/镜像名小写:标签 docker push ghcr.io/你的用户名小写/镜像名小写:标签6.3 网络超时或者推送中断推送大镜像时偶尔会遇到连接中断或超时。这种情况我一般这样处理检查网络环境是否稳定可以试着访问一下ghcr.io看是否通。给Docker配置镜像加速或者调整网络参数然后重启Docker服务。实在不行就分多次推。Docker的层缓存机制可以让你在重试时跳过已上传的层。6.4 镜像包页面不显示或者版本列表为空推送完登录GitHub的Packages页面偶尔会发现看不到刚才推送的镜像包。可能的原因页面刷新太慢等一会再刷新。你访问的是组织页面而不是个人页面确认你登录的账号和owner一致。如果你的镜像名之前不存在GHCR的包注册可能需要一点时间才出现在列表中。常见问题速查表问题现象可能原因解决办法denied: permission_denied未登录或token无写权限重新登录检查PAT权限unauthorized: authentication required登录信息失效重新生成token重新登录repository name must be lowercase镜像名或owner含大写打标签时全用小写推送中断/超时网络波动或镜像过大重试或调整Docker网络参数包页面不显示页码缓存或owner不一致刷新页面确认owner匹配独家避坑技巧最后分享几个我实际用下来的个人习惯。第一镜像命名里加上项目名和用途比如sim-project-x-api、sim-project-x-worker这样在GHCR里同时管多个镜像时一眼就能看出谁是谁。第二tag尽量不用latest当唯一版本。latest适合做默认指向但真正的发布版本一定要有具体版本号比如v1.0.0。否则回滚时你找不到历史版本。第三定期清理旧镜像。GHCR的免费额度里过期镜像会占用存储空间。一个项目只留着最新两三个版本其他旧镜像删掉。删除入口在Package settings里的Danger Zone。第四把推送操作写进README。团队协作时把推送命令、命名规范、验证方式都写清楚可以省下大量沟通成本。我在实际项目里就是这么做的新人按README操作基本不用来问我。