本篇内容,我们来聊聊使用开源软件 Verdaccio 搭建轻量的 NPM 私有仓库。

写在前面

最近折腾项目,经常遇到需要进行前端构建的需求。

其实在几年前,也因为 CI/CD 的需求,写过一些和软件仓库相关的实践,不过马上都 2025 年了,或许应有更新、更简单的方案。

为什么需要私有 NPM 仓库?

在实际开发中,我们经常会遇到以下场景:

  1. 需要管理企业内部的私有包,避免核心代码泄露
  2. 希望降低对公共 NPM 仓库的依赖,提升安装速度
  3. 想要对第三方包进行定制化修改
  4. 需要在内网环境下确保依赖包的可用性

Verdaccio 恰好能够完美解决这些问题。

关于轻量级 NPM 开源仓库:Verdaccio

Verdaccio 是一个轻量级的私有 NPM 包管理工具,项目创建以来接受了来自 docker、crowdin、netlify、jetbrains、algolia、SheetJs、GatsbyJs、pintura 等知名项目和组织的支持。

核心特点:

  1. 零配置开箱即用,自带轻量级数据库,不需要额外的数据库支持
  2. 支持代理和缓存公共仓库(npmjs.org),可以加快包的下载速度
  3. 内置用户认证和私有包权限管理,适合企业和团队内部使用
  4. 支持扩展存储方案,可以对接 S3、Google Cloud Storage 等
  5. 适合用于前端项目的端到端测试
  6. 资源占用少,易于部署和维护

主要使用场景:

  1. 企业私有包管理:比如公司内部的通用组件库、工具库等,可以通过 Verdaccio 进行私密发布和管理,避免将源码发布到公网。
  2. 加速包下载:通过缓存机制,只需从公共仓库下载一次包,后续可直接使用本地缓存,大大提升安装速度。
  3. 离线开发环境:在内网环境下,可以使用 Verdaccio 搭建本地仓库,确保依赖包的可用性。
  4. 测试和调试:很多开源项目如 create-react-app、babel.js 等都使用 Verdaccio 进行端到端测试。

软件的基本使用方法:

# 私有仓库服务端
# 安装
npm install -g verdaccio
# 启动服务
verdaccio


# 用户端使用
# 配置 registry
npm set registry http://localhost:4873/
# 创建用户
npm adduser --registry http://localhost:4873
# 发布包
npm publish --registry http://localhost:4873

Verdaccio 的特点是轻量、简单、易用,特别适合中小型团队使用。它不仅可以管理私有包,还能作为公共 NPM 仓库的缓存层,提升团队的开发效率。

实践部署:面向本地开发场景

因为 Verdaccio 足够轻量,所以除了能够服务私有网络中的开发之外,还可以实现本地开发前端项目时的依赖包安装加速。

1. 基础环境准备

首先,确保服务器已安装 Node.js 18 或更高版本:

# 确认版本
# node -v

v22.11.0

我使用的是 v22.11.0 版本的 Node.js,如果你已经安装了 NVM,可以通过下面的命令,快速下载安装这个版本的 Node.js:

# NVM_NODEJS_ORG_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/nodejs-release/ nvm install v22.11.0

Downloading and installing node v22.11.0...
Downloading https://mirrors.tuna.tsinghua.edu.cn/nodejs-release//v22.11.0/node-v22.11.0-darwin-arm64.tar.xz...
################################################################################################################ 100.0%
Computing checksum with shasum -a 256
Checksums matched!
Now using node v22.11.0 (npm v10.9.0)

然后,将系统中的 Node 版本切换为刚刚下载后的版本:

# nvm use v22.11.0

Now using node v22.11.0 (npm v10.9.0)

# nvm alias default 22.11.0
default -> 22.11.0 (-> v22.11.0)

2. 安装软件

软件的安装很简单,我们可以借助国内的 NPM 加速镜像来完成工具的下载:

npm install --registry=https://registry.npmmirror.com -g verdaccio

