DEV Community

Cover image for How to set up a private npm registry: from install to .npmrc, scope, and publish
Wen Ping
Wen Ping

Posted on Originally published at anguskit.com

How to set up a private npm registry: from install to .npmrc, scope, and publish

npm publish fails with E401 Unable to authenticate, or installs of internal packages return E404—usually the token is fine and the private registry is not wired: service down, wrong repo types, missing trailing / on the registry URL, or _authToken path / scope mismatch.

This article walks through AngusRepo Community self-hosted in order: install → repos and token → .npmrc (including scope) → minimal-package publish / install → verify. Examples use repo.example.com—swap your domain. Deeper 401: npm publish 401 troubleshooting.

1. Install (get a working console)

Prerequisites: Docker Engine + Compose v2; about 2 CPU / 4 GB; open 80; resolve gm.example.com and repo.example.com (labs can use /etc/hosts). Full steps: First run.

curl --fail --location --progress-bar -o AngusRepo-Community-1.0.0.zip \
  https://repo.anguskit.com/raw/raw-public/AngusKit/repo/AngusRepo-Community-1.0.0.zip
unzip AngusRepo-Community-1.0.0.zip
cd AngusRepo-1.0.0
./install.sh
Enter fullscreen mode Exit fullscreen mode

Wizard aligned with quickstart: Compose; Access → 2 (bundled proxy); TLS → 4 (HTTP :80); Database → Compose MySQL; Public DNS name → suffix such as example.com. After Install finished.:

./bin/angusctl.sh doctor   # wait for doctor: OK
Enter fullscreen mode Exit fullscreen mode

Open http://gm.example.com/, then http://repo.example.com/. Windows: curl.exe + .\install.bat. For HTTPS see Install & deploy.

The rest assumes HTTP self-hosted with URLs like http://repo.example.com/.... After trusted TLS, change both the registry protocol and the _authToken host path to the https:// form together—mixed protocols often surface as 401. Once the console opens, create repos and a token before client config.

2. Repos, token, and topology

npm publish only goes to HOSTED; PROXY and GROUP are read-only. On http://repo.example.com/, create (names are examples—keep them consistent):

Example name Type Role
npm-hosted HOSTED Internal publish target; disable overwrite
npmjs-proxy PROXY Proxy npmjs (https://registry.npmjs.org)
npm-all GROUP Unified install entry; member order hosted → proxy

For npm GROUPs, packuments (versions / dist-tags) are a union across members; when the same version exists on more than one member, the first match wins. Tarball binaries still resolve with member-order first-match. Keep hosted → proxy so internal versions enter the union and HOSTED wins on conflicts—that also helps reduce dependency-confusion risk. Developers configure one install URL.

Also prepare:

  • [ ] Access token (shown once—save it)
  • [ ] CI has Write on npm-hosted; developers have Read on npm-all
  • [ ] GROUP member order is hosted → npmjs-proxy
export NPM_TOKEN="your_access_token"   # .npmrc _authToken value
export REPO_HOST="repo.example.com"    # your repo.<domain>
Enter fullscreen mode Exit fullscreen mode

Prefer console “connection config”; field detail: npm Format.

3. Install: .npmrc and scope

Default rule: install via GROUP (npm-all), publish via HOSTED (npm-hosted). Never point a scope at PROXY.

Option A: company scope only (recommended during migration)

Public packages keep their existing source; only @mycompany hits the private GROUP:

@mycompany:registry=http://repo.example.com/npm/npm-all/
//repo.example.com/npm/npm-all/:_authToken=${NPM_TOKEN}
//repo.example.com/npm/npm-hosted/:_authToken=${NPM_TOKEN}
Enter fullscreen mode Exit fullscreen mode

Commit URLs in the project .npmrc; for the npm CLI / Yarn 1, the token lines may use ${NPM_TOKEN} from the environment. npm removed always-auth in 7.10—setting it does nothing. Rely on the two URL-scoped _authToken lines for metadata and tarball paths. Pipelines on pnpm ≥11.5.3 ignore ${...} credentials in the project .npmrc; move tokens to user-level config or pnpm_config_, see How to use a private registry with pnpm and Yarn.

Option B: default the whole project to the private registry

registry=http://repo.example.com/npm/npm-all/
//repo.example.com/npm/npm-all/:_authToken=${NPM_TOKEN}
//repo.example.com/npm/npm-hosted/:_authToken=${NPM_TOKEN}
Enter fullscreen mode Exit fullscreen mode

Three high-frequency pitfalls:

  1. The registry URL must end with /—omitting it is the most common slip.
  2. The _authToken line has no http:// / https://; the host path must match the registry exactly (path and trailing slash), or you get E401.
  3. package-lock.json locks the old registry when you migrate from npmjs—delete the lock and re-run npm install, or rewrite resolved fields, or installs keep hitting the public registry.

Confirm the token works against HOSTED before publishing:

npm whoami --registry http://repo.example.com/npm/npm-hosted/
Enter fullscreen mode Exit fullscreen mode

It should print your username. If this fails, fix auth before npm publish.

4. Publish: HOSTED only

The publish target must be HOSTED—not GROUP or PROXY. Scoped packages default to restricted access:

npm publish --registry http://repo.example.com/npm/npm-hosted/ --access restricted
Enter fullscreen mode Exit fullscreen mode

Or pin it in package.json:

{
  "name": "@mycompany/ui-kit",
  "version": "1.0.0",
  "publishConfig": {
    "registry": "http://repo.example.com/npm/npm-hosted/",
    "access": "restricted"
  }
}
Enter fullscreen mode Exit fullscreen mode

Republishing the same version yields 409. That is expected: bump the patch; do not overwrite. Switch to "access": "public" only when you intentionally expose the package and the repo ACL allows anonymous read.

5. Verify the loop (minimal publishable package)

Do not start by installing a package that does not exist yet. Build → publish → inspect → install in a clean directory:

mkdir -p /tmp/angus-npm-demo && cd /tmp/angus-npm-demo
cat > package.json <<'EOF'
{
  "name": "@mycompany/ui-kit",
  "version": "1.0.0",
  "description": "AngusRepo npm smoke package",
  "main": "index.js",
  "publishConfig": {
    "registry": "http://repo.example.com/npm/npm-hosted/",
    "access": "restricted"
  }
}
EOF
echo "module.exports = { ok: true };" > index.js

cat > .npmrc <<EOF
@mycompany:registry=http://repo.example.com/npm/npm-all/
//repo.example.com/npm/npm-all/:_authToken=${NPM_TOKEN}
//repo.example.com/npm/npm-hosted/:_authToken=${NPM_TOKEN}
EOF

npm publish
npm info @mycompany/ui-kit --registry http://repo.example.com/npm/npm-all/
Enter fullscreen mode Exit fullscreen mode

Then verify install in a clean directory:

mkdir -p /tmp/angus-npm-consume && cd /tmp/angus-npm-consume
cat > .npmrc <<EOF
@mycompany:registry=http://repo.example.com/npm/npm-all/
//repo.example.com/npm/npm-all/:_authToken=${NPM_TOKEN}
//repo.example.com/npm/npm-hosted/:_authToken=${NPM_TOKEN}
EOF
npm init -y
npm install @mycompany/ui-kit@1.0.0
node -e "console.log(require('@mycompany/ui-kit'))"
Enter fullscreen mode Exit fullscreen mode

The console artifact list should show 1.0.0; node should print { ok: true }.

Minimal CI snippet

Inject NPM_TOKEN (never an Admin token). Write a .npmrc that matches local config, then:

- run: |
    printf '@mycompany:registry=http://repo.example.com/npm/npm-all/\n' > .npmrc
    printf '//repo.example.com/npm/npm-all/:_authToken=%s\n' "$NPM_TOKEN" >> .npmrc
    printf '//repo.example.com/npm/npm-hosted/:_authToken=%s\n' "$NPM_TOKEN" >> .npmrc
    npm ci
    npm publish --registry http://repo.example.com/npm/npm-hosted/ --access restricted
Enter fullscreen mode Exit fullscreen mode

Full pipelines: Jenkins build, test, publish. pnpm / Yarn against the same repos: How to use a private registry with pnpm and Yarn (pnpm ≥11.5.3 does not expand ${NPM_TOKEN} in a project .npmrc).

6. Day-two ops (after publish / install works)

Version cleanup — keep the last N versions on npm-hosted; Dry Run first—Cleanup Policies. Keep releases immutable.

Cold PROXY — first public pulls are slow; warm frequently used dependencies if needed.

Ingest checks — uploads get a light admission gate; deeper vuln/license work can use an external scanner. Clear false positives before enabling block—admission gates and SBOM.

Error cheat sheet

Symptom Likely cause Fix
E401 Unable to authenticate _authToken path mismatch or expired token Align host path and trailing /; 401 troubleshooting
ENEEDAUTH Private coords pulled without a token Confirm _authToken lines for npm-all / npm-hosted with matching paths (npm ≥7.10 has no always-auth)
E403 Forbidden Authenticated but no Write / Read Fix ACL; publishers need Write on npm-hosted
E404 Not Found Wrong GROUP order, scope not applied, or unpublished npm info; open the metadata URL
Still hitting npmjs package-lock.json locked old resolved Delete lock and reinstall, or rewrite resolved
E409 Conflict Republishing the same version Bump the version
Stale install Cached metadata Wait for TTL, or invalidate in the console
ETIMEDOUT Cold PROXY Expected; warm hot dependencies

Field detail: npm Format. More ops issues: troubleshooting manual.

FAQ

Can I publish to a GROUP?

No. GROUP / PROXY are read-only; publish only to HOSTED. Scope routes installs; pin publish with publishConfig or --registry to HOSTED.

Should I switch the global registry on day one?

Prefer scope first: cut over @mycompany, leave public sources alone, then consolidate globally once stable. Verdaccio can still bridge a small npm-only team; fold into a unified artifact repo when you need multi-format or shared ACL.

Does the _authToken line include https://?

No. Omit the protocol; the host path must match the registry host + path + trailing / exactly so npm attaches credentials.

Why do installs still hit npmjs after migration?

npm v7+ lockfiles record the registry. Delete package-lock.json and reinstall, or update resolved fields inside it.

Takeaways

  • Install and open the console → HOSTED + PROXY + GROUP → issue a token.
  • Install via GROUP (.npmrc / scope); publish via HOSTED (publishConfig or --registry).
  • Trailing / plus matching _authToken paths for both npm-all and npm-hosted prevent most E401 / ENEEDAUTH failures; see the pnpm/Yarn article for pnpm token injection.
  • Smoke-test with a minimal package: publish → info → clean-dir install; fix lockfiles when migrating.

Reference articles

Top comments (0)