2026.11 / reproducible builds 082
Coolify npm ci failed: fix package-lock.json out of sync
A Coolify Node.js deployment may stop during dependency installation with an error such as “npm ci can only install packages when your package.json and package-lock.json are in sync”, followed by one or more missing or invalid package entries. That failure is useful: the deployment has detected that the declared dependencies and the committed resolution no longer describe the same build.
The safe fix is not to replace npm ci with npm install in production. Reproduce the failure from the deployed commit, update the lockfile deliberately with the project’s intended npm version and dependency-tree flags, review the diff, and commit both manifests together.
A frozen install is a release gate. Repair the inputs in source control rather than teaching the build to rewrite them.
What the error actually means
npm documents four important properties of npm ci: an existing lockfile is required; the command fails when the lockfile disagrees with package.json; it removes an existing node_modules before installing; and it never writes back to either manifest. This is why a developer machine can appear healthy while a clean Coolify build fails.
| Observation | Likely cause | Correct boundary |
|---|---|---|
| Package is missing from the lockfile | package.json changed without committing the generated lockfile | Regenerate and review the lockfile |
| Dependency version does not satisfy the manifest | Manual edit, incomplete merge, or stale lockfile | Resolve the source-control change |
| Peer-dependency failure appears only in Coolify | Different npm version or missing project-level install flag | Align toolchain and committed configuration |
| Wrong workspace package fails | Build root or monorepo context is wrong | Correct Coolify’s base directory or Docker context |
| Failure persists after a correct commit | Coolify built another branch, commit, or application | Trace source commit through deployment and live release |
1. Read the exact failed deployment
Open the specific deployment in Coolify and record the source commit, branch, build pack or Dockerfile, base directory, Node image, and the complete npm error. Do not start by forcing a no-cache deployment. Cache cannot make a frozen install accept inconsistent manifests; a clean rebuild merely repeats the same deterministic failure.
Find the directory where the failed command ran. In a monorepo, the relevant files might be at the repository root, in apps/web, or managed through npm workspaces. The error concerns the manifest pair visible to that command, not whichever package.json you happened to open locally.
git show --stat --oneline EXPECTED_COMMIT
git show EXPECTED_COMMIT:package.json >/dev/null
git show EXPECTED_COMMIT:package-lock.json >/dev/null
# If Coolify builds a subdirectory, inspect that path instead
git show EXPECTED_COMMIT:apps/web/package.json >/dev/null
These commands prove file presence without printing private registry configuration or environment values. If the lockfile is absent from the commit, check whether .gitignore, an incomplete merge, or an uncommitted local change excluded it.
2. Match the Node and npm toolchain
Record the versions used by the failed build, then reproduce with the same major versions. A Dockerfile makes this boundary explicit; Nixpacks or another generated build still selects a Node/npm toolchain that should be visible in the deployment log.
node --version
npm --version
npm config get legacy-peer-deps
npm config get install-links
npm config get strict-peer-deps
Pinning an intentional Node major in the Dockerfile or project configuration reduces surprise. Avoid reaching immediately for the newest local npm. Lockfile formats have compatibility support, but dependency resolution and peer-dependency behaviour can still vary across toolchain generations and flags.
3. Reproduce a clean install from the intended commit
Use a clean worktree, disposable clone, or container so an existing node_modules cannot hide the problem. The important test is the same npm ci command at the same project root with the same committed configuration.
git switch --detach EXPECTED_COMMIT
npm ci
If the production Dockerfile is the authoritative build path, run it locally instead:
docker build --progress=plain -t app-lockfile-check .
Do not publish the application’s private registry URL, authentication headers, .npmrc credentials, or environment files while sharing logs. A project-level .npmrc may contain safe behaviour flags, but registry tokens belong in Coolify secrets or a build-secret mechanism—not in Git.
4. Regenerate the lockfile deliberately
Return to the branch that contains the intended dependency edit. Run npm install once with the project’s chosen npm version and any required dependency-tree flags. npm should update package-lock.json to match package.json. Then inspect the diff before accepting it.
git switch main
npm install
git diff -- package.json package-lock.json
npm ci
npm test
npm run build
If only the manifest was intended to change, a large unrelated lockfile rewrite is evidence to stop. Check npm versions, line endings, workspaces, registry configuration, and whether another package manager previously owned the project. Never hand-edit hundreds of resolved entries to make the error disappear.
5. Preserve required install flags in the project
npm’s official guidance is explicit: if the lockfile was generated with a tree-shaping flag such as --legacy-peer-deps or --install-links, npm ci must receive the same setting. If that flag is genuinely part of the project’s dependency policy, store the non-secret setting in a committed project-level .npmrc rather than relying on one developer’s global config.
# Example only; choose this only when the dependency graph requires it
legacy-peer-deps=true
Do not use legacy-peer-deps as a reflexive universal bypass. First inspect the reported peer conflict and update incompatible direct dependencies where practical. The committed flag should document a reviewed compatibility decision, not conceal an unknown graph.
6. Check monorepo and workspace boundaries
npm workspaces normally use the root manifest and root lockfile. A Coolify base directory pointed at one workspace can change what files are available during installation. Similarly, a Dockerfile can copy only a child package.json while omitting the root lockfile or workspace manifests needed to resolve the tree.
Verify all four boundaries:
- Coolify’s configured base directory and Dockerfile location;
- the Docker build context sent to the builder;
- the manifest and lockfile copied before
npm ci; - the workspace package files needed by the root dependency graph.
A typical single-package dependency stage copies both files before the rest of the source:
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
This ordering lets Docker reuse the dependency layer when application source changes but the manifests do not. The cache mount saves downloaded package data; it does not weaken the lockfile check. Add node_modules, local build output, logs, and environment files to .dockerignore so host state and secrets do not enter the build context.
7. Commit the complete dependency change
Commit the intended manifest, generated lockfile, and any reviewed non-secret project configuration together. Before pushing, rerun the frozen install from a clean state. This catches the common mistake of fixing package-lock.json locally but leaving it untracked or unstaged.
git status --short
git diff --check
git diff --cached -- package.json package-lock.json .npmrc
# After staging the intended files
git commit -m 'Fix npm lockfile for reproducible build'
git push origin main
If a private package is involved, the lockfile may contain registry URLs and integrity metadata but must not contain an access token. Review the staged diff for credentials before pushing. Keep package-read credentials narrowly scoped and inject them only for the build step.
8. Redeploy once and verify the release
Trigger one Coolify deployment for the new commit and follow its opaque deployment identifier to a terminal state. A no-cache deploy is useful only when investigating a suspected stale layer after the inputs are correct; Coolify’s normal Docker cache is safe to retain when both manifest files are copied into the dependency layer.
- Confirm the deployment checks out the new commit and expected branch.
- Confirm
npm ciruns at the intended directory and completes. - Confirm tests and the production build complete.
- Confirm the replacement container becomes healthy.
- Verify a unique release marker, changed route, or expected behaviour at the public domain.
- Check application logs after one real request.
- Confirm the next ordinary cached build still succeeds.
A green queue response is not completion, and HTTP 200 at a single-page-app fallback is not proof of the intended route. Match the running release to the source commit and assert content unique to that release.
Repairs to avoid
- Changing
npm citonpm installin deployment: this lets production resolve and rewrite an unreviewed tree. - Deleting
package-lock.json: this removes reproducibility rather than repairing it. - Committing
node_modules: host-specific installed files are not a lockfile or portable build artifact. - Using
--forceblindly: forcing resolution can replace a clear failure with an incompatible dependency graph. - Purging every Docker cache: cache cleanup does not reconcile two different manifests and may slow unrelated builds.
- Copying a global
.npmrcinto Git: it may contain private registry credentials or unrelated machine policy. - Editing the lockfile manually: regenerate it with npm and review the resulting dependency diff.
Compact recovery order
- Capture the failed Coolify commit, branch, build method, working directory, toolchain versions, and exact npm error.
- Prove which manifest and lockfile that commit contains.
- Reproduce
npm cicleanly with the same Node/npm major and project settings. - Regenerate the lockfile on the intended branch using the chosen toolchain.
- Review dependency, workspace, registry, and project-level configuration changes.
- Run clean
npm ci, tests, the build, and the Docker build. - Commit the complete dependency change and push it.
- Deploy once, poll to terminal status, and verify the exact live release.
Authoritative references: npm’s official npm ci documentation defines frozen-install behaviour and the requirement to preserve tree-shaping flags; its package-lock.json guide explains why the generated tree belongs in source control. Docker’s build-cache guide covers copying package-management files before source and using cache mounts. Coolify’s application build documentation and advanced build settings explain source builds, normal cache reuse, and one-off no-cache deployments.