--- title: "How to Set Up a Private npm Registry with Verdaccio" description: "Install Verdaccio on Ubuntu 24.04 as a private npm registry with systemd, Nginx and HTTPS, cache public packages, publish scoped packages and use it from CI." category: "Development Tools" language: en source: https://cubepath.com/docs/development-tools/verdaccio-private-npm-registry-installation updated: 2026-09-25 --- # How to Set Up a Private npm Registry with Verdaccio 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: ```bash 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: ```bash node --version npm --version ``` ```output v22.19.0 10.9.3 ``` ## Step 2 - Installing Verdaccio Install Verdaccio globally with npm: ```bash sudo npm install --global verdaccio ``` With NodeSource packages, global binaries are placed in `/usr/bin`. Confirm the location and version: ```bash command -v verdaccio verdaccio --version ``` ```output /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: ```bash 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: ```bash sudo nano /etc/verdaccio/config.yaml ``` ```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: ```bash sudo nano /etc/systemd/system/verdaccio.service ``` ```ini [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: ```bash 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: ```bash curl -s http://127.0.0.1:4873/-/ping ``` ```output {} ``` 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: ```bash sudo apt install nginx certbot python3-certbot-nginx ``` Create the server block: ```bash sudo nano /etc/nginx/sites-available/verdaccio ``` ```nginx 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: ```bash 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: ```bash curl -s https://registry.your_domain/-/ping ``` ```output {} ``` 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: ```bash npm adduser --registry https://registry.your_domain/ --auth-type=legacy ``` ```output 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`: ```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: ```bash sudo systemctl restart verdaccio ``` To add a user later, install `apache2-utils` and add a bcrypt entry directly to the file: ```bash 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: ```bash npm view lodash version --registry https://registry.your_domain/ ``` ```output 4.17.21 ``` The first request is proxied to npmjs and cached. On the server, the package now exists in the storage directory: ```bash sudo ls /var/lib/verdaccio/storage ``` ```output lodash ``` To use the registry for everything in a project, create an `.npmrc` file in the project root: ```ini 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: ```ini @your_scope:registry=https://registry.your_domain/ ``` ## Step 8 - Publishing a private package Create a small scoped package on your workstation: ```bash mkdir hello && cd hello npm init -y --scope=@your_scope echo "module.exports = () => 'hello from Verdaccio';" > index.js ``` Publish it to your registry: ```bash npm publish --registry https://registry.your_domain/ ``` ```output + @your_scope/hello@1.0.0 ``` From another project that has the scoped `.npmrc` entry and a logged-in user, install it: ```bash 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`: ```bash grep registry.your_domain ~/.npmrc ``` ```output //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`): ```ini @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.