我在多个生产环境里帮团队落地过 Kubernetes 集群,其中“镜像到底放哪”这个问题,几乎是每个初建集群的团队都会卡住的点。直接用 Docker Hub 吧,拉取慢、有配额限制,尤其在内网环境里根本没法用;自己随便搭个 Registry 呢,又缺权限管理、镜像清理这些正经仓库该有的能力。这篇文章就围绕标题里的“Kubernetes 专项:搭建私有 Harbor 本地仓库”这件事,把从环境准备到 K8s 节点拉取镜像的完整链路拆开讲清楚,包括为什么选 Harbor、证书目录怎么配、containerd 怎么对接、以及 KubeKey 装集群时怎么把离线包推进 Harbor 这类实操细节。适合正在搭 K8s 环境、或者在搞内网部署流水线的运维和开发同学参考。
1. 为什么 Kubernetes 环境需要私有 Harbor
先扯一个很多新手会踩的误区:我明明有 Docker Hub,也有阿里云镜像加速,为什么还要折腾一套私有 Harbor?如果你只是在本机 docker run 一个 nginx,那确实用不上。但一旦上了 Kubernetes,事情就变了。
K8s 的调度是“分散”的,你的 Pod 可能被调度到集群里的任意一台 Node 上。这意味着集群里的每一台机器,都得能拉到你业务用的那个镜像。如果你把镜像放在 Docker Hub 的私有仓库里,每个节点都得配置账号密钥,还受限于 Docker Hub 的拉取频率限制——我记得匿名用户每 6 小时只能拉 200 次,一个稍微大点的集群、发布频繁一点,分分钟就被限流。再说内网环境,压根连不上外网,这时候你必须在集群内部有一个所有节点都能访问的镜像源。
那为什么不是随便开一个带认证的 Registry?Harbor 的价值在于它不只是“存镜像”的:它有基于项目的权限隔离,有镜像漏洞扫描(用的 Trivy),有镜像签名和审计日志,还有回收策略。我们在实际运维中最常用的功能是“镜像保留策略”——比如“保留每个项目最近 30 个 tag,其余自动清理”,这功能能让你在长期迭代中不用手工去清磁盘。生产环境里磁盘被 /var/lib/docker 或 /var/lib/containerd 撑爆,几乎是每个集群都遇到过的故障,Harbor 的清理策略能缓解这个问题。
还有一个很实际的原因:Harbor 天然就是为 K8s 设计的。K8s 的 kubelet 在拉取私有仓库镜像时,支持用 imagePullSecrets 指定 docker-registry 类型的 Secret,而 Harbor 的 Robot Account(机器人账号)机制,正好可以给每个项目创建最小权限的专用拉取账号。这让“开发能推送、节点能拉取、权限互不干扰”这件事变得非常顺滑。
我在选型时也对比过 Nexus 的 Docker 仓库。Nexus 是通用制品库,也能存镜像,但它的 UI 和权限模型偏开发库风格,容器镜像的 Tag 管理、层(layer)清理之类的体验都远不如 Harbor。如果团队已经在用 Nexus 管 Maven 或 npm 包,那顺带用一下也行;但如果你是专门为 K8s 搭镜像仓库,Harbor 是更省心的选择。
2. 环境准备、版本选型与安装包获取
2.1 硬件与系统要求
先聊聊硬件。Harbor 本身算不上重,它由多个容器组成(nginx、portal、registry、redis、postgres/database、trivy、core 等模块),但如果你启用了漏洞扫描模块,内存占用会明显上涨。我常用的评估标准是:2 核 4G 起步,4 核 8G 比较舒服,再高看并发。如果你只是自己测试、不启用扫描,2G 内存也能跑起来,就是 docker ps 的时候看到一堆容器会觉得有点挤。
操作系统方面,CentOS 7 是我见过的最常见的部署环境,但 CentOS 7 默认的内核版本是 3.10,跑 Docker 20.10+ 和 Harbor 都没问题。这里有一个前置条件:机器务必能正常访问外网来拉取 Harbor 的依赖镜像(除非你打算一台完全离线的机器去导入离线包)。正常情况下,我们用离线安装包安装 Harbor,因为它体积大约在 600MB 到 1GB 左右,包含了所有 Harbor 组件镜像,安装时直接 load 进去,非常省事。
2.2 Docker 与 Docker Compose 的版本选择
Harbor 的安装底层依赖两个东西:Docker 引擎和 Docker Compose(V2 版本用docker compose子命令,V1 版本是docker-compose二进制)。我用的是 Docker 20.10.17 + Docker Compose v2.12.2,这套组合在 CentOS 7 上很稳。Docker 20.10 是兼容性非常好的版本,支持 containerd 快照器、也支持配置 registry-mirror,是 Harbor 官方测试覆盖最成熟的版本。
这里有个要注意的细节:Harbor 离线安装包里的install.sh脚本会检测 docker 和 docker-compose 是否可用。如果你装的是 Docker Compose V2,它检测的是docker compose version;如果是 V1,它检测的是docker-compose version。为了少踩坑,我建议直接用 V2 插件形式安装,并且确认docker compose version能正常输出。
装 Docker 的步骤不复杂,但有几个点容易出错。CentOS 7 上不要直接yum install docker,那个版本是 1.13,太老了,Harbor 装了也容易出诡异问题。正确姿势是先装 yum-utils,然后配置 Docker 官方 yum 源或国内镜像源,再安装 docker-ce、docker-ce-cli、containerd.io。
# 卸载旧版本 sudo yum remove -y docker docker-client docker-common docker-engine # 安装依赖工具 sudo yum install -y yum-utils # 配置国内 Docker 源(这里用阿里云镜像站) sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo # 安装指定版本(20.10.17) sudo yum install -y docker-ce-20.10.17 docker-ce-cli-20.10.17 containerd.io # 启动并设置开机自启 sudo systemctl start docker sudo systemctl enable docker装完记得确认一件事:docker info | grep "Cgroup Driver",如果显示的是cgroupfs,而你的 K8s 是用 systemd 的 Cgroup Driver,后续会有冲突。尽早在/etc/docker/daemon.json里统一改成"exec-opts": ["native.cgroupdriver=systemd"]。这个细节如果不处理,后面 KubeKey 初始化预检的时候会直接给 warning。
2.3 Harbor 离线安装包获取
Harbor 的 GitHub Releases 页面(github.com/goharbor/harbor/releases)会提供两种包:在线安装包(harbor-online-installer-v2.x.x.tgz)和离线安装包(harbor-offline-installer-v2.x.x.tgz)。区别很好理解,在线包体积小,安装时现场从 Docker Hub 拉取镜像;离线包把所有镜像都打进了一个harbor.v2.x.x.tar.gz压缩文件里,安装脚本会直接 docker load。生产环境我建议你下载离线包,因为安装过程更可控,而且后续如果要多台机器部署,这个离线包可以复用。
版本选型上,Harbor 2.8.x、2.9.x、2.10.x 都是当前主力版本,我个人推荐 2.9.x 或 2.10.x。2.10 开始默认启用了新的 Webhook 和 SBOM 相关功能,但整体稳定性和 2.9 差别不大。版本号尽量别太激进,因为 Harbor 大版本升级涉及数据库 schema 迁移,很麻烦,不是必要情况不值得折腾。
下载完成后解压:
tar -xzf harbor-offline-installer-v2.9.4.tgz cd harbor解压后目录里会有一个harbor.yml.tmpl模板文件。注意,Harbor 安装脚本只认harbor.yml,所以你必须先复制一份出来再做修改:
cp harbor.yml.tmpl harbor.yml3. Harbor 核心配置与 HTTPS 证书方案
3.1 harbor.yml 中最关键的配置项
harbor.yml是安装的“命门”,里面每一项都直连安装结果。我不建议你用默认配置直接跑,因为默认 hostname 是reg.mydomain.com,你要是不改,装完连 UI 都访问不了。
先看最核心的几项:
# 对外服务的访问地址。可以是 IP,也可以是域名。 hostname: 192.168.1.100 # HTTP 配置(默认是 80 端口) http: port: 80 # HTTPS 配置:生产环境强烈建议启用 https: port: 443 certificate: /data/cert/harbor.crt private_key: /data/cert/harbor.key # 管理员初始密码 harbor_admin_password: Harbor12345 # 数据库密码 database: password: root123 max_idle_conns: 50 max_open_conns: 100 # 数据持久化目录 data_volume: /data/harbor其中hostname这个地方有个隐藏坑:配成 IP 也可以,但 Harbor 会根据这个值生成访问 URL 和 Docker 登录地址。如果你是纯内网用、没有内部 DNS 服务,直接写 IP 是最稳的,比如192.168.1.100,后续所有节点都通过这个 IP 访问仓库。如果有内部域名,那就写域名,比如harbor.internal.example.com,这样后续给 K8s 节点签证书时更方便。
3.2 自签名证书的生成与信任配置
Kubernetes 节点上 prefer 的镜像拉取方式默认走 HTTPS。Harbor 如果只开了 HTTP,kubelet 拉镜像时会因为“HTTP 协议不被支持”而直接拒绝(除非你给 containerd 配置了skip_verify或者在/etc/docker/daemon.json里加了insecure-registries)。所以我的建议是:给 Harbor 配 HTTPS,哪怕用的是自签名证书。这比让每台节点在 containerd 里跳过 TLS 校验更安全,也更符合生产习惯。
我们在内网环境没有公共 CA 签发证书,所以选择自建 CA 然后签发一张服务器证书。脚本大致长这样:
# 1. 生成 CA 私钥 openssl genrsa -out ca.key 4096 # 2. 生成 CA 根证书 openssl req -x509 -new -nodes -sha256 -days 3650 \ -subj "/CN=Harbor Local CA" \ -key ca.key -out ca.crt # 3. 生成 Harbor 服务器私钥 openssl genrsa -out harbor.key 2048 # 4. 生成证书签发请求(CSR) openssl req -new \ -subj "/CN=192.168.1.100" \ -key harbor.key -out harbor.csr # 5. 创建扩展文件,把 IP 和域名都加到 SAN 里(这一步特别关键) cat > ext.cnf <<EOF subjectAltName = IP:192.168.1.100, DNS:harbor.internal.example.com EOF # 6. 用 CA 签发服务器证书 openssl x509 -req -in harbor.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -days 825 -sha256 -extfile ext.cnf -out harbor.crt第 5 步不加 SAN 是这里最高频的翻车点,现在很多客户端(Go 语言写的 Docker、containerd)认证时会校验 SAN 字段,只有 CN(Common Name)会被直接拒绝,报错通常是 “x509: certificate relies on legacy Common Name field”。
把生成的ca.crt、harbor.crt、harbor.key放到统一目录,例如/data/cert/。然后在harbor.yml里将 HTTPS 段的两个路径指过去。这里我提醒一下,安装脚本install.sh会检查这两个路径的文件是否存在,如果路径写错,它会在prepare阶段报错退出。
3.3 执行安装脚本
配置好harbor.yml之后,执行:
sudo ./install.sh这个脚本会做几件事:加载离线包里的 Docker 镜像、生成 docker-compose.yaml、创建 Harbor 依赖的 volume 和网络、然后启动全部容器。整个过程大概持续 1-3 分钟,取决于磁盘速度。
装完验证一下:
docker ps你应该能看到harbor-core、harbor-db、harbor-portal、harbor-registry、nginx、redis等容器处于 Up 状态。然后在浏览器访问https://192.168.1.100,用admin/Harbor12345登录(如果没改默认密码的话)。
如果只是内网测试,不想每次访问浏览器都提示证书不可信,可以把这个自建的ca.crt加入你本机或客户端的信任列表。至于生产环境,后续如果上了公网域名,建议直接换正规 CA 签发的证书,流程一样,只是把自签名的harbor.crt内容换成 CA 签发的那份即可。
4. 与 Kubernetes 节点拉取链路全打通
4.1 节点侧配置 containerd 信任自签名证书
Harbor 一旦启用 HTTPS 且证书是自签名的,集群里的每个 kubelet 节点在拉镜像时就会面临一个信任问题。K8s 1.26 及之后的版本默认容器运行时是 containerd,它不再像旧版那样依赖 Docker 引擎。所以你要在每一台 Node 上修改/etc/containerd/config.toml,告诉它“访问某个地址时,要使用哪些证书,或者干脆跳过校验”。
拿 KubeKey 创建的集群举例,KubeKey 在初始化集群时已经给各节点装好并配置了 containerd,配置里默认的 snapshotter 是overlayfs,sandbox 镜像是pause:3.9之类。我们需要在/etc/containerd/config.toml中找到[plugins."io.containerd.grpc.v1.cri".registry]这一段,按下面方式配置:
[plugins."io.containerd.grpc.v1.cri".registry] [plugins."io.containerd.grpc.v1.cri".registry.configs] [plugins."io.containerd.grpc.v1.cri".registry.configs."192.168.1.100"] [plugins."io.containerd.grpc.v1.cri".registry.configs."192.168.1.100".tls] ca_file = "/etc/containerd/harbor-ca.crt" cert_file = "/etc/containerd/harbor-cert.crt" key_file = "/etc/containerd/harbor-cert.key" [plugins."io.containerd.grpc.v1.cri".registry.headers] X-Forwarded-Proto = "https"如果只是内网快速验证,尤其是证书里 SAN 正好覆盖了你的 Harbor 地址的情况下,你也可以配置insecure_skip_verify = true来跳过验证:
[plugins."io.containerd.grpc.v1.cri".registry.configs."192.168.1.100".tls] insecure_skip_verify = true但我的建议是:skip_verify 只用于临时测试。一旦你要接 CI/CD 流水线,镜像名里包含地址、端口、项目名,任何拼写错误都很难排查,略过验证会让“我到底连对了没”这个问题更模糊。而且安全审计时,裸奔的 registry 绝对是第一个被质疑的点。
配置完以后,务必重启 containerd:
sudo systemctl restart containerd然后可以在节点上手工拉一次镜像试试:
crictl pull 192.168.1.100/library/nginx:1.26如果这一步报错,一定先怀疑证书信任和地址访问问题,用curl -v https://192.168.1.100/v2/看看服务端证书是否正常返回。
4.2 如果节点上是 Docker 和 kubelet 组合
如果你的集群还是老式的 Docker 运行时(K8s 1.26 及以上的 kubelet 不再推荐直接对接 Docker,但 containerd 封装了 dockershim 的场景偶尔还有),或者是 Docker 环境中调试,那么配置路径是/etc/docker/daemon.json。格式如下:
{ "insecure-registries": ["192.168.1.100"], "registry-mirrors": [] }重启 Docker 后,用 docker login 验证:
docker login 192.168.1.100 -u admin -p Harbor12345insecure-registries一旦配置,docker 会以明文 HTTP 或非信任证书访问仓库。这里请注意,一旦一个地址被列进 insecure-registries,它就不会再走系统信任链,而是无条件信任该地址的证书。所以这个配置也有限制条件:仅用于内网可信网络。
4.3 创建 Namespace、项目与镜像推送
Harbor 的逻辑模型里,“项目(Project)”是第一层命名空间。比如我推一个镜像进 Harbor,最终镜像名是:
192.168.1.100/k8s-apps/backend:v1.0.0其中k8s-apps就是 Harbor 里的一个项目名。在 Harbor UI 中手动创建项目,或者在测试环境中你也可以用 admin 账户直接创建。
我习惯的做法是给每个业务线建一个独立项目,比如library(对应 Docker Hub 官方镜像的收藏项目)、bigdata、middleware,然后为每个项目创建独立 Robot 账号。Robot 账号是一串类似projectname+robotname$token的凭证,它只能访问特定项目,做特定操作(拉取或推送),比共享 admin 密码安全得多。
从本地向 Harbor 推镜像的标准动作是:
# 先给镜像打个带 Harbor 地址前缀的 tag docker tag nginx:1.26 192.168.1.100/k8s-apps/nginx:1.26 # 登录 docker login 192.168.1.100 -u admin -p Harbor12345 # 推送 docker push 192.168.1.100/k8s-apps/nginx:1.26然后到 Harbor UI 的k8s-apps项目仓库页里就能看到这个镜像了。
4.4 kubelet 拉取私有仓库镜像:imagePullSecrets 配置
镜像推到 Harbor 之后,K8s 节点默认无法直接拉取私有项目里的镜像,因为需要认证信息。K8s 官方支持的方案是:在 Namespace 里创建一个docker-registry类型的 Secret,然后在 Pod 模板里通过imagePullSecrets引用它。
创建 Secret 的命令:
kubectl create secret docker-registry harbor-registry-secret \ --docker-server=192.168.1.100 \ --docker-username=robot$k8s-apps \ --docker-password='从Harbor Robot账号里复制出的token' \ --docker-email=devops@example.com \ -n your-namespace注意:
docker-username里的$符号有特殊含义,命令行里不加引号会被 shell 转义掉,导致用户名解析不正确。建议给整段参数加单引号。
然后在 Deployment 里声明:
spec: template: spec: imagePullSecrets: - name: harbor-registry-secret containers: - name: app image: 192.168.1.100/k8s-apps/backend:v1.0.0如果你集群里的每个命名空间都要拉镜像,一个个创建 Secret 太繁琐了。可以把 Secret 创建在kube-system或某个公共命名空间里,然后用--namespace参数在多个命名空间里复制一份,或者用 Kubernetes 的Sealed Secrets、External Secrets这类工具统一管理。我这里多说一句:别把 admin 密码放到 Secret 里,Robot 账号最小权限原则才是正道。
5. KubeKey 离线部署时,怎么把 tar.gz 包里的镜像推到 Harbor
这个是我在标题相关热词里看到的,也是很多人会遇到的场景。KubeKey(kk)是 KubeSphere 生态里的集群部署工具,它支持离线部署,会先把集群需要的所有镜像导出成一个tar.gz格式的 artifact 包。你下载到一个.tar.gz之后,如果内网已经搭好了 Harbor,并不需要把每个节点都导入这个 artifact,而是可以先把镜像解包出来、推到 Harbor,然后让 KubeKey 直接从 Harbor 拉。这样集群节点本身不再需要保存离线包,流程度会顺很多。
KubeKey 比较新的版本里有个kk artifact image push子命令,可以直接把 artifact 里的镜像推送到一个 registry。用法大致是:
kk artifact image push \ --artifact ./kubesphere-offline-v3.4.1.tar.gz \ --registry 192.168.1.100 \ --username admin \ --password Harbor12345 \ --project library它会自动解包、登录、给镜像重打 tag 并推送。这里有个细节:KubeKey 内置的推送逻辑会把镜像推送到192.168.1.100/project/原始镜像名这种格式下,而不是保留原始第三方前缀(比如docker.io、registry.k8s.io),这一点在你之后用kk create cluster --with-kubesphere时,需要同步修改镜像的 rewrite 规则或临时 tag,否则集群拉取不到镜像。
如果你的 KubeKey 版本比较旧、没有image push子命令,那就走手动流程:
- 解压 artifact:
tar -xzf kubesphere-offline-v3.4.1.tar.gz - 找里边的 images 列表:一般有个
images目录或manifest文件 - 用
docker load -i xxx.tar把镜像载入本地 Docker - 逐个
docker tag,把镜像名改成192.168.1.100/library/xxx:tag docker push推上去
这个方法虽然繁琐,但胜在可控。你可以在中途过滤掉不需要的镜像,只推必要的。
另外有个常见情况:artifact 里有些镜像的 tag 是v1.26.0这种没有时间戳的格式,推到 Harbor 时同一个名字会被覆盖。这时你需要给镜像打好带构建号或日期的 tag,避免后续回滚时找不到历史版本。Harbor 里对应的项目配置里打开“自动打标签”功能也行,但我们一般更倾向于在推送时就把版本控制好。
实操里还有一个经验:KubeKey 部署集群时会在节点上预执行 preflight 检查,日志里出现[init] using kubernetes version: v1.26.0 [preflight] running pre-flight checks是正常的。如果 preflight 阶段卡住或者报错,先检查节点能否访问 Harbor 的 443 端口、能否成功解析 Harbor 域名或 IP,还有 containerd 配置是否已经生效。因为 KubeKey 拉镜像的组件其实也是走 containerd 接口,containerd 到 Harbor 的链路不通,你 artifact 推上去也是白搭。
6. 常见问题与排查技巧实录
6.1 Harbor 安装时报 “The nginx container is not running”
Harbor 装完后docker ps看到 nginx 容器反复重启,多半是证书或端口冲突。优先看日志:
docker logs harbor-nginx如果是 “configuration file /etc/nginx/nginx.conf test failed”,一般是harbor.yml里配置的证书路径不对或证书内容异常。重新检查一下/data/cert/下的证书文件权限,确保 644 权限,Docker 容器内用户才能读。
6.2 x509 证书信任报错
节点上执行 crictl pull 时如果报类似x509: certificate signed by unknown authority,说明 containerd 不认你这个 CA。要么把 CA 文件拷贝到节点(推荐),要么临时 skip_verify。之前我遇到过一种很隐蔽的情况:证书 SAN 里 IP 写的是192.168.1.100,但 Harbor 实际监听在127.0.0.1,节点解析 Harbor 域名时走 DNS 解析到了别的 IP,导致证书校验必然失败。所以配置域名时,DNS 解析正确性和 SAN 内容必须完全对得上。
6.3 docker login 失败:401 Unauthorized
Harbor 的 Robot 账号密码串是类似eyJhbGciOiJSUzI1NiIsImtpZCI6...的一大串,直接拿来 docker login 时会因为里面包含特殊字符被 shell 处理掉。我推荐的做法是先把密码写到一个临时文件里,然后:
docker login 192.168.1.100 --username 'robot$app' --password-stdin < harbor-pwd.txt如果是 UI 上创建的用户,确认你登录的是正确的项目,而不是整个 Harbor 系统的 admin 权限。Harbor 的项目成员权限有项目管理员、开发者、访客等角色,访客只能拉取不能推送,这也会导致 docker push 时 401。
6.4 K8s 节点无法拉取镜像:ImagePullBackOff
排障标准三步走:
kubectl describe pod <pod-name>看 Events 里最底部的具体报错crictl pull <image-name>手工在节点上拉一次,看是否同样是 x509 或 401 错误- 确认 Secret 的 docker-server 地址与镜像前缀完全一致,比如 Secret 里写的是
192.168.1.100,但 YAML 里镜像名写的是harbor.internal.example.com/...,那肯定拉不到
这几类问题我在现场排查时遇到过次数不少,有八成以上是“证书没配好”或“仓库地址不一致”这两个原因。
6.5 Harbor 磁盘被撑满
Harbor 的数据默认存在data_volume目录,日积月累肯定会被镜像占满。Harbor 本身不自动清理过期 tag,所以定期执行 GC(Garbage Collection)和 Retention 策略,是运维 Harbor 的基本功。在 Harbor 的“管理”里可以配置自动清理任务,也可以用脚本定期通过 API 触发 GC。我一般会写一个 cron 任务,每天凌晨触发一次 Harbor 的 GC 接口,同时根据项目保留策略自动清理旧镜像。有一点要提醒:GC 时 registry 会临时变成只读模式,最好放在业务低峰期执行。
7. 一些实操层面的个人体会
搭建私有 Harbor 这个事,说起来一行 docker run 就够了,但真正落地牵扯到的链路很长——从证书体系、到容器运行时配置、到 K8s 的认证机制,每一环都是独立知识域。你不用一次性搞懂所有原理,照着文档能跑通是最重要的,跑通之后再去想为什么。因为只有先把链路搭起来,你才有实际环境去验证证书信任机制、镜像拉取策略这些概念。
我自己刚开始接触时,最大的感悟是“镜像仓库是集群的基础设施,不是开发工具”。它的稳定性直接影响线上发布,所以像 Harbor 本身的数据备份(/data/database和/data/registry)一定要放到可靠的存储上,有条件就挂在独立数据盘或 NAS 上,别跟系统盘挤在一起。后面节点重启、磁盘扩容、甚至迁移机房时,你都会庆幸自己当初做了这个决定。
最后再分享一个小技巧:Harbor 的配置修改后,不用重新执行整个 install.sh,直接跑docker-compose down -v不行——-v会删数据,千万慎用。正确做法是改完harbor.yml后执行sudo ./install.sh --with-trivy --with-notary或者直接docker-compose up -d重新创建配置容器。我建议把harbor.yml视作重要资产,纳入 git 管理,每次改完记录变更原因,这个习惯能在你排查疑难杂症时省下好几小时。