louwers.dev

2026-03-29

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:

  1. 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).

  2. 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.

Footnotes

  1. It is still possible to create short-lived tokens for publishing, but this would be a huge hassle to set up.