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 sudo privileges.
  • A domain name such as registry.your_domain with a DNS A record 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:

  • storage is where tarballs and metadata are kept, both for your packages and for cached public ones.
  • auth.htpasswd stores users in a local htpasswd file. max_users: 100 allows users to register with npm adduser for now; you will close registration in Step 6.
  • uplinks.npmjs is the upstream registry. Packages that match a rule with proxy: npmjs are fetched from it on the first request and cached.
  • Packages under @your_scope have no proxy, so they never leak to or get shadowed by the public registry, and only logged-in users can install them.
  • listen binds 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.