Warum Container gewonnen haben
Vor Containern bedeutete Deployment, eine Umgebung exakt zu reproduzieren: die richtige Runtime-Version, die passenden System-Libraries, die korrekte Konfiguration. „Auf meinem Rechner funktioniert es“ war ein echtes Problem, und Server wichen im Laufe der Zeit immer weiter von der Entwicklungsumgebung ab.
Docker löste dieses Problem, indem die Anwendung und ihre Umgebung in ein einziges Image verpackt wurden. Das Image läuft auf einem Laptop, in der CI und in der Produktion identisch, da es alles Notwendige mitbringt. Diese Konsistenz ist der Grund, warum Container zum Standard für Deployments wurden und warum das Erlernen von Docker eine der wertvollsten Fähigkeiten für das Ausliefern von Software ist.
Images und Container
Ein Image ist eine schreibgeschützte Vorlage, die aus einem Dockerfile erstellt wird und aus gecachten Layern besteht. Ein Container ist eine laufende Instanz eines Images mit eigenem beschreibbarem Layer, Prozess und Netzwerk. Man baut einmal und startet viele Instanzen.
docker build -t my-app:1.0 .
docker run -p 3000:3000 my-app:1.0
docker ps
docker logs <container>
Das -p Flag mappt einen Host-Port auf einen Container-Port. Container sind per Design ephemeral: Sie können diese beliebig stoppen, entfernen und neu erstellen, da das Image die „Source of Truth“ ist.
Das Dockerfile
Das Dockerfile ist das Rezept für den Build-Prozess. Jede Anweisung erstellt einen Layer.
# Dockerfile
FROM node:22-alpine
WORKDIR /app
# install dependencies first for better caching
COPY package*.json ./
RUN npm ci --omit=dev
# then copy source
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
USER node
CMD ["node", "server.js"]
Die Reihenfolge ist entscheidend. Wenn die Dependency-Manifeste kopiert und die Installation durchgeführt wird, bevor der Quellcode kopiert wird, führt eine Code-Änderung nur dazu, dass der kostengünstige Source-Layer ungültig wird und nicht die aufwendige Installation. npm ci sorgt für eine reproduzierbare Installation und USER node stellt sicher, dass der Prozess als Non-Root-User ausgeführt wird.
Layer-Caching
Docker cached jeden Layer und verwendet ihn wieder, wenn die Inputs unverändert sind. Deshalb ist die oben genannte Reihenfolge bewusst gewählt: Platzieren Sie Dinge, die sich selten ändern, oben und Dinge, die sich häufig ändern, unten.
# cached unless package files change
COPY package*.json ./
RUN npm ci
# invalidated on every source change
COPY . .
Eine .dockerignore-Datei ist ebenfalls wichtig. Ohne sie werden node_modules, .git und Build-Outputs in den Build-Kontext kopiert, was den Build verlangsamt und dazu führen kann, dass Dateien in das Image gelangen.
node_modules
.git
dist
.env
Multi-stage Builds
Ein Multi-stage Build kompiliert in einem Image und liefert nur das Ergebnis in einem anderen aus.
# Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Das finale Image enthält nur die gebauten statischen Dateien und einen Webserver, nicht aber die Node-Runtime, den Quellcode oder die Build-Tools. Das Ergebnis ist kleiner, lässt sich schneller pullen und bietet eine wesentlich geringere Angriffsfläche. Für eine Server-App wäre die finale Stage ein schlankes Node-Image, das nur die Production-Dependencies enthält.
Compose für Multi-Service-Apps
Echte Anwendungen benötigen in der Regel mehr als einen Prozess. Compose definiert diese gemeinsam.
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: example
volumes:
- data:/var/lib/postgresql/data
volumes:
data:
depends_on steuert die Startreihenfolge, volumes sorgt für die Persistenz von Daten über Container-Neustarts hinweg, und Services erreichen sich gegenseitig über ihren Namen im gemeinsamen Netzwerk. Compose ist ideal für die lokale Entwicklung und einfache Single-Host-Deployments.
Registries und CI/CD
Images durchlaufen eine Registry, wie zum Beispiel Docker Hub, GitHub Container Registry oder eine private Registry. Eine typische Pipeline sieht so aus:
- Abhängigkeiten installieren und Tests ausführen.
- Das Image bauen und mit dem Commit-SHA taggen.
- Das Image in die Registry pushen.
- Den neuen Tag in der Zielumgebung deployen.
docker build -t ghcr.io/me/app:$GIT_SHA .
docker push ghcr.io/me/app:$GIT_SHA
Da das Image unveränderlich (immutable) ist und über den Commit getaggt wird, bestehen Rollbacks lediglich darin, einen vorherigen Tag zu deployen. Dies ist der Kern eines zuverlässigen Deployment-Prozesses: Einmal bauen, dasselbe Artefakt durch die verschiedenen Umgebungen befördern und niemals erneut für die Produktion bauen.
Best Practices für die Produktion
- Health Checks. Stellen Sie einen Endpoint bereit, den die Plattform abfragen kann, um zu prüfen, ob der Container bereit ist.
- Nicht-Root-Benutzer. Führen Sie den Prozess mit einem Benutzer aus, der nur die minimal erforderlichen Berechtigungen besitzt.
- Schreibgeschütztes Dateisystem, wo immer möglich, mit explizit definierten beschreibbaren Volumes.
- Graceful Shutdown. Verarbeiten Sie
SIGTERM, um laufende Anfragen ordnungsgemäß zu beenden. - Ressourcenlimits. Legen Sie CPU- und Memory-Limits fest, damit ein einzelner Container nicht alle Ressourcen beansprucht.
- Base-Image-Versionen fixieren (Pinning) für reproduzierbare Builds.
- Secrets zur Laufzeit bereitstellen, niemals direkt in das Image einbetten.
- Kleine Base-Images wie Alpine oder distroless verwenden, um die Anzahl potenzieller Schwachstellen zu reduzieren.
Best Practices
- Ordnen Sie die Dockerfile-Instruktionen so an, dass die am seltensten geänderten zuerst kommen.
- Nutzen Sie Multi-Stage Builds und liefern Sie nur das aus, was wirklich benötigt wird.
- Fügen Sie ein
.dockerignorefürnode_modules,.git,distund.envhinzu. - Führen Sie Prozesse als Non-Root-User aus.
- Taggen Sie Images mit dem Commit-SHA und nicht nur mit
latest. - Bauen Sie das Image einmal und befördern Sie es durch die verschiedenen Umgebungen.
- Behandeln Sie
SIGTERMfür ein Graceful Shutdown.
Häufige Fehler
- Kopieren von
node_modulesin das Image. - Ausführen als root.
- Einbetten von Secrets in das Image oder Committen von
.env. - Verwendung von
latestals einzigem Tag, wodurch die Reproduzierbarkeit verloren geht. - Unterschiedliches Neu-Builden des Images für jede Umgebung.
- Ignorieren von
.dockerignore, was zu riesigen und langsamen Builds führt.
Wie geht es weiter?
Container machen das Deployment vorhersagbar, und die gleichen Fähigkeiten kommen zum Einsatz, egal ob Sie an eine VM, Kubernetes oder eine serverless Container-Plattform liefern. Vertiefen Sie Ihr Wissen über die Runtime im Node.js-Guide, verwalten Sie Abhängigkeiten mit npm, stellen Sie die Anwendung über HTTP bereit und sichern Sie diese mit Web Security ab. Containerisieren Sie anschließend eine kleine App und führen Sie ein vollständiges End-to-End-Deployment durch.