← 返回首页 K8S Web / 文档 / Harbor 安装与使用

🐳 Harbor 安装与使用 私有镜像仓库:K8S+Helm 部署(单节点 hostPath 独立 PVC)→ 流量链路 NGINX→Ingress→Svc→Pod → 日常运维 → 实战推送 → Robot 对接 CI → 标签保留 → Pending 23h 排障实录

00流量链路架构:NGINX → Ingress → Svc → Pod(实景)

Harbor 对外只有一个域名 harbor.ergesanbaishou.com,背后是五层接力。Host 请求头是贯穿全程的路由钥匙 —— 外层 NGINX 一旦不透传它,Ingress 立刻 404:

浏览器 / docker client(办公网) │ http://harbor.ergesanbaishou.com Host: harbor.ergesanbaishou.com ▼ ┌───────────────────────────────────────────────────┐ │ ① 外层 NGINX(办公网入口机) │ │ /etc/nginx/conf.d/harbor.ergesanbaishou.com.conf│ │ upstream → 10.18.170.34:32588(ingress NodePort) │ │ ★ proxy_set_header Host $host ← 灵魂行,丢了必404│ └────────────────────────┬──────────────────────────┘ ▼ NodePort 32588 ┌───────────────────────────────────────────────────┐ │ ② ingress-nginx(ns: ingress-nginx) │ │ svc ingress-harbor-nginx-controller │ │ ClusterIP 10.106.98.186 80:32588 443:31443 │ │ controller Pod ×1(node: qingfeng) │ └────────────────────────┬──────────────────────────┘ ▼ 按 Host 匹配规则 ┌───────────────────────────────────────────────────┐ │ ③ Ingress harbor-ingress(ns: harbor,class: nginx) │ │ /v2 /api/ /service/ /c/ → harbor-core :80 │← API/镜像协议 │ / → harbor-portal:80 │← UI 首页 └────────────────────────┬──────────────────────────┘ ▼ ClusterIP ┌───────────────────────────────────────────────────┐ │ ④ Service → Pod(ns: harbor,node: qingfeng) │ │ harbor-core 10.102.185.70:80 → ×2 双副本 │ │ harbor-portal 10.110.19.4:80 → ×2 双副本 │ │ harbor-registry 10.110.89.208:5000/8080 → ×2 │ │ (镜像推拉,由 core 反代 /v2 流量进入) │ └────────────────────────┬──────────────────────────┘ ▼ 仅集群内部调用 ┌───────────────────────────────────────────────────┐ │ ⑤ 内部依赖(不在公网流量链路上) │ │ harbor-database 5432(postgres,StatefulSet 单实例) │ │ harbor-redis 6379(缓存,StatefulSet 单实例) │ │ harbor-jobservice 80(retention/GC 定时任务) │ └───────────────────────────────────────────────────┘

环节① 外层 NGINX 反代配置(完整文件·80+443 终版,实测通过)

办公网机器访问域名 → 外层 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 Ingress 清单(k8s/harbor-ingress.yaml)

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}}}

链路验证(逐环节 curl)

# ③④ 环节:直连 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 报 "failed calling webhook validate.nginx.ingress.kubernetes.io"。ingress-nginx 的准入 webhook 引用的 service 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"}]'
💡 错误码速判:404 = Ingress 规则没匹配上(缺规则/Host 没透传);502/503 = 规则匹配了但后端 Pod 没 Ready;超时/拒绝 = 外层 upstream 端口或防火墙配错。

01Harbor 是什么

Harbor 是 VMware 开源的企业级 容器镜像仓库,本环境地址:https://harbor.ergesanbaishou.com。核心能力:

docker build → docker tag → docker push → Harbor(项目/仓库/tag) ↓ Jenkins/K8S 拉取 ← docker pull / imagePullPolicy

02安装准备(K8S 环境)

依赖要求检查命令
K8S 集群1.20+,预留约 2C / 3G(Harbor 全家桶资源)kubectl get nodes
Helmv3.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
⚠️ 为什么必须 RWX:registry 要跑 2 副本共用同一个 PVC,存储必须支持 ReadWriteMany(多节点同时读写)。默认 StorageClass 的 RWO 只允许单节点挂载,第二个副本会永远卡在 ContainerCreating。没有现成 NFS 可先单副本跑通,后续再迁。
💡 单节点测试可跳过 NFS:只有一个 node、双副本也都在同一节点时,不需要 NFS/RWX —— 用节点本地目录给每个组件建独立 hostPath PV(RWO 允许同节点多 Pod 挂载),后面 NFS 相关步骤全部跳过。
★ 先删旧 NFS PV: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