3. 使用软件

想要使用软件,只需要执行 verdaccio 这个命令:

# verdaccio

(node:39844) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
info --- config file  - /Users/soulteary/.config/verdaccio/config.yaml
info --- the "crypt" algorithm is deprecated consider switch to "bcrypt" in the configuration file. Read the documentation for additional details
info --- using htpasswd file: /Users/soulteary/.config/verdaccio/htpasswd
info --- plugin successfully loaded: verdaccio-htpasswd
info --- plugin successfully loaded: verdaccio-audit
warn --- http address - http://localhost:4873/ - verdaccio/6.0.2

当服务启动后,我们就可以通过访问 http://localhost:4873/ 来使用它了。

默认的 WebUI 界面

4. 默认配置

默认情况下,程序会使用系统用户目录中的默认配置,如:

/Users/soulteary/.config/verdaccio/config.yaml

默认配置文件如下(我进行了一些翻译):

#
# 这是默认配置文件。
#
# 查看更多配置文件示例:
# https://github.com/verdaccio/verdaccio/tree/6.x/conf
#
# 阅读最佳实践
# https://verdaccio.org/docs/best

# 存储所有包的目录路径
storage: ./storage
# 包含插件的目录路径
plugins: ./plugins

# https://verdaccio.org/docs/webui
web:
  title: Verdaccio
  # 注释此行以禁用 gravatar 支持
  # gravatar: false
  # 默认情况下包按升序排列 (asc|desc)
  # sort_packages: asc
  # 将您的 UI 转换为暗色模式
  # darkMode: true
  # html_cache: true
  # 默认情况下显示所有功能
  # login: true
  # showInfo: true
  # showSettings: true
  # 结合 darkMode,您可以强制使用特定主题
  # showThemeSwitch: true
  # showFooter: true
  # showSearch: true
  # showRaw: true
  # showDownloadTarball: true
  # 在manifest <scripts/> 之后注入的 HTML 标签
  # scriptsBodyAfter:
  #    - '<script type="text/javascript" src="https://my.company.com/customJS.min.js"></script>'
  # 在 </head> 结束前注入的 HTML 标签
  # metaScripts:
  #    - '<script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>'
  #    - '<script type="text/javascript" src="https://browser.sentry-cdn.com/5.15.5/bundle.min.js"></script>'
  #    - '<meta name="robots" content="noindex" />'
  # 在 <body/> 的第一个子元素处注入的 HTML 标签
  # bodyBefore:
  #    - '<div id="myId">html before webpack scripts</div>'
  # 模板manifest脚本的公共路径(仅manifest)
  # publicPath: http://somedomain.org/

# https://verdaccio.org/docs/configuration#authentication
auth:
  htpasswd:
    file: ./htpasswd
    # 允许注册的最大用户数,默认为 "+inf"。
    # 您可以将其设置为 -1 以禁用注册。
    # max_users: 1000
    # 哈希算法,可选项为:"bcrypt"、"md5"、"sha1"、"crypt"。
    # algorithm: bcrypt # 默认为 crypt,但建议新安装使用 bcrypt
    # "bcrypt" 的轮数,对其他算法将被忽略。
    # rounds: 10

# https://verdaccio.org/docs/configuration#uplinks
# 可以连接的其他已知仓库列表
uplinks:
  npmjs:
    url: https://registry.npmjs.org/

# 了解如何保护您的包
# https://verdaccio.org/docs/protect-your-dependencies/
# https://verdaccio.org/docs/configuration#packages
packages:
  '@*/*':
    # 作用域包
    access: $all
    publish: $authenticated
    unpublish: $authenticated
    proxy: npmjs

  '**':
    # 允许所有用户(包括未认证用户)读取和发布所有包
    #
    # 您可以指定用户名/组名(取决于您的认证插件)
    # 和三个关键字:"$all"、"$anonymous"、"$authenticated"
    access: $all

    # 允许所有已知用户发布包
    # (默认情况下任何人都可以注册,请注意!)
    publish: $authenticated
    unpublish: $authenticated

    # 如果包在本地不可用,则将请求代理到 'npmjs' 注册表
    proxy: npmjs

