Skip to content

perf(nodejs): reduce Docker image file count with nft static analysis#7079

Open
rochdev wants to merge 17 commits into
mainfrom
rochdev/feat-nodejs-base-images-perf
Open

perf(nodejs): reduce Docker image file count with nft static analysis#7079
rochdev wants to merge 17 commits into
mainfrom
rochdev/feat-nodejs-base-images-perf

Conversation

@rochdev
Copy link
Copy Markdown
Member

@rochdev rochdev commented Jun 3, 2026

Summary

Reduces the file count in Node.js base images using @vercel/nft static analysis. nft traces the import graph from the app entry point and deletes everything in node_modules that is statically unreachable, giving ~4,500 files per image vs ~21,600 from a raw bun install (−79%).

nft works at two levels simultaneously: it eliminates entire unreachable packages (eslint and its transitive deps, @types in JS images, unused AWS SDK v3 packages, etc.) and within kept packages it retains only the specific files that are imported, not the full package tree.

How it works

A single nft-prune.mjs script calls nodeFileTrace() from @vercel/nft, then deletes every node_modules file not in the result. @vercel/nft is added as a devDependency so it installs with bun install and is then pruned away by its own trace (the app never imports it). App source is COPY'd into the base image before bun install so nft has an entry point to trace from; both steps run in the same RUN layer so deleted files never appear in the image.

The script accepts a --keep-types flag used by express4-typescript, which preserves node_modules/@types — needed because tsc scans for type declarations dynamically rather than importing them statically.

Other changes

  • Next.js build moved into base image — compiled during base image creation; stable across dd-trace versions since dd-trace is loaded via NODE_OPTIONS at runtime.
  • App source now lives in base images — required for nft tracing; final Dockerfiles no longer need to re-copy source.

Note: New base image tags (v2) need to be pushed to Docker Hub by the R&P team via #apm-shared-testing before this PR can merge.

Test plan

  • CI builds base images via build-nodejs-base-images label
  • All five weblogs start correctly with dd-trace loaded
  • express4-typescript compiles successfully (verifies --keep-types keeps @types and typescript)
  • Next.js healthcheck and routes return 200

🤖 Generated with Claude Code

rochdev and others added 2 commits June 3, 2026 13:43
Build the Next.js app during base image creation rather than at weblog
build time, since dd-trace is loaded via NODE_OPTIONS at runtime and has
no compile-time coupling to the app. This avoids rebuilding on every
weblog image update.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the monolithic aws-sdk v2 (~80k files) with three targeted v3
packages (@aws-sdk/client-kinesis, client-sns, client-sqs). The smaller
dependency tree reduces Docker image extraction time significantly since
extraction cost scales with file count rather than compressed size.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

CODEOWNERS have been resolved as:

utils/build/docker/nodejs/nft-prune.mjs                                 @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/docker-bake.hcl                               @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express4-typescript.Dockerfile                @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express4-typescript.base.Dockerfile           @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express4-typescript/bun.lock                  @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express4-typescript/package.json              @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express4-typescript/tsconfig.json             @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express4.Dockerfile                           @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express4.base.Dockerfile                      @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express4/bun.lock                             @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express4/package.json                         @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express5.Dockerfile                           @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express5.base.Dockerfile                      @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express5/bun.lock                             @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/express5/package.json                         @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/fastify.Dockerfile                            @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/fastify.base.Dockerfile                       @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/fastify/bun.lock                              @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/fastify/package.json                          @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/nextjs.Dockerfile                             @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/nextjs.base.Dockerfile                        @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/nextjs/bun.lock                               @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/nextjs/package.json                           @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/nextjs/src/app/customResponseHeaders/route.js  @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/nextjs/src/app/exceedResponseHeaders/route.js  @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/nextjs/src/app/healthcheck/route.js           @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/nextjs/src/app/sample_rate_route/[i]/route.js  @DataDog/dd-trace-js @DataDog/system-tests-core
utils/build/docker/nodejs/uds-express4.Dockerfile                       @DataDog/dd-trace-js @DataDog/system-tests-core

