Einleitung: Warum TypeScript-Anwendungen containerisieren?
Moderne Softwareentwicklung ist ohne Containerisierung nicht denkbar. Docker hat sich zum De-facto-Standard für das Verpacken, Ausliefern und Ausführen von Anwendungen in isolierten Umgebungen entwickelt. In Kombination mit TypeScript – einer streng typisierten Obermenge von JavaScript – sorgt Docker für Vorhersagbarkeit, Portabilität und vereinfacht CI/CD-Prozesse.
In diesem Artikel werden wir uns ansehen, wie man eine TypeScript-Anwendung richtig containerisiert: vom einfachen Dockerfile bis hin zu einem production-ready Build mit docker-compose und Multi-Stage-Builds. Sie erfahren, wie Sie typische Fehler vermeiden, die Image-Größe reduzieren und die Bereitstellung beschleunigen.
Grundlagen: TypeScript und Node.js in Docker
TypeScript ist eine Sprache, die zu JavaScript kompiliert wird. Das bedeutet, dass wir zum Ausführen einer TypeScript-Anwendung in einem Container zuerst den Quellcode kompilieren und dann das resultierende JavaScript mit Node.js ausführen müssen. Docker eignet sich hervorragend zur Automatisierung dieses Prozesses.
Erstellen einer einfachen TypeScript-Anwendung
Zunächst erstellen wir eine minimale TypeScript-Anwendung. Die Projektstruktur:
my-ts-app/├── src/│ └── index.ts├── package.json├── tsconfig.json└── Dockerfilesrc/index.ts:
import express, { Request, Response } from 'express';
const app = express();const PORT = process.env.PORT || 3000;
app.get('/', (req: Request, res: Response) => { res.json({ message: 'Hello from TypeScript + Docker!' });});
app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`);});package.json:
{ "name": "my-ts-app", "version": "1.0.0", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "ts-node src/index.ts" }, "dependencies": { "express": "^4.18.2" }, "devDependencies": { "@types/express": "^4.17.21", "@types/node": "^20.11.0", "typescript": "^5.3.3", "ts-node": "^10.9.2" }}tsconfig.json:
{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"]}Einfaches Dockerfile
Die erste, einfachste Variante eines Dockerfiles:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]Dieses Dockerfile kopiert das gesamte Projekt, installiert Abhängigkeiten, kompiliert TypeScript und startet die Anwendung. Dieser Ansatz hat jedoch Nachteile: Die Image-Größe ist groß und das Caching der Layer ist ineffizient.
Optimierung: Multi-Stage-Build und Größenreduzierung
Für die Production-Umgebung wird die Verwendung eines Multi-Stage-Builds empfohlen. Dies ermöglicht die Trennung von Build- und Ausführungsphasen, sodass im finalen Image nur die notwendigen Dateien verbleiben.
Production Dockerfile mit Multi-Stage-Build
# Stage 1: BuildFROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./RUN npm ci --only=production
COPY . .RUN npm run build
# Stage 2: ProductionFROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesCOPY --from=builder /app/package.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]Vorteile dieses Ansatzes:
- Geringere Image-Größe – Die finale Schicht enthält nur den kompilierten Code und die Production-Abhängigkeiten.
- Sicherheit – Im Container verbleibt kein TypeScript-Quellcode, dev