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
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
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 onnpm-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>
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}
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}
Three high-frequency pitfalls:
-
The registry URL must end with
/—omitting it is the most common slip. -
The
_authTokenline has nohttp:///https://; the host path must match the registry exactly (path and trailing slash), or you get E401. -
package-lock.jsonlocks the old registry when you migrate from npmjs—delete the lock and re-runnpm install, or rewriteresolvedfields, 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/
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
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"
}
}
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/
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'))"
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
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 (publishConfigor--registry). - Trailing
/plus matching_authTokenpaths for bothnpm-allandnpm-hostedprevent most E401 /ENEEDAUTHfailures; see the pnpm/Yarn article for pnpm token injection. - Smoke-test with a minimal package: publish → info → clean-dir install; fix lockfiles when migrating.
Top comments (0)