Skip to main content
Technology & EngineeringPackage Management309 lines

Private Registries

Setting up and using private npm registries with Verdaccio, GitHub Packages, and GitLab Package Registry

Quick Summary30 lines
You are an expert in configuring and operating private npm registries, including Verdaccio, GitHub Packages, GitLab Package Registry, and AWS CodeArtifact for internal package distribution.

## Key Points

- **Internal packages**: Share code across teams without publishing to public npm.
- **Access control**: Restrict who can publish and install packages.
- **Caching/proxy**: Cache public npm packages locally for faster installs and offline resilience.
- **Audit trail**: Track which packages are used and who published them.
- **Security**: Prevent dependency confusion attacks by reserving internal scope names.
- name: Publish to GitHub Packages
- Package names must match the repository owner scope (`@owner/package`).
- Packages are tied to a GitHub repository.
- Deleting package versions is restricted (organizations can configure policies).
- name: Configure CodeArtifact
1. **Use scoped packages**: `@myorg/utils` on your private registry. Scope routing in `.npmrc` ensures resolution from the correct registry.
2. **Claim the scope on public npm**: Register `@myorg` on npmjs.com even if you only publish privately.

## Quick Example

```ini
# .npmrc
@myorg:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
```

```ini
//registry.example.com/:_authToken=TOKEN_HERE
```
skilldb get package-management-skills/Private RegistriesFull skill: 309 lines
Paste into your CLAUDE.md or agent config

Private Registries — Package Management

You are an expert in configuring and operating private npm registries, including Verdaccio, GitHub Packages, GitLab Package Registry, and AWS CodeArtifact for internal package distribution.

Core Philosophy

Overview

Private npm registries allow organizations to publish and consume internal packages without exposing them to the public npm registry. They also serve as caching proxies for public packages, improving install speed and providing resilience against registry outages. Common options range from self-hosted (Verdaccio) to managed services (GitHub Packages, GitLab, AWS CodeArtifact, Artifactory).

Core Concepts

Why Private Registries

  • Internal packages: Share code across teams without publishing to public npm.
  • Access control: Restrict who can publish and install packages.
  • Caching/proxy: Cache public npm packages locally for faster installs and offline resilience.
  • Audit trail: Track which packages are used and who published them.
  • Security: Prevent dependency confusion attacks by reserving internal scope names.

Scoped Packages and Registry Routing

npm supports routing scoped packages (@myorg/*) to a specific registry while all other packages go to the public registry:

# .npmrc
@myorg:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

This means @myorg/shared resolves from GitHub Packages while lodash resolves from registry.npmjs.org.

Authentication

All private registries require authentication. Tokens are configured in .npmrc:

//registry.example.com/:_authToken=TOKEN_HERE

For CI, use environment variables:

//registry.example.com/:_authToken=${NPM_TOKEN}

Implementation Patterns

Verdaccio (Self-Hosted)

Verdaccio is a lightweight, open-source npm registry that proxies to the public registry and stores private packages locally.

Installation:

# Run with Docker
docker run -d --name verdaccio \
  -p 4873:4873 \
  -v verdaccio-storage:/verdaccio/storage \
  -v verdaccio-conf:/verdaccio/conf \
  verdaccio/verdaccio

# Or install globally
npm install -g verdaccio
verdaccio

Configuration (config.yaml):

storage: ./storage
plugins: ./plugins

auth:
  htpasswd:
    file: ./htpasswd
    max_users: 100

uplinks:
  npmjs:
    url: https://registry.npmjs.org/
    cache: true

packages:
  '@myorg/*':
    access: $authenticated
    publish: $authenticated
    unpublish: $authenticated

  '**':
    access: $all
    publish: $authenticated
    proxy: npmjs

middlewares:
  audit:
    enabled: true

listen: 0.0.0.0:4873

Client configuration:

# .npmrc
registry=http://localhost:4873
//localhost:4873/:_authToken=TOKEN

Docker Compose for production:

version: '3.8'
services:
  verdaccio:
    image: verdaccio/verdaccio:5
    ports:
      - "4873:4873"
    volumes:
      - verdaccio-storage:/verdaccio/storage
      - ./config.yaml:/verdaccio/conf/config.yaml
    environment:
      VERDACCIO_PORT: 4873
    restart: unless-stopped

volumes:
  verdaccio-storage:

GitHub Packages

Publishing setup:

// package.json
{
  "name": "@myorg/shared",
  "version": "1.0.0",
  "publishConfig": {
    "registry": "https://npm.pkg.github.com"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/myorg/shared.git"
  }
}

Authentication (.npmrc):

@myorg:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

The token needs read:packages scope to install and write:packages to publish.

GitHub Actions publishing:

- name: Publish to GitHub Packages
  run: npm publish
  env:
    NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Constraints:

  • Package names must match the repository owner scope (@owner/package).
  • Packages are tied to a GitHub repository.
  • Deleting package versions is restricted (organizations can configure policies).

GitLab Package Registry

Publishing setup:

# .npmrc
@myorg:registry=https://gitlab.com/api/v4/projects/PROJECT_ID/packages/npm/
//gitlab.com/api/v4/projects/PROJECT_ID/packages/npm/:_authToken=${GITLAB_TOKEN}

For group-level installs:

@myorg:registry=https://gitlab.com/api/v4/groups/GROUP_ID/-/packages/npm/
//gitlab.com/api/v4/groups/GROUP_ID/-/packages/npm/:_authToken=${GITLAB_TOKEN}

CI/CD publishing (.gitlab-ci.yml):

publish:
  image: node:20
  script:
    - echo "@myorg:registry=https://${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/" > .npmrc
    - echo "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}" >> .npmrc
    - npm publish
  only:
    - tags

AWS CodeArtifact

# Login (generates temporary token)
aws codeartifact login \
  --tool npm \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository internal-npm

# This updates .npmrc with the registry URL and auth token
# Token expires after 12 hours by default

CI configuration:

- name: Configure CodeArtifact
  run: |
    TOKEN=$(aws codeartifact get-authorization-token \
      --domain myorg \
      --domain-owner 123456789012 \
      --query authorizationToken \
      --output text)
    npm config set registry https://myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/internal-npm/
    npm config set //myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/internal-npm/:_authToken=$TOKEN

Preventing Dependency Confusion

Dependency confusion occurs when an attacker publishes a public package with the same name as your private package. Mitigations:

  1. Use scoped packages: @myorg/utils on your private registry. Scope routing in .npmrc ensures resolution from the correct registry.

  2. Claim the scope on public npm: Register @myorg on npmjs.com even if you only publish privately.

  3. Use upstream blocking: Configure Verdaccio to not proxy scoped packages:

packages:
  '@myorg/*':
    access: $authenticated
    publish: $authenticated
    # No proxy line = never look upstream
  1. Use CodeArtifact upstream restrictions: CodeArtifact can block specific packages from being fetched upstream.

Multi-Registry Configuration

# .npmrc - route different scopes to different registries
@myorg:registry=https://npm.pkg.github.com
@partner:registry=https://registry.partner.com
registry=https://registry.npmjs.org

//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
//registry.partner.com/:_authToken=${PARTNER_TOKEN}

Migrating to a Private Registry

  1. Set up the registry (Verdaccio, GitHub Packages, etc.).
  2. Update .npmrc in all projects to route your scopes.
  3. Republish existing internal packages to the private registry.
  4. Update CI configurations with appropriate authentication.
  5. Claim your scope on public npm to prevent confusion attacks.

Best Practices

  • Use scoped packages (@myorg/*) and route them to the private registry via .npmrc.
  • Never hardcode tokens in .npmrc. Use environment variables (${NPM_TOKEN}) or credential helpers.
  • Claim your organization scope on the public npm registry, even if you only publish privately.
  • Use Verdaccio as a caching proxy in addition to private hosting to speed up installs.
  • Configure CI authentication using short-lived tokens where possible (GitHub's GITHUB_TOKEN, AWS CodeArtifact's temporary tokens).
  • Commit a project-level .npmrc with registry routing (without tokens) so all developers get the correct configuration.
  • Use publishConfig.registry in package.json to prevent accidentally publishing internal packages to public npm.
  • Audit registry access periodically. Remove tokens for former team members.
  • Set up monitoring and alerting for self-hosted registries (Verdaccio uptime, storage usage).
  • Test registry failover: if your private registry is down, can the team still install public packages?

Common Pitfalls

  • Token in committed .npmrc: Accidentally committing authentication tokens to Git. Use environment variable interpolation and add .npmrc entries with tokens to .gitignore (or use a separate ~/.npmrc for user tokens).
  • Dependency confusion: Not scoping internal packages, allowing an attacker to publish a same-named package on public npm with a higher version.
  • Expired tokens in CI: Tokens (especially CodeArtifact) expire. CI pipelines break silently when tokens are not refreshed.
  • Wrong registry for publish: Running npm publish without publishConfig sends the package to public npm. Always set publishConfig.registry for private packages.
  • Mixed lock file registries: When switching registries, the lock file retains old resolved URLs. Regenerate the lock file after registry changes.
  • Verdaccio storage growth: Without periodic cleanup, Verdaccio's storage grows unbounded as it caches every public package version fetched. Set up storage pruning.
  • GitHub Packages scope requirement: GitHub Packages requires the package scope to match the repository owner. You cannot publish @other-org/pkg to your GitHub Packages.
  • Forgetting to proxy public packages: Configuring Verdaccio without an uplink to npmjs means public packages cannot be installed through it.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add package-management-skills

Get CLI access →