Troubleshooting guide

Build or deploy broke and your code didn't change: pin your versions

The last good deploy and the failing one have the same commit, yet one builds and the other does not. Something referenced by a moving name, such as a Node major, a CLI, a base image tag or an open version range, resolved to something new. This is the most repeated pattern in our data: 34 verified builder reports describe it, across CSS builds, auth checks, serverless runtimes, containers and server packages.

34 verified cases11 min read Updated 14 Nov 2026How we collect cases

What you'll see

  • A deploy fails on a commit you have already deployed successfully, or CI goes red with no code change.
  • The build log shows a different Node, npm, CLI or base image version from the last green run.
  • The build passes on your laptop but fails on the host, or the other way round.
  • A fresh clone or a clean install fails with a peer-dependency conflict or a native module that has no prebuilt binary.
  • The site deploys but renders unstyled, authenticated pages return 500, or serverless functions time out on a newer runtime.
  • A container updater or a server package upgrade restarts a service overnight and it does not come back.

Why it happens

Cause 1

A moving name instead of a version

Tags such as latest, a bare Node major, or an engines range like >=22 resolve to whatever is newest on the day you build. Docker's docs say image tags are mutable, so the same tag can point at a different release after a rebuild. Vercel maps >=22.0.0 to the latest Node version it offers, and Render warns that an open-ended range resolves to the newest release.

Cause 2

The host's default changed, or you never set one

If you set no version, you get the host's default, and each host moves that default differently. Render's default depends on when the service was created. On Cloudflare Pages build image v3, minor updates can happen without notice and old image versions are migrated on a published date. Vercel rolls out minor and patch updates itself and only offers major versions to choose from.

Cause 3

The install does not follow the lock file

npm install can resolve fresh versions and rewrite the lock file. A Dockerfile that copies package.json without the lock file, or a lock file that was never committed, ships dependency versions nobody tested. That is how some reports ran for months with broken usage tracking or startup tracebacks.

Cause 4

Problems that a warm cache hides until a clean install

Peer-dependency conflicts and missing prebuilt binaries for a native module appear only on a clean install or an older runtime. A cached node_modules on your laptop or a cached CI layer can hide them for weeks. Some hosts also ignore settings you rely on: Cloudflare Pages build image v3 does not read engines from package.json.

Cause 5

Unattended updaters and OS packages

A container auto-updater, an unpinned apt-get install or a server package upgrade changes the running system without a deploy. Docker's docs note that images are snapshots and recommend rebuilding them deliberately with --pull. Binaries compiled for newer CPU features can also crash on an older VPS.

How to fix it

  1. Print the versions and diff two build logs

    Add the version line to the build command, then compare the last green log with the failing one. On Netlify, if the build still misbehaves after you change a version, redeploy with the clear cache option. For a function's runtime, log process.version inside the function.

    node -v && npm -v && npm ci && npm run build
  2. Pin the runtime where the host reads it

    Use an exact version on hosts that accept one: NODE_VERSION on Netlify, Cloudflare Pages and Render, or a .nvmrc or .node-version file. Vercel offers only majors, so pin the major in Project Settings or in engines. Cloudflare Pages build image v3 ignores engines, and Render advises a range with an upper bound.

    { "engines": { "node": "22.x" } }
  3. Install from the lock file and commit it with every dependency change

    npm ci errors if package.json and the lock file disagree, never writes to either, and deletes node_modules first. Use it in CI and in your Dockerfile. If you generated the lock file with flags such as --legacy-peer-deps, pass the same flags to npm ci, or commit them in .npmrc.

    COPY package.json package-lock.json ./
    RUN npm ci
  4. Pin base images by tag and digest, and package managers exactly

    Put the digest after an exact tag so a moved tag cannot change your build. Install CLIs as exact dev dependencies instead of calling whatever npx finds. For pnpm or Yarn on Netlify, set packageManager to an exact version, because Corepack does not accept ranges there. Pin apt packages too, and keep apt-get update and install in the same RUN.

    FROM node:<exact-version>-slim@sha256:<digest>
    # npm install --save-dev --save-exact <deploy-cli>
  5. Run a clean install on every runtime you deploy to

    A matrix job gives each Node version its own clean checkout and npm ci. Peer conflicts and missing native binaries show up here instead of in production.

    strategy:
      matrix:
        node-version: ['22.x', '24.x']
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v7
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm
      - run: npm ci
      - run: npm test
  6. Upgrade one thing per change, on a schedule

    Pinning without updates only delays the break, and Docker's docs note that digest pinning opts you out of automated security fixes. Let Dependabot open pull requests for npm, Docker and Actions. Merge each toolchain, CLI or image bump on its own, so a failure has one suspect.

    version: 2
    updates:
      - package-ecosystem: "docker"
        directory: "/"
        schedule:
          interval: "weekly"
      - package-ecosystem: "npm"
        directory: "/"
        schedule:
          interval: "weekly"

Check it's fixed

  • Two consecutive builds print the same node -v and npm -v, and both match the version you pinned.
  • On a fresh clone with no node_modules, npm ci passes on every Node version in your CI matrix.
  • docker build --pull --no-cache still produces a working image, and the FROM line carries a digest.
  • A deployed function logs the Node version you expect from process.version.

Fix it with Gemmein

On Gemmein, putting a version on the Live site copies one Development version (the same files, never new ones), and a rebuild puts the same version up again. Every publish runs the same test first: an uncaught JavaScript error in the draft's own files, or a page that never finishes loading, stops it with nothing put up, though if the test truly can't run, the publish goes ahead and says "Published UNTESTED — <why>".

  1. Promote a Development version to Live

    Once the app is live, npx gemmein site promote <version> asks your human to approve putting one Development version on the Live site: the same files, copied, never new ones. Before go-live it is refused (not_live).

    npx gemmein site promote <version>
  2. Read the step that failed

    Every publish, rollback, promote, unpublish and rebuild is kept, failed ones too, and npx gemmein site log (or site_history) reads that history: both sites unless --live or --dev. A failure leads with its step, the file and line, the reason and the fix.

    npx gemmein site log --dev
  3. Put the same version up again

    npx gemmein site rebuild puts the same version up again, every file re-checked. With --live it asks your human to approve "Rebuild the live site (version N, same files)"; if Live serves another version by then, nothing is rebuilt.

    npx gemmein site rebuild --live
  4. Check what changed in Gemmein itself

    npx gemmein changes --since <version> prints everything after the version you knew, with no project, account or network needed. The docs say to run it whenever what you remember might be older than the guide.

    npx gemmein changes --since <version>

Questions

Should I pin exact versions or use ranges?

Pin exactly for toolchains, CLIs and base images. Ranges in package.json are fine when a committed lock file fixes what gets installed. Where you must use an engines range, give it an upper bound, as Render advises.


Does pinning stop me getting security fixes?

It can. Docker's docs say that pinning a digest opts you out of automated security fixes. Pair every pin with a scheduled update bot such as Dependabot's docker and npm ecosystems, and review those updates one at a time.


Why does it build on my laptop but not on the host?

Your laptop has its own Node version and a populated node_modules, while the host builds from a clean checkout on its own default. Run node -v in the build command, compare it with your local version, and reproduce the host's install with npm ci.


Is it safe to follow the newest Node release?

The Node.js release page says production applications should only use Active or Maintenance long-term support releases. Even then, pin the major, because the host or an open range can move you to the next one without a code change.


How do I find which version moved?

Print the tool versions at the top of the build, then diff the last green log against the failing one. Check the lock file diff and the base image digest as well, since either can change with no source edit.


Sources

← Back to the full report