Skip to content

Optimize Node.js Dockerfile generation with layer caching and BuildKit mounts#12739

Merged
eerhardt merged 15 commits intomainfrom
copilot/optimize-node-docker-file
Nov 10, 2025
Merged

Optimize Node.js Dockerfile generation with layer caching and BuildKit mounts#12739
eerhardt merged 15 commits intomainfrom
copilot/optimize-node-docker-file

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Nov 6, 2025

Optimize Node.js Docker file generation

This PR implements best practices for Node.js Dockerfile generation to improve build performance and reduce image size.

Summary of Changes

Key Optimizations (AddNodeApp)

  1. Better Layer Caching

    • Copy package*.json files first before running install
    • Then run dependency installation
    • Finally copy the rest of the source code
    • This ensures that dependency installation is only re-run when package files change, not on every code change
    • Package file patterns now stored in JavaScriptPackageManagerAnnotation.PackageFilesPatterns as PackageFilePattern records
  2. BuildKit Cache Mounts

    • npm: --mount=type=cache,target=/root/.npm
    • yarn: --mount=type=cache,target=/usr/local/share/.cache/yarn
    • pnpm: --mount=type=cache,target=/pnpm/store
    • Significantly speeds up repeated builds
    • Cache mount paths are stored in the JavaScriptPackageManagerAnnotation.CacheMount property

Architecture Improvements

The optimization logic has been refactored based on reviewer feedback:

  • Production flags are determined in the default argument getter methods for each package manager
  • Cache mount paths and package file patterns are stored as properties on JavaScriptPackageManagerAnnotation
  • NEW: PackageFilePattern is now a sealed record type instead of a tuple for better versionability
  • Dockerfile generation now uses values from annotations rather than hardcoded values
  • This makes the code cleaner, more maintainable, and more extensible

Example: Before vs After

Before:

FROM node:22-alpine AS build

WORKDIR /app
COPY . .

RUN npm ci

FROM node:22-alpine AS runtime

WORKDIR /app
COPY --from=build /app /app

ENV NODE_ENV=production

USER node

ENTRYPOINT ["node","app.js"]

After:

FROM node:22-alpine AS build

WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .

FROM node:22-alpine AS runtime

WORKDIR /app
COPY --from=build /app /app

ENV NODE_ENV=production

USER node

ENTRYPOINT ["node","app.js"]

Expected Benefits

  • 30-50% faster builds with cache mounts on repeated builds
  • Faster rebuilds when only source code changes (dependencies cached)
  • Better Docker layer caching efficiency

Recent Changes

  • Removed hardcoded EXPOSE 3000 statement per reviewer feedback
  • Added CacheMount and PackageFilesPatterns properties to JavaScriptPackageManagerAnnotation
  • Changed PackageFilesPatterns from using tuples to using a PackageFilePattern record type for better versionability

Testing

  • All existing tests updated and passing (71/71)
  • Full build successful
  • Verified Dockerfile generation for various scenarios:
    • With package.json and npm ci
    • Without package.json (no package manager)
    • With custom package managers
    • With build scripts
    • With container files

