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.
When to prune instead of bundle
Section titled “When to prune instead of bundle”| Approach | Best for | Trade-off |
|---|---|---|
| Bundling | Serverless functions, simple APIs | Single file output, no node_modules needed |
| Pruning | Docker deployments, native dependencies | Keeps 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.jsonchanges - You consume workspace libraries as packages rather than bundling them
How pruning works
Section titled “How pruning works”Pruning uses four Nx targets that run in sequence:
buildcompiles your application (esbuild, webpack, tsc, etc.).prune-lockfile(@nx/js:prune-lockfile) reads your projectpackage.json, generates a minimalpackage.json, and creates a pruned lockfile containing only production dependencies.copy-workspace-modules(@nx/js:copy-workspace-modules) copies workspace libraries referenced viaworkspace:*into aworkspace_modules/directory and rewrites their dependency references tofile:paths.prune(nx:noop) depends on bothprune-lockfileandcopy-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 └── ...Set up prune targets
Section titled “Set up prune targets”Add the following targets to your project's package.json or project.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" } } }}{ "name": "@my-org/my-app", "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:
nx prune my-appBoth prune-lockfile and copy-workspace-modules set cache: true, so subsequent runs are instant when nothing changes.
Use pruned output in Docker
Section titled “Use pruned output in Docker”The generated Dockerfile copies the build output and runs npm install:
# apps/my-app/DockerfileFROM docker.io/node:lts-alpine
ENV HOST=0.0.0.0ENV 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:
# Build the app and prune dependenciesnx prune my-app
# Build the Docker imagenpx nx docker:build my-app
# Run the containernx docker:run my-app -p 3000:3000Migrate from generatePackageJson
Section titled “Migrate from generatePackageJson”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:
{ "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:
{ "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" } } } }}Remove generatePackageJson from your webpack config:
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');const { join } = require('path');
module.exports = { output: { path: join(__dirname, '../../dist/apps/my-app'), }, plugins: [ new NxAppWebpackPlugin({ target: 'node', compiler: 'tsc', main: './src/main.ts', tsConfig: './tsconfig.app.json', }), ],};Remove generatePackageJson from your rollup or vite build options. With TS Solution Setup, the project package.json is used directly.
Step 3: Add prune targets
Section titled “Step 3: Add prune targets”Add the prune-lockfile, copy-workspace-modules, and prune targets to your project package.json as shown in the set up prune targets section.
Step 4: Update your Dockerfile
Section titled “Step 4: Update your Dockerfile”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.