NestJS mit Docker bereitstellen

NestJS mit Docker bereitstellen

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

Bevor du eine NestJS-App startest, musst du sie bauen. Dafür brauchst du den Compiler und die Nest CLI. Mit einem mehrstufigen Dockerfile bleiben diese Tools in der Build-Stufe. Ins Runtime-Image kommen die kompilierte App und die Pakete, die sie beim Start braucht.

Projekt vorbereiten

Die Beispiele nutzen eine einzelne NestJS-App mit npm und einer package-lock.json im Repository. Das build-Skript erzeugt dist/main.js. Führ zuerst lokal npm run build aus und prüf, ob die Datei dort liegt. Bei einem Monorepo oder einer anderen TypeScript-Config kann der Pfad anders sein.

Leg eine .dockerignore an:

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

Für einen ersten lokalen Test reicht dieses einfache Dockerfile:

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"]

Es installiert die Pakete, kopiert den Code, kompiliert TypeScript und startet die App. Die Build-Tools bleiben dabei im fertigen Image. Im nächsten Schritt räumen wir sie raus.

Build und Runtime trennen

Ersetze das einfache Dockerfile durch diese Variante:

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"]

Die Build-Stufe installiert alle Abhängigkeiten. Compiler und Nest CLI stehen normalerweise in devDependencies, werden hier also noch gebraucht. Erst nach dem Build entfernt npm prune --omit=dev diese Pakete.

Die Runtime-Stufe bekommt dist, die Produktionsabhängigkeiten und package.json. Mit USER node läuft sie ohne Root-Rechte. Ein USER node in einer früheren Stufe gilt für diese neue Stufe nicht. Deshalb steht die Anweisung auch hier.

Beide Stufen nutzen dasselbe Basis-Image. Das ist besonders bei nativen Paketen wichtig. Ein Modul, das du gegen Debians glibc baust, läuft nicht unbedingt unter Alpine. Hier bleiben wir bei Debian slim. Wenn du Alpine nutzt, nimm es für beide Stufen und test deine nativen Pakete.

Bauen und testen

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

Öffne http://localhost:3000 oder eine deiner API-Routen. Startet die App nicht, schau mit docker logs nest-app in einem zweiten Terminal in die Logs. Mit docker image ls nest-app siehst du die Image-Größe.

Lass den Port über eine Umgebungsvariable setzen. Mit dem Standard-Adapter Express brauchst du dafür diese Zeile in main.ts:

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

Mit dem Fastify-Adapter sieht das so aus:

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();

Durch 0.0.0.0 nimmt der Server Anfragen von außerhalb des Containers an. Lauscht er nur auf der Loopback-Adresse, hilft auch eine Portfreigabe nicht.

Häufige Fehler

  • nest: not found: Die Build-Stufe braucht die Nest CLI. Installier vor dem Build auch die Entwicklungsabhängigkeiten.
  • webpack oder ein anderes Build-Paket fehlt: Prüf package.json und die Lockdatei. Eine andere Node-Version fügt kein fehlendes Paket hinzu.
  • Cannot find module .../dist/main.js: Schau nach, wo der Build die Startdatei ablegt, und pass CMD an. Kopier auch Templates und andere Dateien mit, die deine App zur Laufzeit braucht.
  • Config fehlt: Gib Umgebungsvariablen beim Start mit. Deine lokale .env gehört nicht ins Image.

Wenn du pnpm nutzt

Bleib bei der pnpm-Version deines Projekts und committe die pnpm-lock.yaml. Für eine App ohne Workspace sieht die Build-Stufe so aus:

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

Das Beispiel nutzt pnpm 10. Für reproduzierbare Builds nimmst du die genaue Version aus deinem Projekt. Die Runtime-Stufe bleibt wie im npm-Beispiel. Bei Workspaces müssen auch interne Pakete ins Deployment. Einfaches Pruning wie bei einer einzelnen App reicht dafür nicht immer aus.

Mehr zum kompilierten Einstiegspunkt steht in der NestJS-Dokumentation zum Deployment. Läuft der Container lokal, committe die Dateien und deploye das Repository auf Sliplane. Setz den HTTP-Port auf 3000 und ergänze deine Umgebungsvariablen.

Willkommen auf deiner Cloud-Plattform

Sliplane macht es einfach, deine Apps in der Cloud zu deployen und zu skalieren. Starte mit einem Container oder deinem Lieblings-Framework und wachse von da aus. Probier es jetzt aus!