Remote Work Tools

Verdaccio is a lightweight Node.js private npm registry that proxies the public npm registry and lets your team publish internal packages. It supports scoped packages, htpasswd auth, S3 storage, and all package managers (npm, yarn, pnpm, bun). This guide deploys it with Docker and configures team publishing workflows.

Table of Contents

Prerequisites

Before you begin, make sure you have the following ready:

Step 1: Docker Deployment

# docker-compose.yml
version: "3.8"

services:
  verdaccio:
    image: verdaccio/verdaccio:5
    container_name: verdaccio
    environment:
      - VERDACCIO_PUBLIC_URL=https://npm.example.com
    volumes:
      - ./verdaccio/config:/verdaccio/conf
      - ./verdaccio/storage:/verdaccio/storage
      - ./verdaccio/plugins:/verdaccio/plugins
    ports:
      - "4873:4873"
    restart: unless-stopped
# Create config directory
mkdir -p verdaccio/config verdaccio/storage verdaccio/plugins
sudo chown -R 10001:65533 verdaccio/

Step 2: Verdaccio Configuration

# verdaccio/config/config.yaml
storage: /verdaccio/storage
auth:
  htpasswd:
    file: /verdaccio/conf/htpasswd
    max_users: 100
    algorithm: bcrypt
    rounds: 10

uplinks:
  npmjs:
    url: https://registry.npmjs.org/
    cache: true
    timeout: 30s
    max_fails: 3
    fail_timeout: 5m

packages:
  # Private scoped packages - only your team can access/publish
  "@acme/*":
    access: authenticated
    publish: authenticated
    unpublish: authenticated

  # Read-only public mirror - authenticated users can read, nobody publishes
  "@types/*":
    access: authenticated
    proxy: npmjs

  "**":
    access: authenticated
    proxy: npmjs
    unpublish: authenticated

server:
  keepAliveTimeout: 60

middlewares:
  audit:
    enabled: true

logs:
  - { type: stdout, format: pretty, level: http }

security:
  api:
    legacy: true
    jwt:
      sign:
        expiresIn: 30d
      verify:
        someProp: [secret]
  web:
    sign:
      expiresIn: 7d

web:
  title: "ACME npm Registry"
  enable: true
  primary_color: "#4D4D4D"
  scope: "@acme"

Step 3: User Management

# Install verdaccio CLI
npm install -g verdaccio

# Add users via htpasswd
docker exec verdaccio htpasswd -B -b /verdaccio/conf/htpasswd alice alicepassword
docker exec verdaccio htpasswd -B -b /verdaccio/conf/htpasswd bob bobpassword
docker exec verdaccio htpasswd -B -b /verdaccio/conf/htpasswd ci-runner cipassword

# Self-service via npm (if registration is enabled)
npm adduser --registry https://npm.example.com

Step 4: Nginx Reverse Proxy

# /etc/nginx/sites-available/verdaccio
server {
    listen 80;
    server_name npm.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name npm.example.com;

    ssl_certificate /etc/letsencrypt/live/npm.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/npm.example.com/privkey.pem;

    client_max_body_size 100m;

    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;
        proxy_set_header X-NginX-Proxy true;
    }
}

Step 5: Developer Configuration

Each developer configures their npm to use the private registry:

# Method 1: .npmrc in project root (recommended, committed to git)
# .npmrc
registry=https://npm.example.com/
@acme:registry=https://npm.example.com/
//npm.example.com/:_authToken=${NPM_TOKEN}
always-auth=false

# Method 2: Global npm config
npm config set registry https://npm.example.com
npm config set @acme:registry https://npm.example.com

# Login
npm login --registry https://npm.example.com
# Username: alice
# Password: alicepassword
# Email: alice@example.com

# Verify
npm whoami --registry https://npm.example.com
# pnpm configuration
# .npmrc (pnpm reads the same file)
@acme:registry=https://npm.example.com/
//npm.example.com/:_authToken=${NPM_TOKEN}

# yarn .yarnrc.yml
npmRegistries:
  "https://npm.example.com":
    npmAuthToken: "${NPM_TOKEN}"

npmScopes:
  acme:
    npmRegistryServer: "https://npm.example.com"

Step 6: Publish Internal Packages

// packages/ui-components/package.json
{
  "name": "@acme/ui-components",
  "version": "1.0.0",
  "description": "Shared UI component library",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "publishConfig": {
    "registry": "https://npm.example.com",
    "access": "restricted"
  },
  "scripts": {
    "build": "tsc && vite build",
    "prepublishOnly": "npm run build"
  }
}
# Build and publish
cd packages/ui-components
npm run build
npm publish

# Verify it's available
npm info @acme/ui-components --registry https://npm.example.com

# Install in another project
npm install @acme/ui-components

Step 7: Configure CI/CD Publishing Workflow

# .github/workflows/publish.yml
name: Publish Package

on:
  push:
    tags:
      - 'v*'

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://npm.example.com'

      - name: Install dependencies
        run: npm ci
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Build
        run: npm run build

      - name: Publish
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Step 8: S3 Storage Backend

For production with multiple replicas, use S3 instead of local filesystem:

# Install S3 storage plugin
docker exec verdaccio npm install -g verdaccio-aws-s3-storage
# config.yaml: replace storage section
store:
  aws-s3-storage:
    bucket: your-npm-registry-bucket
    region: us-east-1
    keyPrefix: verdaccio/
    endpoint: https://storage.example.com  # or remove for AWS S3
    s3ForcePathStyle: true  # Required for MinIO

# Set env vars:
# AWS_ACCESS_KEY_ID=your-access-key
# AWS_SECRET_ACCESS_KEY=your-secret

Step 9: Backup and Restore