# 为提高安全性配置并避免依赖混淆
# 考虑移除私有包的代理属性
# https://verdaccio.org/docs/best#remove-proxy-to-increase-security-at-private-packages

# https://verdaccio.org/docs/configuration#server
# 您可以为传入连接指定 HTTP/1.1 服务器保持活动超时时间(秒)。
# 值为 0 时会使 http 服务器的行为类似于 8.0.0 之前的 Node.js 版本,它们没有保持活动超时。
# 解决方案:通过给定配置,您可以解决以下问题 https://github.com/verdaccio/verdaccio/issues/301。如果 60 不够用,请设置为 0。
server:
  keepAliveTimeout: 60
  # 当 Verdaccio 在代理或负载均衡器后面时,允许 `req.ip` 正确解析
  # 参见:https://expressjs.com/en/guide/behind-proxies.html
  # trustProxy: '127.0.0.1'

# https://verdaccio.org/docs/configuration#offline-publish
# publish:
#   allow_offline: false

# https://verdaccio.org/docs/configuration#url-prefix
# url_prefix: /verdaccio/
# VERDACCIO_PUBLIC_URL='https://somedomain.org';
# url_prefix: '/my_prefix'
# // url -> https://somedomain.org/my_prefix/
# VERDACCIO_PUBLIC_URL='https://somedomain.org';
# url_prefix: '/'
# // url -> https://somedomain.org/
# VERDACCIO_PUBLIC_URL='https://somedomain.org/first_prefix';
# url_prefix: '/second_prefix'
# // url -> https://somedomain.org/second_prefix/'

# https://verdaccio.org/docs/configuration#security
# security:
#   api:
#     legacy: true
#     # 建议为旧安装设置为 true
#     migrateToSecureLegacySignature: true
#     jwt:
#       sign:
#         expiresIn: 29d
#       verify:
#         someProp: [value]
#    web:
#      sign:
#        expiresIn: 1h # 默认为1小时
#      verify:
#         someProp: [value]

# https://verdaccio.org/docs/configuration#user-rate-limit
# userRateLimit:
#   windowMs: 50000
#   max: 1000

# https://verdaccio.org/docs/configuration#max-body-size
# max_body_size: 10mb

# https://verdaccio.org/docs/configuration#listen-port
# listen:
# - localhost:4873            # 默认值
# - http://localhost:4873     # 相同
# - 0.0.0.0:4873             # 监听所有地址 (INADDR_ANY)
# - https://example.org:4873  # 如果您想使用 https
# - "[::1]:4873"             # ipv6
# - unix:/tmp/verdaccio.sock # unix socket

# 如果您不考虑使用 HTTP 代理,HTTPS 配置会很有用
# https://verdaccio.org/docs/configuration#https
# https:
#   key: ./path/verdaccio-key.pem
#   cert: ./path/verdaccio-cert.pem
#   ca: ./path/verdaccio-csr.pem

# https://verdaccio.org/docs/configuration#proxy
# http_proxy: http://something.local/
# https_proxy: https://something.local/

# https://verdaccio.org/docs/configuration#notifications
# notify:
#   method: POST
#   headers: [{ "Content-Type": "application/json" }]
#   endpoint: https://usagge.hipchat.com/v2/room/3729485/notification?auth_token=mySecretToken
#   content: '{"color":"green","message":"New package published: * {{ name }}*","notify":true,"message_format":"text"}'

middlewares:
  audit:
    enabled: true

