Harbor 对外只有一个域名 harbor.ergesanbaishou.com,背后是五层接力。Host 请求头是贯穿全程的路由钥匙 —— 外层 NGINX 一旦不透传它,Ingress 立刻 404:
办公网机器访问域名 → 外层 NGINX(443 TLS 终止 / 80 协议路径直反代)→ ingress-nginx NodePort:32588 → Harbor。注意 upstream 端口是 32588 不是 80。这份是 FAQ ⑩ 系列全部踩坑(301 剥凭证 / realm http / Location http)打磨后的终版,三个关键点已用 ★ 标出:
# /etc/nginx/conf.d/harbor.ergesanbaishou.com.conf —— 终版(80 直反代 + 443 TLS + proxy_redirect)
upstream harbor_ergesanbaishou {
server 10.18.170.34:32588; # ★ harbor 的 ingress NodePort(http)
keepalive 32;
}
# 80:/v2/ 和 /service/ 直接反代(registry 协议严禁 301);其余路径跳 https 给 UI
server {
listen 80;
server_name harbor.ergesanbaishou.com;
location /v2/ { # ★ registry 入口,严禁 301(docker 跟 301 会丢 Authorization)
proxy_pass http://harbor_ergesanbaishou;
proxy_set_header Host $host; # ★ 灵魂行:透传 Host,Ingress 靠它分流
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 0; # push 大镜像层不限
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location /service/ { # ★ token 签发入口,同样严禁 301(realm 是 http 时 docker 从这换 token)
proxy_pass http://harbor_ergesanbaishou; # 与 /v2/ 相同的 upstream
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / { # 浏览器 UI 才跳 https
return 301 https://$host$request_uri;
}
}
# 443:SSL 终止,反代 UI/API
server {
listen 443 ssl;
server_name harbor.ergesanbaishou.com;
ssl_certificate /etc/nginx/ssl/harbor.ergesanbaishou.com.pem; # ← 证书路径
ssl_certificate_key /etc/nginx/ssl/harbor.ergesanbaishou.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL_harbor:10m;
access_log /var/log/nginx/harbor-ergesanbaishou.access.log;
error_log /var/log/nginx/harbor-ergesanbaishou.error.log;
client_max_body_size 0; # 镜像层大,不限
location / {
proxy_pass http://harbor_ergesanbaishou;
# ★★★ 银弹行:把 registry 吐出的 http:// Location(镜像层上传地址)实时改写成 https,
# 否则 docker 的 PUT 跟 Location 走 80 端口被杀,报 unauthorized(实测 FAQ ⑩.4)
proxy_redirect ~^http://harbor\.ergesanbaishou\.com(:\d+)?(/.*)$ https://harbor.ergesanbaishou.com$2;
proxy_set_header Host $host; # ★ 灵魂行
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
Harbor 的流量分两类:UI 静态页走 portal,API 与镜像协议(/v2)走 core。core 内部再反代 registry 的 5000 端口 —— 所以 Ingress 只需两个后端:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: harbor-ingress
namespace: harbor
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0" # 镜像层大,不限制上传体积
nginx.ingress.kubernetes.io/ssl-redirect: "false" # 外层暂走 HTTP
spec:
ingressClassName: nginx
rules:
- host: harbor.ergesanbaishou.com
http:
paths:
- path: /v2 # docker push/pull 协议入口
pathType: Prefix
backend: {service: {name: harbor-core, port: {number: 80}}}
- path: /api/ # UI/CI 调用的 REST API
pathType: Prefix
backend: {service: {name: harbor-core, port: {number: 80}}}
- path: /service/
pathType: Prefix
backend: {service: {name: harbor-core, port: {number: 80}}}
- path: /c/ # CVE 漏洞数据
pathType: Prefix
backend: {service: {name: harbor-core, port: {number: 80}}}
- path: / # 其余全部 = UI 静态页
pathType: Prefix
backend: {service: {name: harbor-portal, port: {number: 80}}}
# ③④ 环节:直连 ingress-nginx,手动带 Host(绕过外层 NGINX)
curl -H "Host: harbor.ergesanbaishou.com" http://10.18.170.34:32588/api/v2.0/health
# {"components":[...全部 healthy...],"status":"healthy"}
# ①② 环节:走域名过外层 NGINX(办公网机器上)
curl http://harbor.ergesanbaishou.com/api/v2.0/health
# 同上 = 全链路通;若 404 = 外层 NGINX 没透传 Host;若超时 = upstream 端口/防火墙错
ingress-harbor-nginx-controller-admission 不存在(装第二套 controller 时改名没对齐)→ 所有新建 Ingress 被 API Server 拦截。修复:把 webhook 指回真实存在的 admission service:kubectl patch validatingwebhookconfiguration ingress-nginx-admission --type='json' -p='[{"op":"replace","path":"/webhooks/0/clientConfig/service/name","value":"ingress-nginx-controller-admission"}]'Harbor 是 VMware 开源的企业级 容器镜像仓库,本环境地址:https://harbor.ergesanbaishou.com。核心能力:
base/aurora_ci| 依赖 | 要求 | 检查命令 |
|---|---|---|
| K8S 集群 | 1.20+,预留约 2C / 3G(Harbor 全家桶资源) | kubectl get nodes |
| Helm | v3.x(Harbor 官方 chart 的唯一安装方式) | helm version |
| Ingress Controller | 已装 ingress-nginx(harbor 专用 NodePort 32588,老入口 31588) | kubectl -n ingress-nginx get svc |
| 共享存储 ★ | NFS 等 RWX 后端,预建 PVC harbor-data-rwx(建议 100Gi+) | kubectl get pvc -n harbor |
ContainerCreating。没有现成 NFS 可先单副本跑通,后续再迁。kubectl delete pv harbor-nfs-pv —— PV 是集群级资源,删命名空间删不掉它;不删的话新建 PVC 可能又被旧 PV 绑走,kubelet 去挂一个不存在的 NFS → mount.nfs: Connection refused → 永远卡 ContainerCreating。# ① 每组件独立目录(★ PG 要求数据目录为空,别让多组件共用同一目录)
mkdir -p /data/harbor-local/{registry,database,redis,jobservice}
# ② 四对 PV/PVC
kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolume
metadata: {name: harbor-pv-registry}
spec: {capacity: {storage: 50Gi}, accessModes: ["ReadWriteOnce"], hostPath: {path: /data/harbor-local/registry}}
---
apiVersion: v1
kind: PersistentVolume
metadata: {name: harbor-pv-database}
spec: {capacity: {storage: 10Gi}, accessModes: ["ReadWriteOnce"], hostPath: {path: /data/harbor-local/database}}
---
apiVersion: v1
kind: PersistentVolume
metadata: {name: harbor-pv-redis}
spec: {capacity: {storage: 5Gi}, accessModes: ["ReadWriteOnce"], hostPath: {path: /data/harbor-local/redis}}
---
apiVersion: v1
kind: PersistentVolume
metadata: {name: harbor-pv-jobservice}
spec: {capacity: {storage: 5Gi}, accessModes: ["ReadWriteOnce"], hostPath: {path: /data/harbor-local/jobservice}}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: harbor-registry, namespace: harbor}
spec: {accessModes: ["ReadWriteOnce"], resources: {requests: {storage: 50Gi}}}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: harbor-database, namespace: harbor}
spec: {accessModes: ["ReadWriteOnce"], resources: {requests: {storage: 10Gi}}}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: harbor-redis, namespace: harbor}
spec: {accessModes: ["ReadWriteOnce"], resources: {requests: {storage: 5Gi}}}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: harbor-jobservice, namespace: harbor}
spec: {accessModes: ["ReadWriteOnce"], resources: {requests: {storage: 5Gi}}}
EOF
# ③ values 里四个 existingClaim 分别指:harbor-registry / harbor-database / harbor-redis / harbor-jobservice
第一步:NFS 服务端(10.18.170.34)准备 —— 不做这步,registry 会永远卡在 ContainerCreating:
mkdir -p /data/nfs/harbor
echo "/data/nfs/harbor *(rw,sync,no_subtree_check,no_root_squash)" >> /etc/exports
systemctl enable --now nfs-server # CentOS;Ubuntu 对应 nfs-kernel-server
exportfs -rv # 导出立即生效
# ★ 其余每个 K8S 节点也要装客户端(不装则挂载报 "wrong fs type"):
yum install -y nfs-utils
# 自检(任一 K8S 节点执行,两步都通 NFS 才算 OK;不通先查 firewalld 和 /etc/exports):
showmount -e 10.18.170.34 # 应列出 /data/nfs/harbor
mount -t nfs 10.18.170.34:/data/nfs/harbor /mnt && ls /mnt && umount /mnt
第二步:预建共享存储(NFS 示例,accessModes 是关键):
kubectl create namespace harbor
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
name: harbor-nfs-pv
spec:
capacity:
storage: 100Gi
accessModes: ["ReadWriteMany"] # ★ 多副本共用的前提
nfs:
server: 10.18.170.34 # NFS 服务端地址
path: /data/nfs/harbor
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: harbor-data-rwx
namespace: harbor
spec:
accessModes: ["ReadWriteMany"]
resources:
requests:
storage: 100Gi
EOF
# ① 装 Helm(任一管理节点)
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# ② 添加 Harbor 官方 chart 仓库并建命名空间
helm repo add harbor https://helm.goharbor.io
helm repo update
kubectl create namespace harbor # 上一节建 PVC 时已建则跳过
③ 编写 values-harbor.yaml(★ 为关键项):
expose:
type: ingress
tls:
enabled: false # 先 HTTP 跑通,有证书后再启用
ingress:
className: nginx # ★ 对应已装的 ingress-nginx controller
host: harbor.ergesanbaishou.com # ★ 仓库域名 = docker login 的地址
externalURL: http://harbor.ergesanbaishou.com # ★ UI/镜像地址,必须与访问入口一致
harborAdminPassword: "改成强密码"
persistence:
enabled: true
imageChartStorage:
type: filesystem
persistentVolumeClaim: # ★ 四组件分别指向第 2 节预建的独立 PVC
registry:
existingClaim: harbor-registry # 镜像数据(registry ×2 同节点可共用 RWO)
jobservice:
existingClaim: harbor-jobservice # 任务日志
database:
existingClaim: harbor-database # 元数据(单实例)
redis:
existingClaim: harbor-redis # 缓存
trivy:
existingClaim: harbor-trivy # 漏洞库(启用扫描时再预建)
# ⚠️ 血泪教训(existingClaim 必须与实际 PVC 名一字不差):
# 本环境曾把四个 existingClaim 全写成 harbor-data-rwx(一个并不存在的 PVC 名),
# 导致 database/redis/registry Pod Pending 整整 23 小时、core/jobservice 连锁
# CrashLoop 236 次(详见 7.5 节排障实录)。写完 values 务必先比对:
# kubectl get pvc -n harbor ←→ values 里的每个 existingClaim
# ⚠️ 漏配任何一个,集群又没有默认 StorageClass 时,该组件 PVC 永远 Pending → Pod Pending。
# 补配后要先 kubectl delete pvc <卡住的PVC> 再 helm upgrade(旧 Pending PVC 不会自动绑定)。
# 删除 PVC 卡在 Terminating = 还有 Pod 引用它(pvc-protection 保护);StatefulSet 的 PVC
# 删掉后还会被 STS 立刻重建(现象:PVC 的 AGE 突然变小)。顺序必须是:先删 STS 及 Pod → 再删 PVC——
# kubectl -n harbor delete statefulset harbor-database harbor-redis harbor-trivy (连带删 Pod)
# kubectl -n harbor scale deploy/harbor-jobservice --replicas=0 (缩掉再删)
# 另:多组件共用同一个 PVC 时,PG(database)要求数据目录为空 —— 其他组件先写入会导致 initdb
# 失败;稳妥做法 = 各组件独立 PVC(单节点用第 2 节的四对 hostPath PV/PVC)。
# 若首次安装时 database/redis/trivy 走了动态 PVC(volumeClaimTemplates),切换前还要先孤立化:
# kubectl -n harbor delete statefulset harbor-database harbor-redis harbor-trivy --cascade=orphan
# (StatefulSet 的 volumeClaimTemplates 字段不可变;孤立化保留 Pod,由 upgrade 重建后接管)
portal:
replicas: 2 # ★ UI 入口双副本
core:
replicas: 2 # ★ API 核心双副本
registry:
replicas: 2 # ★ 镜像存取双副本(必须 RWX 存储)
jobservice:
replicas: 1 # 任务执行器保持单副本,避免同一任务被重复跑
# 注:database、redis 是单实例 StatefulSet,不支持直接 replicas:2(见下方说明)
# ④ 安装
helm install harbor harbor/harbor -n harbor -f values-harbor.yaml
# ⑤ 观察就绪(约 1~3 分钟,全部 Running)
kubectl -n harbor get pods
# NAME READY 预期
# harbor-core-5cd8..-a / -b 1/1 Running ← core ×2
# harbor-registry-7d9..-a / -b 2/2 Running ← registry ×2,共用 PVC
# harbor-portal-66f..-a / -b 1/1 Running ← portal ×2
# harbor-jobservice-7f6..-x 1/1 Running ×1
# harbor-database-0 1/1 Running ×1(StatefulSet)
# harbor-redis-0 1/1 Running ×1(StatefulSet)
# harbor-trivy-0 1/1 Running (启用扫描时)
# ⑥ 创建 Harbor Ingress(chart 的 expose.type=ingress 可自动生成;本环境手工管理,见 00 节)
kubectl apply -f k8s/harbor-ingress.yaml
kubectl -n harbor get ingress
curl -H "Host: harbor.ergesanbaishou.com" http://10.18.170.34:32588/api/v2.0/health
# 返回 {"status":"healthy"...} 即成功(32588 = harbor 专用 ingress-nginx 的 NodePort)
server_name harbor.ergesanbaishou.com 反代到 NodePort 32588(完整配置见 00 节),办公网即可用域名打开 UI、docker login 推拉镜像(别忘了客户端 insecure-registries,见第 8 节①)。# 状态总览
kubectl -n harbor get pods,svc,ingress,pvc # 全家健康 + 入口 + 存储一览
kubectl -n harbor top pods # 各组件资源占用
# 日志排障
kubectl -n harbor logs deploy/harbor-core --tail=100 -f # 页面/API
kubectl -n harbor logs deploy/harbor-jobservice --tail=100 -f # ★ retention/GC/复制任务
kubectl -n harbor logs harbor-database-0 --tail=100 # 数据库
# 平滑重启某个组件(替代 docker-compose restart)
kubectl -n harbor rollout restart deploy/harbor-core
# ★ 改配置的正确姿势:改 values-harbor.yaml 后 helm upgrade
# (别 kubectl edit 手改资源 —— 下次 upgrade 会被覆盖回去)
helm -n harbor upgrade harbor harbor/harbor -f values-harbor.yaml
# 扩缩容:改 values 里 replicas 后 upgrade,或临时直接 scale
kubectl -n harbor scale deploy/harbor-core --replicas=2
# 磁盘回收:进 registry 容器执行 GC(UI 删除是软删除,低峰操作)
kubectl -n harbor exec deploy/harbor-registry -c registry -- \
registry garbage-collect /etc/registry/config.yml
# 卸载(chart 名 harbor;PVC 默认保留,数据不丢)
helm -n harbor uninstall harbor
harbor-registry,元数据在 harbor-database,任务日志在 harbor-jobservice —— helm uninstall 不会删 PVC,误卸载后用相同 existingClaim 重装即可恢复;真要清空才手动 kubectl -n harbor delete pvc + 清节点目录 /data/harbor-local/*。完整走一遍 登录 → 拉取 → 打标 → 推送 → 验证,用最简单的 nginx:alpine 练手:
# ① 登录(在任一 docker 客户端机器)
docker login harbor.ergesanbaishou.com -u admin -p '你的密码'
# Login Succeeded
# ② 先在 UI 建好项目:base(公开/私有按需;已有则跳过)
# ③ 从公网拉一个现成镜像
docker pull nginx:alpine
# ④ 打标:格式必须是 <仓库地址>/<项目名>/<镜像名>:
docker tag nginx:alpine harbor.ergesanbaishou.com/base/nginx:v1-test
# ⑤ 推送
docker push harbor.ergesanbaishou.com/base/nginx:v1-test
# ⑥ 验证:删本地再从 Harbor 拉
docker rmi harbor.ergesanbaishou.com/base/nginx:v1-test
docker pull harbor.ergesanbaishou.com/base/nginx:v1-test
# UI 打开 项目 base → 仓库 nginx → 应能看到 tag v1-test
应用名-分支-构建号,如 aurora_ci:Algorithm-aurora_fcb_test-1875-GR3 —— 出问题能一眼定位是哪次构建推的。# ① UI 创建:项目 base → Robot 账户 → 添加机器人账号
# 名字如 jenkins-push → 权限只勾"推 artifact/拉取"(最小化)
# 生成后立即复制 token(只显示一次!)
# ② 账号格式:<机器人名>+<后缀>(UI 会给全名),密码就是生成的 token
docker login harbor.ergesanbaishou.com -u 'base+jenkins-push' -p '生成的token'
# ③ Jenkins:Manage Jenkins → Credentials → 添加 Username with password
# Username: base+jenkins-push Password: token
# Jenkinsfile 里:
# docker.withRegistry('https://harbor.ergesanbaishou.com', 'harbor-robot-cred') {
# sh 'docker push harbor.ergesanbaishou.com/base/aurora_ci:xxx'
# }
位置:UI → 项目 → 标签策略 → 标签保留。由 jobservice 定时(默认每日)按规则删旧 tag。多条规则是 OR 并集,命中任一即保留。
应用到仓库: 匹配 aurora_ci # ★ 只写项目内相对名,不带 base/ 前缀!
条件: 保留最近 # 个/天 被推送过的 artifacts
Tags: 匹配 Algorithm-aurora* # ★ tag 匹配用单星;** 是给仓库路径用的
无Tag: 可勾选(把 untagged 也纳入保留)
| 模板 | 含义 | 评价 |
|---|---|---|
| 保留最近 # 天内推送的 | 时间窗口,按 push 时间算 | 依赖时间字段,有隐性坑(见下) |
| 保留最近 # 个推送的 | 按新旧排序留前 N 个 | ⭐ CI 推荐:不依赖时间字段,行为可预期 |
| 坑 | 现象 | 正解 |
|---|---|---|
| glob 大小写敏感 | tag 是 Algorithm-*,pattern 写 algorithm-* → 不匹配 → 被删 | 逐字符核对大小写 |
tag 匹配误用 ** | 非独立段的 xx** 行为不可靠 | tag 用单星 xx*;** 只用于仓库名跨 / |
| 同 digest 重推不刷新 push_time | 内容没变的构建重推,artifact 的推送时间停在首次 → "7天内"判过期被删 | 改用数量型规则兜底(如留 500 个) |
# 规则页顶部 → 模拟运行(Dry Run)→ 看删除清单:
# 要保的 tag 出现在清单里 = 规则没罩住,继续改;不在 = 安全。
# 真实案例复盘:镜像 1875 于 18:42 推送成功,次日 08:00 被 harbor-jobservice 删除
# (审计日志可查:操作者 harbor-jobservice、资源 base/aurora_ci@sha256:xxx、operation=delete)
# → 时间型规则失效(上述坑之一)→ 加"保留最近 500 个"数量型规则兜底 → Dry Run 验证通过。
一次真实故障的完整复盘 —— Helm 部署完成后,Pod 状态如下,卡了整整 23 小时:
harbor-core-xxx-4vzfj 0/1 CrashLoopBackOff 232 ← 连锁反应
harbor-core-xxx-rsw8r 0/1 CrashLoopBackOff 233 ← 连锁反应
harbor-database-0 0/1 Pending 0 ← 源头①
harbor-jobservice-xxx-8wdcq 0/1 CrashLoopBackOff 280 ← 连锁反应
harbor-portal-xxx-7nbxp 1/1 Running 0
harbor-redis-0 0/1 Pending 0 ← 源头①
harbor-registry-xxx-gkhsf 0/2 Pending 0 ← 源头①
harbor-registry-xxx-jpl6x 0/2 Pending 0 ← 源头①
# 第 1 步:describe 看 Events,一锤定音
kubectl -n harbor describe pod harbor-database-0 | tail -5
# Warning FailedScheduling default-scheduler
# 0/1 nodes are available: persistentvolumeclaim "harbor-data-rwx" not found. not found
# ^^^^^^^^^^^^^^^^^^ Pod 要的 PVC 不存在!
# 第 2 步:列出实际 PVC,和 Pod 要的比对
kubectl -n harbor get pvc
# NAME STATUS VOLUME
# harbor-database Bound harbor-pv-database ← 实际建的是这 4 个
# harbor-jobservice Bound harbor-pv-jobservice
# harbor-redis Bound harbor-pv-redis
# harbor-registry Bound harbor-pv-registry
# 结论:values 里 existingClaim 写的是 harbor-data-rwx,和实际 PVC 名对不上!
# 第 3 步:CrashLoop 的 core 看上个崩溃日志,确认是连锁
kubectl -n harbor logs harbor-core-xxx-rsw8r --previous | tail -3
# [FATAL] [/core/main.go:170]: failed to initialize cache:
# retry timeout: dial tcp 10.108.219.116:6379: i/o timeout ← 连不上 redis
# ① 单个 Pod:sts 的 pod 名固定(harbor-database-0/redis-0),deploy 的带随机后缀(先 get pods 拿全名)
kubectl -n harbor logs -f harbor-database-0 # -f 实时滚动,排障最常用
kubectl -n harbor logs -f deploy/harbor-core # deploy 简写,自动挑一个副本,不用抄全名
kubectl -n harbor logs -f sts/harbor-database # sts 简写同理
kubectl -n harbor logs -f svc/harbor-portal # svc/ 简写也可(新版 kubectl 自动挑一个后端 Pod,本集群实测可用)
# ⚠ 踩坑实录:logs -f harbor-portal / harbor-portal* / harbor-portal-5d9f48888c 全报 NotFound,为什么?
# · 裸名字(不带资源前缀)kubectl 一律按 Pod 名查,而 Pod 要三段全名
# harbor-portal-5d9f48888c-7nbxp(deploy名-rs哈希-pod哈希),少一段都查不到;
# 想省事就带 deploy/ 或 svc/ 前缀(上面两行)
# · harbor-portal* 也不行:* 是 shell 通配符,kubectl 不展开,只拿字面量去查
# · 概念:Service 本身没有日志!它只是转发规则(iptables/IPVS + DNS 名),不跑进程;
# svc/xxx 能看到日志,是 kubectl 帮你解析到后端 Pod —— 日志永远产自 Pod 里的容器。
# 想知道某个 svc 背后挂着哪些 Pod,查 Endpoints:
kubectl get endpoints -n harbor harbor-portal # → 10.244.0.63:8080,10.244.0.64:8080(2 个后端)
# ⚠ 踩坑实录×2:deploy/harbior-redis 报 NotFound,两层原因?
# · 拼写:报错里原样回显了 "harbior",和要查的名字一对照就露馅;
# · 类型:就算拼对也不行 —— harbor-redis 不是 Deployment 是 StatefulSet!
# deploy/sts 名单一条命令全列出(名字就从这查):
kubectl get deploy,sts -n harbor
# Deployment(4): harbor-core harbor-jobservice harbor-portal harbor-registry
# StatefulSet(2): harbor-database harbor-redis
# · 从 Pod 名也能反推类型:两段 harbor-redis-0 = sts(名-序号);
# 三段 harbor-portal-5d9f48888c-7nbxp = deploy(名-rs哈希-pod哈希)
# · 懒人一屏看全(pod/svc/deploy/sts/rs 一次全出):
kubectl get all -n harbor
# 本环境速查:core/portal/registry/jobservice 用 deploy/xxx;database/redis 用 sts/xxx
kubectl -n harbor logs -f sts/harbor-redis # ← redis 的正确打开方式
# ② 进阶开关
kubectl -n harbor logs harbor-core-xxx --previous # 看上一次崩溃的日志(CrashLoop 必杀)
kubectl -n harbor logs harbor-registry-xxx -c registry # 多容器 Pod 指定容器(registry pod 里有 2 个)
kubectl -n harbor logs harbor-core-xxx --tail=100 # 只看最后 100 行
# ③ ★ 一条命令看整个命名空间全部 9 个 Pod(本集群实测可用)
# 按 label 选中所有 Pod;--prefix 每行标 [pod名/容器名];
# --max-log-requests 必须给够(默认 5,这里容器共 11 个,给 20)
kubectl logs -f -n harbor -l app.kubernetes.io/instance=harbor \
--prefix --all-containers --max-log-requests=20
# 输出效果(9 个 Pod 的日志汇成一条流,每行自带出处):
# [pod/harbor-portal-5d9f48888c-kb7tx/portal] 10.244.0.74 "GET / HTTP/1.1" 200
# [pod/harbor-core-5cf65fb78f-7hpdw/core] [ERROR] UserLogin: Invalid credentials
# [pod/harbor-database-0/database] LOG: checkpoint complete ...
# [pod/harbor-jobservice-5d55688bb7-sxqml/jobservice] [INFO] reap outdated job stats
duplicate key value violates unique constraint "properties_k_key" (skip_audit_log_database) already exists —— core/jobservice 每次启动都往 properties 表插这条配置,已存在就报一下,纯无害;后面跟着一串正常的 checkpoint complete 恰恰证明数据库是健康的。(若追求一条命令看全部的极致体验,可装 stern 工具:stern -n harbor .,本机未装)| 层 | 根因 | 修复 |
|---|---|---|
| ① 调度层 (Pending 23h) | values 四个 existingClaim 全写 harbor-data-rwx,该 PVC 从未创建;实际存在的是 4 个独立 PVC | strategic patch 三个工作负载,claimName 指回真实 PVC(见下) |
| ② 权限层 (initdb 失败) | 卷修好后 database 仍崩:initdb: could not create directory /var/lib/postgresql/data/pgdata: Permission denied —— hostPath 目录属主 root:root,postgres(uid 999) 写不进 | 节点上 chown 999:999 /data/harbor-local/{database,redis} + chown 10000:10000 /data/harbor-local/{registry,jobservice} |
| ③ 连锁层 (CrashLoop×3) | core/jobservice 启动时连 redis/database 超时,重试耗尽进程自杀 → 源头恢复后自动好转(退避最长 5 分钟,可删 Pod 立即拉起) | kubectl delete pod 跳过退避等待 |
# 修复① volumes 按 name 合并 patch,claimName 指回真实 PVC(注意转义)
kubectl -n harbor patch sts harbor-database --type=strategic \
-p '{"spec":{"template":{"spec":{"volumes":[{"name":"database-data",
"persistentVolumeClaim":{"claimName":"harbor-database"}}]}}}}'
kubectl -n harbor patch sts harbor-redis --type=strategic \
-p '{"spec":{"template":{"spec":{"volumes":[{"name":"data",
"persistentVolumeClaim":{"claimName":"harbor-redis"}}]}}}}'
kubectl -n harbor patch deploy harbor-registry --type=strategic \
-p '{"spec":{"template":{"spec":{"volumes":[{"name":"registry-data",
"persistentVolumeClaim":{"claimName":"harbor-registry"}}]}}}}'
# 修复② 节点目录属主(database/redis=999,registry/jobservice=10000)
chown 999:999 /data/harbor-local/database /data/harbor-local/redis
chown 10000:10000 /data/harbor-local/registry /data/harbor-local/jobservice
# 修复③ 源头就绪后,删 Pod 跳过 CrashLoop 退避
kubectl -n harbor delete pod harbor-core-xxx harbor-jobservice-xxx
# 验证:全部 1/1(或 registry 2/2),core API 健康
kubectl -n harbor get pods
curl -H "Host: harbor.ergesanbaishou.com" http://10.18.170.34:32588/api/v2.0/health
helm install 完第一件事就是 kubectl get pvc -n harbor 与 values 逐个比对helm upgrade 会被打回原形,故障复发# HTTP 仓库必须在 docker 客户端声明"不安全仓库"
vi /etc/docker/daemon.json
{
"insecure-registries": ["harbor.ergesanbaishou.com"]
}
systemctl restart docker # 改完必须重启 docker(注意影响本机容器)
# 改 values-harbor.yaml 里的 harborAdminPassword 后升级,密码即被重置
vi values-harbor.yaml # harborAdminPassword: "新密码"
helm -n harbor upgrade harbor harbor/harbor -f values-harbor.yaml
# 升级完用 admin + 新密码登录验证
UI 删除是软删除,需 GC(见第 4 节末尾),并确认 retention/GC 低峰执行。
UI → 日志 → 审计日志:过滤 资源 = 镜像 digest 或仓库名、操作 = 删除。看到 harbor-jobservice = 系统任务(retention/GC/复制),看到具体人名/Robot = 手动删。
kubectl create secret docker-registry harbor-cred \
--docker-server=harbor.ergesanbaishou.com \
--docker-username='base+jenkins-push' --docker-password='token' \
-n ergesanbaishou
# Pod spec: imagePullSecrets: [{name: harbor-cred}]
helm -n harbor uninstall harbor # ① 卸载 release
kubectl delete namespace harbor # ② 删命名空间(连带 Pod/STS/PVC/Ingress,等 1~2 分钟)
kubectl delete pv harbor-nfs-pv # ③ PV 不属于命名空间,必须单独删
rm -rf /data/nfs/harbor/* # ④ NFS 服务端清空旧数据(防旧 database 残留捣乱)
kubectl get ns harbor; kubectl get pv | grep harbor # ⑤ 都查无 = 清干净,重走第 2、3 节
showmount + 手动 mount)必须先通过;PVC 必须 Bound 才 helm install;values 五组件 existingClaim 一次配齐,避免再走 Pending 弯路。# 原因:手建 PVC 的名字(如 harbor-jobservice)与 chart 要建的 PVC 同名,
# Helm 3 拒绝接管"非自己创建"的资源 → 打上属主标记让它认领即可:
kubectl -n harbor label pvc harbor-jobservice app.kubernetes.io/managed-by=Helm
kubectl -n harbor annotate pvc harbor-jobservice \
meta.helm.sh/release-name=harbor meta.helm.sh/release-namespace=harbor
# 然后重试 helm install
helm uninstall 会连带删掉这个 PVC(数据在 PV/hostPath 目录里,目录内容不会被 PV 删除清空,重装可恢复)。kubectl -n harbor describe pod <pod名> | tail -20 # ① Events:挂卷/调度失败的真实原因
kubectl -n harbor get pvc # ② 卷是否 Bound?引用的 PVC 名真的存在?
kubectl -n harbor logs <pod名> --previous # ③ 上一次崩溃日志(连锁崩溃先查上游)
完全能并存,这台节点现在就是活例子:docker 与 containerd 双 active。当时连不上的真相:装集群当天 docker-ce 被卸载(dockerd 二进制没了)、服务被 mask,而 docker CLI 却残留着。排障 + 修复全过程:
# ① 三连诊断
kubectl get nodes -o wide # K8S 运行时 = containerd://2.2.1(1.24+ 直接用 containerd)
systemctl status docker # masked = 被人为屏蔽;unmask 后 start 仍失败就查日志:
sudo journalctl -u docker -n 20 | grep -i 'not present'
# → "/usr/bin/dockerd not present or not executable" = dockerd 包被卸了!
dpkg -l | grep -E 'docker|containerd' # docker-ce=rc(已卸载) docker-ce-cli=ii(残留) → 真相大白
# ② 修复:装 Ubuntu 官方 docker.io(依赖系统 containerd,不碰 K8S 运行时,零风险)
sudo systemctl unmask docker # 先解除屏蔽(装集群时人为设置的)
sudo dpkg -r docker-ce-cli # 卸掉残留 CLI(避免 /usr/bin/docker 文件冲突)
sudo apt-get install -y docker.io # v29.1.3,daemon+CLI 一起回来
sudo systemctl enable --now docker # 开机自启 + 立即启动
# ③ 验证"真并存":两个 daemon 都活,K8S 容器无感
docker ps # ✅ 正常返回(空列表)
systemctl is-active docker containerd # → active + active
sudo crictl ps | head -3 # K8S 容器照常 Running,全程零影响
containerd.io,安装时会替换 K8S 正在用的系统 containerd(unit 重启,容器虽不丢但有无谓风险);docker.io 依赖系统 containerd,与 K8S 天然共存。另记两个概念:K8S 1.24 起移除 dockershim,容器直接交 containerd 跑;docker ps/images 与 crictl ps/images 是两套视角 —— 排查 K8S 问题永远用 crictl,推镜像用 docker(第 5 节 login/tag/push 实操现已可在节点直接跑)。docker push harbor.ergesanbaishou.com/test/nginx:v1-test
3bf3671a4e6a: Unavailable
6638b436592c: Unavailable
error from registry: authorization header is required to send HEAD to repository
根因:Harbor 返回 401 —— 你还没 docker login。push 前的 HEAD 探测请求没带 Authorization 头,Harbor 拒绝;所有 layer 显示 Unavailable 也是同一原因。连接本身是通的,差一步登录:
docker login harbor.ergesanbaishou.com
# Username: admin ← Harbor Web 登录账号(不是邮箱)
# Password: ****** ← Web 界面能登的密码,忘记走 FAQ ②
# Login Succeeded ← 必须看到这个,凭证存 ~/.docker/config.json
docker push harbor.ergesanbaishou.com/test/nginx:v1-test # 再推即成
x509/HTTPS client → 先做 FAQ ① 的 insecure-registries;② push 改报 unauthorized to access repository: xxx, action: push → 常见是项目不存在(Harbor 报 unauthorized 而非 not found,admin 也被拒);但项目已存在仍报 → 多半是 core/registry 的 token 密钥不同步,走 FAQ ⑩.3;③ CI/Jenkins 别用个人密码 → 用第 6 节 Robot 账号。凭证有效、登录成功,但 push 的 HEAD 请求没带 Authorization 头 → 三连定位:
# ① 关键:不带凭证看 401 有没有 challenge 头
curl -si http://harbor.ergesanbaishou.com/v2/ | head -8
# 期望: HTTP/1.1 401 + Www-Authenticate: Bearer realm=".../service/token"
# 缺 Www-Authenticate = 中间层(外层nginx/ingress)吞头或 /v2 打错后端
# ② 绕过 docker 直接带 Basic 访问
curl -si -u admin:'密码' http://harbor.ergesanbaishou.com/v2/_catalog | head -3
# 200 = Harbor 本体和凭证都好,问题在链路/客户端
# ③ 清旧凭证重登再推
docker logout harbor.ergesanbaishou.com
docker login harbor.ergesanbaishou.com
docker push harbor.ergesanbaishou.com/test/nginx:v1-test
/v2/ 做了 301 强跳(重定向会剥 Authorization,registry 协议严禁 301,80 必须直接反代)或 server_name 不匹配落到了默认 server;②是 401 unauthorized → 凭证/项目权限;②是 200 而 ③ 仍报错 → 客户端凭证缓存脏,cat ~/.docker/config.json 看 credsStore,logout 清掉重登。curl -si http://harbor.ergesanbaishou.com/v2/ | head -8
HTTP/1.1 301 Moved Permanently ← 元凶!/v2/ 被强跳 https
Location: https://harbor.ergesanbaishou.com/v2/
事故链:80 上 return 301 https://... → docker push 的 HEAD 走 http 收到 301 → docker 跟随重定向时按 HTTP 规范剥掉 Authorization → Harbor 收到裸请求 → 401 "authorization header is required"。login 能成功是因为它重新做完整认证;push 是自动携带缓存凭证,301 后即丢。修复:harbor 域名 80 的 /v2/ 直接反代,其余路径才跳 https:
# /etc/nginx/conf.d/harbor.ergesanbaishou.com.conf —— 完整版(80+443,/v2/ 例外不 301)
upstream harbor_ergesanbaishou {
server 10.18.170.34:32588; # harbor 的 ingress NodePort(http)
keepalive 32;
}
# 80:/v2/ 直接反代(registry 协议严禁 301);其余路径跳 https 给 UI
server {
listen 80;
server_name harbor.ergesanbaishou.com;
location /v2/ { # ★ registry 入口,严禁 301
proxy_pass http://harbor_ergesanbaishou;
proxy_set_header Host $host; # ★ 灵魂行
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 0; # push 大镜像层不限
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location / { # 浏览器 UI 才跳 https
return 301 https://$host$request_uri;
}
}
# 443:SSL 终止,反代 UI/API
server {
listen 443 ssl;
server_name harbor.ergesanbaishou.com;
ssl_certificate /etc/nginx/ssl/ergesanbaishou.com.pem; # ← 你的证书路径
ssl_certificate_key /etc/nginx/ssl/ergesanbaishou.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL_harbor:10m;
access_log /var/log/nginx/harbor-ergesanbaishou.access.log;
error_log /var/log/nginx/harbor-ergesanbaishou.error.log;
client_max_body_size 0;
location / {
proxy_pass http://harbor_ergesanbaishou;
proxy_set_header Host $host; # ★ 灵魂行
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1; # WebSocket(可选)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
nginx -t && systemctl reload nginx
ss -lntp | grep -E ':80 |:443 ' # 80/443 都必须在监听
curl -si http://harbor.ergesanbaishou.com/v2/ | head -4 # 期望 401+Www-Authenticate
curl -ski https://harbor.ergesanbaishou.com/v2/ | head -4 # 同上(443 也要 401)
docker push harbor.ergesanbaishou.com/test/nginx:v1-test
nginx -t 报 duplicate upstream "harbor_ergesanbaishou" = 把 harbor 配置粘进了别的域名的 conf(如 k8s....conf),同名 upstream 定义了两次 → 一个域名一个 conf 文件,粘错的删掉;② push 改报 dial tcp x.x.x.x:443: connection refused = 301 把 docker 引向 https,但 443 根本没配(死链) → 证书 443 配好之前整站走 80 直反代,别写任何 return 301 https。上游只回 Server: nginx/1.18.0 (Ubuntu) 这种系统 nginx 头时,说明 301 是外层发的(ingress-nginx 的 Server 头无 Ubuntu 字样)。项目列表里明明有 test,push 仍报 unauthorized to access repository: test/nginx, action: push → 按序三板斧:
# ① 清旧凭证手动重登(别用 existing credentials,亲测关键一步)
docker logout harbor.ergesanbaishou.com
docker login harbor.ergesanbaishou.com # 手动输 admin/密码
docker push harbor.ergesanbaishou.com/test/nginx:v1-test
# ② 查 admin 对 test 的实际归属/权限
curl -sk -u admin:'密码' 'https://harbor.ergesanbaishou.com/api/v2.0/projects?name=test'
# ③ 终极解法:core 和 registry 一起重启(token 签名密钥不同步)
# 特征:项目存在、admin 是 owner、curl API 权限正常,push 仍 unauthorized;
# 且 push 输出里层状态 Pushing/Unavailable 混杂 = 轮询到不同 registry 副本(helm 升级后旧副本揣旧公钥)
kubectl -n harbor rollout restart deployment/harbor-registry deployment/harbor-core
kubectl -n harbor get pods # 全部 Running 后再 push
# ④ 验证 token 链路哪段断
curl -sk -u admin:'密码' 'https://harbor.../service/token?service=harbor-registry&scope=repository:test/nginx:pull,push' | head -c 200
# {"token":"eyJ...=签发正常(问题在registry验签);UNAUTHORIZED=core签发就拒
kubectl -n harbor logs deploy/harbor-registry -c registry --tail=30 | grep -iE 'token|auth|401'
json: cannot unmarshal bool into ... ProjectMetadata.metadata.public of type string = public 要字符串 "true"(带引号),不是布尔;且 API 必须走 https -k(http 的 /api/ 会被 301 挡成 HTML):
curl -sk -u admin:密码 -X POST https://harbor.ergesanbaishou.com/api/v2.0/projects -H 'Content-Type: application/json' -d '{"project_name":"test","metadata":{"public":"true"}}'(201=建成/409=已存在);嫌麻烦直接 UI 建。症状:手动 curl 拿 token 100% 成功(单/双 scope 都行,token 里 pull/push/delete 全有),docker/skopeo push 却必报 unauthorized to access repository: xxx, action: push;core 日志每次 push 必现 token.go:42 Unable to handle service。权限/项目/凭证/credsStore/双 core 副本/scope 顺序/多 scope 合并/OAuth2 —— 全查全排除。铁证在 nginx 双端口 access 日志:
# 443 日志: HEAD blobs 重试 404(token有效) + POST /v2/.../blobs/uploads/ 202 全过 ✓
# 80 日志: GET /service/token 200 拿到全权限 token ✓
# 但 PUT /v2/.../blobs/uploads/{uuid}?_state=...&digest=... 401/400/499 全挂 ✗
# → 同一 token:POST 走 443 全过,PUT 走 80 全灭
根因链:helm 的 externalURL=http:// 三连毒 —— ①challenge realm 是 http(token 靠 80 的 /service/ 直反代才拿到);②POST 202 响应的 Location 头是 http:// → docker 跟着它把镜像层 PUT 到 80 端口 → 该链路 PUT 被 registry 401。修复一行(外层 nginx 443 的 location /):
# 把上游吐出的 http Location 实时改写成 https → PUT 回到 202 全通的 443 链路
proxy_redirect ~^http://harbor\.ergesanbaishou\.com(:\d+)?(/.*)$ https://harbor.ergesanbaishou.com$2;
# nginx -t && nginx -s reload → docker push 立即出 digest
proxy_redirect 是外层银弹;③根治:helm upgrade 改 externalURL=https://harbor.ergesanbaishou.com;④成功输出里 "Not all multiplatform-content is present" 只是多平台镜像提示(仅推了本地平台架构),不是错误。