跳到主要内容

Registry 与镜像管理

镜像构建出来之后总要有个地方存放和分发,这个地方就是 Registry(镜像仓库)。本篇前半部分讲 Registry 的原理和常见服务(Docker Hub、Harbor、各大云厂商的仓库),后半部分讲日常运维会用到的事:镜像同步、拉取加速、签名、漏洞扫描和过期镜像清理。

Registry 原理​

镜像命名规则​

一个完整的镜像地址长这样:

registry.example.com/命名空间/仓库名:标签

以 registry.cn-hangzhou.aliyuncs.com/myteam/nginx:1.27 为例,拆开看:

部分示例说明
Registry 地址registry.cn-hangzhou.aliyuncs.com仓库服务器的域名,省略时默认是 Docker Hub(docker.io)
命名空间myteam也叫项目名或组织名,用来分组和控权限
仓库名nginx一个镜像仓库,存同一个应用的各个版本
标签1.27区分版本,省略时默认 latest

几个容易搞混的点:

  • nginx:1.27 是 docker.io/library/nginx:1.27 的简写,官方镜像的命名空间固定是 library,可以省略。
  • myuser/nginx:1.27 实际指向 docker.io/myuser/nginx:1.27,不是本地起的名字。
  • 标签只是指针,同一个镜像可以同时打多个标签;镜像真正的身份是 digest(内容摘要),形如 sha256:xxxxx。

push / pull 时分层传输​

镜像是由一层一层的只读层叠起来的(见前面《核心概念与架构》一篇)。Registry 存的就是这些层加一份 manifest(清单,记录镜像由哪些层组成、每层 digest 是什么)。

push 的时候:

  1. 客户端先问 Registry「这些层你有没有」(按 digest 查询)。
  2. 已有的层直接跳过,只上传缺失的层。
  3. 层传完后上传镜像配置和 manifest,打上标签。

pull 的时候反过来:先拿 manifest,再逐层下载本地没有的层,最后组装成镜像。所以两台机器拉同一个镜像,第二次往往快很多——大部分层本地已经有了。

提示

这也是「镜像不要做得太大、公共部分尽量放基础镜像里」的原因之一:层可以跨镜像复用。同一个仓库里十个应用都基于 ubuntu:24.04 构建,这个基础层在 Registry 上只存一份。

Docker Hub​

Docker Hub(hub.docker.com)是 Docker 官方仓库,也是最常用的公共 Registry。

官方仓库与命名空间​

  • 不带前缀的镜像名(nginx、redis、mysql)是 Docker 官方维护的 Official Image,质量有保障,优先用。
  • 带 用户名/ 或 组织名/ 前缀的是个人或组织发布的,用之前看一眼下载量和更新频率。
  • dockercoins 这类 Verified Publisher 徽章表示发布者经过官方认证,比纯匿名账号靠谱。

push 到自己的命名空间:

# 登录,按提示输入用户名和密码(或 Access Token)
docker login

# 打上带自己用户名的标签再推
docker tag myapp:1.0 kuizuo/myapp:1.0
docker push kuizuo/myapp:1.0
注意

Docker Hub 从 2023 年起收紧了安全策略,建议用 Access Token 代替密码登录(网页端 Security 页面生成)。另外免费账号的私有仓库数量有限,私有镜像多的话考虑 Harbor 或云厂商仓库。

拉取限速​

匿名拉取 Docker Hub 镜像是限速的,按 IP 计数,超出后报 429 Too Many Requests。大致的额度:

用户类型拉取额度
匿名用户每 6 小时 100 次
登录的免费账号每 6 小时 200 次
付费账号不限

实际踩坑场景:公司内网几十台机器走同一个出口 IP 拉镜像,很容易就撞到匿名限额。解决办法有三个:

  1. docker login 后再拉,按账号计数而不是按 IP。
  2. 配置镜像加速器,见后文「镜像加速」一节。
  3. 把常用镜像同步到内部 Harbor,一劳永逸。

Harbor​

Harbor 是 CNCF 毕业的开源私有 Registry,企业自建仓库的首选。除了存镜像,还提供项目管理、用户权限、镜像复制、漏洞扫描等一整套功能。

用 docker compose 部署​

官方提供 offline installer(离线安装包),里面打包了所有需要的镜像,机器不能上外网也能装。

# 下载 offline installer(版本号换成当时的最新版)
wget https://github.com/goharbor/harbor/releases/download/v2.11.0/harbor-offline-installer-v2.11.0.tgz

