说实话,最初意识到必须把镜像仓库当成独立基础设施来对待,并不是因为“微服务化”或者“CI/CD改造”这些漂亮口号,而是一次非常狼狈的线上事故。我接手团队基础设施维护时,发现所有人拉镜像基本靠三样东西:公共镜像站、服务器上的临时目录、以及各自的脑子。镜像命名混乱,今天叫service-final-2,明天叫service-20240601-fix,还有人顺手把本地build出来的镜像用 scp 传上服务器,替代掉仓库里的正式版本。直到有次深夜上线,代码明明构建通过,跑起来却还是旧逻辑,排查半天发现是某位同事把他本机的“紧急补丁镜像”直接覆盖了服务器上的镜像标签,而我拉到的正好是旧的。那一刻我意识到,问题根本不在某个人的操作,而在团队根本没有一套“镜像仓库”该有的流转规则。
这篇文章不打算写成产品文档,而是想从我自己搭建、运维、排障 docker 镜像仓库的经历出发,把这件事拆开揉碎:为什么需要私有仓库、选型时到底在选什么、部署后哪些环节最容易翻车,以及镜像从构建到拉取再到清理,这条路每个节点有哪些经验。无论你是一个人维护小项目,还是所在团队正在从“人人手动导镜像”向规范化过渡,这篇内容应该都能给你一个相对完整的参照系。
1. 镜像管理成为瓶颈的起点:从一次“线上跑错版本”说起
1.1 那篇文章最开始提到的“旧镜像事故”,根子在哪
那次事故最终定位到的问题链路是这样的:开发者A在本地基于最新代码构建了gateway:v1.9.3,但构建过程依赖了本机缓存的某个旧依赖,所以产物并不干净;开发者B发现服务器上有同名 tag,基于“反正这是最新版”的假设,直接把该镜像推到了生产目录,覆盖了流水线产生的版本。线上服务读取 tag 拉取时,Pull 到的自然是这份覆盖后的镜像。整个过程没有任何一层能拦截“这个镜像不是来自可信构建流程”这件事。
回头看,缺的不是某个命令,而是一个完整的镜像流转机制。如果你的团队现在还在用“手动指定版本号 + 服务器上的某台机器当临时仓库 + 互相口头确认”的方式,那么这种事故只是时间问题。镜像仓库在团队规模小的时候看起来是“多此一举”,但当镜像数量跨过几十个、参与人数超过几个人之后,它承担的角色已经从“存储工具”变成了“发布管道的唯一事实来源”。
1.2 仓库需要承担的四件事:存储、分发、权限、审计
很多人对镜像仓库的理解停留在“像网盘一样存镜像”的层面,这是远远不够的。我后来在做方案梳理时,把仓库需要承担的职责收敛成四条主线:
- 存储:除了把镜像层数据落盘之外,还要考虑数据持久化、备份、容量规划、GC(垃圾回收)机制能否回收不再被引用的层。
- 分发:同一份镜像要被多个节点拉取,仓库要能扛住并发请求;跨区域或跨机房时需要复制能力,避免每个节点都穿越公网拉取同一份大文件。
- 权限:不同团队、不同项目之间要隔离,哪些人能推送、哪些人只能拉取,通通要能配置,而不是干脆放个裸仓库所有人都有删除权限。
- 审计:谁在什么时间推送了什么镜像、拉取了什么 tag、删除了哪个仓库,这些历史对于追问题来说极其重要。查“线上为什么跑错版本”时,审计日志能直接在几分钟内告诉你答案,而不需要到处问人。
1.3 先判断一件事:你这规模是否真的需要自建仓库
不是所有场景都一上来就要自建。我自己给出的判断,一般落在下面三个维度上:
第一,团队是否已有强制 CI/CD 流程。如果代码还在靠人工打包、人工传服务器,那么即使搭了个仓库,大家也很快会发现“推到仓库再拉下来比直接传文件更麻烦”,然后绕开它。第二,是否有权限隔离和多项目并存需求。多个业务线共用一个集群时,没有项目级隔离的裸仓库根本没法用,谁都可能误删别人的镜像。第三,网络与合规因素。下载公共基础镜像是否稳定、是否允许将业务镜像推到外部平台、是否有离线环境要求,这些都决定着你到底是自建一套完整私有仓库,还是只用云上托管服务就够了。
就我观察,小型创业团队通常从托管仓库起步完全够用,等需要定制审计、镜像扫描、多区域复制时再迁移到自建。而如果你们已经处在“镜像数量增长很快+多项目隔离+审计需求明确”的阶段,那么自建仓库是绕不开的一条路。
2. 选型之前,先把“仓库要解决什么问题”想清楚
2.1 常见方案的定位差异:原生仓库、开源企业级仓库、软件制品仓库
我先后接触过几类方案,简单说下它们各自的定位,方便你做初步筛选:
一类是 Docker 官方的原生仓库实现,也就是比较朴素的registry镜像。它非常轻量,一个容器就能跑起来,适合个人实验或非常简单的内部使用。但它天生没有 Web UI、没有项目级权限模型、没有审计功能,更大的问题是镜像删除后层数据不会立即清理,需要靠 GC 任务处理,这些对规模化使用来说都是隐患。
另一类是在仓库之上叠加了权限、Web UI、复制、扫描等能力的开源项目,比如 Harbor 这类云原生仓库项目。它本质上是把原生仓库作为核心存储引擎,外面又封装了一层完整的管理体系,支持多租户项目隔离、镜像签名、漏洞扫描、垃圾回收策略、跨实例复制等,基本能覆盖我前面列的四大职责里的大部分。它跑起来依赖较多,部署时对配置、数据库、对象存储都需要一定熟悉程度,但这换来的是完整能力。
还有一类是多用途的软件制品仓库,比如 Nexus 这类统一制品管理工具,不止能存 Docker 镜像,还能存 Maven、npm、PyPI 等。如果团队的多语言构建产物非常多,需要一套统一管理入口,这类工具有它的价值;缺点是 Docker 镜像相关的高级能力比如复制策略、扫描、P2P 分发等往往不如专门仓库那么完善。
2.2 我最终定下的几个硬性指标:审计、复制、扫描、可维护性
我当年给团队做选型时,列了一张对比表,重点不是看功能多少,而是看下面几条:
| 能力 | 我的硬性要求 | 原因 |
|---|---|---|
| Web UI 与项目隔离 | 必须有 | 多团队共用,不能只靠命令行,也不能所有人都有全部仓库权限 |
| 审计日志 | 必须有且可导出 | 出问题后要能回答“谁在什么时候操作了什么” |
| 镜像扫描 | 尽量有 | 漏洞要能在上生产之前暴露,而不是等到运行时被攻击 |
| 复制/同步 | 按需 | 如果有多个环境或机房,复制能力能明显降低拉取延迟 |
| 备份与迁移 | 必须有清晰方案 | 仓库数据一旦损坏,全部环境都跟着受影响 |
| 运维复杂度 | 尽量可控 | 人员精力有限,组件越多越容易失火 |
基于这些条件,我最终选择了 Harbor 作为主力仓库,并不是因为它所有方面都天下第一,而是在“功能完整度”“社区资料丰富度”“团队学习成本”之间是最均衡的。后文提到的部署和实践,也都围绕这条主线展开,但其中的设计思路同样适用于其他仓库方案。
2.3 访问路径与 HTTPS:仓库入口最容易被人忽视的环节
很多人搭仓库第一反应是“把端口暴露出来”,然后docker login一把梭。实际操作里最容易被忽视的其实是访问入口和 HTTPS。Docker 客户端从某个版本开始,对非加密的 HTTP 仓库是默认拒绝的。你如果直接给客户端写一个裸 IP 地址,又没配证书,大概率会遇到certificate signed by unknown authority或server gave HTTP response to HTTPS client的报错。
我的做法是给仓库一个稳定的内部域名,比如registry.internal.example(内部解析到仓库服务器),并统一使用 TLS 证书。客户端所在机器需要信任对应 CA。这一步别省,否则后面每加一台机器都要处理一次证书问题,而且是跨部门级的踩坑。
另外还要思考一件事:镜像仓库入口的带宽和并发能力。很多拉取慢的问题根本不是仓库软件性能不行,而是流量全挤在单一入口链路上。如果多个机房都要拉同一份镜像,与其让每台机器都去中心仓库拉,不如考虑在各区域做只读复制代理,把流量摊开。后面我会专门讲复制配置。
3. 实操部署:搭建 Harbor 这套带鉴权、扫描、复制能力的镜像仓库
3.1 环境准备:组件清单与我的分配建议
Harbor 虽然是“一套系统”,但实际跑起来会拉起好几个容器组件,包括核心服务、数据库(默认 PostgreSQL)、缓存(Redis)、注册中心(registry 核心)、扫描组件、日志收集等。如果只是测试环境,一台至少 4C8G 的机器就够了;如果要在生产环境长期使用,我建议把数据库和对象存储独立出来,不要让它们随 Harbor 容器一起裸奔。
具体的准备我在做的时候是这样划分的:
- 操作系统:64 位 Linux,内核版本不用刻意追求最新,但需要支持 Docker 最新稳定版。
- Docker 与 Compose:Harbor 离线安装包会利用 Compose 来编排多个组件,所以这两个工具版本要和 Harbor 官方推荐的版本对应,别用太老的版本。
- 数据目录:镜像数据的存放目录,建议挂到独立磁盘,尽量别和系统盘混在一起,否则镜像一多,系统盘直接满了。
- 域名与证书:提前准备好给仓库用的域名以及对应的 TLS 证书。如果是内网系统,可以用内部 CA 签发的证书;如果是暴露到外网,就需要真实有效的证书。
- 存储后端:Harbor 支持本地文件系统、各类对象存储等。生产环境我最终选了对象存储,因为可以做备份和多副本,扩容也比直接给磁盘扩容灵活。
3.2 安装步骤和关键配置解读
安装这块其实不复杂,官方安装包已经优化到很傻瓜的程度,但前提是你得理解几个关键配置项在干什么。下载离线安装包后,解压目录里会有一个harbor.yml.tmpl,复制成harbor.yml,然后重点修改这些地方:
hostname: registry.internal.example http: port: 80 https: port: 443 certificate: /data/certs/registry.crt private_key: /data/certs/registry.key harbor_admin_password: "这里放一个足够复杂的初始密码" database: password: "数据库专用密码,别和 admin 密码相同" data_volume: /data/harbor trivy: enabled: true github_token: "" # 如果网络受限,使用离线扫描数据库需要注意一个新项目首次部署时常犯的错误:hostname不要配成localhost或127.0.0.1,因为这不只是登录时显示用的字符串,docker login时客户端会把这一项和实际访问地址做匹配,配成 localhost 会导致证书域名校验失败。
data_volume是镜像层数据的落盘位置。如果使用本地存储,这一项直接决定了后续扩容和备份的难度,我建议把它放在有独立挂载点的大容量磁盘上,并且纳入你现有的监控告警范围。
trivy选项开启镜像漏洞扫描后会额外拉取漏洞数据库,如果部署环境根本无法访问外网,这块要在离线模式下预置漏洞库,否则扫描组件一直处于未就绪状态,表面上不挡功能,但你推送后查看镜像详情时永远看到“扫描未完成”。
配置好后,进入安装包目录执行:
sudo ./install.sh安装脚本会自动把各个容器组件启动起来。启动完成后,你可以先通过 Web UI 访问仓库地址确认页面能打开,再用管理员账号登录。注意,默认管理员账号是admin,密码来自harbor_admin_password。首次登录后,我建议立刻修改密码,顺便开启两步验证。这个仓库是整个软件供应链的中枢,密码保护绝不能只靠“内网很安全”这种话自我安慰。
3.3 配置 Docker 客户端登录及项目隔离
仓库页面能访问不代表所有节点都能正常拉取镜像。集群里的各台机器需要额外做两件事:一是信任仓库证书,二是登录。
信任证书这一步,不同发行版路径略有差别。最常见的做法是把证书拷到各节点的系统信任链目录,然后执行更新证书命令。如果你用的是 containerd 或 Kubernetes 这一类容器运行时,配置还不完全一样,需要看各自运行时的证书目录格式。很多团队在这里卡很久,实际上核心逻辑都是一样的:让运行时在访问仓库时不再对证书报不信任错误。
登录则直接使用:
docker login registry.internal.example输入你在 Harbor 里创建的用户名密码即可。登录状态会记录在~/.docker/config.json里。注意,这里开始出现第一个需要认真设计的点:项目隔离。
Harbor 里“项目”是最小权限边界。项目可以是公开的或私有的,公开项目允许任何人拉取,私有项目则必须在项目中添加成员并分配角色(拉取者、维护者、项目管理员等)。我的建议很简单:默认全部私有,有明确共享需求的再单独设公开。因为一旦项目公开,任何能访问仓库的人都能拉走镜像,这在多租户场景下是灾难级别的问题。
推送镜像时需要把本地镜像的 tag 前缀改成仓库地址和项目路径:
docker tag app-server:1.2.0 registry.internal.example/backend/app-server:1.2.0 docker push registry.internal.example/backend/app-server:1.2.0你可能会觉得多敲这么一截名称很麻烦,但这恰恰是命名规范的入口。后续如果接入 CI/CD,镜像全名会更加规范化,所有人工拼接前缀的操作都会被自动构建流程替代,降低犯错概率。
3.4 数据持久化与高可用的补充说明
Harbor 默认是把所有状态都写在本地容器里的,但生产环境不能接受“所有鸡蛋放在同一个篮子”。我在实践中走通的方案是:
- 镜像层数据放到对象存储。Harbor 支持把后端存储切换为 S3 兼容接口,这样即便 Harbor 实例挂了,镜像数据仍然在对象存储里,换一台机器重新部署,挂载同样的存储桶即可恢复。
- 元数据库独立部署或使用托管数据库。Harbor 的元数据里存着项目、用户、标签、复制规则等信息,这些数据一旦丢失,就算镜像层在,管理面也全乱了。
- Redis 可以沿用 Harbor 内置的,也可以切换到外部 Redis。如果实例规模大、并发拉取多,外部 Redis 能减少一些资源竞争。
高可用方面,一个 Harbor 实例本身可以承担大量并发,但单机架构天然有单点风险。更稳妥的架构是“主备”或“多实例共享后端存储”。如果两个 Harbor 实例把配置指向同一个数据库和同一个对象存储桶,它们的页面和 API 可以做到基本一致,配合负载均衡就能实现快速容错。需要注意的是,这种模式下,部分后台任务比如 GC、复制规则不要在多个实例同时开启,否则会互相踩到。规则很简单:后台清理类任务集中在一个实例上执行。
4. 从构建到运行:一次完整镜像流转中的细节与坑
4.1 镜像命名与版本标记的规范设计
镜像仓库一旦成为团队唯一入口,镜像名称就是所有人都需要遵守的“接口协议”。最怕的是每个人自己定义一套风格,最终仓库里既有app又有backend-app-latest还有service_v2_fix,维护成本瞬间上涨。
我定下的命名模板大概是这样的:
仓库域名/项目名称/业务模块:版本描述比如registry.internal.example/order-system/order-api:v1.4.2。这里“项目名称”用来对应权限边界,业务模块对应具体服务,版本可以是用语义化版本,也可以是 git commit 短哈希。两种都有它的价值:
- 语义化版本适合给人工阅读、稳定发布使用。
- commit 短哈希适合和代码提交直接关联,便于溯源“这次构建对应的是哪一次提交”。
我见过比较合理的方式是两者配合:每次构建出来的镜像可以用tag_<commit>作为不可变标志,同时给最新稳定版打一个语义化版本 tag。不要迷信latest标签,它最大的问题是“可变”,今天拉到的 latest 可能已经不是昨天的那个镜像。对线上部署来说,这是隐患。如果一定要用latest,务必只在开发环境用,生产环境一律用不可变 tag。
4.2 客户端认证机制:登录之后为什么还会遇到权限错误
登录命令能成功,并不代表任何节点都可以为所欲为。Harbor 用户的基础权限模型是“用户在项目中的角色决定能做什么”。如果某账号只被赋予了拉取者角色,那么它推送镜像时,Harbor 会返回 401 或 403 错误。但奇怪的是,有些人在排查时先看到的报错是unauthorized: authentication required,往往会以为账号密码错了,实际上是没权限。
这种情况非常常见,特别是 CI/CD 账号只配置了pull权限,却在流水线里执行了推送命令。我的经验是:单独创建流水线专用账号,并把账号权限收敛到最小范围。比如构建流水线只需要推送到对应项目,就不需要给它项目管理员权限,更不需要全局管理员权限。给全局管理员给构建账号用的做法,属于给自己埋雷。
有些时候,Docker 客户端会因登录缓存问题导致行为诡异。比如你先用 A 账号登录了仓库,之后再用 B 账号拉取镜像,其实并不需要重新登录,A 账号的凭证就存在本地。这看起来方便,但在多账号切换时有副作用。如果你需要彻底切换身份,可以手动删除~/.docker/config.json中的 auth 字段,或直接执行一次新的docker login来覆盖旧凭证。
4.3 镜像清理与 GC:删掉 tag 不代表马上省空间
很多人在清理仓库时都会踩同一个坑:在 Web UI 或者 API 里删除了某个 tag,然后满心期待着磁盘空间立刻释放出来。但镜像层是分层的,删除 tag 只是去掉了一个引用入口,那些层数据依然停留在存储后端里,必须要跑一次垃圾回收(GC)才会真正清理。
Harbor 提供了自动垃圾回收策略,你可以按计划任务的方式调度 GC。关于 GC 有几个必须知道的行为:
- GC 只会回收“没有被任何 tag 引用的层”,所以如果多个镜像共享基础层,只要还有一个镜像在用,这层就不会被回收。
- GC 过程中,仓库的推送行为可能要暂停,因为 GC 和推送同时操作同一个层索引会引起竞争。所以尽量选在低峰期执行 GC。
- 为了控制镜像数量增长,需要配置保留策略,比如每个项目保留最近 N 个 tag。Harbor 的保留规则可以按时间、数量、tag 正则来定义,属于必备配置。
我自己在设计中还加了一条:不可变 tag 和可清理 tag 区分开。比如稳定版本 tag 打上不放行删除的保护规则,中间构建产物则允许被保留策略定期清掉。这样既保留了追溯能力,又不会让仓库无限膨胀下去。
4.4 跨机房或跨环境的复制与同步
复制是我在运维镜像仓库时觉得价值极高但很多人没真正用起来的能力。复制的基本模型有“推送复制”和“拉取复制”两种:
- 推送复制:主仓库在中心机房,通过复制规则把镜像推到各分区的从仓库。
- 拉取复制:从仓库主动到主仓库拉取满足条件的镜像。
实际操作中,我更推荐使用拉取模式。原因是拉取模式在网络的健壮性上更可控,如果某个分区网络临时抖动,从仓库会自己恢复重试,不需要中心仓库去维护到每个分区的连接状态。复制规则需要定义“镜像来源项目/仓库”、“目标实例的地址和认证信息”、“触发策略(事件驱动或定时)”。配置好后,每次有新的 tag 推送到源项目,符合条件的镜像就会被自动同步到目标端。
需要注意的一点是,复制是异步的,不是原子操作。某次复制失败后,目标仓库不会收到该镜像,拉取时会 404。所以我在跨环境发布时,部署前会先确认目标仓库里已经存在对应 tag,再触发 Kubernetes 等运行时的拉取动作,避免出现“源端有,目标端没有”的尴尬。副本仓库不是备份,它更接近“边缘缓存”。真正的数据备份,还是要靠对象存储本身的快照或上节提到的导出机制。
5. 日常运维中这些故障,我把排查链路给你梳理一遍
5.1 拉取超时和连接失败:不要一上来就怪仓库软件
遇到客户端拉取镜像报超时,第一反应应该是区分问题边界。我一般按照这样一条链路排查:
- 直接在对端节点上请求仓库的 HTTPS 端点,看端口是否可达、证书是否有效、HTTP 状态码是否正常。
- 检查客户端机器到仓库之间的 DNS 解析,确认解析到的地址是预期地址。
- 在客户端上执行
docker pull复现,观察报错是在握手阶段还是传输阶段。握手阶段基本是证书或网络代理问题,传输阶段则要关注带宽、包括前文提到的镜像层在对象存储上的下载链路。 - 如果仓库前面有负载均衡或反向代理,还要检查是否配置了过小的超时时间。镜像层文件很大,反向代理默认的超时时间有时不够,导致传输中段被切断,客户端一直卡在重试。
一个容易被忽略的问题是客户端和仓库之间的 MTU 或防火墙规则。尤其跨机房时,某些防火墙只允许特定端口,TLS 握手建立得很慢。如果仓库不放行443之外的回源端口,客户端请求 Docker Registry API 的 blob 下载时就会被拦。这类问题光看报错很难定位,用抓包工具很快能看到是哪一段 TCP 连接被拒了。
5.2 磁盘写满:存储规划、监控与快速止损
仓库磁盘写满几乎是每个自建仓库都必经的“成人礼”。最直接的原因往往是镜像只进不出、没有保留策略,或者 GC 没有按计划执行。遇到磁盘写满,建议按照应急三步走:
- 先把即将写入的节点摘掉或暂停推送,保护仓库核心服务不挂掉。
- 执行一次手动 GC 清理,同时调整保留策略,释放可用空间。
- 长期来看,迁移镜像数据到容量更大的对象存储,而不是继续给本地磁盘扩容。
这里有一个经验之谈:监控务必要加到仓库的数据目录上,而不是只监控系统盘。很多人把镜像数据放到了独立挂载点却忘记给它设置告警,等发现时系统盘没满,业务却因为数据盘满了而写不进去镜像,排查起来一头雾水。我会建议在监控面板上直接给数据目录设置 70%、85%、95% 三级告警线,超过 85% 就开始人工介入清理。
5.3 镜像扫描失败或漏洞数据陈旧
镜像扫描是仓库“增值功能”里我很看重的一项,但它并不是开箱即用永远不出问题的。最常见的两个问题:
第一,离线环境的漏洞数据库没有更新。扫描器拿不到最新漏洞库,报出来的结果永远是旧的,甚至无法完成扫描。对策是在可联网的机器上提前把漏洞数据库下载好,再导入离线仓库。具体导入方式以当前 Harbor 版本的文档为准,核心是让扫描器本地有一份能用的数据库文件。
第二,扫描报告显示“漏洞严重”但实际影响并不大。漏洞扫描是静态匹配,它不会模拟攻击路径,也不会分析这个漏洞是否真的可以从当前网络边界被利用。我把扫描结果当作“风险提示”,不当作“最终裁决”。当镜像存在高危漏洞时,更合理的动作是确认镜像是否暴露在公网、是否有额外的网络隔离,同时督促对应团队升级依赖重新构建,而不是直接把镜像删掉或者把所有漏洞当成运营指标。
5.4 审计日志与操作追踪
做镜像仓库运维,审计日志既是合规的一部分,也是平时排查问题的“显微镜”。Harbor 的审计日志会记录用户的操作行为,包括登录、推送、拉取、删除、修改配置等。碰到“某个镜像被误删了”这种场景,依靠审计日志能快速定位是哪个账号、在哪个时间点执行了删除。
我把审计日志接收出来,统一存到日志平台里,留了 90 天的可检索窗口。这样能解决两个问题:一是仓库容器重启后内置日志可能丢失,二是跨多个仓库实例时需要一个统一的检索入口。设置日志轮转时也要小心,不要把日志文件无限增大,否则日志盘先爆掉。建议日志保留策略根据你的排查频度和容量而定,90 天对我来说足够覆盖绝大多数问题追溯窗口。
6. 安全加固方向:把镜像仓库塞进可控的软件供应链
6.1 内容信任机制:让“这个镜像是不是正规产物”不再靠猜
镜像签名和内容信任是很多人没有认真用的能力。内容信任要解决的问题非常朴素:你拉到的镜像到底是不是真正由你(或你的 CI/CD 系统)签名发布的那份,有没有被替换或被篡改。
Harbor 支持对镜像进行签名,签名后的镜像会被标记为可信。当 Kubernetes 集群配置了相应的信任策略时,只有带有效签名的镜像才允许被部署。这套机制和流程结合起来,能有效避免前文那种“临时补丁镜像被覆盖正式 tag”的事故。签名不是一件能骗过人的“贴纸”,它是基于私钥的密码学操作,私钥由可信维护者保管。
实操上,一个基本流程是构建账号在推送镜像时自动签名,签名私钥放在专用的签名服务里,不要在每台开发机上散落。密钥的丢失会导致对应镜像无法通过信任校验,所以私钥本身要做好备份和访问控制。这项配置有一定复杂度,但它能让供应链安全从“靠流程自觉”上升到“靠密码学保障”。
6.2 镜像扫描与漏洞治理的闭环
光有扫描能力不够,还要建立“扫描到修复”的闭环。我习惯在流水线里加一步:镜像推送到仓库后,自动触发扫描,当高危漏洞数量超过阈值时,流水线失败,不允许进入发布阶段。同时,仓库端也开启推送后扫描回调,这样即使有人手动 push 镜像,也会被记录并标注漏洞状态。
这里要处理“老旧镜像”的问题。扫描只能检测扫描时刻的漏洞库,三个月前打上去的 tag 不会自动变安全。需要定期对现有镜像重新扫描,把结果汇总到安全看板,再结合前面提到的保留策略,把不再使用且漏洞严重的旧 tag 清理掉。否则仓库里积累的旧镜像,本质上就是一份份无人维护的“债务”。
6.3 与 CI/CD 流水线的集成:把人工环节压缩到最少
镜像仓库发挥最大价值的场景,是它已经无缝嵌进了 CI/CD 流水线里。我在集成环节做过的模板大致如下:
# 构建并推送镜像 docker build -t registry.internal.example/backend/app-server:${COMMIT_SHA} . docker push registry.internal.example/backend/app-server:${COMMIT_SHA} # 如果有稳定版本 tag 的需求,再打一个语义化版本 docker tag registry.internal.example/backend/app-server:${COMMIT_SHA} registry.internal.example/backend/app-server:${VERSION} docker push registry.internal.example/backend/app-server:${VERSION}流水线账号要用独立的机器人账号,在 Harbor 里为它创建项目级权限。这样即使某个项目的构建账号泄露,攻击者的操作范围也只被限制在那个项目里,不会波及整个仓库。构建节点上的 Docker 配置也应当由自动化统一分发,不要靠每个开发者手动往服务器上贴证书和密码。
改完流水线后,还需要注意一件事:生产环境的拉取不应该直接依赖latest标签,而是应该引用环境中记录的不可变 tag 版本。流水线在构建完成后,把最终部署使用的镜像版本写入部署配置。有一种做法是在流水线里先确认镜像 push 成功,再更新 Kubernetes 清单中的镜像版本,触发滚动更新。整个链路里,“镜像是否存在于仓库”这件事应该作为明确的检查项,而不是假设前提。
6.4 灾备与恢复预案:仓库凉了,部署链路不能全断
最后说说仓库的备份和恢复。镜像仓库的“数据”不仅仅是那一堆镜像层,还包括用户、项目、复制规则、保留策略等元数据。完整备份方案至少应该覆盖两边:
一是镜像数据本体。如果后端使用对象存储,对象存储的快照或跨区域复制就能提供不错的灾备能力;如果使用本地磁盘,建议用同步工具把数据目录里的镜像子目录同步到另一台服务器或对象存储里。二是数据库。Harbor 的元数据都在数据库里,备份这套数据库,再结合镜像数据本体,基本就能完整还原一个仓库实例。
恢复演练这件事,我建议至少每季度做一次,不要只在出事故时手忙脚乱。我自己经历过一次对象存储回源错误导致主实例不可用,当时因为提前演练过,直接拉起了备用实例,修改 DNS 指向后,约十几分钟就恢复了服务。没有演练的方案只是纸面文档,在真正故障时你根本不知道哪个步骤会拖住你。
另一个细节是管理员密码和证书备份。管理员密码忘了,可以走重置流程,但如果能找回更好;证书过期会直接导致客户端拒绝连接,证书有效性告警一定要纳入监控项。我在实际维护中设了证书到期前 30 天自动提醒,免得某个节假日一大早被电话打醒去处理证书过期这种本可避免的事故。
上面这些,都是我在镜像仓库这块真正实操后沉淀下来的经验。在长久用下来后,最大的感受是:镜像仓库绝不是一个“装完就完事”的工具,它更像整个研发流程的流量中枢。推什么、存什么、谁可以拉、谁能改、什么时候清理、怎么恢复,这些规则不提前想好,后面大概率会以事故的形式来找你。
最后分享一个很小的技巧:每次把镜像 tag 推上去之前,先在本地把目标 tag 拉一遍,确认内容是你想发布的内容。听起来很基础,但就是这一步,帮我拦下过好几次“构建环境缓存不一致导致镜像错误”的问题。自动化的流程很重要,但在关键节点保留一层人工检查和可追溯记录,往往能救你于水火之中。