I had a utility library used in six different projects. Every time I updated it, I was manually copying files or maintaining a git submodule — both of which are annoying in their own ways. Publishing to public npm wasn't an option because the code was proprietary.
Verdaccio is the solution: a private npm registry that runs on your own server. Your team runs npm install @company/utils like any other package, it resolves from your registry, and public packages transparently proxy through to npm. It's the cleanest way to share internal JavaScript packages without exposing them publicly.
Setup is simpler than you might expect — Verdaccio is itself an npm package.
With Verdaccio running on a VPS, your team can npm install @company/utils just like any other package — except it pulls from your server, not the public internet. Public packages are proxied through to npm automatically, so you don't need to configure anything extra for external dependencies.
I run Verdaccio on Tencent Cloud Lighthouse. The entry-level plan handles Verdaccio comfortably — it's one of the lightest self-hosted services. The key advantage of running a private npm registry on a dedicated server: your team's packages are always available from any CI/CD pipeline or developer machine through a single, consistent URL (
https://npm.yourdomain.com). Lighthouse's static public IP and stable infrastructure mean the registry stays available even during heavy CI build activity, unlike a registry running on a developer's local machine.
- Key Takeaways
A self-hosted npm registry makes sense when you:
Verdaccio handles all these cases. It proxies public npm packages through to the real registry by default, so your team doesn't need to configure anything differently for external packages.
| Requirement | Details |
|---|---|
| Server | Ubuntu 22.04, 1 GB+ RAM |
| Node.js | v18+ |
| Domain | For HTTPS setup |
| Ports | 4873 (default Verdaccio) or 443 via Nginx |
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt install -y nodejs
sudo npm install -g verdaccio
verdaccio
# Press Ctrl+C to stop after it generates the config
The default config is created at ~/.config/verdaccio/config.yaml.
nano ~/.config/verdaccio/config.yaml
Key sections:
# Where packages are stored
storage: ~/.local/share/verdaccio/storage
# Authentication plugin
auth:
htpasswd:
file: ./htpasswd
max_users: 1000
# Upstream registries
uplinks:
npmjs:
url: https://registry.npmjs.org/
packages:
'@*/*':
# Scoped packages - auth required to publish
access: $all
publish: $authenticated
unpublish: $authenticated
proxy: npmjs
'**':
# All other packages
access: $all
publish: $authenticated
unpublish: $authenticated
proxy: npmjs
# Restrict access to authenticated users only
web:
enable: true
title: Private npm Registry
To restrict all reads to authenticated users, change access: $all to access: $authenticated.
Create /etc/systemd/system/verdaccio.service:
sudo nano /etc/systemd/system/verdaccio.service
[Unit]
Description=Verdaccio private npm registry
After=network.target
[Service]
Type=simple
User=ubuntu
ExecStart=/usr/bin/verdaccio --config /home/ubuntu/.config/verdaccio/config.yaml
Restart=on-failure
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable verdaccio
sudo systemctl start verdaccio
sudo systemctl status verdaccio
sudo apt install -y nginx certbot python3-certbot-nginx
sudo nano /etc/nginx/sites-available/verdaccio
server {
listen 80;
server_name npm.yourdomain.com;
location / {
proxy_pass http://localhost: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;
# Allow large package uploads
client_max_body_size 50m;
}
}
sudo ln -s /etc/nginx/sites-available/verdaccio /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d npm.yourdomain.com
In ~/.config/verdaccio/config.yaml, add:
url_prefix: https://npm.yourdomain.com/
Restart:
sudo systemctl restart verdaccio
Users are managed via the htpasswd command or the npm CLI:
# On your local machine, against your registry
npm adduser --registry https://npm.yourdomain.com
# Follow prompts: username, password, email
Or add via htpasswd directly on the server:
sudo apt install -y apache2-utils
htpasswd -B ~/.config/verdaccio/htpasswd newuser
npm login --registry https://npm.yourdomain.com
# Enter username and password
Verify:
npm whoami --registry https://npm.yourdomain.com
mkdir my-utils && cd my-utils
npm init -y
Edit package.json:
{
"name": "@mycompany/utils",
"version": "1.0.0",
"description": "Internal utility functions",
"main": "index.js",
"publishConfig": {
"registry": "https://npm.yourdomain.com"
}
}
Create index.js:
exports.formatDate = (date) => {
return new Date(date).toLocaleDateString('en-US');
};
exports.slugify = (str) => {
return str.toLowerCase().replace(/\s+/g, '-');
};
npm publish --registry https://npm.yourdomain.com
You should see: + @mycompany/utils@1.0.0
Navigate to https://npm.yourdomain.com in a browser. Your package appears in the registry. The web UI shows package details, README, and version history.
.npmrcAdd a .npmrc file to each project:
@mycompany:registry=https://npm.yourdomain.com/
//npm.yourdomain.com/:_authToken=YOUR_AUTH_TOKEN
Get your auth token from ~/.npmrc on your machine after logging in.
Now npm install @mycompany/utils resolves from your private registry. All other packages still come from the public npm registry.
Set the scoped registry globally on each developer machine:
npm config set @mycompany:registry https://npm.yourdomain.com/
Route all packages through Verdaccio (it proxies public packages upstream):
npm config set registry https://npm.yourdomain.com/
This works well for teams in controlled environments where you want to cache all packages locally.
In config.yaml, packages are matched by pattern. Add a stricter rule for your company scope:
packages:
'@mycompany/*':
access: $authenticated
publish: $authenticated
unpublish: $authenticated
# No proxy — these must come from your registry only
'@*/*':
access: $all
publish: $authenticated
proxy: npmjs
'**':
access: $all
publish: $authenticated
proxy: npmjs
With this config, @mycompany/ packages are never proxied to npm — they must exist in your registry.
Publishing a new version of a package was failing with 403 Forbidden even though I was logged in.
The issue: Verdaccio's default htpasswd configuration limits registrations but also enforces package ownership. Once a user publishes a package, only that user can publish new versions by default.
I'd created a test user for initial testing and published the package. When I tried to publish with my real user, I got a 403.
The fix:
# On the server, edit the storage to change ownership
nano ~/.local/share/verdaccio/storage/@mycompany/utils/package.json
Find the _npmUser field and update it to your new username. Or simply delete the package and republish:
# Delete from Verdaccio storage
rm -rf ~/.local/share/verdaccio/storage/@mycompany/utils/
# Republish as the correct user
npm publish --registry https://npm.yourdomain.com
Going forward, keep consistent usernames and don't publish under test accounts you'll delete.
| Issue | Likely Cause | Fix |
|---|---|---|
npm install returns 401 |
Not logged in | npm login --registry https://npm.yourdomain.com |
| Package not found | Scope not configured | Add scope to .npmrc or npm config set @scope:registry |
| 413 Request Entity Too Large | Nginx body limit | Set client_max_body_size 50m in Nginx config |
| Can't publish: 403 | Package owned by different user | Edit storage package.json or delete and republish |
| Public packages not resolving | Proxy misconfigured | Check uplinks.npmjs.url in Verdaccio config |
| Verdaccio not starting | Port 4873 in use | sudo lsof -i :4873; stop the conflicting process |
| Storage growing large | Cached upstream packages | Add max_fails: 10 to uplink config to limit caching |
✅ What you built:
https://npm.yourdomain.com@yourcompany/package-name)Teams using this setup stop copy-pasting utility code between projects and start treating shared libraries as proper versioned packages.
Is self-hosted Verdaccio suitable for production use?
Yes — Verdaccio is used in production environments ranging from individual developers to small teams. Pair it with regular Lighthouse snapshots and stay current with updates.
How do I migrate from a cloud-hosted Verdaccio to self-hosted?
Export your data from the cloud service, import it to the self-hosted instance, update DNS or internal service configurations, and verify everything works before switching fully.
How much disk space does Verdaccio need?
Initial installation is minimal. Disk usage grows with usage — artifacts, repositories, and caches accumulate over time. Monitor with df -h and use CBS cloud disk expansion when needed.
How do I set up backups for Verdaccio?
Use Lighthouse snapshots for full-server recovery. Additionally, export Verdaccio's application data directly (usually a backup command or data directory export) for granular restore capability.
👉 Get started with Tencent Cloud Lighthouse
👉 View current pricing and launch promotions
👉 Explore all active deals and offers