#!/bin/bash
# scripts/backup-verdaccio.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_PATH="/backups/verdaccio-${DATE}.tar.gz"

tar czf "$BACKUP_PATH" ./verdaccio/storage ./verdaccio/config

# Ship to MinIO
mc cp "$BACKUP_PATH" company/backups/verdaccio/

echo "Verdaccio backup: $BACKUP_PATH"
# Restore
tar xzf "/backups/verdaccio-20260322_020000.tar.gz"
docker compose restart verdaccio

Verdaccio Plugins for Team Workflows

Verdaccio’s plugin system extends its capabilities well beyond basic auth and storage. The most useful plugins for remote teams are:

verdaccio-github-oauth-ui — Replaces htpasswd with GitHub OAuth login, so developers authenticate with their GitHub accounts and token rotation is automatic. Configuration is minimal: set the GitHub OAuth app credentials and the registry handles the rest.

verdaccio-audit — Enables npm audit against your private registry by proxying the npm audit endpoint. Developers running npm audit in a project that uses private packages get results from both the public advisory database and your registry’s metadata.

verdaccio-ldap — Connects to an existing corporate LDAP or Active Directory for authentication, which avoids managing a separate htpasswd user database when you already have an identity provider.

Installing a plugin requires placing it in the plugins volume directory and referencing it in config:

# Add plugin to running container (for testing)
docker exec verdaccio npm install verdaccio-github-oauth-ui

# Or add to Dockerfile for a custom image
FROM verdaccio/verdaccio:5
RUN npm install -g verdaccio-github-oauth-ui
# config.yaml — GitHub OAuth auth section
auth:
  github-oauth-ui:
    client-id: your-github-app-client-id
    client-secret: your-github-app-client-secret
    org: your-github-org

Scoped Package Strategy for Large Teams

Flat package names in a private registry become hard to manage as teams grow. A scoped namespace strategy keeps packages discoverable and enforces ownership:

@acme/ui-*        — Frontend design system and shared components (owned by UI team)
@acme/api-*       — Shared API clients and SDK wrappers (owned by Platform team)
@acme/config-*    — Shared ESLint, TypeScript, and build configs
@acme/shared-*    — Cross-team utilities (any team can publish, PR required)

Enforce this in Verdaccio config by giving each scope a separate access rule:

packages:
  "@acme/ui-*":
    access: authenticated
    publish: ui-team
    unpublish: ui-team

  "@acme/api-*":
    access: authenticated
    publish: platform-team
    unpublish: platform-team

  "@acme/config-*":
    access: authenticated
    publish: platform-team
    unpublish: platform-team

  "@acme/*":
    access: authenticated
    publish: authenticated
    unpublish: authenticated

This requires using verdaccio-htpasswd-groups or a plugin that understands user groups. With plain htpasswd, all authenticated users can publish to any scope — the pattern above enforces per-scope ownership only with group-aware auth.

Monitoring Verdaccio

Verdaccio exposes basic metrics at /-/ping and logs HTTP traffic to stdout. For production, ship logs to your observability stack and set up an uptime check:

# docker-compose.yml — add healthcheck
services:
  verdaccio:
    image: verdaccio/verdaccio:5
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:4873/-/ping"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

For richer metrics, pair Verdaccio with a Loki log aggregation setup: Verdaccio’s http-level logs capture every install, publish, and auth event with timestamps. A simple Grafana dashboard tracking publish frequency, install rates, and auth failures gives your platform team visibility into registry health without custom instrumentation.

Caching Strategy and Offline Resilience

One of the most underused Verdaccio features for remote teams is its aggressive caching of public registry packages. When a developer installs a package routed through Verdaccio, the tarball is stored locally under verdaccio/storage. Subsequent installs of the same version — from any developer’s machine or CI runner — hit the local cache without reaching npmjs.org.

This matters for three reasons. First, it eliminates dependency on npm’s CDN uptime; a registry outage does not break your builds. Second, it makes CI pipelines faster: a cold runner installing react from a Verdaccio cache on your LAN is faster than pulling from a remote CDN. Third, it freezes public package versions at the point they were first installed, so you cannot silently get a different tarball for the same version string later.

Configure the uplink timeout aggressively to prefer cache over network:

uplinks:
  npmjs:
    url: https://registry.npmjs.org/
    cache: true
    timeout: 10s
    max_fails: 2
    fail_timeout: 10m
    maxage: 30m    # Cache metadata for 30 minutes before re-fetching

To pre-warm the cache for critical packages before a deploy, install them through Verdaccio from a script:

#!/bin/bash
# scripts/warm-verdaccio-cache.sh
REGISTRY=https://npm.example.com
PACKAGES=(
  "react@18.2.0"
  "react-dom@18.2.0"
  "@types/react@18.2.0"
  "typescript@5.3.3"
  "vite@5.1.0"
)

for pkg in "${PACKAGES[@]}"; do
  npm pack "${pkg}" --registry "${REGISTRY}" --dry-run
  echo "Cached: ${pkg}"
done

The npm pack --dry-run forces Verdaccio to fetch and cache the tarball without writing anything locally. After this runs, CI runners pulling those exact versions will get them from the local cache consistently.

Verdaccio vs. Alternatives

Verdaccio is the right choice for teams that want a self-hosted registry with zero vendor dependency and minimal infrastructure cost. It runs on a single Docker container, uses local filesystem storage by default, and has no external service dependencies for basic operation.

The trade-off compared to managed alternatives:

For a team of 5-50 developers publishing a handful of internal packages, Verdaccio is the practical choice. When you need HA, cross-format support (Maven, PyPI, Docker in one tool), and enterprise RBAC, Nexus or Artifactory become worth the complexity.


Built by theluckystrike — More at zovo.one