Harbor 私有镜像仓库部署
一、方案概述
1.1 目标
在 K8s 高可用集群上,部署一套 Harbor 私有镜像仓库,具备:
- 镜像存储:存储 Docker 镜像和 Helm Chart
- 访问控制:用户认证、项目权限
- HTTPS:Let’s Encrypt 泛域名证书
- 持久化:Longhorn 分布式存储
- 高可用:核心组件多副本
- 外部访问:FRP + Ingress 从公网访问
1.2 技术选型
| 组件 | 用途 |
|---|---|
| Harbor Helm Chart | 部署 Harbor |
| Nginx Ingress Controller | 外部访问入口 |
| Longhorn | 持久化存储 |
| Let’s Encrypt | TLS 证书 |
| FRP | 内网穿透 |
1.3 架构
text
外网用户 ↓ https://harbor.<域名>:40443 ↓ frps(vhost_https_port = 40443) ↓ frpc ↓ 内网 VIP:443 ↓ HAProxy ↓ Ingress Controller(TLS 终止) ↓ Harbor Service ↓ Harbor Pod(core、portal、registry、jobservice)二、前置条件
2.1 集群状态
- K8s 集群
kubectl get nodes全部Ready - Nginx Ingress Controller 已部署
- Longhorn 已部署,
longhornStorageClass 就绪 - FRP 已配置
2.2 存储要求
Harbor 需要 PVC 持久化数据。本文档使用Longhorn作为存储后端。
关键约束:
- Harbor registry 组件不支持多副本共享 RWO 卷,所以
registry和jobservice必须单副本。 - Longhorn 卷默认 3 副本,如果节点磁盘空间紧张,可以降到 2 副本。
- Longhorn 的
storage-over-provisioning-percentage和storage-minimal-available-percentage需要提前调整,否则卷会因insufficient storage调度失败。
2.3 TLS 证书
需要泛域名证书*.<域名>。本文档使用acme.sh + Let’s Encrypt申请。
2.4 FRP 配置
frpc 需要加 HTTP 和 HTTPS 的 vhost 映射:
[harbor-http] type = http local_ip = <内网VIP> local_port = 80 custom_domains = harbor.<域名> [harbor-https] type = https local_ip = <内网VIP> local_port = 443 custom_domains = harbor.<域名>三、申请 TLS 证书
3.1 安装 acme.sh
curl https://get.acme.sh | sh -s email=<邮箱> source ~/.bashrc3.2 配置 DNS API
以阿里云为例:
export Ali_Key="<AccessKeyID>" export Ali_Secret="<AccessKeySecret>"3.3 申请泛域名证书
acme.sh --issue --server letsencrypt --dns dns_ali --dnssleep 60 -d <域名> -d '*.<域名>'解析:
--server letsencrypt:指定 Let’s Encrypt,不用 ZeroSSL。--dnssleep 60:加完 TXT 记录后等 60 秒再验证。-d <域名> -d '*.<域名>':同时申请根域名和泛域名。
证书路径:
~/.acme.sh/<域名>_ecc/fullchain.cer ~/.acme.sh/<域名>_ecc/<域名>.key3.4 配置自动续期脚本
创建/root/update-tls-secrets.sh:
#!/bin/bash CERT_DIR="/root/.acme.sh/<域名>_ecc" NAMESPACES=("harbor" "argocd" "jenkins" "gitlab" "default") for ns in "${NAMESPACES[@]}"; do kubectl create secret tls <域名>-tls \ --cert="$CERT_DIR/fullchain.cer" \ --key="$CERT_DIR/<域名>.key" \ --namespace "$ns" \ --dry-run=client -o yaml | kubectl apply -f - done加执行权限:
chmod +x /root/update-tls-secrets.sh配置 acme.sh 续期回调:
acme.sh --install-cert -d <域名> --ecc --reloadcmd "/root/update-tls-secrets.sh"四、安装 Harbor
4.1 添加 Helm 仓库
helm repo add harbor https://helm.goharbor.io helm repo update helm search repo harbor/harbor如果helm.goharbor.io访问超时,从 GitHub 代理下载 chart:
curl -fsSL https://ghproxy.net/https://github.com/goharbor/harbor-helm/releases/download/v1.19.2/harbor-1.19.2.tgz -o harbor-1.19.2.tgz4.2 创建命名空间
kubectl create namespace harbor4.3 创建 TLS Secret
kubectl create secret tls <域名>-tls \ --cert=/root/.acme.sh/<域名>_ecc/fullchain.cer \ --key=/root/.acme.sh/<域名>_ecc/<域名>.key \ --namespace harbor4.4 创建 values.yaml
expose: type: ingress tls: enabled: true certSource: secret secret: secretName: <域名>-tls notarySecretName: <域名>-tls ingress: hosts: core: harbor.<域名> className: nginx annotations: nginx.ingress.kubernetes.io/proxy-body-size: "0" nginx.ingress.kubernetes.io/proxy-read-timeout: "600" nginx.ingress.kubernetes.io/proxy-send-timeout: "600" nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" externalURL: https://harbor.<域名>:40443 persistence: enabled: true resourcePolicy: "keep" persistentVolumeClaim: registry: storageClass: longhorn size: 15Gi database: storageClass: longhorn size: 20Gi redis: storageClass: longhorn size: 10Gi jobservice: storageClass: longhorn size: 10Gi trivy: storageClass: longhorn size: 10Gi harborAdminPassword: "<强密码>" core: replicas: 2 resources: requests: cpu: "500m" memory: "1Gi" limits: cpu: "2" memory: "2Gi" portal: replicas: 2 resources: requests: cpu: "200m" memory: "256Mi" limits: cpu: "1" memory: "1Gi" jobservice: replicas: 1 resources: requests: cpu: "500m" memory: "512Mi" limits: cpu: "2" memory: "2Gi" registry: replicas: 1 resources: requests: cpu: "500m" memory: "512Mi" limits: cpu: "2" memory: "2Gi" database: type: internal internal: resources: requests: cpu: "500m" memory: "1Gi" limits: cpu: "2" memory: "2Gi" redis: type: internal internal: resources: requests: cpu: "200m" memory: "256Mi" limits: cpu: "1" memory: "1Gi" trivy: enabled: true replicas: 1 resources: requests: cpu: "200m" memory: "512Mi" limits: cpu: "1" memory: "1Gi"4.5 逐项解析
| 字段 | 含义 |
|---|---|
expose.type: ingress | 用 Ingress 暴露 |
tls.enabled: true | 开启 HTTPS |
tls.certSource: secret | 证书从 K8s Secret 读 |
tls.secret.secretName | TLS Secret 名字 |
ingress.hosts.core | Harbor 域名 |
proxy-body-size: "0" | 不限上传大小 |
proxy-read-timeout: "600" | 读超时 10 分钟 |
ssl-redirect: "true" | HTTP 自动跳转 HTTPS |
externalURL | 外部访问地址,带端口 |
persistence | 各组件 PVC 配置 |
harborAdminPassword | admin 初始密码 |
core.replicas: 2 | core 双副本 |
portal.replicas: 2 | portal 双副本 |
jobservice.replicas: 1 | jobservice 单副本 |
registry.replicas: 1 | registry 单副本 |
关键点:
registry和jobservice必须单副本,因为它们的卷是 RWO,多副本会报Multi-Attach error。externalURL必须带端口,因为外网访问是https://harbor.<域名>:40443。
4.6 安装 Harbor
helm install harbor harbor/harbor \ --namespace harbor \ -f /root/harbor-values.yaml如果helm.goharbor.io不通,用本地 chart:
helm install harbor ./harbor-1.19.2.tgz \ --namespace harbor \ -f /root/harbor-values.yaml4.7 观察 Pod 启动
kubectl get pods -n harbor -w按Ctrl+C退出。
预期:
harbor-core-xxx 1/1 Running (2个) harbor-database-0 1/1 Running harbor-jobservice-xxx 1/1 Running harbor-portal-xxx 1/1 Running (2个) harbor-redis-0 1/1 Running harbor-registry-xxx 2/2 Running harbor-trivy-0 1/1 Running五、验证 Harbor
5.1 查看 Pod 和 PVC
kubectl get pods -n harbor kubectl get pvc -n harbor kubectl get ingress -n harbor预期:
- 所有 Pod
Running。 - PVC 全部
Bound,STORAGECLASS是longhorn。 - Ingress 有
harbor-ingress,域名harbor.<域名>。
5.2 测试 HTTPS 访问
curl -v https://harbor.<域名>:40443 2>&1 | grep "SSL certificate"预期:
SSL certificate verify ok.5.3 浏览器访问
https://harbor.<域名>:40443用户名:admin
密码:你设置的强密码
第一次登录会要求修改密码。
5.4 创建项目
登录后,创建项目,比如library。
解析:
- 项目是 Harbor 里镜像的组织单位。
- 推送镜像时路径是
harbor.<域名>:40443/library/<image>:<tag>。
六、配置 K8s 拉取 Harbor 镜像
6.1 配置 containerd 信任 Harbor
在每台节点上编辑/etc/containerd/config.toml,在registry.mirrors段加:
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."harbor.<域名>:40443"] endpoint = ["https://harbor.<域名>:40443"]用 Ansible 批量推:
ansible k8s_cluster -i inventory.ini -m shell -a "grep -q 'registry.mirrors.\"harbor.<域名>:40443\"' /etc/containerd/config.toml || sed -i '/\[plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors\]/a\ [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors\".\"harbor.<域名>:40443\"]\n endpoint = [\"https://harbor.<域名>:40443\"]' /etc/containerd/config.toml && systemctl restart containerd" -b6.2 创建镜像拉取 Secret
kubectl create secret docker-registry harbor-secret \ --docker-server=harbor.<域名>:40443 \ --docker-username=admin \ --docker-password=<强密码> \ --namespace default6.3 在 Deployment 里引用
spec: template: spec: imagePullSecrets: - name: harbor-secret containers: - name: myapp image: harbor.<域名>:40443/library/myapp:1.06.4 验证拉取
crictl pull harbor.<域名>:40443/library/nginx:latest七、常见问题
7.1 Pod 卡在ImagePullBackOff
Harbor 镜像在docker.io/goharbor上,containerd 镜像加速应该能覆盖。如果拉不下来,手动拉取并打标签:
crictl pull docker.m.daocloud.io/goharbor/harbor-jobservice:v2.15.2 ctr -n k8s.io images tag docker.m.daocloud.io/goharbor/harbor-jobservice:v2.15.2 docker.m.daocloud.io/goharbor/harbor-jobservice:v2.15.2如果某个节点拉不下来,从其他节点导出:
ctr -n k8s.io images export /tmp/jobservice.tar docker.m.daocloud.io/goharbor/harbor-jobservice:v2.15.2 scp /tmp/jobservice.tar root@<目标节点>:/tmp/ ctr -n k8s.io images import /tmp/jobservice.tar7.2 registry 多副本时Multi-Attach error
原因:registry 用 RWO 卷,多副本无法共享。
解决方案:
registry.replicas: 1。- 或者改 Deployment 的更新策略为
Recreate:
kubectl patch deployment -n harbor harbor-registry -p '{"spec":{"strategy":{"type":"Recreate"}}}'7.3 PVC 绑不上,卷faulted或degraded
原因:Longhorn 卷调度失败。
排查:
kubectl get volumes.longhorn.io -n longhorn-system kubectl get volume.longhorn.io <volume-name> -n longhorn-system -o yaml | grep -A5 "type: Scheduled"如果报insufficient storage:
kubectl patch settings.longhorn.io storage-over-provisioning-percentage -n longhorn-system --type=merge -p '{"value":"200"}' kubectl patch settings.longhorn.io storage-minimal-available-percentage -n longhorn-system --type=merge -p '{"value":"10"}'然后触发副本重建:
kubectl get engines.longhorn.io -n longhorn-system | grep <volume-name> kubectl delete engine.longhorn.io <engine-name> -n longhorn-system7.4 Harbor 登不上,报relation "properties" does not exist
原因:database 重建后,表没建。
解决方案:删掉 Harbor 和所有 PVC,重建。
helm uninstall harbor -n harbor kubectl delete pvc -n harbor --all kubectl delete namespace harbor kubectl create namespace harbor kubectl create secret tls <域名>-tls --cert=/root/.acme.sh/<域名>_ecc/fullchain.cer --key=/root/.acme.sh/<域名>_ecc/<域名>.key --namespace harbor helm install harbor harbor/harbor -n harbor -f /root/harbor-values.yaml7.5 413 Request Entity Too Large
原因:Ingress 的proxy-body-size太小。
解决方案:设为"0",不限大小。
7.6 HTTPS 访问报证书错误
原因:证书链不完整,或 Secret 没配对。
排查:
kubectl get secret <域名>-tls -n harbor kubectl describe ingress -n harbor harbor-ingress | grep TLS7.7 镜像拉取超时
Harbor 镜像在docker.io/goharbor上。如果拉不下来,配置 containerd 镜像加速,或手动导入。
八、常用操作
8.1 查看 Harbor 状态
kubectl get pods -n harbor kubectl get svc -n harbor kubectl get ingress -n harbor kubectl get pvc -n harbor8.2 查看日志
kubectl logs -n harbor -l component=core --tail=100 kubectl logs -n harbor -l component=registry --tail=100 kubectl logs -n harbor -l component=database --tail=1008.3 取 admin 密码
kubectl get secret -n harbor harbor-core -o jsonpath='{.data.HARBOR_ADMIN_PASSWORD}' | base64 -d echo8.4 升级 Harbor
helm upgrade harbor harbor/harbor -n harbor -f /root/harbor-values.yaml8.5 删除 Harbor
helm uninstall harbor -n harbor kubectl delete pvc -n harbor --all kubectl delete namespace harbor警告:删除 PVC 会删除所有镜像数据,操作前先备份。
九、架构总结
9.1 组件清单
| 组件 | 副本数 | 作用 |
|---|---|---|
| harbor-core | 2 | API 服务、认证 |
| harbor-portal | 2 | Web UI |
| harbor-registry | 1 | 镜像存储 |
| harbor-jobservice | 1 | 镜像复制、扫描任务 |
| harbor-database | 1 | PostgreSQL 数据库 |
| harbor-redis | 1 | 缓存 |
| harbor-trivy | 1 | 镜像扫描 |
9.2 端口速查
| 端口 | 用途 |
|---|---|
| 40443 | 外网 HTTPS 入口 |
| 40080 | 外网 HTTP 入口 |
| 443 | 内网 HAProxy HTTPS |
| 80 | 内网 HAProxy HTTP |
| 30443 | Ingress Controller HTTPS NodePort |
| 30080 | Ingress Controller HTTP NodePort |
9.3 已知隐患
| 隐患 | 影响 | 解决方案 |
|---|---|---|
| registry/jobservice 单副本 | 节点故障服务中断 | 需要 RWX 存储才能多副本 |
| Longhorn 空间调度保守 | 卷报insufficient storage | 改 over-provisioning 和 minimal-available |
| 证书手动更新 | 续期后 Secret 需更新 | 配 acme.sh 续期回调脚本 |
| database 重建后表没建 | Harbor 登不上 | 删掉 Harbor 和 PVC,重建 |
十、总结
这套 Harbor 基于官方 Helm Chart 部署,提供了:
- 镜像存储:私有 Docker 镜像仓库
- 访问控制:用户认证和项目权限
- 外部访问:FRP + Ingress 从公网访问
- HTTPS:Let’s Encrypt 泛域名证书
- 持久化:Longhorn 分布式存储
- 高可用:core、portal 多副本
部署中最容易踩的坑:
- chart 拉取超时:用 GitHub 代理下载本地安装。
- registry 多副本 Multi-Attach:registry 和 jobservice 必须单副本。
- 卷容量超过节点可用空间:registry 卷不要超过 20Gi。
- Longhorn 空间调度保守:改
storage-over-provisioning-percentage=200和storage-minimal-available-percentage=10。 - Harbor database 表没建:删掉 Harbor 和所有 PVC,重建。
- HTTPS 证书链不完整:用
fullchain.cer。 - externalURL 不带端口:外网访问带 40443,
externalURL必须包含端口。
解决了这七个问题,Harbor 就能稳定运行。