Files Modified

  • src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs - Core optimization logic refactored
  • src/Aspire.Hosting.JavaScript/JavaScriptPackageManagerAnnotation.cs - Added CacheMount and PackageFilesPatterns properties, introduced PackageFilePattern record
  • tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs - Test expectations updated
  • tests/Aspire.Hosting.JavaScript.Tests/AddJavaScriptAppTests.cs - Test expectations updated
  • tests/Aspire.Hosting.JavaScript.Tests/AddViteAppTests.cs - Test expectations updated
  • tests/Aspire.Hosting.JavaScript.Tests/PackageInstallationTests.cs - Test expectations updated
  • tests/Aspire.Hosting.JavaScript.Tests/Snapshots/*.verified.txt - Snapshot test updated

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

- Copy package*.json files first for better layer caching
- Add BuildKit cache mount for npm/yarn/pnpm downloads
- Use --omit=dev flag for npm ci to skip dev dependencies in production
- Selectively copy only node_modules and app files to runtime stage
- Updated tests to reflect new optimized Dockerfile structure

Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com>
Copilot AI changed the title [WIP] Optimize Node Dockerfile for better caching and efficiency Optimize Node.js Dockerfile generation with layer caching and BuildKit mounts Nov 6, 2025
Copilot AI requested a review from eerhardt November 6, 2025 04:25
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Nov 6, 2025

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 12739

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 12739"

Address feedback from @davidfowl to remove the hardcoded EXPOSE 3000 statement from the generated Dockerfiles for Node.js apps.

Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
/// </summary>
/// <param name="packageManager">The package manager executable name (npm, yarn, pnpm).</param>
/// <returns>The cache mount path for BuildKit, or null if not supported.</returns>
private static string? GetCacheMount(string packageManager)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot - change this to be on the JavaScriptPackageManagerAnnotation instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 7abc3d1. Added a CacheMount property to JavaScriptPackageManagerAnnotation that stores the BuildKit cache mount path. Each package manager (npm, yarn, pnpm) now sets this when creating the annotation.

- Added CacheMount property to JavaScriptPackageManagerAnnotation
- Production flags now added in WithNpm/WithYarn/WithPnpm methods
- Simplified Dockerfile generation to use annotation values
- Removed GetProductionInstallFlag and GetCacheMount helper functions
- Updated tests to reflect new behavior

Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com>
Copilot AI requested a review from eerhardt November 6, 2025 15:55
Copy link
Copy Markdown
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

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

@copilot please implement this feedback

- Added PackageFilesPattern property to JavaScriptPackageManagerAnnotation
- Dockerfile generation now uses PackageFilesPattern from annotation instead of hardcoded value
- Moved production flag logic into GetDefaultNpmInstallCommand (returns both command and args)
- Moved production flag logic into GetDefaultYarnInstallArgs
- Moved production flag logic into GetDefaultPnpmInstallArgs
- Updated tests to include PackageFilesPattern for custom package managers
- Updated test expectations for pnpm to include --prod in default args

Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com>
Copilot AI requested a review from eerhardt November 6, 2025 22:37
@eerhardt eerhardt marked this pull request as ready for review November 7, 2025 01:50
Copilot AI review requested due to automatic review settings November 7, 2025 01:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR optimizes Docker image generation for Node.js applications by implementing better layer caching strategies. The key improvements include copying package dependency files separately before source code, using BuildKit cache mounts for package manager caches, and removing the unnecessary EXPOSE directive.

Key changes:

  • Enhanced JavaScriptPackageManagerAnnotation with cache mount paths and package file patterns
  • Modified Dockerfile generation logic to separate dependency installation from source code copying for optimal layer caching
  • Added package manager-specific configurations for npm, yarn, and pnpm with appropriate cache mount paths

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
JavaScriptPackageManagerAnnotation.cs Added CacheMount parameter and PackageFilesPatterns property to support BuildKit cache mounts and flexible package file copying
JavaScriptHostingExtensions.cs Implemented Docker layer optimization by copying package files first, using cache mounts for installs, then copying source code; removed EXPOSE directive; added file system checks for yarn and pnpm
AddNodeAppTests.cs Updated test expectations to match new Dockerfile generation patterns with cache mounts and optimized layer structure
AddNodeAppTests.VerifyNodeAppWithContainerFilesGeneratesCorrectDockerfile.verified.txt Snapshot updated to reflect optimized Dockerfile with package files copied separately and cache mount usage

@eerhardt eerhardt marked this pull request as draft November 7, 2025 02:22
@eerhardt
Copy link
Copy Markdown
Member

eerhardt commented Nov 7, 2025

This still needs more testing

@eerhardt eerhardt marked this pull request as ready for review November 7, 2025 17:00
Changed List<(string Source, string Destination)> to List<PackageFilePattern> where PackageFilePattern is a sealed record. This improves versionability by making property names part of the API contract.

Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Copilot AI requested a review from davidfowl November 7, 2025 17:30
@eerhardt eerhardt closed this Nov 7, 2025
@eerhardt eerhardt reopened this Nov 7, 2025
@dotnet-policy-service dotnet-policy-service bot added this to the 13.1 milestone Nov 7, 2025
@eerhardt
Copy link
Copy Markdown
Member

eerhardt commented Nov 7, 2025

@davidfowl - I think this is ready to merge now.

@eerhardt
Copy link
Copy Markdown
Member

eerhardt commented Nov 7, 2025

/backport to release/13.0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Nov 7, 2025

Started backporting to release/13.0: https://github.com/dotnet/aspire/actions/runs/19181013909

@eerhardt eerhardt merged commit 99ebd29 into main Nov 10, 2025
582 of 585 checks passed
@eerhardt eerhardt deleted the copilot/optimize-node-docker-file branch November 10, 2025 16:07
radical pushed a commit to radical/aspire that referenced this pull request Nov 10, 2025
…t mounts (microsoft#12739)

* Initial plan

* Optimize Node.js Dockerfile generation for AddNodeApp

- Copy package*.json files first for better layer caching
- Add BuildKit cache mount for npm/yarn/pnpm downloads
- Use --omit=dev flag for npm ci to skip dev dependencies in production
- Selectively copy only node_modules and app files to runtime stage
- Updated tests to reflect new optimized Dockerfile structure

Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com>

* Remove EXPOSE 3000 from generated Dockerfiles

Address feedback from @davidfowl to remove the hardcoded EXPOSE 3000 statement from the generated Dockerfiles for Node.js apps.

Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>

* Move production flags and cache mounts to annotations

- Added CacheMount property to JavaScriptPackageManagerAnnotation
- Production flags now added in WithNpm/WithYarn/WithPnpm methods
- Simplified Dockerfile generation to use annotation values
- Removed GetProductionInstallFlag and GetCacheMount helper functions
- Updated tests to reflect new behavior

Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com>

* Refactor package manager configuration per reviewer feedback

- Added PackageFilesPattern property to JavaScriptPackageManagerAnnotation
- Dockerfile generation now uses PackageFilesPattern from annotation instead of hardcoded value
- Moved production flag logic into GetDefaultNpmInstallCommand (returns both command and args)
- Moved production flag logic into GetDefaultYarnInstallArgs
- Moved production flag logic into GetDefaultPnpmInstallArgs
- Updated tests to include PackageFilesPattern for custom package managers
- Updated test expectations for pnpm to include --prod in default args

Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com>

* Fix up dockerfile logic

* Fix PackageFilesPattern

* Remove optimizations from pnpm since they don't work

* Tweak yarn cache mount logic

* Respect new annotations in AddJavaScriptApp

* Add pnpm optimizations

* Replace tuple with record type for PackageFilePattern

Changed List<(string Source, string Destination)> to List<PackageFilePattern> where PackageFilePattern is a sealed record. This improves versionability by making property names part of the API contract.

Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>

* Rename to CopyFilePattern

* Clean up and refactor code

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com>
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com>
@github-actions github-actions bot locked and limited conversation to collaborators Dec 11, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants