Self hosting an npm registry with Verradacio and NixOS
npm was hit by a series of supply chain attacks recently. The response was to require two-factor authentication for local publishing and OIDC for publishing from continuous integration. The result:
-
For local publishing you now need two-factor auth with a security key. If your computer does not have one built-in, you will need to buy one. Which is not a bad idea, but the fact that it is required is a bit draconian in my opinion (authenticator apps are not supported).
-
As of March 2026, publishing with OIDC is only supported on GitHub and GitLab. Not my new favorite CI provider sourcehut. npm itself is already sort of problematic, because it is a critical piece of infrastructure owned and operated by Microsoft, not allowing1 publishing from other CI systems is pushing the web infrastructure further towards centralization.
This led me to explore Verdaccio, a relatively simple to host npm registry. In this post I'd like to share how I set it up on my NixOS box.
First I created a standalone verdaccio.nix file, which I will wire up into my configuration.nix later.
{ config, pkgs, ... }:
let
verdaccioPort = "4873";
domain = "npm.mydomain.com";
in
{
# configuration goes here
}
Since there is no NixOS package for Verdaccio yet, I declaratively set up a container.
virtualisation.oci-containers.containers.verdaccio = {
image = "verdaccio/verdaccio";
ports = [ "${verdaccioPort}:${verdaccioPort}" ];
volumes = [
"/etc/verdaccio:/verdaccio/conf"
"/var/lib/verdaccio/storage:/verdaccio/storage"
"/var/lib/verdaccio/plugins:/verdaccio/plugins"
];
};
Then I needed to make sure that these directories exist with the right permissions, for which the systemd-tmpfiles system can be used. NixOS allows a declarative configuration:
systemd.tmpfiles.settings.verdaccio."/var/lib/verdaccio".d = {
user = "10001";
group = "65533";
mode = "2750";
};
systemd.tmpfiles.settings.verdaccio."/etc/verdaccio".d = {
user = "10001";
group = "65533";
mode = "2750";
};
Next, I made sure NixOS generates a configuration file. I used (pkgs.formats.yaml { }).generate to generate YAML from a Nix attribute set. These are mostly the defaults, but I changed the hashing algorithm and set max_users to 1:
environment.etc."verdaccio/config.yaml" = {
uid = 10001;
gid = 65533;
mode = "0644";
source = (pkgs.formats.yaml { }).generate "verdaccio-config.yml" {
storage = "/verdaccio/storage";
web.title = "Verdaccio";
auth.htpasswd = {
max_users = 1;
file = "./htpasswd";
algorithm = "bcrypt";
rounds = 10;
};
uplinks.npmjs.url = "https://registry.npmjs.org/";
packages = {
"@*/*" = {
access = "$all";
publish = "$authenticated";
unpublish = "$authenticated";
proxy = "npmjs";
};
"**" = {
access = "$all";
publish = "$authenticated";
unpublish = "$authenticated";
proxy = "npmjs";
};
};
server = {
keepAliveTimeout = 60;
dotfiles = "ignore";
};
middlewares.audit.enabled = true;
filters."@verdaccio/package-filter" = { };
log = {
type = "stdout";
format = "pretty";
level = "http";
};
i18n.web = "en-US";
};
};
Then I configured Caddy as a reverse proxy, which can be done very concisely:
services.caddy.virtualHosts.${domain}.extraConfig = ''
reverse_proxy http://127.0.0.1:${verdaccioPort}
'';
The file needs to be wired up in configuration.nix:
imports = [
./hardware-configuration.nix
./verdaccio.nix
];
And that's it! With npm register --registry https://npm.mydomain.com an account can be created, npm login is used to log in. You can even add this to your ~/.npmrc:
registry=https://npm.mydomain.com
Verradacio will simply use upstream npmjs.org and cache the result when a package is missing.
-
It is still possible to create short-lived tokens for publishing, but this would be a huge hassle to set up.


