
先把话说在前面如果你的集群还在靠docker pull从公网拉镜像或者靠手动导出 tar 包在节点间来回搬运那这篇关于 k8s 环境下部署 Harbor 私有镜像仓库的经验你迟早用得上。我在实际维护集群时被镜像分发这件事折磨过很久——开发打好包推到一个临时仓库运维再到各个节点执行命令导入版本一多直接乱套。后来把 Harbor 引入 K8s 环境从部署、配置节点拉取、到日常清理备份整条链路才算真正理顺。这篇文章就把我从零开始部署 Harbor 到 K8s 集群、再让集群节点正常使用它的完整过程写下来包含了一些只有实际操作才会碰到的坑。适合刚接触私有镜像仓库的运维也适合已经部署过 Harbor 但对某些细节比如 imagePullSecret、containerd 配置、GC 清理理解得不够透彻的开发者。1. 为什么K8s集群需要一个像Harbor这样的私有镜像仓库1.1 从镜像分发痛点说起刚开始集群规模小的时候镜像基本都放公网仓库部署一个 nginx 直接写image: nginx:1.25Kubelet 自己拉没有任何额外成本。但随着应用增多问题开始变得现实生产环境的网络策略往往禁止访问外部镜像仓库拉取直接卡住部署只能靠导出离线包。镜像没有统一的版本管理开发随手打一个app:v2推上去过几天连他自己都分不清哪个才是线上版本。业务镜像属于内部资产推到公网仓库既不安全也受私有仓库数量限制。涉及敏感业务的镜像只能在内网流转必须自建一套仓库。我见过不少团队在第一步选了最简单的方案找一台机器起一个裸的 Registry 容器端口一开就算完事。头两周还挺爽等到第二个团队入驻、第三个项目上线账号怎么分、命名空间怎么隔离、镜像要不要做漏洞扫描这些问题全部涌上来这时候再回头看当初为了省半小时做的选型代价其实很大。1.2 K8s 场景下 Harbor 解决了什么Harbor 并不是一个单纯的镜像存储它是一整套镜像管理平台。落到 K8s 环境里它解决的是下面几件事集中管理所有环境的镜像都推到同一个 Harbor集群节点从 Harbor 拉取不再依赖外部网络也不再需要逐台导入 tar 包。项目级隔离每个团队建一个独立项目互不可见权限独立控制。这比在同一个仓库里靠目录区分镜像要干净得多。细粒度访问控制可以为不同场景创建用户或机器人账号并限制为只读或只写权限。K8s 拉镜像只需要只读账号推送流水线只需要写权限两边互不牵连。安全能力内置漏洞扫描器镜像推送后可以自动扫描还可以开启镜像签名保证线上拉取的镜像就是构建链路上生成的那个。复制与容灾镜像可以从一个 Harbor 同步到另一个 Harbor多机房部署时可以就近拉取主仓库故障时备仓库还能继续服务。生命周期管理通过标签保留规则、项目配额、垃圾回收机制控制镜像数量增长和磁盘占用。这个对长期运行的集群尤其重要不然半年后一个 500G 的磁盘就无声无息被占满了。1.3 为什么不是裸 Registry 或 Nexus做一个最直接的对比对比项HarborDocker RegistryNexusWeb 管理界面丰富面向多团队几乎没有有但镜像管理只是其中一块项目与用户隔离原生支持简陋支持但配置不直观漏洞扫描内置 Trivy无需要额外插件镜像复制原生支持规则灵活无支持有限API 完整性面向镜像管理场景设计仅 Registry API通用制品库 API与 K8s 生态契合度高Helm 部署、Robot 账号、K8s 拉取集成顺畅低中纯 Registry 不是不能用但当你需要给多个团队开账号、配配额、做清理、搞复制的时候几乎所有能力都得自己搭。Nexus 也能存镜像但它的镜像 API 支持长期以来不够“原生”跟 Docker Client 和 containerd 的兼容性不如 Harbor 顺滑。所以我的结论很直接在 K8s 风格的运维团队里Harbor 几乎是默认选项。既然确定要上 Harbor部署前先把它的组件结构摸透否则 Helm 一装出来一堆 pod出问题时根本不知道谁管谁。2. Harbor 的架构底子先搞清楚组件再去部署2.1 Harbor 的核心组件各管什么Harbor 部署到 K8s 后你会看到一组 Pod它们不是摆设每个都有明确分工。我用官方 Helm Chart 部署之后组件大致分成下面几类组件作用是否必选Harbor Core核心 API 服务处理认证、权限、项目管理、标签策略、Webhook 等必选Harbor Registry镜像的实际存储与分发遵循 Docker Registry HTTP API V2 规范必选Harbor Jobservice异步任务执行器负责镜像复制、垃圾回收、标签保留、扫描调度必选Harbor Portal浏览器访问的 Web 管理界面必选Harbor DBPostgreSQL 数据库保存用户、项目、复制规则、审计日志等元数据必选Harbor Redis缓存与任务队列给 Core 和 Jobservice 使用必选Harbor Trivy漏洞扫描器分析镜像层中的已知漏洞按需Harbor Notary镜像签名服务提供内容信任能力按需这里要特别理解一点镜像的元数据和镜像文件本身是分开存储的。Registry 负责保存和分发大块文件Core 和 DB 负责管“谁有权限、属于哪个项目、标签怎么保留”这类信息。因此备份时必须数据库和存储一起备份只备份其中一个恢复的时候都会出问题。2.2 哪些组件可以裁剪如果你只做基础镜像分发扫描和签名可以先不开。扫描器很吃内存小规格机器上开着反而容易导致 Pod OOMNotary 对很多业务团队来说用不上还会增加证书和签名配置的复杂度。所以我建议初始部署时把 Trivy、Notary 关掉等规模大了再按需开启控制面成本会低很多。我自己第一次部署时全部组件默认开着结果在只有 4G 内存的节点上连续出现 Pod 重启排查了半天才发现是扫描器把内存打满了。关掉之后整个 Harbor 立刻稳定下来。这个判断对中小集群尤其重要。2.3 部署到 K8s 后资源规划参考规模建议资源说明测试 / 小团队2核4G关闭扫描和签名日常推送和拉根本跑不满中等规模4核8G开启扫描但控制并发支持几十个镜像的日常更新较大规模8核16G 及以上建议外部数据库和 Redis保证稳定性需要提醒的是节点上还有 Kubelet 和各种 Daemon 在跑尽量给 Harbor 的各组件设置资源请求与限制。尤其是 Jobservice复制规则多的时候容易占满 CPU不设限制会拖垮整个节点。3. 在 K8s 集群里部署 Harbor从证书到 Helm Chart 的完整落地3.1 部署前的四项准备部署前我习惯先列一个检查单避免装到一半发现没有存储类或者入口没规划存储Harbor 需要持久化存储确认集群里有默认 StorageClass。如果没有需要提前创建 PV或者在测试环境临时禁用持久化。入口Harbor 要暴露给集群内外使用生产环境通常走 Ingress。准备一个域名比如harbor.example.com并做好 DNS 解析规划。证书HTTPS 证书是必需品。可以放在 Ingress 层也可以直接交给 Harbor 自己管理后续操作二进制。生产环境建议使用内部 CA 签发或可信 CA 证书。工具链准备好 Helm 3、kubectl并且客户端能访问集群。同时确认节点能拉取 Harbor 镜像或者已经提前把镜像放入内网仓库。3.2 生成自签 TLS 证书并放进 K8s如果你的环境没有现成证书用自签证书先跑通流程是完全可以的。下面是我常用的生成命令mkdir -p /opt/harbor-certs cd /opt/harbor-certs openssl genrsa -out harbor.key 2048 openssl req -new -x509 \ -key harbor.key \ -out harbor.crt \ -days 3650 \ -subj /CNharbor.example.com \ -addext subjectAltName DNS:harbor.example.com \ -addext extendedKeyUsage serverAuth, clientAuth命令执行完后把证书和私钥放进 K8s Secretkubectl create ns harbor kubectl -n harbor create secret tls harbor-tls \ --keyharbor.key \ --certharbor.crt这里有两个细节容易踩坑证书必须包含 SANsubjectAltName只写 CN 的话Docker 和 containerd 校验时会报x509: certificate relies on legacy Common Name field错误。如果是给生产用别用自签证书糊弄最好用公司内部 CA 签发的证书并且把根 CA 下发到所有节点。3.3 values.yaml 关键配置Harbor 官方提供了 Helm Chart安装时主要通过values.yaml控制行为。我习惯保留一份精简配置把不需要的组件先关掉expose: type: ingress tls: enabled: true secretName: harbor-tls ingress: hosts: core: harbor.example.com annotations: nginx.ingress.kubernetes.io/proxy-body-size: 0 nginx.ingress.kubernetes.io/proxy-read-timeout: 600 externalURL: https://harbor.example.com persistence: enabled: true persistentVolumeClaim: registry: storageClass: size: 200Gi database: storageClass: size: 10Gi redis: storageClass: size: 2Gi harborAdminPassword: 请改成你的强密码 trivy: enabled: false notary: enabled: false几个字段的用意说明一下expose.type选择ingressHarbor 会把 TLS Secret 挂在 Ingress 上。proxy-body-size: 0是必须的否则 Nginx Ingress 默认限制请求体大小推送大镜像时会直接报 413。externalURL必须与访问域名和协议一致。这个字段会影响 Docker Client 在 push 之后收到的响应地址写错了会出现推送半个小时后才发现实际上传失败。persistence.persistentVolumeClaim里Registry、Database、Redis 都需要独立存储。只给 Registry 配存储是不够的数据库丢了等于账号、项目、复制规则全部丢失。3.4 安装与首次验证准备好values.yaml之后执行安装helm repo add harbor https://helm.goharbor.io helm repo update helm install harbor harbor/harbor -n harbor -f values.yaml等待 2 到 3 分钟后检查 Pod 状态kubectl -n harbor get pods所有 Pod 变成Running和Ready状态后用浏览器访问https://harbor.example.com用admin和刚才设置的密码登录。然后到命令行做一次真实推送验证docker login harbor.example.com -u admin docker tag busybox harbor.example.com/library/busybox:1.36 docker push harbor.example.com/library/busybox:1.36如果这里用的是自签证书Docker 登录会报证书校验失败。需要把harbor.crt复制到 Docker 的证书目录或者在 Docker Daemon 配置里把harbor.example.com加入 insecure-registries。这一步别偷懒不然推不上去还以为是 Harbor 本身的问题。4. 让节点能拉取私仓镜像imagePullSecret 与运行时配置4.1 理解 K8s 拉取私有镜像的认证模型Harbor 部署好了镜像也推上去了接下来最关键的一步让 K8s 节点从 Harbor 拉取镜像。Kubelet 在创建 Pod 时会检查 Pod 里声明的imagePullSecrets以及 Pod 绑定的 ServiceAccount 上的imagePullSecrets。它会把这些 secret 里面的认证信息通过 CRI 接口传给容器运行时最后由容器运行时去拉取镜像。也就是说正常情况下你不需要在每个节点上手动docker login。只要 Pod 声明了正确的 secretKubelet 就会自动完成认证。这一点很多新手会搞反总觉得要在节点上弄一个全局登录。4.2 用 kubectl 创建 imagePullSecret最推荐的做法是为每个项目创建专用的机器人账号而不是把 admin 密码写进集群里。Harbor 的机器人账号在 UI 里会显示类似robot$testappmyrobot这种格式用户名里带有$符号在 Shell 里必须用单引号包裹否则会被当成变量展开。创建 Secret 的命令kubectl -n testapp create secret docker-registry harbor-reg \ --docker-serverharbor.example.com \ --docker-usernamerobot$testappmyrobot \ --docker-password机器人账号的完整Token注意--docker-server只需要仓库根地址不要带https://也不要带项目路径。然后在 Deployment 的spec.template.spec下面加一段spec: template: spec: imagePullSecrets: - name: harbor-reg镜像地址写完整路径包括项目名image: harbor.example.com/testapp/busybox:1.36不少人第一次部署时把项目名漏了镜像地址写成了harbor.example.com/busybox:1.36Harbor 会直接返回 404。K8s 的报错信息并不会说“缺少项目名”非常容易误导排查方向。如果不想每个工作负载都单独配一遍可以把 Secret 挂到默认 ServiceAccount 上kubectl -n testapp patch serviceaccount default \ -p {imagePullSecrets:[{name:harbor-reg}]}这样该命名空间下新建的 Pod 默认都带上拉取凭据。4.3 containerd 节点上的特殊注意点当前大多数 K8s 节点用的运行时是 containerd。前面说的imagePullSecrets对 containerd 是有效的因为这本质上是 Kubelet 通过 CRI 注入认证信息。但如果你需要直接在节点上用crictl pull调试镜像或者在节点层面做一些手动拉取就需要在 containerd 里配置仓库信任。方法是在/etc/containerd/config.toml里配置 registry 的config_pathversion 2 [plugins.io.containerd.grpc.v1.cri.registry] config_path /etc/containerd/certs.d然后为 Harbor 单独建一个目录放hosts.toml# /etc/containerd/certs.d/harbor.example.com/hosts.toml server https://harbor.example.com [host.https://harbor.example.com] ca /etc/containerd/certs.d/harbor.example.com/ca.crt把 Harbor 的 CA 证书放到对应目录后重启 containerdsystemctl restart containerd这套配置对 kubelet 拉取也生效算是一个比较彻底的兜底方案。如果你的 K8s 使用 Docker 作为运行时也可以把docker login生成的~/.docker/config.json放到/var/lib/kubelet/.docker/config.jsonKubelet 会读取这个文件作为默认凭据。不过现在 containerd 是主流还是建议理解上面这套配置。4.4 ImagePullBackOff 排查链路有段时间团队反馈某个命名空间的 Pod 一直拉不到镜像Event 里反复报401 Unauthorized。排查过程我整理成了一条链路分享出来先看 Pod 事件kubectl -n testapp describe pod web-xxx典型 Event 长这样Events: Warning Failed 2m kubelet Failed to pull image harbor.example.com/testapp/busybox:1.36: failed to authorize: failed to fetch anonymous token: unexpected status: 401 Unauthorized对照表直接给结论错误特征原因快速处理401 / 403Secret 不存在、过期或权限不够确认 Secret 是否在这个命名空间用相同账号手动docker login试试404 Not Found镜像不存在或者地址里少了项目名检查镜像路径是否包含项目名/镜像名:tagx509 证书错误节点不信任 Harbor 证书把 CA 加入节点参考前面 containerd 配置连接超时DNS 解析、网络策略、Ingress 异常在节点上curl https://harbor.example.com/v2/测试连通性我的习惯是先用kubectl get pod -o yaml看 Pod 的imagePullSecrets字段是否生效再用crictl pull在节点上手动复现。两步能快速把问题定位到“配置问题”还是“网络问题”。5. 部署之后的运维清理、备份、高可用和踩坑记录5.1 镜像空间不会自己瘦身配额、保留规则和 GCHarbor 部署完以后最容易被忽略的就是存储增长问题。开发团队每天构建好几次镜像一直往仓库里堆如果不加控制三个月后磁盘报警是必然的。我通常会做三件事给项目设置配额在项目配置里限制最大存储量比如某个测试项目限额 20GB满了以后推送直接失败。这样即使某个团队疯狂构建也不会拖垮整个仓库。配置标签保留规则在“项目 - 标签保留”里创建规则可以按标签数量保留也可以按时间保留。比如保留最近 30 天的镜像或者每个仓库保留最近 10 个版本。定期触发垃圾回收镜像删除后Registry 里对应的 blob 并不会立即释放需要通过 GC 机制清理。在“系统管理 - 垃圾回收”里触发。GC 期间仓库会进入只读状态所以要选业务低峰期。这里顺便提一句Harbor 提供了完整的 REST API熟悉的人可以直接用脚本触发 GC配合定时任务就能实现自动清理。对新上手的团队先在 UI 上手动跑几次看清楚了再上自动化也不迟。5.2 备份与恢复数据库和存储各占一半Harbor 的备份必须同时覆盖数据库和镜像存储。数据库丢了账号、项目、复制规则全部丢失存储丢了镜像文件彻底没了。两个都备份才是完整备份。数据库备份可以通过 Postgres 的pg_dump完成kubectl exec -n harbor deploy/harbor-db -- \ env PGPASSWORD数据库密码 pg_dump -U postgres harbor harbor-backup.sql密码要从 Harbor 对应的 Secret 里获取。存储部分更建议用存储层快照比如在 StorageClass 层面做卷快照简单可靠不需要停服务。恢复时要遵循顺序先恢复数据库再恢复存储然后重启所有 Harbor Pod。版本不一致会导致数据库迁移失败恢复前务必确认备份时的 Harbor 版本和当前版本一致。5.3 高可用和跨机房复制先定规模再选方案如果你只是单机房、单个团队单实例 Harbor 加持久化加定期备份已经完全够用不需要一上来就追求高可用。当镜像数量和团队规模扩大后有两种升级路径主从复制在“仓库管理”里添加远端 Harbor 地址配置复制规则支持 Push 和 Pull 两种同步方向。可以用这种方式做跨机房同步让每个机房的节点从本机房的 Harbor 拉取镜像拉取速度会明显提升。多副本架构把 PostgreSQL 和 Redis 放到外部集群Core 和 Registry 作为无状态多副本运行存储层使用共享存储或对象存储。这种方式稳定性最高但运维复杂度也随之上升。对多数团队我的建议是先做主从复制。同一个 Harbor 实例崩溃的情况不是没有但更常见的问题反而是服务器迁移或磁盘损坏复制机制至少能保证镜像在另一个地方还有一份。5.4 我踩过并修复的几个坑把印象最深的几个坑列出来给后来的人省点时间Nginx Ingress 请求体限制推送大镜像时报413 Request Entity Too Large。原因是 Nginx Ingress 默认限制请求体 1MB。解决办法是给 Ingress 加nginx.ingress.kubernetes.io/proxy-body-size: 0。externalURL 配置错误我遇到过externalURL写了http但实际用https访问结果 Docker Client 完成 push 之后返回的重定向地址一直是http导致上传失败。这个字段必须和实际访问方式一致。containerd 节点没有配置 CAcrictl pull报x509: certificate signed by unknown authority。每次新加节点都要记得把 CA 放到对应目录。删除了镜像磁盘没释放只删镜像不跑 GCblob 会一直留在存储里。删除操作之后要记得触发垃圾回收。升级版本前没有备份有一次跨版本升级数据库迁移卡住页面直接白屏。后来养成习惯任何升级前先做全量备份并按官方文档逐版本升级不跳版本。最后说两个小习惯分享一个我在实操中的习惯给每个项目创建专用的机器人账号并且只开读权限。这样 K8s 里的 imagePullSecret 即使泄露了影响范围也只会限制在单个项目内不会波及其他团队。第二个习惯是给镜像打固定版本标签比如v1.2.0或者带上 commit 短哈希latest只用于临时调试不进入部署链路。对准备在 K8s 环境里上 Harbor 的朋友我的建议很明确先别追求高可用把单节点部署、imagePullSecret、GC 清理这三件事跑通就已经解决了 80% 的问题。等你真正遇到跨机房同步需求时再回过头来加复制规则整个过程会顺很多。