← Back home

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.

ObservationLikely causeCorrect boundary
Package is missing from the lockfilepackage.json changed without committing the generated lockfileRegenerate and review the lockfile
Dependency version does not satisfy the manifestManual edit, incomplete merge, or stale lockfileResolve the source-control change
Peer-dependency failure appears only in CoolifyDifferent npm version or missing project-level install flagAlign toolchain and committed configuration
Wrong workspace package failsBuild root or monorepo context is wrongCorrect Coolify’s base directory or Docker context
Failure persists after a correct commitCoolify built another branch, commit, or applicationTrace 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:

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.

  1. Confirm the deployment checks out the new commit and expected branch.
  2. Confirm npm ci runs at the intended directory and completes.
  3. Confirm tests and the production build complete.
  4. Confirm the replacement container becomes healthy.
  5. Verify a unique release marker, changed route, or expected behaviour at the public domain.
  6. Check application logs after one real request.
  7. 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

Compact recovery order

  1. Capture the failed Coolify commit, branch, build method, working directory, toolchain versions, and exact npm error.
  2. Prove which manifest and lockfile that commit contains.
  3. Reproduce npm ci cleanly with the same Node/npm major and project settings.
  4. Regenerate the lockfile on the intended branch using the chosen toolchain.
  5. Review dependency, workspace, registry, and project-level configuration changes.
  6. Run clean npm ci, tests, the build, and the Docker build.
  7. Commit the complete dependency change and push it.
  8. 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.