@datadog-prod-us1-3

This comment has been minimized.

rochdev and others added 2 commits June 3, 2026 15:02
Add cleanup-node-modules.sh, called at the end of every base image bun
install, which removes:
- /root/.bun install cache (doubles file count for zero runtime benefit)
- .map, .ts, .md, .eslint* files from all packages
- test/, examples/, docs/, benchmark/ directories
- devDependencies and their orphaned transitives (eslint family,
  ES polyfill shims, rambda)
- es-abstract year directories 2015–2022 (only 2023 is needed by
  the one remaining consumer, es-aggregate-error via tedious)
- @types packages (JavaScript images only)

Result: 38,652 → 11,312 total files in express4 base image (−71%).
The script includes a maintenance comment explaining how to audit the
hardcoded removal list when dependencies change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bun add in install_ddtrace.sh recreates /root/.bun, adding ~17k files
to every weblog image layer. Remove it immediately after the install.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rochdev rochdev changed the title perf(nodejs): reduce base image extraction time perf(nodejs): reduce Docker image file count by 71% Jun 3, 2026
rochdev and others added 7 commits June 3, 2026 15:27
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mage

`next experimental-compile` compiles lazily at request time and requires
source files at runtime. `next build` precompiles all routes, making the
src/ directory safe to remove from the final image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents Next.js from statically caching route handlers, ensuring
dd-trace instruments every request as intended by these tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… prerender

next build statically prerenders routes that don't use request data.
The healthcheck route requires dd-trace at runtime, so it must run
per-request, not be cached from a build-time execution without dd-trace.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dd-trace instruments at the HTTP transport level so it captures every
request regardless of static vs dynamic rendering. force-dynamic on the
layout forces React server-component streaming on every request, which
breaks older dd-trace versions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
skipLibCheck avoids re-type-checking .d.ts files in node_modules,
which is the bulk of tsc's work given the large number of declaration
files kept for compilation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switches all five Node.js weblog base images from the manual
cleanup-node-modules.sh pruning script to @vercel/nft, which uses
static analysis from the app entry point to keep only files reachable
at runtime. This is more principled and more aggressive: ~4,500 files
retained vs ~8,700 with the cleanup script (~48% fewer).

Key changes:
- Add nft-prune.mjs: calls nodeFileTrace() then deletes unreachable
  files, including itself (not imported by the app)
- --keep-types flag preserves node_modules/@types for the TypeScript
  weblog, which needs declarations for tsc at final-image build time
- App source is now COPY'd before bun install so nft has an entry
  point; bun install and nft-prune run in the same RUN layer so
  deleted files never appear in the image
- Delete cleanup-node-modules.sh, which is no longer used

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rochdev rochdev changed the title perf(nodejs): reduce Docker image file count by 71% perf(nodejs): reduce Docker image file count with nft static analysis Jun 4, 2026
rochdev and others added 2 commits June 3, 2026 22:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s route

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rochdev rochdev marked this pull request as ready for review June 4, 2026 02:42
@rochdev rochdev requested review from a team as code owners June 4, 2026 02:42
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 851b7f1010

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread utils/build/docker/nodejs/nft-prune.mjs
rochdev and others added 4 commits June 3, 2026 23:07
nft traces binary targets but not the .bin symlinks that point to them.
Commands like `next start` use ./node_modules/.bin/next, which would be
deleted without this.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Only remove actual caches (/root/.bun, .next/cache). Lockfiles, source
files, and config files are harmless and could be useful in the weblog.
Empty directory cleanup after pruning is also unnecessary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…uild-only dirs

nft cannot trace dynamic requires in next/dist/compiled (e.g. webpack-lib
loaded by config-utils.js at startup). Keep the entire compiled/ directory
then remove known build-only entries (experimental React variants, babel,
terser) to recover ~19 MB / 473 files.

Add --keep-dir flag to nft-prune.mjs for preserving directories regardless
of static reachability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
*babel* glob also matched @babel/runtime which config-utils.js requires
at startup. Only delete the build-only babel and babel-packages entries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant