Private Registries
Setting up and using private npm registries with Verdaccio, GitHub Packages, and GitLab Package Registry
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 linesPrivate 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:
-
Use scoped packages:
@myorg/utilson your private registry. Scope routing in.npmrcensures resolution from the correct registry. -
Claim the scope on public npm: Register
@myorgon npmjs.com even if you only publish privately. -
Use upstream blocking: Configure Verdaccio to not proxy scoped packages:
packages:
'@myorg/*':
access: $authenticated
publish: $authenticated
# No proxy line = never look upstream
- 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
- Set up the registry (Verdaccio, GitHub Packages, etc.).
- Update
.npmrcin all projects to route your scopes. - Republish existing internal packages to the private registry.
- Update CI configurations with appropriate authentication.
- 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
.npmrcwith registry routing (without tokens) so all developers get the correct configuration. - Use
publishConfig.registryinpackage.jsonto 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.npmrcentries with tokens to.gitignore(or use a separate~/.npmrcfor 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 publishwithoutpublishConfigsends the package to public npm. Always setpublishConfig.registryfor 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/pkgto 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
Related Skills
Bundling Libraries
Bundling JavaScript/TypeScript libraries for distribution using tsup, unbuild, and Rollup
Dependency Audit
Security auditing npm dependencies for vulnerabilities, license compliance, and supply chain risks
Lockfile Management
Lock file strategies for deterministic installs across npm, pnpm, and Yarn
Npm Publishing
Publishing packages to the npm registry with proper configuration, access control, and release automation
Pnpm
pnpm workspace management for monorepos with content-addressable storage and strict dependency isolation
Semantic Versioning
Semantic versioning (SemVer) conventions, version ranges, and strategies for managing breaking changes