How to Dockerize NestJS

How to Dockerize NestJS

Jonas Scholz - Co-Founder von sliplane.ioJonas Scholz
6 min

NestJS needs a build step before you can run the application. A multi-stage Dockerfile keeps the compiler and Nest CLI in the build stage, then copies the compiled app into a smaller runtime image.

Prepare the project

These examples use a single NestJS app with npm, a committed package-lock.json, and a build script that produces dist/main.js. Run npm run build locally and check that path first. A monorepo or a different TypeScript configuration can produce another layout.

Start with a .dockerignore:

node_modules
dist
.git
npm-debug.log
.env
.env.*

For a first local test, this single-stage Dockerfile is enough:

Dockerfile
FROM node:24-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/main.js"]

It installs dependencies, copies the source, compiles TypeScript, and starts the compiled app. It also keeps all the build dependencies in the final image. That is what we will change next.

Separate the build from the runtime

Replace the basic Dockerfile with this one:

Dockerfile
FROM node:24-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev

FROM node:24-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
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"]

The build stage installs all dependencies because the compiler and Nest CLI are usually in devDependencies. Only after the build do we remove those packages with npm prune --omit=dev.

The runtime stage receives dist, production dependencies, and package.json. It runs as node, not root. Setting USER node in an earlier stage would not carry over to this new stage, so it belongs here as well.

Both stages use the same base image. This matters for native packages: copying modules built against Debian's glibc into an Alpine runtime can fail. This example uses Debian slim throughout. If you use Alpine, use it consistently and check your native dependencies.

Build and test

docker build -t nest-app .
docker run --rm --name nest-app -p 3000:3000 nest-app

Open http://localhost:3000 or one of your API routes. Use docker logs nest-app from another terminal to inspect startup errors. To see the image size, run docker image ls nest-app.

Keep the application port configurable. For the default Express adapter, the relevant line in main.ts is:

await app.listen(Number(process.env.PORT ?? 3000), "0.0.0.0");

With the Fastify adapter, use:

import { NestFactory } from "@nestjs/core";
import {
  FastifyAdapter,
  NestFastifyApplication,
} from "@nestjs/platform-fastify";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter()
  );
  await app.listen(Number(process.env.PORT ?? 3000), "0.0.0.0");
}
bootstrap();

Binding to 0.0.0.0 makes the server reachable from outside the container. Publishing a port cannot fix a server that only listens on loopback.

Common failures

  • nest: not found: The build stage needs the Nest CLI. Do not install only production dependencies before compiling.
  • Missing webpack or another build package: Check the project's declared build dependencies and lockfile. Changing Node versions does not install a missing package.
  • Cannot find module .../dist/main.js: Inspect the build output and change CMD to the actual entry point. Copy required templates or other runtime assets too.
  • Missing configuration: Supply environment variables at runtime. Do not copy your local .env into the image.

If you use pnpm

Keep your project's pinned pnpm version and pnpm-lock.yaml. For a single-package app, the build-stage equivalent is:

Dockerfile
FROM node:24-bookworm-slim AS build
RUN npm install --global pnpm@10
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build && pnpm prune --prod

This example uses pnpm 10; use the exact version recorded by your project for reproducible builds. Keep the runtime stage from the npm example. Workspaces need a deployment layout that includes their internal packages; do not assume a single-package prune is enough.

The NestJS deployment guide explains the compiled entry point. Once the container works locally, commit the files and deploy the repository on Sliplane with HTTP port 3000 and the required runtime variables.

Welcome to your cloud platform

Sliplane makes it simple to deploy and scale your apps in the cloud. Start with a container or your favorite framework and grow from there. Try it now and get started in minutes!