Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive ยท DevOps

Private npm Registry on an Ubuntu VM with Verdaccio and Nginx

Host a private npm registry with Verdaccio on Ubuntu 22.04: systemd service, Nginx with TLS, users and package access rules, upstream proxying, publishing and CI use.

3 min read

  • Verdaccio 5
  • Node.js 20 LTS
  • Nginx
  • Ubuntu 22.04 LTS
  • systemd
  • npm 10

A private registry gives a team somewhere to publish internal packages without paying per seat, and it caches public packages so builds keep working when npmjs.com is slow. Verdaccio is a small, open-source registry that runs on one VM and stores packages as files.

Versions in use on this date. Verdaccio 5.x is the stable line (the 6.x line is still in development), running on Node.js 20 LTS (Node 18 reaches end of life in April 2025). Ubuntu 24.04 LTS came out on 25 April 2024; this post uses 22.04, and the steps are the same on 24.04.

How it fits together

flowchart TB
  DEV[Developers and CI<br/>npm install and publish] ==>|HTTPS 443| NG[Nginx<br/>TLS and proxy]
  NG ==>|HTTP 127.0.0.1 4873| V[Verdaccio<br/>systemd service]
  V --> ST[(Storage folder<br/>packages and htpasswd)]
  V -.->|cache miss| UP[registry.npmjs.org<br/>uplink]

Packages you publish are kept in the storage folder. Anything else is fetched from the public registry on first request and cached, so the second install is local and fast.

Install

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs nginx
sudo useradd --system --create-home --home-dir /var/lib/verdaccio --shell /usr/sbin/nologin verdaccio
sudo npm install -g verdaccio@5
verdaccio --version

Create the config at /etc/verdaccio/config.yaml:

storage: /var/lib/verdaccio/storage
auth:
  htpasswd:
    file: /var/lib/verdaccio/htpasswd
    max_users: -1          # no self sign-up; add users by hand
uplinks:
  npmjs:
    url: https://registry.npmjs.org/
    cache: true
packages:
  '@mycompany/*':
    access: $authenticated
    publish: $authenticated
    unpublish: admins
  '**':
    access: $authenticated
    publish: $authenticated
    proxy: npmjs
listen: 127.0.0.1:4873
max_body_size: 100mb
log: { type: file, path: /var/log/verdaccio/verdaccio.log, level: warn }

max_users: -1 turns off public registration. Setting access: $authenticated means even installs need a login, which is what you want for a registry holding company code. Add $anonymous to the ** rule if you only want to protect publishing.

Run it as a service

sudo mkdir -p /var/log/verdaccio && sudo chown verdaccio: /var/log/verdaccio /var/lib/verdaccio
sudo tee /etc/systemd/system/verdaccio.service >/dev/null <<'EOF'
[Unit]
Description=Verdaccio private npm registry
After=network.target

[Service]
User=verdaccio
ExecStart=/usr/bin/verdaccio --config /etc/verdaccio/config.yaml
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=full
ReadWritePaths=/var/lib/verdaccio /var/log/verdaccio

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now verdaccio
curl -s http://127.0.0.1:4873/-/ping

Nginx and TLS

server {
    listen 443 ssl http2;
    server_name npm.example.com;
    ssl_certificate     /etc/letsencrypt/live/npm.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/npm.example.com/privkey.pem;
    client_max_body_size 100m;

    location / {
        proxy_pass http://127.0.0.1:4873;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Get the certificate with sudo certbot certonly --nginx -d npm.example.com. The X-Forwarded-Proto header matters: without it Verdaccio writes http:// tarball links into package metadata and installs fail behind the proxy.

Users and publishing

npm adduser --registry https://npm.example.com        # on the server, or add users with htpasswd -B
npm config set @mycompany:registry https://npm.example.com
cd my-lib && npm publish --registry https://npm.example.com

Using a scope (@mycompany/...) keeps internal names from clashing with public ones, and it blocks dependency confusion, where an attacker publishes a public package with the same name as your internal one. For users, create them offline so passwords are hashed with bcrypt: sudo htpasswd -B /var/lib/verdaccio/htpasswd alice.

CI usage

Do not commit tokens. Make a token for CI, store it in the CI secret store, and write .npmrc at build time:

echo "//npm.example.com/:_authToken=${NPM_TOKEN}" > .npmrc
echo "@mycompany:registry=https://npm.example.com" >> .npmrc
npm ci

Operate it

  • Backup: the whole state is /var/lib/verdaccio (packages and htpasswd). A nightly rsync or snapshot of that folder is enough.
  • Disk: the cache grows; watch free space and prune old cached tarballs if needed.
  • Updates: sudo npm update -g verdaccio@5, then restart the service.
  • Limits: one process on one VM is fine for a team, but it is not highly available. If it must scale, move storage to shared or S3-backed storage using a plugin.