Verdaccio is a lightweight private npm registry written in Node.js. It stores the packages you publish yourself and acts as a caching proxy for the public npm registry, so installs keep working and stay fast even when registry.npmjs.org is slow or unreachable. In this tutorial you will install Verdaccio on Ubuntu 24.04, run it as a systemd service under a dedicated user, publish it over HTTPS with Nginx, restrict a package scope to authenticated users, and consume it from a project and from CI.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS with at least 1 GB of RAM, for example a CubePath VPS. Disk usage grows with the packages you cache, so plan several GB of free space.
- A non-root user with
sudoprivileges. - A domain name such as
registry.your_domainwith a DNSArecord pointing to the server. - UFW enabled with OpenSSH allowed.
- Node.js and npm on the workstation you will publish from.
In this guide @your_scope stands for your organization's npm scope, for example @acme.
Step 1 - Installing Node.js
Verdaccio 6 needs Node.js 18 or newer. Install the current LTS release (Node.js 22) from the NodeSource repository. Download the setup script first so you can read it before running it:
curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh
less nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt install nodejs
The script adds the repository with a signed keyring under /etc/apt/keyrings. Check the installed versions:
node --version
npm --version
v22.19.0
10.9.3
Step 2 - Installing Verdaccio
Install Verdaccio globally with npm:
sudo npm install --global verdaccio
With NodeSource packages, global binaries are placed in /usr/bin. Confirm the location and version:
command -v verdaccio
verdaccio --version
/usr/bin/verdaccio
6.1.6
Create a system user that owns the registry data. It has no login shell and its home directory is the data directory:
sudo useradd --system --home-dir /var/lib/verdaccio --create-home --shell /usr/sbin/nologin verdaccio
sudo mkdir -p /var/lib/verdaccio/storage /var/lib/verdaccio/plugins /etc/verdaccio
sudo chown -R verdaccio:verdaccio /var/lib/verdaccio
Step 3 - Configuring Verdaccio
Create the main configuration file:
sudo nano /etc/verdaccio/config.yaml
storage: /var/lib/verdaccio/storage
plugins: /var/lib/verdaccio/plugins
web:
title: Private npm registry
auth:
htpasswd:
file: /var/lib/verdaccio/htpasswd
max_users: 100
uplinks:
npmjs:
url: https://registry.npmjs.org/
packages:
'@your_scope/*':
access: $authenticated
publish: $authenticated
unpublish: $authenticated
'**':
access: $all
publish: $authenticated
unpublish: $authenticated
proxy: npmjs
max_body_size: 100mb
listen: 127.0.0.1:4873
log: { type: stdout, format: pretty, level: http }
What each part does:
storageis where tarballs and metadata are kept, both for your packages and for cached public ones.auth.htpasswdstores users in a local htpasswd file.max_users: 100allows users to register withnpm adduserfor now; you will close registration in Step 6.uplinks.npmjsis the upstream registry. Packages that match a rule withproxy: npmjsare fetched from it on the first request and cached.- Packages under
@your_scopehave noproxy, so they never leak to or get shadowed by the public registry, and only logged-in users can install them. listenbinds Verdaccio to localhost; Nginx will handle public traffic.
Step 4 - Running Verdaccio with systemd
Create a unit file:
sudo nano /etc/systemd/system/verdaccio.service
[Unit]
Description=Verdaccio private npm registry
After=network-online.target
Wants=network-online.target
[Service]
User=verdaccio
Group=verdaccio
ExecStart=/usr/bin/verdaccio --config /etc/verdaccio/config.yaml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Start the service and enable it at boot:
sudo systemctl daemon-reload
sudo systemctl enable --now verdaccio
sudo systemctl status verdaccio
The status should show active (running). Verdaccio has a health endpoint you can query locally:
curl -s http://127.0.0.1:4873/-/ping
{}
If the service fails, sudo journalctl -u verdaccio -e shows the error, which is usually a YAML indentation problem or a permission issue on /var/lib/verdaccio.
Step 5 - Publishing Verdaccio with Nginx and HTTPS
Install Nginx and Certbot:
sudo apt install nginx certbot python3-certbot-nginx
Create the server block:
sudo nano /etc/nginx/sites-available/verdaccio
server {
listen 80;
listen [::]:80;
server_name registry.your_domain;
client_max_body_size 100m;
location / {
proxy_pass http://127.0.0.1:4873;
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 must be at least as large as Verdaccio's max_body_size, or large npm publish requests are rejected by Nginx with a 413 error. The X-Forwarded-Proto header makes Verdaccio generate https:// tarball URLs.
Enable the site, open the firewall and request a certificate:
sudo ln -s /etc/nginx/sites-available/verdaccio /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo ufw allow 'Nginx Full'
sudo certbot --nginx -d registry.your_domain
Check the public endpoint:
curl -s https://registry.your_domain/-/ping
{}
You can also open https://registry.your_domain in a browser to see Verdaccio's web interface.
Step 6 - Creating users and closing registration
On your workstation, create your account in the registry. The legacy auth type makes npm prompt for a username and password instead of trying a browser-based login:
npm adduser --registry https://registry.your_domain/ --auth-type=legacy
Username: your_user
Password:
Email: (this IS public) you@your_domain
Logged in on https://registry.your_domain/.
npm saves an access token for the registry in your ~/.npmrc. Create accounts for your teammates the same way, then stop anyone else from registering. On the server, edit the auth block in /etc/verdaccio/config.yaml:
auth:
htpasswd:
file: /var/lib/verdaccio/htpasswd
max_users: -1
max_users: -1 disables self-registration while existing users keep working. Restart the service:
sudo systemctl restart verdaccio
To add a user later, install apache2-utils and add a bcrypt entry directly to the file:
sudo apt install apache2-utils
sudo htpasswd -B /var/lib/verdaccio/htpasswd new_user
Step 7 - Using the registry as a cache for public packages
Ask Verdaccio for a public package that you have never published yourself:
npm view lodash version --registry https://registry.your_domain/
4.17.21
The first request is proxied to npmjs and cached. On the server, the package now exists in the storage directory:
sudo ls /var/lib/verdaccio/storage
lodash
To use the registry for everything in a project, create an .npmrc file in the project root:
registry=https://registry.your_domain/
To use it only for your private scope and keep the public registry for everything else, use a scoped entry instead:
@your_scope:registry=https://registry.your_domain/
Step 8 - Publishing a private package
Create a small scoped package on your workstation:
mkdir hello && cd hello
npm init -y --scope=@your_scope
echo "module.exports = () => 'hello from Verdaccio';" > index.js
Publish it to your registry:
npm publish --registry https://registry.your_domain/
+ @your_scope/[email protected]
From another project that has the scoped .npmrc entry and a logged-in user, install it:
npm install @your_scope/hello
Because the @your_scope/* rule requires $authenticated, an anonymous request for this package returns an authorization error, which you can confirm with curl -s -o /dev/null -w '%{http_code}\n' https://registry.your_domain/@your_scope%2fhello.
Step 9 - Authenticating from CI
CI jobs authenticate with the same kind of token npm stored for you. Create a dedicated user for CI (for example with htpasswd as in Step 6), log in as that user once, and copy the token from ~/.npmrc:
grep registry.your_domain ~/.npmrc
//registry.your_domain/:_authToken=your_token
Store the token as a secret in your CI system, for example as NPM_TOKEN. Then commit an .npmrc to the project that reads it from the environment (npm expands ${VAR} in .npmrc):
@your_scope:registry=https://registry.your_domain/
//registry.your_domain/:_authToken=${NPM_TOKEN}
Any CI job that exports NPM_TOKEN can now run npm ci and npm publish against your registry. Never commit the token itself.
Troubleshooting
npm ERR! 404 Not Found for a public package. The package does not match a rule with proxy: npmjs, or the server cannot reach registry.npmjs.org. Test outbound access from the server with curl -sI https://registry.npmjs.org/lodash.
npm ERR! 409 Conflict when publishing. That version already exists. Bump it with npm version patch before publishing again.
npm ERR! 413 Payload Too Large. Raise both client_max_body_size in Nginx and max_body_size in Verdaccio, then reload Nginx and restart Verdaccio.
npm adduser fails with user registration disabled. max_users is set to -1. Add the user with htpasswd on the server instead.
Conclusion
You now run a private npm registry with Verdaccio that caches public packages, keeps your scoped packages private, and is reachable over HTTPS by developers and CI. Next, back up /var/lib/verdaccio (it holds storage and users), consider an authentication plugin such as LDAP if your team already has a directory, and add more uplinks if you consume packages from other registries.
