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 nightlyrsyncor 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.