Rocky Linux 10 搭建企业级 npm 私服与 Docker 镜像仓库:Verdaccio、Harbor、Distribution Registry 部署与调优

企业研发体系中,制品通常不只有 Maven 的 JAR,还包括 npm、pnpm、Yarn 使用的 JavaScript/TypeScript 包,以及 Docker、Podman、containerd 和 Kubernetes 使用的 OCI 镜像。

如果所有构建节点都直接访问公网 npm Registry 和 Docker Hub,会逐渐暴露出以下问题:

  • 公网速度和稳定性不可控;
  • 大量机器重复下载同一份依赖和镜像;
  • 企业内部包和镜像缺少统一权限管理;
  • CI/CD 凭证分散,难以轮换;
  • 公共包被删除、篡改或版本漂移后难以复现构建;
  • Docker Hub Pull Rate Limit 影响流水线;
  • 镜像缺少漏洞扫描、签名、保留和清理策略;
  • 内网、弱网和离线环境无法稳定构建;
  • 私有包命名不规范时可能产生 Dependency Confusion;
  • 开发、测试和生产镜像缺少晋级与不可变策略。

本文在 Rocky Linux 10 上搭建两套企业制品服务:

1
2
3
4
5
6
Verdaccio:
提供 npm Registry、私有 npm 包发布和公共依赖代理缓存

Harbor:
提供 Docker/OCI 镜像仓库、项目级 RBAC、Robot Account、
Trivy 漏洞扫描、复制、Proxy Cache、Tag Retention 和 Immutable Tag

另外补充 CNCF Distribution Registry 3.1.1 的轻量部署方案,适合 HomeLab、临时流水线或只需要基础 Push/Pull 的环境。

本文编写时的版本基线为 Verdaccio 6.9.0、Harbor 2.15.2、Distribution Registry 3.1.1。生产部署前应重新查看 Release Notes,并固定到经过测试的 Patch 版本或镜像 Digest。

一、整体架构

本文使用以下示例:

服务 域名 内部端口 数据目录
Verdaccio npm.example.com 4873 /data/verdaccio
Harbor harbor.example.com 443 /data/harbor
Distribution Registry(可选) registry.example.com 5000 /data/registry
flowchart TD
    Dev[开发者] --> NPM[npm.example.com]
    CI[CI/CD] --> NPM
    Dev --> Harbor[harbor.example.com]
    CI --> Harbor
    K8S[Kubernetes / Docker Hosts] --> Harbor

    NPM --> Verdaccio[Verdaccio]
    Verdaccio --> NPMJS[registry.npmjs.org]
    Verdaccio --> NPMStorage[(npm Storage)]

    Harbor --> Core[Harbor Core]
    Harbor --> Registry[OCI Distribution]
    Harbor --> Trivy[Trivy Scanner]
    Harbor --> HarborDB[(PostgreSQL)]
    Harbor --> HarborRedis[(Redis)]
    Registry --> ImageStorage[(Image Storage)]

1.1 是否可以部署在同一台服务器

可以,但生产环境需要评估:

  • npm 包和容器镜像是否争抢同一块数据盘;
  • Harbor Trivy 扫描会占用 CPU、内存和磁盘;
  • Harbor 垃圾回收与 Verdaccio 备份是否同时执行;
  • 故障是否会导致所有制品服务一起不可用;
  • 容量增长是否可预测;
  • 是否需要不同的备份和恢复等级。

推荐:

1
2
3
4
5
6
7
小型团队 / HomeLab:
一台 Rocky Linux 10 服务器部署 Verdaccio + Harbor

中大型生产:
Verdaccio 与 Harbor 分开部署
Harbor 数据盘单独规划
制品备份存储与运行存储分离

二、为什么 npm 使用 Verdaccio、Docker 使用 Harbor

2.1 Verdaccio

Verdaccio 的特点:

  • 轻量;
  • npm、pnpm、Yarn 兼容;
  • 可代理 npmjs;
  • 自动缓存公共依赖;
  • 支持私有包;
  • 默认文件系统存储;
  • 支持 htpasswd 和插件化认证;
  • 适合中小团队与内部组件库。

Verdaccio 不适合直接被当作大规模多活分布式 npm 平台。默认文件系统存储也不适合多个实例同时写同一个普通共享目录。

2.2 Harbor

Harbor 在 OCI Distribution 基础上增加:

  • Web 管理界面;
  • 用户、项目和角色;
  • Robot Account;
  • 漏洞扫描;
  • Tag Retention;
  • Tag Immutability;
  • Proxy Cache;
  • 跨 Registry 复制;
  • LDAP/AD/OIDC;
  • 审计日志;
  • Cosign、Notation 等签名关联;
  • OCI Artifact 与 Helm Chart。

Harbor 更适合作为生产 Docker/OCI 私服。

2.3 Distribution Registry

Distribution Registry 适合:

  • 单机基础 Push/Pull;
  • 临时环境;
  • HomeLab;
  • 离线传输节点;
  • 只需要 Basic Auth 和 TLS;
  • 不需要 Web UI、RBAC、扫描和复制。

三、资源规划

3.1 Verdaccio

规模 CPU 内存 数据盘
测试 1 核 1 GB 20 GB SSD
小型团队 2 核 2~4 GB 100 GB SSD
中型团队 4 核 4~8 GB 300 GB 以上 SSD

3.2 Harbor

Harbor 单机 Compose 部署建议:

项目 最低 推荐
CPU 2 核 4 核以上
内存 4 GB 8 GB 以上
磁盘 40 GB 160 GB 以上

启用 Trivy、Proxy Cache、大量多架构镜像和 Build Cache 时,建议 8 核、16 GB 内存和独立 SSD/NVMe 数据盘。

3.3 容量估算

容器镜像 Layer 可以共享,因此实际容量取决于基础镜像复用率、架构数量、Tag、缓存、扫描数据库和垃圾回收策略。

建议初始容量:

1
2
当前估算制品容量 × 2
+ 30% 扩展余量

持续监控:

1
2
3
4
df -hT /data
df -ih /data
du -sh /data/verdaccio
du -sh /data/harbor

四、Rocky Linux 10 系统初始化

1
2
3
4
cat /etc/rocky-release
uname -r
uname -m
hostnamectl

设置主机名和时间:

1
2
3
4
5
hostnamectl set-hostname registry01.example.com

timedatectl set-timezone Asia/Shanghai
systemctl enable --now chronyd
chronyc tracking

更新系统:

1
2
3
dnf clean all
dnf makecache
dnf upgrade -y

安装工具:

1
2
3
dnf install -y \
vim curl wget jq tar gzip unzip rsync openssl \
httpd-tools bind-utils lsof policycoreutils-python-utils firewalld

保持 SELinux:

1
2
getenforce
sestatus

启动防火墙:

1
systemctl enable --now firewalld

五、安装 Docker Engine

卸载冲突包:

1
2
3
4
dnf remove -y \
docker docker-client docker-client-latest docker-common \
docker-latest docker-latest-logrotate docker-logrotate \
docker-engine podman-docker

添加仓库并安装:

1
2
3
4
5
6
7
8
9
dnf install -y dnf-plugins-core

dnf config-manager \
--add-repo \
https://download.docker.com/linux/rhel/docker-ce.repo

dnf install -y \
docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin

启动:

1
2
3
4
5
systemctl enable --now docker

docker version
docker compose version
docker info

不要开放未认证的 tcp://0.0.0.0:2375

5.1 Docker 日志轮转

创建或合并 /etc/docker/daemon.json

1
2
3
4
5
6
7
{
"log-driver": "local",
"log-opts": {
"max-size": "50m",
"max-file": "5"
}
}
1
2
jq . /etc/docker/daemon.json
systemctl restart docker

六、DNS 与域名

企业 DNS:

1
2
npm.example.com    -> 192.168.9.10
harbor.example.com -> 192.168.9.10

测试环境可临时配置:

1
2
3
4
cat >> /etc/hosts <<'EOF2'
192.168.9.10 npm.example.com
192.168.9.10 harbor.example.com
EOF2
1
2
getent hosts npm.example.com
getent hosts harbor.example.com

第一部分:搭建 Verdaccio npm 私服

七、Verdaccio 目录规划

本文使用 Verdaccio 6.9.0。

1
2
mkdir -p /opt/verdaccio
mkdir -p /data/verdaccio/{conf,storage,plugins,logs}

官方容器使用 UID 10001、GID 65533

1
2
3
chown -R 10001:65533 /data/verdaccio
chmod 0750 /data/verdaccio
chmod 0750 /data/verdaccio/{conf,storage,plugins}

八、配置 Verdaccio

创建 /data/verdaccio/conf/config.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
storage: /verdaccio/storage
plugins: /verdaccio/plugins

auth:
htpasswd:
file: /verdaccio/conf/htpasswd
max_users: -1
algorithm: bcrypt
rounds: 12

uplinks:
npmjs:
url: https://registry.npmjs.org/
timeout: 30s

packages:
# 企业私有 Scope 不配置 proxy,降低 Dependency Confusion 风险。
'@company/*':
access: $authenticated
publish: $authenticated
unpublish: npm-admin

# 其他 Scoped Package 从 npmjs 代理。
'@*/*':
access: $authenticated
publish: npm-admin
unpublish: npm-admin
proxy: npmjs

# 非 Scoped 公共包。
'**':
access: $authenticated
publish: npm-admin
unpublish: npm-admin
proxy: npmjs

middlewares:
audit:
enabled: true

security:
api:
jwt:
sign:
expiresIn: 15d
notBefore: 0
web:
sign:
expiresIn: 1h
notBefore: 0

server:
keepAliveTimeout: 60

publish:
check_owners: true
keep_readmes: tagged

web:
enable: true
title: Company npm Registry
scope: '@company'
login: true
showInfo: true
showSettings: true
showFooter: true

userRateLimit:
windowMs: 900000
max: 100

log:
type: stdout
format: json
level: http

权限:

1
2
chown 10001:65533 /data/verdaccio/conf/config.yaml
chmod 0640 /data/verdaccio/conf/config.yaml

8.1 Dependency Confusion 防护

企业私有包统一使用 @company/*,并且私有 Scope 不配置 proxy: npmjs。这样本地不存在 @company/payment-sdk 时,Verdaccio 不会去公网寻找同名包。

8.2 禁止自助注册

1
max_users: -1

用户由管理员创建,以便审批、回收和审计。

九、创建 Verdaccio 用户

1
2
3
4
5
6
7
8
9
10
11
htpasswd -B -C 12 -c \
/data/verdaccio/conf/htpasswd \
npm-admin

htpasswd -B -C 12 \
/data/verdaccio/conf/htpasswd \
alice

htpasswd -B -C 12 \
/data/verdaccio/conf/htpasswd \
ci-publisher
1
2
3
chown 10001:65533 /data/verdaccio/conf/htpasswd
chmod 0640 /data/verdaccio/conf/htpasswd
cut -d: -f1 /data/verdaccio/conf/htpasswd

删除用户:

1
htpasswd -D /data/verdaccio/conf/htpasswd alice

删除用户不一定会立即撤销已签发 Token,应结合 Token 有效期和 CI Secret 回收。

十、Verdaccio Docker Compose

创建 /opt/verdaccio/compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
services:
verdaccio:
image: verdaccio/verdaccio:6.9.0
container_name: verdaccio
restart: unless-stopped

environment:
VERDACCIO_PORT: "4873"
NODE_OPTIONS: "--max-old-space-size=1536"

ports:
- "127.0.0.1:4873:4873"

volumes:
- /data/verdaccio/conf:/verdaccio/conf:Z
- /data/verdaccio/storage:/verdaccio/storage:Z
- /data/verdaccio/plugins:/verdaccio/plugins:Z

healthcheck:
test:
- CMD-SHELL
- wget -qO- http://127.0.0.1:4873/-/ping | grep -q '{}'
interval: 30s
timeout: 5s
retries: 5
start_period: 20s

security_opt:
- no-new-privileges:true

ulimits:
nofile:
soft: 65536
hard: 65536

mem_limit: 2g
cpus: 2.0

启动:

1
2
3
4
5
6
7
8
cd /opt/verdaccio

docker compose config
docker compose pull
docker compose up -d

docker compose ps
docker compose logs -f verdaccio

验证:

1
2
curl -fsS http://127.0.0.1:4873/-/ping
curl -I http://127.0.0.1:4873/

十一、为 Verdaccio 配置 NGINX HTTPS

1
2
dnf install -y nginx
systemctl enable --now nginx

证书路径:

1
2
/etc/nginx/ssl/npm.example.com.crt
/etc/nginx/ssl/npm.example.com.key

创建 /etc/nginx/conf.d/npm.example.com.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
limit_req_zone $binary_remote_addr zone=verdaccio_login:10m rate=2r/s;

upstream verdaccio_backend {
server 127.0.0.1:4873;
keepalive 32;
}

server {
listen 80;
server_name npm.example.com;
return 301 https://$host$request_uri;
}

server {
listen 443 ssl;
http2 on;
server_name npm.example.com;

ssl_certificate /etc/nginx/ssl/npm.example.com.crt;
ssl_certificate_key /etc/nginx/ssl/npm.example.com.key;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:NPM_SSL:20m;
ssl_session_timeout 1d;

client_max_body_size 100m;

access_log /var/log/nginx/npm-access.log;
error_log /var/log/nginx/npm-error.log warn;

location = /healthz {
access_log off;
proxy_pass http://verdaccio_backend/-/ping;
proxy_set_header Host $host;
}

location ~ ^/(-/user|-/v1/login|-/npm/v1/user) {
limit_req zone=verdaccio_login burst=10 nodelay;

proxy_pass http://verdaccio_backend;
proxy_http_version 1.1;

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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Connection "";
}

location / {
proxy_pass http://verdaccio_backend;
proxy_http_version 1.1;

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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Connection "";

proxy_connect_timeout 5s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
}

允许 NGINX 连接后端:

1
2
3
setsebool -P httpd_can_network_connect 1
nginx -t
systemctl reload nginx

防火墙:

1
2
3
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload

验证:

1
2
curl -fsS https://npm.example.com/-/ping
curl -I https://npm.example.com/

十二、配置 npm、pnpm 和 Yarn 客户端

12.1 只让企业 Scope 走私服

1
2
3
npm config set \
@company:registry \
https://npm.example.com/

项目 .npmrc

1
@company:registry=https://npm.example.com/

12.2 所有包都走 Verdaccio

1
npm config set registry https://npm.example.com/
1
registry=https://npm.example.com/

12.3 登录

1
2
3
4
5
6
npm login \
--registry=https://npm.example.com/ \
--auth-type=legacy

npm whoami \
--registry=https://npm.example.com/

不要将包含 _authToken 的用户级 .npmrc 提交到 Git。

12.4 CI/CD

项目 .npmrc

1
2
3
@company:registry=https://npm.example.com/
//npm.example.com/:_authToken=${NPM_TOKEN}
always-auth=true

CI 平台保存 NPM_TOKEN,不要写入 Dockerfile、Git、构建日志或镜像层。

BuildKit Secret 示例:

1
2
3
4
5
6
7
# syntax=docker/dockerfile:1.7
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
COPY . .
RUN npm run build
1
2
3
4
docker buildx build \
--secret id=npmrc,src="$HOME/.npmrc" \
-t example-app:1.0.0 \
.

12.5 pnpm

pnpm 直接读取 .npmrc

1
pnpm install

12.6 Yarn Berry

.yarnrc.yml

1
2
3
4
5
npmScopes:
company:
npmRegistryServer: "https://npm.example.com"
npmAlwaysAuth: true
npmAuthToken: "${NPM_TOKEN}"

十三、发布企业 npm 包

1
2
3
mkdir company-utils
cd company-utils
npm init --scope=@company

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "@company/utils",
"version": "1.0.0",
"description": "Company shared utilities",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist", "README.md"],
"publishConfig": {
"registry": "https://npm.example.com/",
"access": "restricted"
},
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build && npm test"
}
}

发布:

1
npm publish

查询和安装:

1
2
3
4
npm view @company/utils \
--registry=https://npm.example.com/

npm install @company/utils

预发布版本:

1
2
npm publish --tag beta
npm install @company/utils@beta

已经发布的版本尽量保持不可变,修复时发布新版本,而不是覆盖旧版本。

十四、Verdaccio 调优与备份

14.1 存储和 inode

1
2
3
4
df -hT /data/verdaccio
df -ih /data/verdaccio
du -sh /data/verdaccio/storage
find /data/verdaccio/storage -type f | wc -l

推荐 SSD/NVMe、独立数据盘,不建议多个 Verdaccio 实例同时写同一个普通 NFS 目录。

Verdaccio 自己已经缓存 npmjs 包,不建议在 NGINX 再无差别缓存 Registry API。重点监控 DNS、TLS、npmjs 响应时间和 429/5xx。

14.3 冷备份

1
2
3
4
5
6
7
8
9
10
11
cd /opt/verdaccio
docker compose stop verdaccio

mkdir -p /backup/verdaccio

tar --xattrs --acls \
-czf "/backup/verdaccio/verdaccio-$(date +%F-%H%M%S).tar.gz" \
-C /data \
verdaccio

docker compose start verdaccio

恢复:

1
2
3
4
5
6
7
8
9
10
11
12
cd /opt/verdaccio
docker compose down

mv /data/verdaccio \
"/data/verdaccio.before-restore-$(date +%F-%H%M%S)"

tar -xzf /backup/verdaccio/verdaccio-时间.tar.gz -C /data

chown -R 10001:65533 /data/verdaccio
restorecon -Rv /data/verdaccio

docker compose up -d

验证:

1
2
npm view @company/utils \
--registry=https://npm.example.com/

只备份配置不备份 Storage,等于只保存了仓库门牌,却把货全丢了。

第二部分:搭建 Harbor Docker/OCI 私服

十五、Harbor 安装前规划

本文使用 Harbor 2.15.2,采用官方 Offline Installer 和 Docker Compose 部署。

Harbor 单机安装器包含多个组件:

1
2
3
4
5
6
7
8
9
10
nginx
harbor-core
harbor-portal
harbor-jobservice
registry
registryctl
PostgreSQL
Redis
Trivy Adapter
log

单机 Compose 部署是一个单主机系统。不要通过 docker compose scale 粗暴扩容 Harbor 组件,否则数据库、Registry、Job Service 和共享存储会变成一场状态管理事故。

需要真正高可用时,推荐使用 Harbor Helm Chart 部署到 Kubernetes,并使用:

  • 外部 PostgreSQL;
  • 外部 Redis;
  • S3、对象存储或共享存储;
  • 多副本 Core、Portal、Jobservice、Registry;
  • 外部负载均衡;
  • 完整监控和备份。

十六、创建 Harbor 数据盘

1
2
3
4
mkdir -p /opt/harbor
mkdir -p /data/harbor
mkdir -p /data/cert
mkdir -p /backup/harbor

推荐将 /data/harbor 放在独立 SSD、NVMe、RAID 或可靠云盘上。

检查:

1
2
3
4
lsblk -f
findmnt /data
df -hT /data
df -ih /data

Harbor Registry 数据不应放在系统根分区。根分区被镜像写满以后,Harbor 会停止工作,Docker 也可能停止工作,顺便让日志系统一起参加故障。

十七、下载并验证 Harbor 安装包

定义版本:

1
2
3
export HARBOR_VERSION='2.15.2'
export HARBOR_TAG="v${HARBOR_VERSION}"
export HARBOR_ARCHIVE="harbor-offline-installer-${HARBOR_TAG}.tgz"

下载:

1
2
3
4
5
6
7
cd /opt

curl -fLO \
"https://github.com/goharbor/harbor/releases/download/${HARBOR_TAG}/${HARBOR_ARCHIVE}"

curl -fLO \
"https://github.com/goharbor/harbor/releases/download/${HARBOR_TAG}/${HARBOR_ARCHIVE}.sigstore.json"

从 Harbor 2.15.0 开始,Release Artifact 使用 Cosign 签名。安装 Cosign 2.x 后验证:

1
2
3
4
5
6
7
cosign verify-blob \
--bundle "${HARBOR_ARCHIVE}.sigstore.json" \
--certificate-oidc-issuer \
https://token.actions.githubusercontent.com \
--certificate-identity-regexp \
'^https://github.com/goharbor/harbor/.github/workflows/publish_release.yml@refs/tags/v.*$' \
"${HARBOR_ARCHIVE}"

预期:

1
Verified OK

验证失败时不要继续安装。重新确认:

  • 下载 URL;
  • Release Tag;
  • Bundle 文件;
  • 系统时间;
  • Cosign 版本;
  • 文件是否被代理或镜像站替换。

解压:

1
2
tar -xzf "${HARBOR_ARCHIVE}" -C /opt
cd /opt/harbor

检查:

1
ls -lah

十八、配置 Harbor TLS 证书

生产环境优先使用企业 CA、公开 CA 或 ACME 签发证书。

证书 SAN 必须包含:

1
DNS:harbor.example.com

18.1 实验环境生成内部 CA

1
2
3
4
5
6
7
8
9
10
11
mkdir -p /root/harbor-pki
cd /root/harbor-pki

openssl genrsa -out company-root-ca.key 4096
chmod 0600 company-root-ca.key

openssl req \
-x509 -new -sha512 -days 3650 \
-key company-root-ca.key \
-out company-root-ca.crt \
-subj '/C=CN/O=Example Corporation/OU=Infrastructure/CN=Example Internal Root CA'

生成 Harbor 私钥和 CSR:

1
2
3
4
5
6
7
8
9
10
11
openssl genrsa \
-out harbor.example.com.key \
3072

chmod 0600 harbor.example.com.key

openssl req \
-sha512 -new \
-key harbor.example.com.key \
-out harbor.example.com.csr \
-subj '/C=CN/O=Example Corporation/OU=Infrastructure/CN=harbor.example.com'

扩展:

1
2
3
4
5
6
7
cat > harbor.example.com.ext <<'EOF2'
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:harbor.example.com,IP:192.168.9.10
EOF2

部分 OpenSSL 版本不接受独立的 digitalSignature= 配置。如果报错,请删除该行,只保留 keyUsage

签发:

1
2
3
4
5
6
7
8
openssl x509 \
-req -sha512 -days 825 \
-extfile harbor.example.com.ext \
-CA company-root-ca.crt \
-CAkey company-root-ca.key \
-CAcreateserial \
-in harbor.example.com.csr \
-out harbor.example.com.crt

检查:

1
2
3
openssl x509 \
-in harbor.example.com.crt \
-noout -subject -issuer -dates -ext subjectAltName

复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
install -o root -g root -m 0644 \
company-root-ca.crt \
/data/cert/company-root-ca.crt

install -o root -g root -m 0644 \
harbor.example.com.crt \
/data/cert/harbor.example.com.crt

install -o root -g root -m 0600 \
harbor.example.com.key \
/data/cert/harbor.example.com.key

restorecon -Rv /data/cert

十九、配置 harbor.yml

1
2
3
cd /opt/harbor
cp harbor.yml.tmpl harbor.yml
vim harbor.yml

生产基础配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
hostname: harbor.example.com

http:
port: 80

https:
port: 443
certificate: /data/cert/harbor.example.com.crt
private_key: /data/cert/harbor.example.com.key

harbor_admin_password: "替换为高强度管理员密码"

database:
password: "替换为高强度数据库密码"
max_idle_conns: 100
max_open_conns: 900
conn_max_lifetime: 5m
conn_max_idle_time: 0

data_volume: /data/harbor

trivy:
ignore_unfixed: false
skip_update: false
skip_java_db_update: false
offline_scan: false
security_check: vuln
insecure: false

google_analytics_account:

jobservice:
max_job_workers: 10
logger_sweeper_duration: 14

notification:
webhook_job_max_retry: 3

log:
level: info
local:
rotate_count: 10
rotate_size: 200M
location: /var/log/harbor

_version: 2.15.0

proxy:
http_proxy:
https_proxy:
no_proxy:
components:
- core
- jobservice
- trivy

upload_purging:
enabled: true
age: 168h
interval: 24h
dryrun: false

cache:
enabled: false
expire_hours: 24

注意:

  • hostname 必须与客户端使用的域名一致;
  • 不要把 hostname 写成 https://harbor.example.com
  • 密码不能使用简单默认值;
  • data_volume 必须在可靠数据盘;
  • _version 由模板控制,不要随意改成不存在的 Schema 版本;
  • 代理环境应正确填写 no_proxy,至少排除 Harbor 自身域名和内部容器通信;
  • harbor.yml 包含明文密码,权限应设置为 0600
1
chmod 0600 /opt/harbor/harbor.yml

19.1 Trivy 离线环境

完全离线时:

1
2
3
4
trivy:
skip_update: true
skip_java_db_update: true
offline_scan: true

但这并不会凭空产生漏洞数据库。你仍需通过受控流程导入和更新 Trivy DB,否则扫描结果会逐渐过时。

二十、安装 Harbor

先检查 Docker:

1
2
docker version
docker compose version

运行安装器并启用 Trivy:

1
2
cd /opt/harbor
./install.sh --with-trivy

查看:

1
2
docker compose ps
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'

日志:

1
2
3
4
5
docker compose logs -f core

docker compose logs -f registry

docker compose logs -f trivy-adapter

验证:

1
2
3
4
5
curl -I https://harbor.example.com/

curl -fsS \
https://harbor.example.com/api/v2.0/health \
| jq

浏览器访问:

1
https://harbor.example.com

初始用户:

1
admin

密码为 harbor.yml 中的 harbor_admin_password

首次登录后立即:

  1. 修改管理员密码;
  2. 创建普通管理员或运维用户;
  3. 创建项目;
  4. 创建 Robot Account;
  5. 配置扫描、保留和不可变规则;
  6. 检查邮件、LDAP/OIDC 或审计设置。

二十一、客户端信任 Harbor CA

将 CA 安装到 Rocky Linux 系统信任:

1
2
3
4
5
cp \
/data/cert/company-root-ca.crt \
/etc/pki/ca-trust/source/anchors/company-root-ca.crt

update-ca-trust

Docker Daemon 信任目录:

1
2
3
4
5
6
7
8
mkdir -p \
/etc/docker/certs.d/harbor.example.com

cp \
/data/cert/company-root-ca.crt \
/etc/docker/certs.d/harbor.example.com/ca.crt

systemctl restart docker

如果 Harbor 使用非 443 端口:

1
/etc/docker/certs.d/harbor.example.com:8443/ca.crt

登录:

1
docker login harbor.example.com

不要用:

1
2
3
{
"insecure-registries": ["harbor.example.com"]
}

去绕过证书问题。应修复 CA、SAN、DNS 和系统时间。

二十二、创建 Harbor 项目和账号

建议项目:

1
2
3
4
5
6
base
platform
middleware
applications
sandbox
proxy-dockerhub

角色建议:

角色 用途
Project Admin 项目配置、成员、策略
Maintainer 推送、拉取和管理 Artifact
Developer 推送和拉取
Guest 只读并可查看详情
Limited Guest 更受限的只读
Robot Account CI/CD、节点拉取或跨系统集成

不要让 CI 使用 admin

22.1 Robot Account

为应用项目创建:

1
robot$application-ci

赋予:

1
2
Pull Repository
Push Repository

生产节点只拉取时创建:

1
robot$production-puller

只授予 Pull。

Robot Secret 只在创建时完整展示,应立即保存到 CI Secret 或 Vault,不要贴到群聊和流水线日志。

二十三、推送和拉取镜像

登录:

1
docker login harbor.example.com

拉取基础镜像:

1
docker pull nginx:1.30.4-alpine

重新打 Tag:

1
2
3
docker tag \
nginx:1.30.4-alpine \
harbor.example.com/base/nginx:1.30.4-alpine

推送:

1
2
docker push \
harbor.example.com/base/nginx:1.30.4-alpine

拉取:

1
2
docker pull \
harbor.example.com/base/nginx:1.30.4-alpine

查看 Digest:

1
2
3
4
docker inspect \
harbor.example.com/base/nginx:1.30.4-alpine \
--format '{{json .RepoDigests}}' \
| jq

生产部署最好使用 Digest:

1
harbor.example.com/base/nginx@sha256:...

Tag 是指针,可以变化;Digest 才是具体内容身份。

二十四、多架构镜像

创建 Builder:

1
2
3
4
5
docker buildx create \
--name company-builder \
--use

docker buildx inspect --bootstrap

构建并推送:

1
2
3
4
5
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t harbor.example.com/applications/example-app:1.0.0 \
--push \
.

检查 Manifest:

1
2
docker buildx imagetools inspect \
harbor.example.com/applications/example-app:1.0.0

多架构镜像会增加 Artifact、Manifest 和 Layer 数量,保留与 GC 策略必须考虑所有架构。

二十五、Harbor 漏洞扫描

安装时启用:

1
./install.sh --with-trivy

Harbor UI 中可以:

  • 手工 Scan;
  • Push 后自动扫描;
  • 定时扫描全部 Artifact;
  • 查看 CVE、严重级别和修复版本;
  • 阻止存在高风险漏洞的 Artifact 被拉取。

25.1 扫描不是运行时安全

Trivy 静态扫描不能替代:

  • 基础镜像升级;
  • SBOM;
  • 签名与来源验证;
  • Runtime Security;
  • Kubernetes Admission Policy;
  • Secret 扫描;
  • Dockerfile 最小化;
  • 容器非 Root;
  • 定期重建镜像。

扫描报告也会因漏洞数据库更新而变化,同一个镜像今天可能是绿色,明天可能突然被新 CVE 点亮。

25.2 调优

Trivy 扫描消耗 CPU、内存、网络和磁盘。建议:

  • 扫描任务错峰;
  • max_job_workers 不要盲目调大;
  • 大型 Java 镜像预留更多扫描时间;
  • 监控 Trivy DB 更新时间;
  • 避免所有历史镜像每天同时扫描;
  • 高峰构建期间降低扫描并发。

二十六、Tag 不可变和保留策略

26.1 Immutable Tag

建议正式项目设置:

1
release-*、v*、语义版本 Tag 不允许覆盖

例如:

1
2
3
1.0.0
v1.0.0
release-20260801

允许变化的 Tag:

1
2
3
4
dev
snapshot
nightly
latest

即便允许 latest 变化,生产部署仍应使用版本或 Digest。

26.2 Retention Policy

示例:

1
2
3
4
5
保留最近 30 个 dev Tag
保留最近 10 个 nightly Tag
保留最近 20 个非 Release 构建
永久保留所有正式语义版本
保留最近 90 天被拉取过的镜像

先 Dry Run,确认不会删除重要 Artifact,再正式执行。

26.3 Retention 与 Garbage Collection

二者职责不同:

1
2
3
4
5
Retention:
删除不需要的 Manifest、Tag 或 Artifact 引用

Garbage Collection:
回收已经没有任何引用的 Blob 物理空间

正确顺序:

1
2
3
4
Retention
-> 等待删除任务完成
-> Garbage Collection Dry Run
-> Garbage Collection

只执行 GC 不设置 Retention,绝大多数镜像仍有引用,磁盘不会凭空瘦身。

二十七、Proxy Cache

Harbor 可创建 Proxy Cache Project,代理 Docker Hub 或其他兼容 Registry。

示例项目:

1
proxy-dockerhub

拉取 Docker Hub 的 NGINX:

1
2
docker pull \
harbor.example.com/proxy-dockerhub/library/nginx:1.30.4-alpine

第一次:

1
Harbor -> Docker Hub -> 下载并缓存

以后:

1
客户端 -> Harbor 本地缓存

注意:

  • Proxy Cache 项目不能当普通 Push 项目使用;
  • 仍需遵守上游服务条款和限流;
  • 使用上游私有账号时,代理项目必须严格授权;
  • Harbor 2.15.0 曾有 Proxy Cache 已知问题,本文选择 2.15.2 Patch 版本;
  • 升级前仍应查看当前 Patch 的 Known Issues。

二十八、Harbor Garbage Collection

在 Harbor UI:

1
2
3
Administration
-> Clean Up
-> Garbage Collection

推荐:

  1. 先 Dry Run;
  2. 检查预计回收空间;
  3. 在低峰期执行;
  4. 控制 Worker 数量;
  5. 监控 Registry 和 Job Service;
  6. 不在大量 Push 时同时执行高强度 GC。

不要手工删除:

1
/data/harbor/registry/docker/registry/v2/blobs

Blob 被多个镜像共享。手工删目录可能让多个镜像同时损坏,破坏方式很高效,恢复方式却一点都不高效。

二十九、Harbor 备份与恢复

Harbor Docker Compose 单机部署最可靠、最容易验证的是冷备份。

备份对象:

1
2
3
4
5
6
/opt/harbor
/data/harbor
/data/cert
Harbor CA 私钥的离线备份
Docker 配置
DNS 和防火墙配置

29.1 冷备份

先在 Harbor 管理界面设置 Read Only,停止新的 Push、删除和策略变更。

停止:

1
2
cd /opt/harbor
docker compose down

不要加:

1
-v

备份:

1
2
3
4
5
6
7
8
9
10
11
mkdir -p /backup/harbor

tar --xattrs --acls \
-czf "/backup/harbor/harbor-config-$(date +%F-%H%M%S).tar.gz" \
/opt/harbor \
/data/cert

tar --xattrs --acls \
-czf "/backup/harbor/harbor-data-$(date +%F-%H%M%S).tar.gz" \
-C /data \
harbor

启动:

1
2
cd /opt/harbor
docker compose up -d

验证:

1
2
3
curl -fsS \
https://harbor.example.com/api/v2.0/health \
| jq

退出 Read Only。

29.2 恢复

恢复到相同 Harbor 版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cd /opt/harbor
docker compose down

mv /data/harbor \
"/data/harbor.before-restore-$(date +%F-%H%M%S)"

mkdir -p /data/harbor

tar -xzf /backup/harbor/harbor-data-时间.tar.gz -C /data

tar -xzf /backup/harbor/harbor-config-时间.tar.gz -C /

restorecon -Rv /data/harbor /data/cert /opt/harbor

cd /opt/harbor
./prepare
docker compose up -d

验证:

1
2
3
4
docker login harbor.example.com

docker pull \
harbor.example.com/base/nginx:1.30.4-alpine

备份必须实际恢复演练。没恢复过的备份,严格来说只是对未来表达了良好愿望。

三十、Harbor 升级

升级前:

  1. 阅读目标版本 Release Notes;
  2. 检查升级路径;
  3. 备份 /opt/harbor/data/harbor
  4. 保留旧安装器;
  5. 在测试环境验证;
  6. 进入维护窗口;
  7. 设置 Read Only。

大致流程:

1
2
cd /opt/harbor
docker compose down

解压新安装器到新目录,将旧 harbor.yml 复制过去,然后迁移配置:

1
2
3
4
5
docker run -it --rm \
-v /:/hostfs \
goharbor/prepare:v2.15.2 \
migrate \
-i /opt/harbor/harbor.yml

再执行:

1
./install.sh --with-trivy

Harbor 支持升级和升级失败后的回滚流程,但不支持随意 Downgrade。数据库 Schema 一旦升级,不能只换回旧镜像就当一切没发生。

三十一、Harbor 调优

31.1 数据盘

推荐:

  • SSD/NVMe;
  • 独立逻辑卷;
  • 预留 20%~30% 空间;
  • 监控 inode;
  • 不使用延迟和一致性未知的网络挂载;
  • 备份盘与运行盘分离。
1
2
3
4
df -hT /data/harbor
df -ih /data/harbor
iostat -xz 1
iotop

31.2 Job Worker

1
2
jobservice:
max_job_workers: 10

更多 Worker 意味着更多并发:

  • 扫描;
  • 复制;
  • GC;
  • Retention;
  • Webhook;
  • 数据库连接;
  • 磁盘 I/O。

不要只看队列长就把 Worker 从 10 改成 200。先判断瓶颈在 CPU、数据库、Trivy、存储还是上游网络。

31.3 数据库连接

单机 Harbor 内置 PostgreSQL。max_open_conns 不应无限增大,应结合:

  • Core 副本数;
  • Job Worker;
  • PostgreSQL max_connections
  • 管理和监控连接;
  • 查询延迟;
  • 事务等待。

31.4 日志

Harbor 日志路径:

1
/var/log/harbor

检查:

1
2
du -sh /var/log/harbor
find /var/log/harbor -type f -printf '%s %p\n' | sort -n | tail

31.5 不要将 Docker Build Cache 永久堆在 Harbor

BuildKit Cache 可以推送到 OCI Registry:

1
2
3
4
5
6
7
8
docker buildx build \
--cache-to \
type=registry,ref=harbor.example.com/cache/example-app:buildcache,mode=max \
--cache-from \
type=registry,ref=harbor.example.com/cache/example-app:buildcache \
-t harbor.example.com/applications/example-app:1.0.0 \
--push \
.

Cache 项目应有独立 Retention,否则缓存会比正式镜像更擅长占磁盘。

第三部分:Distribution Registry 轻量 Docker 私服

三十二、适用场景

如果只需要:

1
2
3
4
5
Docker Push
Docker Pull
TLS
Basic Auth
本地文件存储

可以部署 CNCF Distribution Registry 3.1.1。

它不提供 Harbor 的:

  • 项目级 RBAC;
  • Robot Account;
  • Web UI;
  • Trivy 扫描;
  • Tag Retention;
  • Immutable Tag;
  • LDAP/OIDC;
  • 跨 Registry 策略复制;
  • 审计和治理界面。

三十三、创建 Registry 目录

1
2
mkdir -p /opt/distribution-registry
mkdir -p /data/registry/{data,auth,certs}

创建用户:

1
2
3
4
5
6
7
htpasswd -B -C 12 -c \
/data/registry/auth/htpasswd \
registry-admin

htpasswd -B -C 12 \
/data/registry/auth/htpasswd \
ci-publisher

权限:

1
2
3
chmod 0750 /data/registry
chmod 0750 /data/registry/{data,auth,certs}
chmod 0640 /data/registry/auth/htpasswd

复制证书:

1
2
/data/registry/certs/registry.example.com.crt
/data/registry/certs/registry.example.com.key

三十四、Registry 配置

创建 /opt/distribution-registry/config.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
version: 0.1

log:
level: info
formatter: json

storage:
filesystem:
rootdirectory: /var/lib/registry

delete:
enabled: true

cache:
blobdescriptor: inmemory

maintenance:
uploadpurging:
enabled: true
age: 168h
interval: 24h
dryrun: false

http:
addr: :5000

headers:
X-Content-Type-Options:
- nosniff

tls:
certificate: /certs/registry.example.com.crt
key: /certs/registry.example.com.key

debug:
addr: 127.0.0.1:5001
prometheus:
enabled: true
path: /metrics

auth:
htpasswd:
realm: Company Registry
path: /auth/htpasswd

health:
storagedriver:
enabled: true
interval: 10s
threshold: 3

三十五、Registry Compose

创建 /opt/distribution-registry/compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
services:
registry:
image: registry:3.1.1
container_name: distribution-registry
restart: unless-stopped

ports:
- "5000:5000"
- "127.0.0.1:5001:5001"

volumes:
- ./config.yml:/etc/distribution/config.yml:ro,Z
- /data/registry/data:/var/lib/registry:Z
- /data/registry/auth:/auth:ro,Z
- /data/registry/certs:/certs:ro,Z

healthcheck:
test:
- CMD-SHELL
- wget --no-check-certificate -qO- https://127.0.0.1:5000/v2/ >/dev/null || exit 1
interval: 30s
timeout: 5s
retries: 5

security_opt:
- no-new-privileges:true

ulimits:
nofile:
soft: 65536
hard: 65536

mem_limit: 2g
cpus: 2.0

启动:

1
2
3
4
5
cd /opt/distribution-registry

docker compose config
docker compose pull
docker compose up -d

防火墙:

1
2
firewall-cmd --permanent --add-port=5000/tcp
firewall-cmd --reload

验证:

1
2
curl -u registry-admin \
https://registry.example.com:5000/v2/

正确结果:

1
{}

客户端信任 CA:

1
2
3
4
5
6
7
mkdir -p \
/etc/docker/certs.d/registry.example.com:5000

cp company-root-ca.crt \
/etc/docker/certs.d/registry.example.com:5000/ca.crt

systemctl restart docker

登录与推送:

1
2
3
4
5
6
7
8
9
10
docker login registry.example.com:5000

docker pull busybox:1.37

docker tag \
busybox:1.37 \
registry.example.com:5000/base/busybox:1.37

docker push \
registry.example.com:5000/base/busybox:1.37

三十六、Distribution Registry 垃圾回收

Distribution Registry GC 是 Mark and Sweep。

必须先让 Registry 进入 Read Only 或完全停止,否则正在上传的 Layer 可能被错误回收,造成镜像损坏。

36.1 Dry Run

停止写入:

1
2
cd /opt/distribution-registry
docker compose stop registry

执行 Dry Run:

1
2
3
4
5
6
7
docker run --rm \
-v /opt/distribution-registry/config.yml:/etc/distribution/config.yml:ro,Z \
-v /data/registry/data:/var/lib/registry:Z \
registry:3.1.1 \
garbage-collect \
--dry-run \
/etc/distribution/config.yml

执行正式 GC:

1
2
3
4
5
6
7
docker run --rm \
-v /opt/distribution-registry/config.yml:/etc/distribution/config.yml:ro,Z \
-v /data/registry/data:/var/lib/registry:Z \
registry:3.1.1 \
garbage-collect \
--delete-untagged \
/etc/distribution/config.yml

启动:

1
docker compose start registry

仅删除 Tag 不一定删除 Manifest。只有 Manifest 引用被删除,Blob 才有机会被 GC。

三十七、Distribution Registry Pull-through Cache

Docker Hub Pull-through Cache 配置:

1
2
3
proxy:
remoteurl: https://registry-1.docker.io
ttl: 168h

如果配置 Docker Hub 私有账号:

1
2
3
4
5
proxy:
remoteurl: https://registry-1.docker.io
username: dockerhub-user
password: dockerhub-token
ttl: 168h

必须严格保护该 Mirror。否则这个账号能访问的 Docker Hub 私有镜像可能通过 Mirror 暴露给其他用户。

Docker Daemon:

1
2
3
4
5
{
"registry-mirrors": [
"https://mirror.example.com"
]
}
1
2
jq . /etc/docker/daemon.json
systemctl restart docker

Docker Daemon 的 Pull-through Mirror 主要面向 Docker Hub,不能把它当成任意 Registry 的通用透明镜像代理。

第四部分:客户端、CI/CD 与 Kubernetes

三十八、CI/CD 登录 Harbor

不要在命令行直接写明文密码:

1
2
3
4
printf '%s' "${HARBOR_ROBOT_SECRET}" \
| docker login harbor.example.com \
--username "${HARBOR_ROBOT_USERNAME}" \
--password-stdin

构建并推送:

1
2
3
4
5
6
7
IMAGE="harbor.example.com/applications/example-app:${GIT_COMMIT_SHA}"

docker buildx build \
--platform linux/amd64 \
-t "${IMAGE}" \
--push \
.

流水线完成后可以:

1
docker logout harbor.example.com

CI 凭证应使用 Robot Account,且只授予目标项目所需的 Push/Pull 权限。

三十九、Kubernetes 使用 Harbor

39.1 containerd 信任 CA

在每个节点安装 CA:

1
2
3
4
cp company-root-ca.crt \
/etc/pki/ca-trust/source/anchors/company-root-ca.crt

update-ca-trust

containerd 证书目录:

1
2
3
4
5
mkdir -p \
/etc/containerd/certs.d/harbor.example.com

cp company-root-ca.crt \
/etc/containerd/certs.d/harbor.example.com/ca.crt

创建 /etc/containerd/certs.d/harbor.example.com/hosts.toml

1
2
3
4
5
server = "https://harbor.example.com"

[host."https://harbor.example.com"]
capabilities = ["pull", "resolve", "push"]
ca = "/etc/containerd/certs.d/harbor.example.com/ca.crt"

确认 /etc/containerd/config.toml 启用了 Registry Config Path:

1
2
3
4
version = 2

[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"

不同 containerd 版本的 Plugin 路径可能不同,应先执行:

1
2
containerd config dump
containerd --version

重启:

1
systemctl restart containerd

测试:

1
2
3
ctr -n k8s.io images pull \
--user 'robot$production-puller:SECRET' \
harbor.example.com/base/nginx:1.30.4-alpine

39.2 ImagePullSecret

1
2
3
4
5
kubectl create secret docker-registry harbor-pull \
--docker-server=harbor.example.com \
--docker-username='robot$production-puller' \
--docker-password='ROBOT_SECRET' \
--namespace=production

Pod:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: nginx
namespace: production
spec:
imagePullSecrets:
- name: harbor-pull
containers:
- name: nginx
image: harbor.example.com/base/nginx:1.30.4-alpine

不要把 Robot Secret 直接写在 Git 中。推荐使用 External Secrets、Vault、Sealed Secrets 或 GitOps Secret 管理方案。

四十、npm 与 Harbor 联合流水线

flowchart LR
    Git[Git Commit] --> CI[CI Pipeline]
    CI --> NPMInstall[npm ci from Verdaccio]
    NPMInstall --> Test[Test / Lint / Build]
    Test --> Image[Build OCI Image]
    Image --> HarborPush[Push to Harbor]
    HarborPush --> Scan[Trivy Scan]
    Scan --> Policy{Policy Pass?}
    Policy -- Yes --> Deploy[Deploy by Digest]
    Policy -- No --> Stop[Block Release]

示例流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
set -Eeuo pipefail

npm ci
npm test
npm run build

IMAGE="harbor.example.com/applications/example-app:${GIT_COMMIT_SHA}"

docker buildx build \
--secret id=npmrc,src="$HOME/.npmrc" \
--platform linux/amd64,linux/arm64 \
-t "${IMAGE}" \
--push \
.

部署阶段通过 Harbor API 或工具取得 Digest,再以 Digest 发布。

第五部分:监控、调优与故障排查

四十一、健康检查

41.1 Verdaccio

1
2
curl -fsS \
https://npm.example.com/-/ping

41.2 Harbor

1
2
3
curl -fsS \
https://harbor.example.com/api/v2.0/health \
| jq

41.3 Distribution Registry

未认证时通常返回 401 也能说明 HTTPS 和 Registry API 已响应:

1
2
curl -I \
https://registry.example.com:5000/v2/

认证:

1
2
curl -u registry-admin \
https://registry.example.com:5000/v2/

四十二、建议监控指标

Verdaccio

1
2
3
4
5
6
7
8
9
10
进程/容器健康
HTTP 2xx/4xx/5xx
登录失败
npm publish 成功率
npm install P95/P99
npmjs Uplink 延迟和错误
Storage 容量
inode
Node.js RSS 与 Event Loop
备份时间和结果

Harbor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
组件健康
Registry Push/Pull 成功率
Core API 延迟
PostgreSQL 连接与查询延迟
Redis 状态
Job Queue 长度
Trivy DB 更新时间
扫描任务成功率
复制任务状态
Retention 和 GC 状态
Blob 存储容量
审计日志
证书有效期
Robot Account 过期时间

Registry

1
2
3
4
5
6
7
/v2/ 可用性
Push/Pull 错误率
存储容量
上传临时目录
GC 结果
HTTP 401/5xx
证书有效期

四十三、系统调优

43.1 文件句柄

Docker 容器配置:

1
2
3
4
ulimits:
nofile:
soft: 65536
hard: 65536

宿主机:

1
2
3
4
5
6
7
cat > /etc/sysctl.d/99-registry.conf <<'EOF2'
fs.file-max = 2097152
vm.swappiness = 1
net.core.somaxconn = 4096
EOF2

sysctl --system

不要复制大量来历不明的 TCP 参数。Registry 的瓶颈更常见于存储、网络、数据库、扫描任务和上游限流。

43.2 数据盘

推荐:

  • 独立 SSD/NVMe;
  • XFS 或经过验证的 ext4;
  • 预留 20% 以上空间;
  • 监控 inode;
  • 持续观察磁盘延迟;
  • 避免备份、GC、扫描和大量 Push 同时执行。
1
2
3
4
iostat -xz 1
iotop
df -hT /data
df -ih /data

43.3 网络

镜像分发对网络吞吐敏感:

1
2
3
sar -n DEV 1
ss -s
ethtool eth0

大量节点同时拉取时:

  • 分批发布;
  • 使用 Proxy Cache;
  • 就近部署 Harbor;
  • 多数据中心使用复制;
  • 考虑对象存储重定向;
  • 避免所有节点在整点同时更新。

四十四、常见问题:Verdaccio

44.1 容器无法读取 config.yaml

1
2
docker compose logs verdaccio
ls -lnZ /data/verdaccio/conf

检查:

  • UID 10001;
  • GID 65533;
  • :Z
  • 文件名必须为 config.yaml
  • YAML 缩进;
  • 父目录执行权限。

44.2 npm login 失败

1
2
3
npm login \
--registry=https://npm.example.com/ \
--auth-type=legacy

检查:

1
2
3
npm config get registry
npm config get @company:registry
curl -I https://npm.example.com/-/ping

44.3 403 Publish Forbidden

检查:

  • 包名是否是 @company/*
  • 当前用户是否已登录;
  • npm whoami
  • publishConfig.registry
  • Verdaccio packages 顺序;
  • 当前包 Owner;
  • check_owners

44.4 包找不到

1
2
3
npm view @company/utils \
--registry=https://npm.example.com/ \
--loglevel verbose

检查本地 Storage、Web UI、Scope 配置和客户端 .npmrc

44.5 公共包下载慢

检查:

1
2
curl -I https://registry.npmjs.org/react
getent hosts registry.npmjs.org

以及 Verdaccio 日志、DNS、代理出口和 npmjs 限流。

四十五、常见问题:Harbor

45.1 x509: certificate signed by unknown authority

1
2
3
4
openssl s_client \
-connect harbor.example.com:443 \
-servername harbor.example.com \
-showcerts

检查:

1
2
ls -l /etc/docker/certs.d/harbor.example.com/ca.crt
update-ca-trust check

常见原因:

  • CA 没安装;
  • SAN 不含 Harbor 域名;
  • 客户端使用 IP,但证书只有 DNS;
  • 系统时间错误;
  • 证书链不完整;
  • 非 443 端口目录名缺少端口。

45.2 docker login 返回 401

检查:

  • 用户名;
  • Robot Account 名称中的 $ 是否被 Shell 展开;
  • Secret;
  • 项目权限;
  • Harbor Authentication Mode;
  • Token Service;
  • 系统时间。

Robot 用户名应加单引号:

1
2
3
docker login \
--username 'robot$application-ci' \
harbor.example.com

45.3 Push Denied

1
denied: requested access to the resource is denied

检查镜像名称必须包含 Harbor Project:

1
harbor.example.com/applications/example-app:1.0.0

而不是:

1
harbor.example.com/example-app:1.0.0

同时检查 Robot 是否有 Push 权限、Tag 是否不可变、Project 是否超出 Quota。

45.4 Manifest Unknown

常见原因:

  • Tag 不存在;
  • Retention 已删除;
  • Proxy Cache 路径错误;
  • 多架构 Manifest 未完整推送;
  • GC 或手工删除造成损坏;
  • 项目名错误。
1
2
docker buildx imagetools inspect \
harbor.example.com/applications/example-app:1.0.0

45.5 Harbor UI 正常但 Push 超时

检查:

  • NGINX 上传限制;
  • Registry 日志;
  • 数据盘;
  • 网络 MTU;
  • 防火墙;
  • 客户端代理;
  • Harbor no_proxy
  • Docker Daemon 日志。
1
2
3
journalctl -u docker -n 300 --no-pager
cd /opt/harbor
docker compose logs -f registry core nginx

45.6 Trivy 扫描失败

1
docker compose logs -f trivy-adapter

检查:

  • 漏洞 DB 网络;
  • 代理;
  • DNS;
  • 数据目录;
  • 内存;
  • Java DB;
  • 离线配置;
  • Job Worker。

45.7 磁盘没有在删除镜像后释放

正确流程:

1
2
3
4
删除 Tag/Artifact 或执行 Retention
-> 等待任务完成
-> GC Dry Run
-> GC

Blob 仍被其他 Manifest 引用时不能回收。

45.8 Harbor 重启后组件不健康

1
2
3
cd /opt/harbor
docker compose ps
docker compose logs --tail=300

检查:

  • 数据盘权限;
  • 磁盘满;
  • PostgreSQL;
  • Redis;
  • 证书;
  • harbor.yml
  • Docker 网络;
  • 升级迁移是否失败。

四十六、常见问题:Distribution Registry

46.1 Basic Auth + HTTP 无法使用

Distribution Registry 不支持在不安全 HTTP 上可靠使用 Basic Auth。生产必须配置 TLS。

46.2 405 Method Not Allowed 删除失败

需要:

1
2
3
storage:
delete:
enabled: true

重启 Registry 后生效。

46.3 GC 后镜像损坏

常见原因是在 Registry 仍允许上传时执行 Stop-the-world GC。

GC 前必须 Read Only 或停止 Registry,并先 Dry Run。

四十七、安全检查清单

Verdaccio

  • 使用固定 6.9.0 或经过验证的版本;
  • 私有包统一使用企业 Scope;
  • 私有 Scope 不配置 npmjs Proxy;
  • 禁止匿名注册;
  • 发布必须认证;
  • Unpublish 仅管理员;
  • Token 有过期时间;
  • HTTPS 正常;
  • .npmrc Token 未提交 Git;
  • CI 使用独立账号;
  • Storage 和 .verdaccio-db 已备份。

Harbor

  • 使用固定 2.15.2 或经过验证的 Patch;
  • 安装器 Cosign 验证通过;
  • HTTPS 证书 SAN 正确;
  • 管理员密码已修改;
  • CI 不使用 admin;
  • 使用 Robot Account;
  • 正式 Tag 不可变;
  • 配置 Retention;
  • 定期 GC Dry Run;
  • Trivy DB 正常更新;
  • 扫描策略已启用;
  • 项目 Quota 已配置;
  • 数据盘和 inode 已监控;
  • 备份和恢复已演练;
  • 升级前设置 Read Only 并备份。

Distribution Registry

  • 使用固定 3.1.1;
  • 使用 TLS;
  • 启用 Basic Auth;
  • 数据目录持久化;
  • GC 前停止写入;
  • 不手工删除 Blob;
  • CA 已部署到客户端;
  • 已配置备份。

四十八、生产选型建议

场景 推荐方案
npm 私有组件库 Verdaccio
npm 公共依赖代理缓存 Verdaccio
小型 Docker 私服 Distribution Registry
企业 Docker/OCI 私服 Harbor
需要漏洞扫描 Harbor + Trivy
需要 LDAP/OIDC、RBAC Harbor
需要跨数据中心复制 Harbor
需要高可用 Docker Registry Harbor Helm + Kubernetes
纯离线临时镜像中转 Distribution Registry

四十九、总结

在 Rocky Linux 10 上搭建 npm 和 Docker 私服,核心并不是简单启动两个容器,而是建立完整的制品治理链路:

1
2
3
4
5
6
7
8
9
10
11
源码
-> 依赖下载
-> 构建
-> 测试
-> npm 包或 OCI 镜像发布
-> 权限与签名
-> 漏洞扫描
-> 保留策略
-> 部署
-> 审计
-> 备份恢复

核心原则:

  1. npm 私有包使用企业 Scope;
  2. 私有 Scope 不代理公网,降低 Dependency Confusion 风险;
  3. Verdaccio 禁止匿名注册并使用短生命周期 Token;
  4. npm Token 使用 CI Secret 和 BuildKit Secret;
  5. Docker 生产私服优先选择 Harbor;
  6. CI 使用 Robot Account,不使用 admin;
  7. 正式镜像使用不可变 Tag,并优先按 Digest 部署;
  8. 先 Retention,再 Garbage Collection;
  9. Trivy 扫描不能替代镜像升级、签名和运行时安全;
  10. Harbor Compose 是单主机部署,不要靠 compose scale 伪造高可用;
  11. Distribution Registry GC 前必须停止写入;
  12. 制品仓库必须进行真实备份和恢复演练。

私服平时只是构建日志里一行不起眼的下载地址;一旦它出问题,前端装不了依赖、后端构建不了镜像、Kubernetes 拉不到版本,整个流水线会非常团结地一起停工。

参考资料


Rocky Linux 10 搭建企业级 npm 私服与 Docker 镜像仓库:Verdaccio、Harbor、Distribution Registry 部署与调优
https://allendericdalexander.github.io/2026/08/01/devops/linux/rocky/rocky-linux-10-npm-docker-private-registry/
作者
AtLuoFu
发布于
2026年8月1日
许可协议