Why Upgrade?
Node.js 20 hits EOL on April 30, 2026. Node 24 is the current Active LTS ("Krypton") with support until April 2028. Seemed like a good time to jump.
I went from Node 20.20.0 straight to 24.14.0, upgrading one major version at a time (20 → 22 → 24) to isolate any issues.
What Needed to Change
The straightforward part — updating version references everywhere:
.node-versionpackage.json(engines and volta)@types/node- Dockerfile base images (
node:24.14.0-slimandgcr.io/distroless/nodejs24-debian12)
Node 22 was a clean upgrade. Node 24 is where things got interesting.
Issue 1: npm prune --production is Dead
npm 9+ deprecated --production. npm@11 (bundled with Node 24) still accepts it but throws warnings. The fix:
1# Before2RUN npm prune --production34# After5RUN npm prune --omit=dev
Simple enough. But this change also introduced Issue 3 (below).
Issue 2: --ignore-scripts Now Blocks Everything
This was the big one.
I wrote about using --ignore-scripts for security a while back. It's great for preventing supply chain attacks — skip all lifecycle scripts during install, then selectively rebuild trusted packages.
In npm@10 and below, --ignore-scripts skipped preinstall and postinstall but still ran prepare. In npm@11, it blocks everything, including prepare.
Why does this matter? I have an internal icon library (@myorg/icons) installed as a git dependency. It uses a prepare script to build itself:
1{2 "scripts": {3 "prepare": "npm run build",4 "build": "npm run optimize && node ./lib/build.js"5 }6}
With npm@10, npm ci --ignore-scripts would skip postinstall but still run prepare, so the icon library would build its dist/ directory. With npm@11, prepare is blocked too, so dist/ is never generated and the build fails:
1Type error: Module '"@/components/UI/Icon"' has no exported member 'IconSample'.
What Didn't Work
npm rebuild — I tried adding the package to the existing rebuild command:
1npm ci --ignore-scripts && npm rebuild dd-trace @myorg/icons
But npm rebuild only runs native addon scripts (install, postinstall), not prepare. So the icon library still wasn't built.
Running the build script directly — Next attempt:
1npm ci --ignore-scripts && npm rebuild dd-trace && npm run --prefix node_modules/@myorg/icons build
This failed because the package's build script needs svgo, which is a devDependency of the icon library. Since --ignore-scripts prevented the full install, svgo wasn't available:
1sh: 1: svgo: not found
Also, for git-based dependencies, npm doesn't keep the source files (src/, lib/) after install — only the build output. So even if svgo was available, the source files to build from weren't there.
What Worked
Reinstall the package individually without --ignore-scripts:
1RUN --mount=type=cache,target=/root/.npm \2 npm ci --ignore-scripts && npm rebuild dd-trace && \3 npm cache clean --force && npm install @myorg/icons
The npm install @myorg/icons runs without --ignore-scripts, so prepare executes normally and dist/ is generated.
Since this is an internal private package (not a random npm package from the internet), running its scripts doesn't compromise the security posture that --ignore-scripts was set up for.
Issue 3: npm prune Needs Git Auth in Multi-Stage Builds
After fixing the icon library, the Docker build failed at a different stage:
1npm error command git --no-replace-objects ls-remote ssh://git@github.com/myorg/icons.git2npm error git@github.com: Permission denied (publickey).
This happened during npm prune --omit=dev in the builder stage. npm@11 apparently validates git dependencies during prune, which means it needs access to the git remote.
The problem: my Dockerfile is a multi-stage build. GitHub credentials (.netrc) are set up in the deps stage but not in the builder stage. With npm@10, prune didn't try to resolve git deps. With npm@11, it does.
The fix — copy .netrc from the deps stage:
1FROM node:24.14.0-slim AS builder2RUN apt-get update && apt-get install -y git3WORKDIR /app4COPY --from=deps /root/.netrc /root/.netrc # npm@11 needs git auth for prune5COPY --from=deps /app/node_modules ./node_modules6COPY . .
This is safe because the builder stage is intermediate — it's discarded after the build. The final production image (distroless) only copies specific app files and never includes .netrc.
Bonus: Node 24 Deprecation Warning
After deploying, I noticed this in the logs:
1(node:7) [DEP0187] DeprecationWarning: Passing invalid argument types to fs.existsSync is deprecated
This is from dd-trace, not my code. It passes undefined to fs.existsSync somewhere (likely checking process.env.DD_TELEMETRY_FORWARDER_PATH when it's not set). This was already fixed in dd-trace 5.x but my version (5.24.0) predates the fix. It's just a warning — harmless for now.
The Final Dockerfile
Here's what the relevant parts look like after all fixes:
1FROM node:24.14.0-slim AS deps2# ... auth setup ...3RUN npm ci --ignore-scripts && npm rebuild dd-trace && \4 npm cache clean --force && npm install @myorg/icons56FROM node:24.14.0-slim AS builder7RUN apt-get update && apt-get install -y git8WORKDIR /app9COPY --from=deps /root/.netrc /root/.netrc10COPY --from=deps /app/node_modules ./node_modules11COPY . .12# ... env vars ...13RUN npm run build14RUN npm prune --omit=dev1516FROM gcr.io/distroless/nodejs24-debian12 AS runner17# ... only app files copied here, no credentials ...
Version Choice
I went with 24.14.0 instead of 24.14.1 (the latest security patch released the day before). Distroless images and Docker tags can take a day or two to stabilize after a new release, so I opted for the slightly older version that I could verify had working Docker images.
TL;DR
Upgrading Node.js in a Dockerized app isn't just bumping version numbers. npm@11 (bundled with Node 24) has breaking changes around --ignore-scripts and dependency resolution that can silently break multi-stage Docker builds. The three issues I hit:
npm prune --production→ use--omit=dev--ignore-scriptsblocksprepare→ reinstall git-based packages individuallynpm prunevalidates git deps → forward credentials to the builder stage
Each one only showed up in CI/CD Docker builds, not locally. Test your Docker builds early.
References
Docker Images
- Builder:
node:24.14.0-slim - Runner:
gcr.io/distroless/nodejs24-debian12