# 解压并进入目录
tar xzvf harbor-offline-installer-v2.11.0.tgz
cd harbor

# 复制配置模板
cp harbor.yml.tmpl harbor.yml

编辑 harbor.yml,至少要改这几项:

hostname: harbor.example.com   # 本机域名或 IP

http:
port: 80

# 管理员初始密码,登录后记得改
harbor_admin_password: Harbor12345

# 数据存放目录
data_volume: /data

然后执行安装脚本(它会生成 compose 文件并启动):

# 不想装扫描器等组件可以加 --with-notary 等参数控制
./install.sh

装完用 docker compose ps 确认各容器状态,浏览器访问 https://harbor.example.com,用 admin 加配置的密码登录。

信息

要启用 HTTPS 需要提前备好证书,在 harbor.yml 里配置 https 段的证书路径。内网自用可以只用 HTTP,但客户端的 Docker 要把这个地址加进 insecure-registries,否则默认拒绝走 HTTP。

核心功能​

  • 项目(Project):镜像的分组单位,对应命名空间。每个项目可设公开或私有,推送地址形如 harbor.example.com/项目名/镜像名:标签。
  • 用户与角色:支持本地用户,也能接 LDAP / OIDC。项目内分项目管理员、开发者、访客等角色,控制谁能推、谁只能拉。
  • 复制(Replication):在两个 Harbor(或 Harbor 与其他 Registry)之间按规则同步镜像,常用于多地部署和灾备。
  • 扫描(Scanner):内置 Trivy 作为默认扫描器,推送后自动或手动扫漏洞,还能配置「高危漏洞镜像禁止拉取」。
  • 保留策略(Retention):按规则自动清理旧镜像,详见最后一节。

日常推送:

docker login harbor.example.com
docker tag myapp:1.0 harbor.example.com/myteam/myapp:1.0
docker push harbor.example.com/myteam/myapp:1.0

GHCR​

GHCR(GitHub Container Registry,ghcr.io)是 GitHub 的镜像仓库,和 GitHub 仓库、Actions 集成得很顺,开源项目用得很多。

登录用的是 GitHub 的 Personal Access Token(PAT),需要勾选 read:packages 和 write:packages 权限:

# 把 PAT 通过管道传给 login,避免出现在 shell 历史里
echo $GITHUB_TOKEN | docker login ghcr.io -u 你的用户名 --password-stdin

镜像地址格式:ghcr.io/用户名或组织名/镜像名:标签。

docker tag myapp:1.0 ghcr.io/kuizuo/myapp:1.0
docker push ghcr.io/kuizuo/myapp:1.0
提示

在 GitHub Actions 里推送时不用自己造 PAT,直接用内置的 GITHUB_TOKEN(secrets.GITHUB_TOKEN)登录即可,记得给 workflow 加上 packages: write 权限。

阿里云 ACR​

阿里云容器镜像服务 ACR 分个人版(免费)和企业版。个人版地址格式:

registry.cn-<地域>.aliyuncs.com/<命名空间>/<仓库名>:<标签>

例如 registry.cn-hangzhou.aliyuncs.com/myteam/myapp:1.0。命名空间和仓库名都要先在控制台创建(仓库也可以推送时自动创建,取决于设置)。

登录密码不是阿里云账号密码,而是在控制台「访问凭证」里单独设置的 Registry 登录密码:

docker login --username=阿里云账号 registry.cn-hangzhou.aliyuncs.com

docker tag myapp:1.0 registry.cn-hangzhou.aliyuncs.com/myteam/myapp:1.0
docker push registry.cn-hangzhou.aliyuncs.com/myteam/myapp:1.0
备注

个人版实例只有部分地域可用,选离自己服务器近的地域。ECS 内网拉取可以把域名换成 registry-vpc.cn-<地域>.aliyuncs.com,走内网不占公网带宽。

腾讯云 TCR​

腾讯云容器镜像服务 TCR 主推企业版实例,每个实例有独立域名。地址格式:

<实例名>.tencentcloudcr.com/<命名空间>/<仓库名>:<标签>

另外还有老的个人版 CCR,地址是 ccr.ccs.tencentyun.com/<命名空间>/<仓库名>:<标签>,新用户建议直接用 TCR。

登录(密码在 TCR 控制台的「访问凭证」里获取):

docker login myinstance.tencentcloudcr.com --username=用户名

docker tag myapp:1.0 myinstance.tencentcloudcr.com/myteam/myapp:1.0
docker push myinstance.tencentcloudcr.com/myteam/myapp:1.0