# https://verdaccio.org/docs/logger
# 日志设置
log: { type: stdout, format: pretty, level: http }
#experiments:
#  # 支持 npm token 命令
#  token: false
#  # 禁止将正文大小写入日志,在票据 1912 中了解更多
#  bytesin_off: false
#  # 启用 tarball URL 重定向,用于在不同服务器托管 tarball,tarball_url_redirect 可以是模板字符串
#  tarball_url_redirect: 'https://mycdn.com/verdaccio/${packageName}/${filename}'
#  # tarball_url_redirect 可以是一个函数,接收 packageName 和 filename 并返回 url,在使用 js 配置文件时
#  tarball_url_redirect(packageName, filename) {
#    const signedUrl = // 生成签名 url
#    return signedUrl;
#  }

# 定制翻译包,api 的 i18n 部分尚不可用
# i18n:
# 可用翻译列表 https://github.com/verdaccio/verdaccio/blob/master/packages/plugins/ui-theme/src/i18n/ABOUT_TRANSLATIONS.md
#   web: en-US

5. 调整配置

如果我们想实现支持完全离线的仓库,可以将配置调整为下面这样,并保存为 config.yaml

# 存储所有包的目录路径
storage: ./storage
# 包含插件的目录路径
plugins: ./plugins

# https://verdaccio.org/docs/webui
web:
  title: Verdaccio
  html_cache: true
  login: true
  showInfo: true
  showSettings: true
  # publicPath: http://somedomain.org/

# https://verdaccio.org/docs/configuration#authentication
auth:
  htpasswd:
    file: ./htpasswd

# https://verdaccio.org/docs/configuration#uplinks
# 可以连接的其他已知仓库列表
uplinks:
  npmmirror:
    url: https://registry.npmmirror.com/
  npmjs:
    url: https://registry.npmjs.org/

# 了解如何保护您的包
# https://verdaccio.org/docs/protect-your-dependencies/
# https://verdaccio.org/docs/configuration#packages
packages:
  '@*/*':
    # 作用域包 (可以考虑删除,提高私密性)
    access: $all
    publish: $authenticated
    unpublish: $authenticated
    # proxy: npmjs
    proxy: npmmirror

  '**':
    # 允许所有用户(包括未认证用户)读取和发布所有包
    # 您可以指定用户名/组名(取决于您的认证插件)
    # 和三个关键字:"$all"、"$anonymous"、"$authenticated"
    access: $all

    # 允许所有已知用户发布包,默认情况下任何人都可以注册
    publish: $authenticated
    unpublish: $authenticated

    # 如果包在本地不可用,则将请求代理到 'npmmirror' 注册表,默认 proxy: npmjs
    proxy: npmmirror

server:
  keepAliveTimeout: 0

security:
  api:
    legacy: true
    migrateToSecureLegacySignature: true

# https://verdaccio.org/docs/configuration#listen-port
listen:
- localhost:4873     # 默认值
- 0.0.0.0:4873       # 监听所有地址 (INADDR_ANY)

middlewares:
  audit:
    enabled: false

# 日志设置
log: { type: stdout, format: pretty, level: http }

上面的配置中,将软件包首次下载缓存的地址从默认的 npmjs 调整为 npmmirror,加速首次软件包的下载。

执行命令,重新启动服务:

verdaccio -c config.yaml

接着,调整 npm 下载偏好,让 npm 始终从我们的仓库下载软件包:

npm config set prefer-online true

6. 验证软件包下载

为了避免本地干扰,我们将本地的缓存和文件锁定都清理掉:

rm -rf node_modules 
rm -rf package-lock.json 

在软件仓库里执行下面的命令,来进行下载验证:

npm install --registry http://localhost:4873/ --verbose

首次软件包下载时,Verdaccio 中因为没有缓存,所以也会连接互联网进行下载:

