Skip to content

When deploying Node.js applications to containers, you typically need only production dependencies, not your entire workspace node_modules. Pruning generates a standalone package.json, a pruned lockfile, and copies any workspace libraries your app depends on. The result is everything you need to run npm ci inside a Docker image with only the packages your application uses.

To bundle your app into a single file instead (no node_modules needed), see Bundling projects for deployment.

ApproachBest forTrade-off
BundlingServerless functions, simple APIsSingle file output, no node_modules needed
PruningDocker deployments, native dependenciesKeeps node_modules but only production deps

Use pruning when:

  • Your app has native dependencies (e.g. bcrypt, sharp) that can't be bundled
  • You want Docker layer caching, where dependency layers rebuild only when package.json changes
  • You consume workspace libraries as packages rather than bundling them

Pruning uses four Nx targets that run in sequence:

  1. build compiles your application (esbuild, webpack, tsc, etc.).
  2. prune-lockfile (@nx/js:prune-lockfile) reads your project package.json, generates a minimal package.json, and creates a pruned lockfile containing only production dependencies.
  3. copy-workspace-modules (@nx/js:copy-workspace-modules) copies workspace libraries referenced via workspace:* into a workspace_modules/ directory and rewrites their dependency references to file: paths.
  4. prune (nx:noop) depends on both prune-lockfile and copy-workspace-modules, giving you a single command to run.

After running nx prune my-app, the build output directory contains:

apps/my-app/dist/
├── main.js # Compiled application
├── package.json # Pruned production dependencies
├── package-lock.json # Pruned lockfile (or yarn.lock / pnpm-lock.yaml)
└── workspace_modules/ # Only present if you have workspace deps
└── @my-org/
└── my-lib/
├── package.json
└── ...

Add the following targets to your project's package.json or project.json:

apps/my-app/package.json
{
"name": "@my-org/my-app",
"nx": {
"targets": {
"prune-lockfile": {
"dependsOn": ["build"],
"cache": true,
"executor": "@nx/js:prune-lockfile",
"outputs": [
"{workspaceRoot}/apps/my-app/dist/package.json",
"{workspaceRoot}/apps/my-app/dist/package-lock.json"
],
"options": {
"buildTarget": "build"
}
},
"copy-workspace-modules": {
"dependsOn": ["build"],
"cache": true,
"outputs": ["{workspaceRoot}/apps/my-app/dist/workspace_modules"],
"executor": "@nx/js:copy-workspace-modules",
"options": {
"buildTarget": "build"
}
},
"prune": {
"dependsOn": ["prune-lockfile", "copy-workspace-modules"],
"executor": "nx:noop"
}
}
}
}

Replace package-lock.json in the outputs array with yarn.lock or pnpm-lock.yaml if needed.

Then run:

Terminal window
nx prune my-app

Both prune-lockfile and copy-workspace-modules set cache: true, so subsequent runs are instant when nothing changes.

The generated Dockerfile copies the build output and runs npm install:

# apps/my-app/Dockerfile
FROM docker.io/node:lts-alpine
ENV HOST=0.0.0.0
ENV PORT=3000
WORKDIR /app
COPY dist .
# You can remove this install step if you build with `--bundle` option.
# The bundled output will include external dependencies.
RUN npm --omit=dev -f install
CMD ["node", "main.js"]

The COPY dist . line works because the Dockerfile lives inside the project directory (apps/my-app/), and the build output goes to apps/my-app/dist/. The pruned package.json, lockfile, and workspace_modules/ are all inside dist/.

Build and run:

Terminal window
# Build the app and prune dependencies
nx prune my-app
# Build the Docker image
npx nx docker:build my-app
# Run the container
nx docker:run my-app -p 3000:3000

If you're upgrading to Nx 20+ with TS Solution Setup (the default for new workspaces), the generatePackageJson option is no longer supported. You'll see this error:

Follow these steps to migrate to the prune workflow:

Step 1: Move dependencies to your project package.json

Section titled “Step 1: Move dependencies to your project package.json”

With TS Solution Setup, each project has its own package.json. List all runtime dependencies there:

apps/my-app/package.json
{
"name": "@my-org/my-app",
"dependencies": {
"express": "^4.18.0",
"@my-org/shared-utils": "workspace:*"
}
}

Use the workspace:* protocol for workspace libraries.

Step 2: Remove generatePackageJson from your build configuration

Section titled “Step 2: Remove generatePackageJson from your build configuration”

Remove generatePackageJson from your esbuild target options:

apps/my-app/package.json
{
"nx": {
"targets": {
"build": {
"executor": "@nx/esbuild:esbuild",
"options": {
"platform": "node",
"outputPath": "dist/apps/my-app",
"format": ["cjs"],
"main": "apps/my-app/src/main.ts",
"tsConfig": "apps/my-app/tsconfig.app.json"
}
}
}
}
}

Add the prune-lockfile, copy-workspace-modules, and prune targets to your project package.json as shown in the set up prune targets section.

Replace references to the old generated package.json with the pruned output. See the use pruned output in Docker section for a recommended Dockerfile structure.