
NestJS mit Docker bereitstellen
Jonas ScholzBevor 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:
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:
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.webpackoder ein anderes Build-Paket fehlt: Prüfpackage.jsonund 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 passCMDan. Kopier auch Templates und andere Dateien mit, die deine App zur Laufzeit braucht.- Config fehlt: Gib Umgebungsvariablen beim Start mit. Deine lokale
.envgehö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:
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.