## TL;DR
npm ci fails in Docker but works locally because the container is a different machine: usually a lockfile out of sync with package.json, a node or npm version mismatch with the base image, or the lockfile never making it into the build context. Check those three first; they cover the large majority of cases.

## The query

```text
npm ci fails in Docker but works locally
```

## Use this when

- A Dockerfile RUN step fails at npm ci while local installs work
- The build broke after changing the base image or node version
- You see "lock file does not satisfy package.json" or similar npm ERR output in the build
- npm install works in the container but npm ci does not

## Not for

- npm ci fails locally too (fix the lockfile or package.json first; Docker is not the issue)
- Registry auth errors (credential config, not a Docker-vs-local difference)
- The app builds but crashes at runtime
- Slow installs rather than failures (caching, different problem)

## Steps

### 1. Read the actual npm error from the failed build

Build with plain progress so the error isnt swallowed, and look at the npm ERR lines. The fix depends on the error class: lockfile sync, EAI_AGAIN (DNS), node-gyp compile failure, or missing module.

```bash
docker build --progress=plain -t debug-build .
```

Expected output: the npm ERR lines naming the real cause, e.g. "lock file does not satisfy package.json".

### 2. Check the lockfile is in sync with package.json

npm ci is strict: package.json and package-lock.json must match exactly. A package.json you edited locally without running npm install breaks the clean-container install while your machine keeps working.

```bash
npm ci --dry-run
```

Expected output: passes locally. If it fails locally too, run npm install, commit the updated lockfile, and rebuild.

### 3. Make sure the lockfile is copied before npm ci runs

The classic Dockerfile bug: only package.json gets copied, or .dockerignore excludes the lockfile. Copy both manifests, install, then copy the source.

```dockerfile
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
```

Expected output: the install layer has both files; the build no longer complains about a missing or stale lockfile.

### 4. Match node and npm versions with the base image

Compare local versions against the image. Native modules compiled for one node major often fail on another.

```bash
node --version && npm --version
docker run --rm node:20 node --version
```

Expected output: versions match. If they dont, pin the FROM tag (e.g. node:20-bookworm-slim) to match your local toolchain.

### 5. Handle architecture mismatch

An arm64 laptop building an amd64 image breaks native modules. Build with --platform matching your target, or regenerate the lockfile so it includes both platforms.

Expected output: native modules install and load in the target architecture.

### 6. Check registry access from inside the build

Private registries need the .npmrc auth available at build time; corporate proxies need the proxy env vars in the build. npm ci needs network access to the registry, full stop.

Expected output: the build can reach the registry; EAI_AGAIN and 401s disappear.

## Variant phrasings

### docker build npm install fails but works on my machine

Almost always a version or environment difference. Steps 2 and 4 cover the two most common ones.

### npm ERR code EAI_AGAIN in docker build

DNS failure inside the build network. Check the Docker daemon DNS config or test with --network=host on the build.

### node-gyp fails in docker

Missing compilers or headers in slim images. Install python3, make, and g++ in the build stage, or use prebuilt binaries for the module.

## Why it happens

Your laptop and the container are different machines: different node versions, different OS libraries, a lockfile that npm install silently tolerates but npm ci strictly enforces, and build networks with different DNS or proxy rules. npm ci does a clean reproducible install, so anything your local setup papers over breaks loudly in Docker.

## Edge cases

- .dockerignore excluding package-lock.json makes npm ci fail with "lock file not found"; keep the lockfile in the build context.
- Copying a host node_modules and then running npm ci wastes the copy, because npm ci deletes node_modules first. Copy manifests, install, then copy source.
- Private registry auth in a committed .npmrc leaks credentials; use a build secret instead.
- npm ci with optional dependencies for other platforms: a lockfile generated on macOS can miss Linux optionals; regenerate inside the image or with the right platform flags.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_7u-7V95uuC6NlVZ25TJB6g
