NestJS Dockerfile with pnpm for Production (Alpine, Multi-Stage)
This NestJS Dockerfile installs dependencies with pnpm through Corepack, compiles the TypeScript build, prunes development packages, and ships only dist, production node_modules and package.json on node:24-alpine. The API runs as the unprivileged node user on port 3000. It suits most REST and GraphQL NestJS services.
- CI status
- Build passing
- Image size (uncompressed)
- 181.1 MB
- Build time
- 20 s
- Runtime image
node:24-alpine- Port
- 3000
- Last built
- 2026-10-15
# syntax=docker/dockerfile:1
FROM node:24-alpine AS build
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build && pnpm prune --prod
FROM node:24-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build --chown=node:node /app/package.json ./
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"].git
node_modules/
dist/
.env*
coverage/
Dockerfile
.dockerignoreBefore you build
- Commit pnpm-lock.yaml so the install is reproducible.
- Set the packageManager field in package.json so Corepack activates the right pnpm version.
- Make main.ts listen on process.env.PORT, falling back to 3000.
- Keep the default Nest build output in dist with main.js as the entry point.
How this Dockerfile works
FROM node:24-alpine AS build RUN corepack enableCorepack ships with Node and provides the pnpm version pinned in package.json, so you don't need a global npm install of pnpm.
COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfileCopying only the manifests first caches the install layer. --frozen-lockfile fails the build instead of silently updating versions if the lockfile is out of date.
RUN pnpm run build && pnpm prune --prodCompiles TypeScript into dist, then removes development dependencies such as the Nest CLI and TypeScript, so only runtime packages remain to be copied.
FROM node:24-alpineThe final stage starts from a clean image. Corepack and pnpm aren't needed here, because the app is started with node directly.
COPY --from=build --chown=node:node /app/node_modules ./node_modules COPY --from=build --chown=node:node /app/dist ./distCopies the pruned dependencies and the compiled output, owned by the node user so the app can read them without root.
USER nodeRuns the API as the unprivileged user built into the official Node image instead of root.
CMD ["node", "dist/main.js"]Starts the compiled entry point directly. Without npm or pnpm as a parent process, shutdown signals reach Nest and its shutdown hooks run cleanly.
Build and run
docker build -t nestjs-app .
docker run --rm -p 3000:3000 nestjs-appPass database URLs and secrets with -e flags or an env file at run time. Then call any route on http://localhost:3000 to confirm the container answers.
Other NestJS Dockerfile variants
pnpm on Alpine gives fast, cache-friendly installs and a lean runtime, so it's a good default for teams already using pnpm. npm or Yarn variants and a Debian slim base for native modules appear below once they pass CI.
| Variant | Runtime image | Image size | CI |
|---|---|---|---|
| Node Alpine + npm | node:24-alpine | 181.4 MB | Passing |
| Node Alpine + pnpm | node:24-alpine | 181.1 MB | Passing |
Troubleshooting
- ERR_PNPM_OUTDATED_LOCKFILE Cannot install with frozen-lockfile
- package.json changed without updating the lockfile. Run pnpm install locally, commit the updated pnpm-lock.yaml, and rebuild.
- Error: Cannot find module '/app/dist/main.js'
- Your build writes to a different path, for example dist/src/main.js. Check nest-cli.json and tsconfig, then point the CMD line at the real compiled entry file.
- A package that was a devDependency is missing at runtime
- pnpm prune --prod removed it. Move the package to dependencies in package.json if your application imports it while running.
FAQ
- Why use Corepack instead of npm install -g pnpm?
- Corepack reads the packageManager field and activates exactly that pnpm version. Local machines, CI and the image then all use the same pnpm, without an extra global install layer.
- Is Alpine safe for NestJS?
- For most NestJS apps, yes. If you depend on native modules compiled for glibc, such as some database drivers or image libraries, a Debian slim base avoids musl compatibility problems.
- How do I enable graceful shutdown?
- Call app.enableShutdownHooks() in main.ts. Because node runs as the main process, the stop signal from docker stop reaches Nest directly.