AWS ECR​

AWS 的 Elastic Container Registry,地址格式里带账号 ID 和区域:

<账号ID>.dkr.ecr.<区域>.amazonaws.com/<仓库名>:<标签>

ECR 的登录密码是临时的(12 小时过期),要用 AWS CLI 现取:

# get-login-password 生成临时密码,管道直接交给 docker login
aws ecr get-login-password --region ap-northeast-1 \
| docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com

# 仓库需要先创建(已存在会报错,可忽略)
aws ecr create-repository --repository-name myapp --region ap-northeast-1

docker tag myapp:1.0 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:1.0
docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:1.0

Google Artifact Registry​

Google Cloud 的 Artifact Registry(GAR)替代了旧的 Container Registry。地址格式:

<区域>-docker.pkg.dev/<项目ID>/<仓库名>/<镜像名>:<标签>

比别家多一层「仓库名」。认证方式一般用 gcloud 配置成 Docker 的凭据助手,一次配置长期有效:

# 配置凭据助手,之后 docker 命令自动带认证
gcloud auth configure-docker asia-northeast1-docker.pkg.dev

docker tag myapp:1.0 asia-northeast1-docker.pkg.dev/my-project/my-repo/myapp:1.0
docker push asia-northeast1-docker.pkg.dev/my-project/my-repo/myapp:1.0
信息

CI 环境里不建议用 gcloud,改用服务账号密钥:把 JSON 密钥文件内容通过 docker login -u _json_key --password-stdin 传入即可。

Registry API​

Registry 提供一套标准的 HTTP API(OCI Distribution Spec),不装 Docker 也能用 curl 直接操作仓库。所有接口都在 /v2/ 路径下。

先准备认证。以 basic auth(Harbor、自建 Registry 常见)为例:

# 变量方便后面复用
REGISTRY=harbor.example.com
AUTH="admin:Harbor12345"

查看仓库里都有哪些镜像仓库:

curl -u "$AUTH" https://$REGISTRY/v2/_catalog
{"repositories":["myteam/myapp","myteam/nginx"]}

查看某个仓库的所有标签:

curl -u "$AUTH" https://$REGISTRY/v2/myteam/myapp/tags/list
{"name":"myteam/myapp","tags":["1.0","1.1","latest"]}

查询某个标签的 manifest(拿 digest 就靠它):

curl -u "$AUTH" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://$REGISTRY/v2/myteam/myapp/manifests/1.0

删除镜像要先拿到 digest,再按 digest 删。获取 digest 用 HEAD 请求读响应头:

# 拿 digest(响应头里的 Docker-Content-Digest)
DIGEST=$(curl -sI -u "$AUTH" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://$REGISTRY/v2/myteam/myapp/manifests/1.0 \
| grep -i docker-content-digest | awk '{print $2}' | tr -d '\r')

# 按 digest 删除
curl -u "$AUTH" -X DELETE https://$REGISTRY/v2/myteam/myapp/manifests/$DIGEST
注意

两个坑:一是删除接口默认可能是关的,Registry(含 Harbor 底层)需要开启 delete 配置;二是删除只是删了 manifest 引用,层文件还在磁盘上,要跑垃圾回收(Harbor 在「垃圾清理」里手动触发或定时执行)才真正释放空间。

另外注意 Docker Hub 的认证方式不同,要先去 auth.docker.io 换 token 再请求 registry-1.docker.io,不能直接 basic auth。

镜像同步​

换 Registry 的时候(比如从 Docker Hub 迁到内部 Harbor,或同步到国外拉不动的镜像),需要把镜像从 A 仓库搬到 B 仓库。

手动三板斧:pull / tag / push​

适合偶尔同步一两个镜像:

# 1. 从源仓库拉下来
docker pull nginx:1.27-alpine

# 2. 打上目标仓库的标签
docker tag nginx:1.27-alpine harbor.example.com/library/nginx:1.27-alpine

# 3. 推到目标仓库
docker push harbor.example.com/library/nginx:1.27-alpine

缺点很明显:要先下载到本地再上传,慢;而且默认只搬当前架构,多架构镜像会丢其他平台的层。

用 skopeo 批量同步​

skopeo 是专门干镜像搬运的工具,优点是不用落地本地 Docker,直接在两个 Registry 之间流式传输,还能保留多架构 manifest:

# 单个镜像同步(--all 保留所有架构)
skopeo copy --all \
docker://docker.io/library/nginx:1.27-alpine \
docker://harbor.example.com/library/nginx:1.27-alpine \
--src-creds 用户名:密码 \
--dest-creds admin:Harbor12345

批量同步整个仓库的所有标签:

skopeo sync --src docker --dest docker \
--src-creds 用户名:密码 \
--dest-creds admin:Harbor12345 \
docker.io/library/nginx harbor.example.com/library

skopeo 也支持同步到本地目录(dir:)、OCI 归档(oci:)等中间格式,内网完全隔离的环境可以先导出成文件再拷进去。

提示

如果只是临时拉一个镜像(比如 CI 里),也可以不同步整个仓库,直接 docker pull harbor.example.com/mirror/nginx:1.27 用同步好的内部地址,业务代码里的镜像名统一切过去。

镜像加速​

国内直连 Docker Hub 慢甚至拉不动,常用两种解决办法。

方式一:配置 registry-mirrors​

编辑 /etc/docker/daemon.json(不存在就新建),加上 registry-mirrors:

{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.xuanyuan.me"
]
}

改完重启 Docker 生效:

sudo systemctl daemon-reload
sudo systemctl restart docker

# 验证配置是否生效
docker info | grep -A 5 "Registry Mirrors"

配置后 docker pull nginx 会自动走镜像站。优点是透明,不用改镜像名;缺点是镜像站质量参差,有些会失效或限速,需要自己挑能用的。

注意

registry-mirrors 只对 Docker Hub 生效,拉 ghcr.io、gcr.io 等其他仓库不会走镜像站。这类仓库要么用下面的前缀代理,要么直接同步到内部仓库。

方式二:用代理仓库前缀​

一些公共加速服务支持「加前缀」的方式拉取,相当于把镜像站当成代理,明确指定走它:

# 把 docker.io 换成代理域名
docker pull docker.1ms.run/library/nginx:1.27-alpine

# ghcr.io 的镜像有些代理也支持
docker pull ghcr.1ms.run/owner/repo:tag

拉下来之后镜像名里带着前缀,为了和部署配置一致,一般要重新打标签:

docker tag docker.1ms.run/library/nginx:1.27-alpine nginx:1.27-alpine

这种方式不用改 daemon 配置、不用重启,临时用一次很方便。

镜像签名(Cosign)​

镜像是分层文件加一份 manifest,传过来的东西有没有被篡改、是不是你信任的人发布的,靠签名来确认。Cosign 是 sigstore 项目的签名工具,目前容器镜像签名的事实标准。

本地密钥对签名​

适合小团队自用,生成一对密钥,私钥签名、公钥验证:

# 生成密钥对(会提示设置私钥密码)
cosign generate-key-pair
# 生成 cosign.key(私钥)和 cosign.pub(公钥)

# 用私钥给镜像签名(镜像必须在 Registry 上,先 push 再签)
cosign sign --key cosign.key harbor.example.com/myteam/myapp:1.0

# 别人拿到公钥后验证
cosign verify --key cosign.pub harbor.example.com/myteam/myapp:1.0

验证通过会输出签名信息;镜像被改过(digest 对不上)就验证失败。

Keyless 签名​

Keyless 模式不管理密钥,用 OIDC 身份(GitHub、Google 账号等)换临时证书签名,签名记录上公共透明日志 Rekor,适合开源项目和 CI:

# 会弹出浏览器做 OIDC 认证(CI 里用 --identity-token 免交互)
cosign sign harbor.example.com/myteam/myapp:1.0

# 验证时指定签名者身份和签发者
cosign verify harbor.example.com/myteam/myapp:1.0 \
--certificate-identity your-email@example.com \
--certificate-oidc-issuer https://accounts.google.com
备注

签名本身也是作为一个特殊的 OCI 对象存在 Registry 里(标签形如 sha256-xxxx.sig),所以 Registry 得支持 OCI artifact——Harbor 2.x、GHCR、ECR、GAR 都支持。私钥一定保管好,丢了等于签名作废,泄露了别人能冒充你签。

镜像扫描(Trivy)​

镜像里装了什么包、有没有已知 CVE 漏洞,靠扫描来查。Trivy 是 Aqua 开源的一体化扫描器,扫镜像、扫文件系统、扫配置都行,Harbor 默认集成的就是它。

扫描镜像:

# 扫描本地或远程镜像
trivy image nginx:1.27-alpine

# 只看高危和严重漏洞(CI 里常用)
trivy image --severity HIGH,CRITICAL nginx:1.27-alpine

# 发现高危漏洞就以非零码退出,方便 CI 卡点
trivy image --exit-code 1 --severity CRITICAL myapp:1.0

扫描文件系统(构建前扫源代码依赖):

# 扫当前目录的依赖清单(package-lock.json、go.sum 等)
trivy fs .

# 连 Dockerfile 错误配置一起扫
trivy config .

CI 里怎么用​

以 GitHub Actions 为例,构建完镜像立刻扫,扫出严重漏洞就失败:

- name: Build image
run: docker build -t myapp:${{ github.sha }} .

- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
severity: 'CRITICAL,HIGH'
exit-code: '1'
提示

扫描结果依赖漏洞库,Trivy 每次运行会自动更新库。内网环境可以提前把漏洞库同步到内部,用 --db-repository 指定内网镜像地址。别扫一次就完事——镜像没重build,新爆出的 CVE 也会影响它,要定期重扫仓库里的存量镜像(Harbor 可以定时全量扫)。

SBOM​

SBOM(Software Bill of Materials,软件物料清单)就是一张「这个软件里装了哪些组件、各是什么版本」的清单。有了它,出漏洞时(比如 Log4Shell 那种)能马上查出来哪些镜像受影响,不用挨个翻镜像。

生成 SBOM 常用两个工具:

syft,Anchore 出的专用 SBOM 工具:

# 扫描镜像,输出 SPDX 格式的 SBOM
syft myapp:1.0 -o spdx-json > sbom.spdx.json

# 也可以扫目录(构建产物、源码)
syft ./dist -o cyclonedx-json > sbom.cdx.json

Trivy 也能顺手生成,不用多装一个工具:

# 把镜像扫描结果输出为 SBOM
trivy image --format cyclonedx -o sbom.json myapp:1.0

# 扫描文件系统生成
trivy fs --format spdx-json -o sbom.spdx.json .

主流格式就两个:SPDX 和 CycloneDX,选哪个都行,关键是每次构建都生成、和镜像一起存起来(可以直接 cosign attach sbom 挂到镜像上,或存到制品库)。出了新漏洞拿 SBOM 一搜就知道影响面。

镜像生命周期管理​

仓库里的镜像只增不减,迟早把磁盘撑爆。镜像生命周期管理就是回答两个问题:哪些镜像该留、留多久,到期的怎么清掉。

保留策略的思路​

常见规则组合着用:

  • 按数量:每个仓库只保留最近 N 个标签,比如保留最近 10 个,CI 每次发版都推新标签,老的自动淘汰。
  • 按时间:保留最近 N 天推送或拉取过的镜像,超过没人动的清掉。
  • 按标签模式:release-* 和 latest 永远保留,*-dev、*-test 这类临时标签只留 3 天。
  • 按拉取情况:「最近 90 天被拉取过」的留着——长期没人拉的镜像多半已经废弃。

Harbor 的保留规则​

Harbor 在项目级别配置保留规则(Retention Policy),支持组合条件:

  1. 进入项目 → 「策略」→ 添加规则。
  2. 选匹配范围:哪些仓库、哪些标签(支持 release-* 这类通配,也能排除)。
  3. 选保留条件:最近 N 个 / 最近 N 天推送的 / 最近 N 天拉取的。
  4. 设置执行周期(每小时 / 每天 / 每周)或手动跑。

规则跑完后被删的标签变成「无标签」的悬空引用,还要再跑一次垃圾清理(Garbage Collection)才真正回收磁盘空间。Harbor 的 GC 可以在系统管理里设成定时任务。

手动清理的思路​

没有 Harbor 这种工具时,用 Registry API 写个脚本也能干:

  1. GET /v2/_catalog 列出所有仓库。
  2. 逐个 GET /v2/<仓库>/tags/list 拿标签。
  3. 对每个标签查 manifest 里的创建时间,挑出要删的。
  4. 按 digest 调 DELETE 接口删除。
  5. 在 Registry 服务器上跑垃圾回收,例如 Distribution 官方 Registry 的 registry garbage-collect /etc/docker/registry/config.yml。
注意

删除策略上线前先用「试运行」验证(Harbor 的规则有 Dry Run 按钮),确认规则匹配的是你想删的那批再放开。误删正在用的镜像,下一次拉取或扩容直接就挂了。另外删除前确认这些镜像能重新构建出来——Dockerfile 和构建环境还在,删了才不可怕。