# npm install --registry http://localhost:4873/ --verbose
npm verbose cli /Users/soulteary/.nvm/versions/node/v22.11.0/bin/node /Users/soulteary/.nvm/versions/node/v22.11.0/bin/npm
npm info using npm@10.9.0
npm info using node@v22.11.0
npm verbose title npm install
npm verbose argv "install" "--registry" "http://localhost:4873/" "--loglevel" "verbose"
npm verbose logfile logs-max:10 dir:/Users/soulteary/.npm/_logs/2024-11-30T14_18_40_831Z-
npm verbose logfile /Users/soulteary/.npm/_logs/2024-11-30T14_18_40_831Z-debug-0.log
npm http fetch GET 200 http://localhost:4873/chart.js 141ms (cache updated)
npm http fetch GET 200 http://localhost:4873/copy-text-to-clipboard 258ms (cache updated)
npm http fetch GET 200 http://localhost:4873/dayjs 41ms (cache updated)
npm http fetch GET 200 http://localhost:4873/lodash 44ms (cache updated)
...

added 137 packages in 2m

33 packages are looking for funding
  run `npm fund` for details
npm verbose cwd /Users/soulteary/SnowLotus/frontend
npm verbose os Darwin 23.6.0
npm verbose node v22.11.0
npm verbose npm  v10.9.0
npm verbose exit 0
npm info ok

第一次完成,时间会比较长,我这里使用了 2 分钟。

接下来,我们再次进行下载验证,为了避免本地干扰,我们将本地的缓存和文件锁定都清理掉:

rm -rf node_modules
rm -rf package-lock.json

能够发现时间缩减到了 7 秒钟。

added 137 packages in 7s

33 packages are looking for funding

最后,我们来模拟 CI/CD 环境中,package-lock.json 就绪,node_modules 初始化时不存在的场景(仅清理 node_modules):

rm -rf node_modules

再次执行下载,能够发现时间缩短至了 3 秒钟。

added 138 packages in 3s

33 packages are looking for funding

7. 验证私有软件发布

为了方便我们进行软件包验证,我们可以将私有仓库设置为默认仓库:

# 获取默认配置,方便还原
# npm config get registry
https://registry.npmjs.org/
# 设置默认仓库为我们的私有仓库
# npm config set registry http://localhost:4873/

然后,使用命令行在我们的私有仓库中注册一个账号:

# 登录并注册一个账号
# npm adduser --registry http://localhost:4873/ --auth-type=legacy

npm notice Log in on http://localhost:4873/
Username: test
Password: 
Email: (this IS public) 

Logged in on http://localhost:4873/.

接着,创建一个私有的 NPM 软件包,package.json 内容可以这样写:

{
  "name": "@your-company/package-name",
  "version": "1.0.0",
  "private": false
}

最后,使用 npm publish 命令进行发布:

# npm publish

npm notice
npm notice 📦  @your-company/package-name@1.0.0
npm notice Tarball Contents
npm notice 85B package.json
npm notice Tarball Details
npm notice name: @your-company/package-name
npm notice version: 1.0.0
npm notice filename: your-company-package-name-1.0.0.tgz
npm notice package size: 166 B
npm notice unpacked size: 85 B
npm notice shasum: e07da1eb9bffbac04aa4400490c532935a3d2c1d
npm notice integrity: sha512-TykUhzEiDswXM[...]cUQeE1KFyXHZQ==
npm notice total files: 1
npm notice
npm notice Publishing to http://localhost:4873/ with tag latest and default access
+ @your-company/package-name@1.0.0

私有化的软件包就发布完毕了。

再次访问浏览器控制台,能够看到我们发布的软件包。

刚刚发布成功的软件包

实践部署:面向生产 CI/CD 环境(Docker)

有了上面的经验后,我们可以很轻松的编写适合生产环境使用的,使用 Docker 提供服务的配置。

首先,使用 Docker 下载软件:

docker pull verdaccio/verdaccio:6.0.2

配置我们使用和上面相同的内容,并将文件保存为 config.yaml,稍后使用。

touch htpasswd

使用命令行创建一个空白文件,如果后续我们需要为这个 NPM 仓库添加简单的认证,可以修改这个文件。

在上一篇文章《折腾基本功:Redis 从入门到 Docker 部署》中,我们聊过了如何从零到一完善配置到中等规模生产环境可用。参考上篇文章和上面的内容,我们可以简单得到下面的配置:

name: npm-registry

services:
  verdaccio:
    image: verdaccio/verdaccio:6.0.2
    command: verdaccio -c /etc/verdaccio.yaml
    restart: always
    ports:
      - 4873:4873
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./config.yaml:/etc/verdaccio.yaml:ro
      - ./storage:/opt/verdaccio/storage:rw
      - ./plugins:/opt/verdaccio/plugins:rw
      - ./htpasswd:/opt/verdaccio/htpasswd:rw
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4873/-/ping"]
      interval: 1s
      timeout: 3s
      retries: 5
    logging:
        driver: "json-file"
        options:
            max-size: "10m"

然后使用命令启动服务:

docker compose up -d

服务启动后,使用 docker compose ps 观察状况,不出意外将看到 (healthy) 的标志,就可以放心使用啦:

docker compose ps
NAME                       IMAGE                       COMMAND                  SERVICE     CREATED         STATUS                   PORTS
npm-registry-verdaccio-1   verdaccio/verdaccio:6.0.2   "uid_entrypoint verd…"   verdaccio   5 minutes ago   Up 5 minutes (healthy)   0.0.0.0:4873->4873/tcp, :::4873->4873/tcp

当服务出现任何异常的时候,它会自动尝试重启进程修复,并在服务就绪的情况下重新开启端口,提供服务。

最佳实践建议

好了,看到这里,基本一个满足中小团队的私有化方案就搞定了。

在进行系统部署时,高可用性是一个重要的考虑因素。如果你使用云端环境,可以考虑将 Compose 配置转换为 Kubernetes 集群进行部署,使用多机来保障不停机服务。

为了确保数据安全,可以建立定期备份对存储数据进行保护,比如使用定时任务工具和 Rsync 来进行经济实惠的数据备份,可以参考《使用 Docker 和 Traefik 搭建轻量美观的计划任务工具》,或者直接使用云端的磁盘快照功能。

或者在存储方案的选择上,也可以进行一些“偷懒”,小型团队存储 500G ~ 1T 左右数据的时候,使用本地存储加备份即可。大型团队或频繁构建的情况下,可以考虑使用云服务的对象存储(S3)来替换本地存储,通常这类服务的可靠性都在六个九以上。

安全性配置也是系统部署中不可忽视的环节。本文中配置的是 HTTP 协议,放开用户的下载和发布软件包权限,在实际生产环境,我们需要配置严格的 Token 访问,用户权限。在传输时,可以将软件挂载网关上。一来可以将 HTTP 无感知转换为 HTTPS,另外可以轻松添加认证,而这一切都无需折腾网关后的软件,利于集中管理。

最后,关于缓存策略,需要结合实际业务场景合理设置缓存时间,并建立定期清理过期缓存的机制。同时,要对存储空间的使用情况进行监控,及时发现和处理潜在的存储问题,确保系统的稳定运行。

常见问题解决

在开发过程中,包发布失败是一个常见的问题。

遇到这种情况时,首先需要检查用户是否具有足够的发布权限,然后确认包的名称是否符合命名规范。同时,还要验证发布的版本号是否与已有版本重复,因为重复的版本号会导致发布失败。(默认情况下不允许覆盖发布)

当遇到包下载速度慢的问题时,需要从多个方面进行优化。

首先要检查上游仓库的配置是否正确,确保连接稳定。其次,可以通过优化缓存策略来提升下载效率。对于国内用户,建议考虑使用国内镜像源来加快下载速度。

存储空间不足是另一个需要重点关注的问题。

为了解决这个问题,我们可以定期清理那些长期未被使用的包文件。同时,建议配置自动清理策略,对过期或不常用的包进行自动清理。如果清理后仍然无法满足需求,则需要考虑扩展存储空间来确保系统的正常运行。

最后

希望这篇文章能够帮助你搭建起可靠的私有 NPM 仓库,如果有任何问题,欢迎讨论。

—EOF