03安装步骤(K8S + Helm,四组件独立 PVC)

# ① 装 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(见下方说明)
⚠️ "2 个 Pod"的正确姿势:Harbor 不是一个单体应用,而是 7~8 个组件 Pod(core / portal / registry / jobservice / database / redis / trivy)。能双副本的是入口与核心服务(portal、core、registry) —— 它们无本地状态或状态在共享 PVC 上;database(postgreSQL)和 redis 是单主有状态服务,不能简单 ×2(要高可用得外置数据库/Redis 集群,内网环境没必要)。registry 双副本的前提正是共用一个 RWX PVC:两个 Pod 挂同一块存储,看到同一份镜像数据。
# ④ 安装
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)
💡 外网访问:与 k8s-web 同一条链路 —— 外层 NGINX 加一个 server_name harbor.ergesanbaishou.com 反代到 NodePort 32588(完整配置见 00 节),办公网即可用域名打开 UI、docker login 推拉镜像(别忘了客户端 insecure-registries,见第 8 节①)。

04日常运维命令(K8S / Helm)

# 状态总览
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
⚠️ 数据安全:镜像数据在 PVC harbor-registry,元数据在 harbor-database,任务日志在 harbor-jobservice —— helm uninstall 不会删 PVC,误卸载后用相同 existingClaim 重装即可恢复;真要清空才手动 kubectl -n harbor delete pvc + 清节点目录 /data/harbor-local/*。

05实战:模拟存一个简单的 nginx 镜像

完整走一遍 登录 → 拉取 → 打标 → 推送 → 验证,用最简单的 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
docker login → docker pull nginx:alpine → docker tag → harbor.ergesanbaishou.com/base/nginx:v1-test → docker push → UI 可见 → docker rmi + pull 回来验证 ✅
💡 tag 命名建议(CI 场景):应用名-分支-构建号,如 aurora_ci:Algorithm-aurora_fcb_test-1875-GR3 —— 出问题能一眼定位是哪次构建推的。

06CI 对接:用 Robot 账号,别用个人密码

⚠️ 铁律:Jenkins/GitLabCI 等机器凭据绝不用个人账号明文密码 —— 密码一改全线崩、泄露即全局失守、审计日志也分不清是人删的还是 CI 删的。一律用 Robot 账号。
# ① 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'
#    }
💡 好处:token 可随时吊销重发、权限限定单项目、审计日志显示为该 Robot,人员离职不牵连 CI。

07标签保留策略(Retention)—— 清旧镜像的双刃剑

位置:UI → 项目 → 标签策略 → 标签保留。由 jobservice 定时(默认每日)按规则删旧 tag。多条规则是 OR 并集,命中任一即保留。

规则三要素

应用到仓库: 匹配 aurora_ci        # ★ 只写项目内相对名,不带 base/ 前缀!
条件:      保留最近 # 个/天 被推送过的 artifacts
Tags:      匹配 Algorithm-aurora*   # ★ tag 匹配用单星;** 是给仓库路径用的
无Tag:      可勾选(把 untagged 也纳入保留)

数量型 vs 天数型(CI 场景选数量型)

模板含义评价
保留最近 # 天内推送的时间窗口,按 push 时间算依赖时间字段,有隐性坑(见下)
保留最近 # 个推送的按新旧排序留前 N 个⭐ CI 推荐:不依赖时间字段,行为可预期

三个真实踩坑(镜像"神秘消失"排查实录)

坑现象正解
glob 大小写敏感tag 是 Algorithm-*,pattern 写 algorithm-* → 不匹配 → 被删逐字符核对大小写
tag 匹配误用 **非独立段的 xx** 行为不可靠tag 用单星 xx*;** 只用于仓库名跨 /
同 digest 重推不刷新 push_time内容没变的构建重推,artifact 的推送时间停在首次 → "7天内"判过期被删改用数量型规则兜底(如留 500 个)

改完规则必做:Dry Run 模拟运行

# 规则页顶部 → 模拟运行(Dry Run)→ 看删除清单:
#   要保的 tag 出现在清单里 = 规则没罩住,继续改;不在 = 安全。
# 真实案例复盘:镜像 1875 于 18:42 推送成功,次日 08:00 被 harbor-jobservice 删除
#   (审计日志可查:操作者 harbor-jobservice、资源 base/aurora_ci@sha256:xxx、operation=delete)
#   → 时间型规则失效(上述坑之一)→ 加"保留最近 500 个"数量型规则兜底 → Dry Run 验证通过。

07.5排障实录:database/redis/registry Pending 23 小时

一次真实故障的完整复盘 —— 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   ← 源头①

排查三步(标准套路,10 分钟定位)

# 第 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 / Deployment / 一条命令看全部)

# ① 单个 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
💡 日志里这条 ERROR 不用慌: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 个独立 PVCstrategic 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

避坑清单(这次学到的)

08常见问题(FAQ)

① push 报 http: server gave HTTP response to HTTPS client

# HTTP 仓库必须在 docker 客户端声明"不安全仓库"
vi /etc/docker/daemon.json
{
  "insecure-registries": ["harbor.ergesanbaishou.com"]
}
systemctl restart docker     # 改完必须重启 docker(注意影响本机容器)

② 忘记 admin 密码(K8S 版)

# 改 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 = 手动删。

⑤ K8S 节点拉取私有镜像

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 节
💡 重装要点:NFS 自检两连(showmount + 手动 mount)必须先通过;PVC 必须 Bound 才 helm install;values 五组件 existingClaim 一次配齐,避免再走 Pending 弯路。

⑦ install 报 PVC "cannot be imported: missing key app.kubernetes.io/managed-by"

# 原因:手建 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 认领后,以后 helm uninstall 会连带删掉这个 PVC(数据在 PV/hostPath 目录里,目录内容不会被 PV 删除清空,重装可恢复)。

⑧ Pod 一直 Pending / CrashLoop 快速定位三连

kubectl -n harbor describe pod <pod名> | tail -20   # ① Events:挂卷/调度失败的真实原因
kubectl -n harbor get pvc                           # ② 卷是否 Bound?引用的 PVC 名真的存在?
kubectl -n harbor logs <pod名> --previous           # ③ 上一次崩溃日志(连锁崩溃先查上游)

⑨ 节点上 docker ps 连不上 —— K8S 和 Docker 不能并存吗?

完全能并存,这台节点现在就是活例子: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,全程零影响
💡 选型关键:别急着重装 docker-ce 官方包 —— 它强制依赖 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 实操现已可在节点直接跑)。

⑩ push 报 authorization header is required to send HEAD to repository(实测)

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   # 再推即成
⚠️ 三个易混点(均实测):① login 时若报 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 账号。

⑩.1 已 Login Succeeded 却仍报 authorization header(实测进阶)

凭证有效、登录成功,但 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
💡 判读:①缺头 → 检查外层 nginx harbor 这段 conf 是否对 /v2/ 做了 301 强跳(重定向会剥 Authorization,registry 协议严禁 301,80 必须直接反代)或 server_name 不匹配落到了默认 server;②是 401 unauthorized → 凭证/项目权限;②是 200 而 ③ 仍报错 → 客户端凭证缓存脏,cat ~/.docker/config.json 看 credsStore,logout 清掉重登。

⑩.2 元凶实录:80→443 的 301 强跳剥掉了 Authorization(实测定位)

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 字样)。

⑩.3 项目已存在仍 unauthorized + 建项目 API 的 422 坑(实测)

项目列表里明明有 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'
⚠️ 建项目 API 的 422: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 建。

⑩.4 push unauthorized 终章:POST 202 但 PUT 跟着 http Location 走 80 被杀(实测已解)

症状:手动 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
💡 教训四条:①排查 unauthorized 若手动 curl 全通,先看 nginx 双端口 access 日志分类各请求走哪个端口/状态码,别死磕权限;②registry 的 realm/Location/API 三个地址可能各自是 http,外层 TLS 终止场景 proxy_redirect 是外层银弹;③根治:helm upgrade 改 externalURL=https://harbor.ergesanbaishou.com;④成功输出里 "Not all multiplatform-content is present" 只是多平台镜像提示(仅推了本地平台架构),不是错误。