1. 为什么非得自己搭Harbor,而不是直接用Docker Hub?
Harbor不是“另一个镜像仓库”,它是企业级私有镜像仓库的工业标准答案。我最早在某高校实验室带学生做微服务实训时,就吃过没搭私有仓库的亏:十来个同学共用一个Docker Hub账号,push镜像时频繁触发速率限制;有人误删了基础镜像,整个CI流水线卡死两小时;更麻烦的是,某次课程作业要求提交含敏感配置的Spring Boot应用镜像——结果学生直接推到了公开仓库,虽然很快删了,但心里一直发毛。从那以后,我所有教学环境、内部项目、甚至个人实验,只要涉及多镜像协作或含配置/凭证的构建,第一件事就是拉起一套Harbor。
它解决的从来不是“能不能存镜像”的问题,而是“能不能安全、可控、可审计地管理镜像生命周期”的问题。Docker Hub免费账户只允许1个私有仓库,且不支持LDAP集成、镜像扫描、策略强制(比如禁止未扫描镜像部署)、项目级权限隔离——这些在Harbor里全是开箱即用的标配功能。更重要的是,Harbor是CNCF毕业项目,它的架构设计天然适配K8s生态:ChartMuseum原生集成、Notary签名支持、与Trivy深度耦合的漏洞扫描,都不是后期拼凑的功能,而是从第一天就写进DNA里的能力。
关键词里虽然没填,但实际落地中绕不开三个核心词:镜像签名、漏洞扫描、基于角色的访问控制(RBAC)。比如某次给某公司做容器化迁移咨询,他们原有Jenkins流水线会自动构建并推送镜像到Docker Hub,但上线前无法验证镜像是否被篡改。我们把Harbor接入后,在CI阶段强制执行cosign sign签名,并在K8s集群的准入控制器里配置imagepolicywebhook,任何未签名或签名失效的镜像一律拒绝调度——这层防护,Docker Hub连影子都看不到。
所以别再问“Harbor和Docker Hub有什么区别”,要问的是:“你的镜像,敢不敢让它在生产环境里裸奔?”
提示:Harbor不是Docker Hub的平替,而是企业容器治理的基础设施。它不解决单机开发效率问题,但能一票否决整条交付链路的安全风险。
2. Harbor的组件拆解:每个服务都在干什么,为什么不能少?
很多人以为Harbor就是个“带UI的镜像存储”,装完就能用。结果第一次重启发现Core服务起不来,日志里满屏failed to connect to database,慌得去查PostgreSQL配置——其实根本没动过数据库。这种问题,90%源于对Harbor多进程架构的误解。它不是单体应用,而是由7个独立容器协同工作的微服务系统,每个容器职责明确,缺一不可:
| 容器名 | 核心职责 | 关键依赖 | 典型故障现象 |
|---|---|---|---|
| harbor-core | 权限校验、策略执行、Webhook分发 | Redis、PostgreSQL、Registry | 登录失败、权限错误、镜像push超时 |
| harbor-registry | 镜像存储与分发(实际存储层) | Storage backend(本地磁盘/S3/OSS) | 404 Not Found、blob unknown错误 |
| harbor-db | 存储项目、用户、策略、扫描报告等元数据 | PostgreSQL | UI显示空白项目、用户列表为空 |
| harbor-redis | 缓存会话、任务队列、临时数据 | — | 登录后立即登出、定时扫描任务堆积 |
| harbor-jobservice | 异步任务调度(扫描、GC、复制) | Redis、Core API | 漏洞扫描不触发、镜像GC不执行 |
| harbor-portal | 前端静态资源服务(Nginx) | — | 页面加载空白、CSS/JS 404 |
| harbor-trivy | 漏洞扫描引擎(可选,但强烈建议启用) | — | 扫描状态始终为pending |
注意:harbor-registry才是真正的镜像存储服务,它背后挂载的存储路径(如/data/registry)才是镜像文件的实际落盘位置。而harbor-db里的PostgreSQL只存元数据——比如你创建了一个叫prod-apps的项目,这个项目ID、创建时间、是否公开等信息存在数据库里,但nginx:alpine这个镜像的layer文件,全在registry容器的存储目录下。很多运维同学误以为清空数据库就能释放磁盘空间,结果发现df -h一点没变,就是因为没碰registry的存储卷。
我踩过最深的坑是在某次离线环境部署:客户网络完全隔离,我提前下载了Harbor离线包,但漏掉了Trivy的离线扫描数据库(trivy-offline.db)。结果所有镜像扫描状态卡在pending,日志里反复报failed to download vulnerability DB。排查了3小时才发现,Trivy默认启动时会尝试联网更新漏洞库,而Harbor的jobservice又不会主动重试——它只是把扫描任务扔进Redis队列,然后静默失败。解决方案很简单:在harbor.yml里显式配置trivy.offline_scan: true,并手动把离线DB文件挂载进容器。这个细节,官方文档藏在“Advanced Configuration”小节里,新手根本找不到。
注意:Harbor的健康检查不是看容器是否running,而是看
core服务能否连通db和redis,且registry能否响应/v2/请求。用docker-compose ps看到全是Up,不代表服务真正可用。
3. 从零部署实操:避开配置陷阱的完整流程
Harbor官方提供install.sh脚本,但它本质是生成docker-compose.yml并调用docker-compose up -d。真正决定成败的,是harbor.yml这个配置文件。我见过太多人直接复制官网示例,改个域名就跑,结果第二天发现镜像推不上去——问题往往出在三个看似不起眼的字段上。
3.1 域名与协议:HTTPS不是可选项,是强制前提
Harbor强制要求HTTPS。哪怕你只是本地测试,也必须配置有效证书。Docker客户端默认拒绝向HTTP仓库push镜像(除非显式加--insecure-registry,但这等于自废武功)。很多人用localhost或127.0.0.1作为hostname,结果docker login时提示x509: certificate is valid for xxx, not localhost。
正确做法是:用真实域名(如harbor.internal),并通过本地/etc/hosts解析到本机IP。证书生成命令如下(使用OpenSSL):
# 生成CA私钥和证书 openssl genrsa -out ca.key 4096 openssl req -x509 -new -nodes -key ca.key -subj "/CN=harbor-ca" -days 3650 -out ca.crt # 生成Harbor服务私钥 openssl genrsa -out harbor.key 4096 # 生成证书签名请求(注意Subject Alternative Name必须包含域名) cat > harbor.csr.cnf <<EOF [req] default_bits = 4096 prompt = no default_md = sha256 req_extensions = req_ext distinguished_name = dn [dn] C = CN ST = Beijing L = Beijing O = MyOrg OU = DevOps CN = harbor.internal [req_ext] subjectAltName = @alt_names [alt_names] DNS.1 = harbor.internal IP.1 = 192.168.1.100 # 替换为你的服务器IP EOF openssl req -new -key harbor.key -out harbor.csr -config harbor.csr.cnf openssl x509 -req -in harbor.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out harbor.crt -days 3650 -extensions req_ext -extfile harbor.csr.cnf生成后,把harbor.crt和harbor.key放到harbor.yml同级目录的common/config/registry/下(需提前创建该目录),并在harbor.yml中指定:
https: port: 443 certificate: /your/path/harbor.crt private_key: /your/path/harbor.key提示:证书的
subjectAltName必须同时包含DNS名称和服务器IP,否则K8s节点上的kubelet可能因证书校验失败而拉取镜像失败。
3.2 存储配置:别让磁盘IO成为瓶颈
Harbor默认使用本地文件系统存储镜像,配置在harbor.yml的storage_service段:
storage_service: filesystem: rootdirectory: /data/registry这里有两个致命陷阱:
陷阱1:
/data/registry目录权限registry容器以UID 10000运行,宿主机目录必须赋予该UID读写权限:chown -R 10000:10000 /data/registry。如果用root创建目录,容器会因权限不足无法写入,日志里只有permission denied,毫无上下文。陷阱2:磁盘空间告警阈值
Harbor默认在磁盘使用率超95%时拒绝push。但很多服务器根分区只有50GB,推几个Java镜像就满了。修改方法是在harbor.yml中添加:
# 全局配置 log: level: info local: rotate_count: 50 rotate_size: 200M location: /var/log/harbor # 存储告警阈值(单位:百分比) storage: maintenance: upload_purging: enabled: true age: 168h interval: 24h dryrun: false更关键的是,生产环境绝不能用本地存储。我给某金融客户部署时,他们要求镜像存储必须满足等保三级——这意味着存储必须支持加密、异地备份、WORM(一次写入多次读取)。我们最终对接了对象存储OSS,配置如下:
storage_service: s3: region: cn-shanghai bucket: harbor-prod-bucket accesskey: YOUR_ACCESS_KEY secretkey: YOUR_SECRET_KEY regionendpoint: https://oss-cn-shanghai.aliyuncs.com encrypt: true # 启用服务端加密 secure: true v4auth: true chunksize: 5242880 rootdirectory: /docker/registry注意encrypt: true和secure: true必须同时开启,否则OSS不会启用AES256加密。而且regionendpoint必须带https://前缀,漏掉就会报connection refused——因为OSS默认只监听HTTPS端口。
3.3 网络与防火墙:端口开放清单必须精确到毫秒
Harbor默认监听4个端口:
- 443(HTTPS):Web UI、API、Docker客户端交互(必须开放)
- 4443(Notary):镜像签名服务(如启用Notary则必须开放)
- 80(HTTP):仅用于HTTP重定向,若强制HTTPS可关闭
- 8080(Admin Server):内部管理端口,严禁对外暴露
很多安全团队会一刀切开放443,结果Notary服务不可用,导致docker trust命令失败。正确的防火墙规则应为:
# 开放HTTPS主服务 iptables -A INPUT -p tcp --dport 443 -j ACCEPT # 如启用Notary,必须开放4443 iptables -A INPUT -p tcp --dport 4443 -j ACCEPT # 禁止访问内部管理端口 iptables -A INPUT -p tcp --dport 8080 -j DROP部署完成后,务必用以下命令验证核心服务连通性:
# 测试HTTPS基础访问 curl -k https://harbor.internal/api/v2.0/systeminfo # 测试Docker客户端登录(需提前配置/etc/docker/daemon.json) echo "password" | docker login harbor.internal -u admin --password-stdin # 测试镜像推送(先打标签) docker tag nginx:alpine harbor.internal/library/nginx:alpine docker push harbor.internal/library/nginx:alpine如果curl返回{"with_notary":false,"with_clair":false,...},说明API正常;如果docker login成功但push报unauthorized: authentication required,大概率是core服务没连上db或redis——此时要看docker-compose logs core,而非registry日志。
4. 权限与策略实战:如何让开发、测试、运维各司其职
Harbor的RBAC不是简单的“管理员/普通用户”两级制,而是基于项目(Project)的四层权限模型:Guest<Developer<Maintainer<Project Admin。但真正让这套模型发挥价值的,是把它和企业的组织架构对齐。我在某电商公司落地时,就按业务线划分了三类项目:
| 项目名称 | 访问模式 | 权限组 | 典型操作 |
|---|---|---|---|
base-images | 公开 | Maintainer: DevOps组Guest: 全体研发 | 推送基础镜像(openjdk、node:18) 拉取构建基础镜像 |
dev-sandbox | 私有 | Developer: 各业务线Dev组Project Admin: Tech Lead | 推送开发分支镜像 创建Webhook触发测试环境部署 |
prod-apps | 私有 | Maintainer: SRE组Guest: QA组 | 仅SRE可push生产镜像 QA可pull验证,不可push |
实现的关键在于项目级别的策略配置。比如prod-apps项目,我们启用了三项强制策略:
- 内容信任(Content Trust):勾选“Enable content trust”,要求所有push的镜像必须经
cosign签名; - 漏洞扫描(Vulnerability Scanning):勾选“Automatically scan images on push”,扫描引擎选Trivy;
- 机器人账号(Robot Account):为CI流水线创建专用机器人账号,权限设为
Pull,避免Jenkins使用个人账号。
创建机器人账号的操作路径:项目 → 成员 → 创建机器人账号。生成的token是长期有效的,但必须妥善保管——它等同于密码。我们把它注入Jenkins的Credentials,然后在pipeline中这样用:
stage('Push to Harbor') { steps { script { docker.withRegistry('https://harbor.internal', 'harbor-robot-prod') { docker.build("harbor.internal/prod-apps/myapp:${BUILD_NUMBER}").push() } } } }这里harbor-robot-prod是Jenkins中预存的凭据ID。相比用个人账号,机器人账号的优势在于:权限可精确到项目、可随时禁用、无密码轮换烦恼。
另一个高频需求是跨项目镜像复制。比如测试环境需要从base-images拉取python:3.9-slim,但生产环境不允许直连公网。解决方案是配置Harbor内置的复制规则:
- 在
base-images项目中,进入复制 → 新建规则 - 源资源过滤器填
python*,目标项目选dev-sandbox - 触发模式选
事件驱动,这样当新版本python:3.11-slim推送到base-images,10秒内自动同步到dev-sandbox
实测发现,复制规则对镜像tag有严格匹配逻辑:python*能匹配python:3.9-slim,但python:3.*会匹配失败。这是正则表达式引擎的特性,不是Bug。
提示:Harbor的“项目”本质是命名空间隔离,不是物理隔离。所有项目共享同一个
registry存储后端,因此跨项目复制是元数据同步+硬链接(同一layer不重复存储),速度极快。
5. 故障排查黄金路径:从日志定位到根因的完整链路
Harbor没有“一键诊断”按钮,但它的日志结构极其清晰。当服务异常时,不要盲目重启,按以下顺序逐层排查,90%的问题能在5分钟内定位:
5.1 第一层:确认容器进程状态
# 查看所有容器状态(重点关注Exit Code) docker-compose ps # 如果某个容器Exit Code非0,立即看日志 docker-compose logs -f <service_name> # 如 core, registry, db常见Exit Code含义:
137:OOM Killer干掉了进程(内存不足)1:配置错误(如harbor.yml语法错误)127:找不到命令(如trivy二进制缺失)
5.2 第二层:聚焦core服务日志(灵魂所在)
core是Harbor的中枢神经,几乎所有权限、策略、Webhook都经它处理。典型问题场景:
场景1:登录成功但无法push镜像
日志中出现failed to authenticate user: invalid username or password,但密码确认无误。
→ 根因:core无法连接redis,导致会话缓存失效。检查docker-compose logs redis是否有Connection refused,再确认harbor.yml中redis配置的host是否为redis(Docker Compose网络内服务名),而非127.0.0.1。
场景2:UI显示项目为空,但docker search能搜到镜像
日志中大量failed to get project list: pq: relation "project" does not exist。
→ 根因:db容器首次启动时,初始化SQL脚本执行失败。进入db容器:docker exec -it harbor-db psql -U postgres,执行\l查看数据库列表,确认是否存在registry库。若不存在,需手动执行/harbor-migration/migrate.sh。
5.3 第三层:registry服务日志(存储真相)
registry日志不记录业务逻辑,只记录存储层操作。关键线索:
error authorizing context: authorization token required:认证失败,通常是core没把token透传给registry,检查core日志中是否有failed to get auth token。blob unknown:镜像layer文件丢失。进入registry容器:docker exec -it harbor-registry sh,执行find /storage -name "*<layer_id>*",若无结果,说明存储卷损坏或被误删。
5.4 第四层:网络连通性验证(常被忽略)
即使所有容器Running,网络不通照样失败。必备验证命令:
# 从core容器内测试连通db和redis docker exec -it harbor-core sh -c "ping -c 3 harbor-db" docker exec -it harbor-core sh -c "nc -zv harbor-db 5432" # 从registry容器内测试连通core(关键!) docker exec -it harbor-registry sh -c "curl -I http://harbor-core:8080/api/v2.0/ping" # 必须返回HTTP/1.1 200 OK,否则registry无法回调core做权限校验我遇到过最诡异的案例:所有服务正常,但docker push卡在waiting for layer。最后发现是registry容器的/etc/hosts里,harbor-core被解析到了错误的IP(Docker DNS缓存污染)。解决方案:重启dockerd服务清除DNS缓存,或在docker-compose.yml中为registry服务显式添加extra_hosts。
注意:Harbor的日志级别默认为
info,遇到疑难问题可临时调高。修改harbor.yml中的log.level: debug,然后./install.sh --with-notary --with-trivy重新安装(会保留数据)。
6. 进阶实践:让Harbor真正融入你的交付流水线
Harbor的价值,不在它本身有多强大,而在它如何成为你CI/CD流水线的“守门人”。我给某物联网公司做的方案中,Harbor不是终点,而是连接开发、安全、运维的枢纽。
6.1 镜像签名自动化:用Cosign堵住供应链漏洞
Docker Hub不支持签名,Harbor通过Notary或Cosign实现。我们选择Cosign,因为轻量且与GitHub Actions深度集成。关键步骤:
- 在CI中生成密钥对(仅首次):
cosign generate-key-pair -r ./cosign.key -p ./cosign.pub - 将公钥上传到Harbor:
项目 → 内容信任 → 上传公钥 - 在build阶段签名:
docker build -t harbor.internal/prod-apps/iot-gateway:v1.2 . cosign sign --key cosign.key harbor.internal/prod-apps/iot-gateway:v1.2
签名后,任何docker pull都会自动验证。如果镜像被篡改,docker pull会报错signature verification failed。这比“人工审核镜像”可靠一万倍。
6.2 漏洞扫描闭环:从告警到修复的SLA
Harbor的Trivy扫描结果,不能只停留在UI里。我们通过Webhook将高危漏洞实时推送到企业微信:
- 在Harbor中配置Webhook:
系统管理 → 配置 → Webhook,URL填企业微信机器人地址; - Payload类型选
Vulnerability,触发条件设为High及以上; - 在企业微信收到告警后,自动创建Jira工单,指派给对应模块负责人;
- 负责人修复后,重新构建并push,Harbor自动触发二次扫描,结果通过Webhook回传,自动关闭Jira工单。
这套流程将平均漏洞修复时间(MTTR)从72小时压缩到4小时以内。
6.3 多集群镜像分发:用Harbor复制替代手工同步
某客户有北京、上海、深圳三个K8s集群,原先靠docker save/load同步镜像,每次升级耗时2小时。改用Harbor复制后:
- 在北京集群Harbor创建
replication rule,源为prod-apps项目,目标为上海Harbor的prod-apps项目; - 复制模式选
Push-based,触发模式选Scheduled(每5分钟同步一次); - 上海集群的Kubelet配置
imagePullSecrets指向本地Harbor,彻底摆脱跨地域拉取。
实测同步1GB镜像,耗时从120分钟降至90秒,且网络带宽占用降低80%(因只同步差异layer)。
最后分享一个血泪教训:Harbor升级必须遵循小版本迭代原则。我们曾试图从v2.4直接升到v2.8,结果database migration脚本因表结构变更过大而失败,回滚都困难。现在我们的规范是:v2.4 → v2.5 → v2.6 → v2.7 → v2.8,每步升级后,必须执行./prepare验证数据一致性,并用harbor-db容器内的psql手动检查关键表(如project、repository)数据是否完整。升级不是技术动作,而是发布管理流程。
我在实际使用中发现,Harbor最强大的地方,是它把“安全左移”这件事,变成了可配置、可审计、可度量的技术动作。当你不再需要靠喊话提醒开发“别用root用户构建镜像”,而是用策略强制拦截时,你就真正拥有了容器时代的交付主权。