A lockfile records the exact graph, and npm 2,036 ci installs exactly that graph or fails. To test that, install BookNest on two "machines", this Ubuntu 225 host and an Alpine 13,255 container with a different C library, and hash every installed file on each, skipping npm's own bookkeeping file, node_modules/.package-lock.json.
cd "${1:-.}" && find node_modules -type f ! -name .package-lock.json -print0 | sort -z \
| xargs -0 sha256sum | sha256sum | cut -c1-16cd ~/v5-ch1/booknest
rm -rf node_modules && npm ci --no-audit --no-fund >/dev/null
echo "Ubuntu host: $(bash ~/v5-ch1/tree-hash.sh)"
docker run --rm --name l1-ci-check -v "$PWD":/src:ro \
-v ~/v5-ch1/tree-hash.sh:/tree-hash.sh:ro node:24-alpine sh -c \
'cp /src/package*.json /tmp && cd /tmp && npm ci --no-audit --no-fund >/dev/null 2>&1
echo "Alpine 3.24: $(sh /tree-hash.sh)"'Ubuntu host: 9ddf2bece073948c Alpine 3.24: 9ddf2bece073948c
Every installed file is identical on both machines. Three conditions make that true:
The lockfile is committed and used. npm ci fails when package.json and the lockfile disagree (Missing: dotenv@17.4.2 from lock file); npm install would quietly re-resolve. In CI use npm ci, pnpm 69,400 install --frozen-lockfile, yarn install --immutable or bun ci.
Every machine reads the same registry configuration, from the project's .npmrc. A lockfile written against a private mirror records that mirror's tarball URLs, and machines that cannot reach it fail.
The tool versions match. Pin the manager with packageManager or devEngines (packageManager Field) and make engines binding: npm only warns about it by default. BookNest now commits a .npmrc with engine-strict=true, so npm ci outside "node": ">=24" stops with EBADENGINE (tested by raising the range to >=30).