Eine React-App lokal zu entwickeln ist einfach:
npm install, npm run dev, fertig. Das Problem
beginnt beim Deployment. Entwickler-Maschine hat Node 18, Server hat
Node 16. Kollege nutzt Windows, Production läuft auf Linux. Dependencies
unterscheiden sich. “Works on my machine” ist kein
Produktionszustand.
Docker löst dieses Problem durch Containerisierung: Die gesamte App inklusive Abhängigkeiten wird in ein Image gepackt, das überall identisch läuft. Einmal gebaut, läuft der Container auf jedem System mit Docker – egal ob MacBook, Linux-Server oder Kubernetes-Cluster.
Das Besondere bei React: Nach dem Build braucht man kein Node.js mehr. Die App ist statischer Content. Der finale Container kann extrem schlank sein – nur ein Webserver, keine Runtime-Dependencies.
Naiver Ansatz: React-App in Node.js-Container packen.
# ❌ Naive Lösung - Viel zu groß!
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Serve mit Node.js Static Server
RUN npm install -g serve
CMD ["serve", "-s", "dist", "-p", "80"]Probleme:
| Problem | Details |
|---|---|
| Image-Größe | ~1 GB (Node.js + npm + node_modules) |
| Security | Node.js Runtime + alle Dev-Dependencies |
| Performance | Node.js ist Overkill für Static Files |
| Overhead | Unnötige Tools im Production-Image |
Besser: Multi-Stage Build mit Nginx.
Konzept: Zwei separate Images – eins zum Bauen, eins zum Ausführen.
# Dockerfile
# ===== Stage 1: Build =====
FROM node:18-alpine AS builder
WORKDIR /app
# Dependencies installieren
COPY package*.json ./
RUN npm ci --only=production=false
# Source kopieren und bauen
COPY . .
RUN npm run build
# ===== Stage 2: Production =====
FROM nginx:alpine
# Build-Output vom Builder kopieren
COPY --from=builder /app/dist /usr/share/nginx/html
# Nginx-Konfiguration
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Vergleich:
| Metrik | Node.js-Container | Multi-Stage (Nginx) |
|---|---|---|
| Image-Größe | ~1000 MB | ~25 MB |
| Startup-Zeit | ~5s | ~0.5s |
| Memory | ~200 MB | ~10 MB |
| Attack Surface | Groß (Node.js) | Klein (nur Nginx) |
React Router braucht Server-Config, damit alle Routes
index.html ausliefern.
# nginx.conf
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip Compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Client-Side Routing: Alle Requests → index.html
location / {
try_files $uri $uri/ /index.html;
}
# Cache Static Assets
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API Proxy (optional)
location /api/ {
proxy_pass http://backend-service:3000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# Health Check Endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
Angepasstes Dockerfile:
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ===== Runtime =====
FROM nginx:alpine
# Nginx Config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Build Output
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Analog zu .gitignore – verhindert, dass große Files ins
Build-Context kopiert werden.
# .dockerignore
node_modules
dist
build
.git
.env.local
.env.*.local
npm-debug.log
.DS_Store
coverage
.vscode
.idea
*.md
!README.md
Warum wichtig?
# Ohne .dockerignore
COPY . .
# Kopiert node_modules (500 MB), .git (200 MB), etc.
# → Docker Build Context: 800 MB
# Mit .dockerignore
COPY . .
# Kopiert nur Source (5 MB)
# → Docker Build Context: 5 MBSchnellerer Build, kleinerer Context, besseres Caching.
React braucht API-URLs zur Build-Zeit (nicht Runtime), weil Environment-Variablen ins Bundle kompiliert werden.
# Dockerfile mit Build Args
FROM node:18-alpine AS builder
WORKDIR /app
# Build Arguments
ARG VITE_API_URL
ARG VITE_ANALYTICS_ID
# Als Environment-Variablen verfügbar machen
ENV VITE_API_URL=$VITE_API_URL
ENV VITE_ANALYTICS_ID=$VITE_ANALYTICS_ID
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ===== Runtime =====
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Build mit verschiedenen Environments:
# Development Build
docker build \
--build-arg VITE_API_URL=http://localhost:3000/api \
--build-arg VITE_ANALYTICS_ID=UA-DEV \
-t myapp:dev .
# Production Build
docker build \
--build-arg VITE_API_URL=https://api.production.com/api \
--build-arg VITE_ANALYTICS_ID=UA-PROD \
-t myapp:latest .Docker cached jeden Layer. Wenn sich nichts ändert, wird Layer wiederverwendet.
Ineffizient:
# ❌ Bei jeder Code-Änderung: npm install neu
COPY . .
RUN npm install
RUN npm run buildJede Code-Änderung → COPY . . invalidiert Cache →
npm install läuft erneut (3 Min).
Effizient:
# ✓ Dependencies cachen
COPY package*.json ./
RUN npm ci
# ← Cache-Layer: Wird nur invallidiert bei package.json-Änderung
COPY . .
RUN npm run build
# ← Läuft nur bei Code-ÄnderungCode-Änderung → Nur npm run build (30s), nicht
npm ci.
Complete Optimized Dockerfile:
FROM node:18-alpine AS builder
WORKDIR /app
# 1. Dependencies installieren (cached)
COPY package*.json ./
RUN npm ci
# 2. Source kopieren
COPY . .
# 3. Build Arguments
ARG VITE_API_URL
ARG VITE_ANALYTICS_ID
ENV VITE_API_URL=$VITE_API_URL
ENV VITE_ANALYTICS_ID=$VITE_ANALYTICS_ID
# 4. Build
RUN npm run build
# ===== Runtime =====
FROM nginx:alpine
# Non-root User (Security)
RUN addgroup -g 1001 -S nodejs && \
adduser -S react -u 1001
# Nginx Config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Build Output
COPY --from=builder /app/dist /usr/share/nginx/html
# Permissions
RUN chown -R react:nodejs /usr/share/nginx/html && \
chown -R react:nodejs /var/cache/nginx && \
chown -R react:nodejs /var/log/nginx && \
chown -R react:nodejs /etc/nginx/conf.d
RUN touch /var/run/nginx.pid && \
chown -R react:nodejs /var/run/nginx.pid
USER react
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]Typisches Setup: React Frontend + API Backend + Database.
# docker-compose.yml
version: '3.8'
services:
frontend:
build:
context: .
dockerfile: Dockerfile
args:
VITE_API_URL: http://localhost:3000/api
ports:
- "80:80"
depends_on:
- backend
networks:
- app-network
backend:
image: node:18-alpine
working_dir: /app
volumes:
- ./backend:/app
command: npm start
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/mydb
depends_on:
- db
networks:
- app-network
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=mydb
volumes:
- db-data:/var/lib/postgresql/data
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
db-data:# Alles starten
docker-compose up
# Mit Rebuild
docker-compose up --build
# Im Hintergrund
docker-compose up -d
# Logs
docker-compose logs -f frontend
# Stoppen
docker-compose down
# Mit Volumes löschen
docker-compose down -vFür lokale Entwicklung: Vite Dev-Server im Container mit Volume-Mounts.
# Dockerfile.dev
FROM node:18-alpine
WORKDIR /app
# Dependencies
COPY package*.json ./
RUN npm install
# Source wird via Volume gemounted
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]# docker-compose.dev.yml
version: '3.8'
services:
frontend:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "5173:5173"
volumes:
- .:/app
- /app/node_modules # Anonymous volume für node_modules
environment:
- VITE_API_URL=http://localhost:3000/api
networks:
- dev-network
networks:
dev-network:
driver: bridgedocker-compose -f docker-compose.dev.yml up
# Hot-Reload funktioniert, weil Source gemounted istDocker Health Checks prüfen, ob Container healthy ist.
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
# Health Check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/health || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]# Health Status prüfen
docker ps
# STATUS: Up 2 minutes (healthy)
# Manuell testen
docker inspect --format='{{.State.Health.Status}}' container-idOptimierungen:
| Technik | Einsparung | Implementierung |
|---|---|---|
| Alpine statt Debian | ~600 MB | FROM node:18-alpine |
| Multi-Stage Build | ~300 MB | Nur dist/ ins Final Image |
| .dockerignore | ~100 MB | Keine node_modules kopieren |
| npm ci statt install | ~50 MB | Exact versions, kein cache |
| Production Dependencies | ~200 MB | npm ci --only=production |
Vergleich:
# ❌ Schlecht: 1.2 GB
FROM node:18
COPY . .
RUN npm install
CMD ["npm", "start"]
# ✓ Gut: 25 MB
FROM node:18-alpine AS builder
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html# Login
docker login
# Tag
docker tag myapp:latest username/myapp:latest
# Push
docker push username/myapp:latest
# Pull (auf anderem Server)
docker pull username/myapp:latest
docker run -p 80:80 username/myapp:latest# Login zu Private Registry
docker login registry.company.com
# Tag mit Registry-Prefix
docker tag myapp:latest registry.company.com/team/myapp:latest
# Push
docker push registry.company.com/team/myapp:latest# .github/workflows/docker.yml
name: Docker Build & Push
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
username/myapp:latest
username/myapp:${{ github.sha }}
build-args: |
VITE_API_URL=${{ secrets.API_URL }}
VITE_ANALYTICS_ID=${{ secrets.ANALYTICS_ID }}
cache-from: type=registry,ref=username/myapp:buildcache
cache-to: type=registry,ref=username/myapp:buildcache,mode=max# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: react-app
labels:
app: react-app
spec:
replicas: 3
selector:
matchLabels:
app: react-app
template:
metadata:
labels:
app: react-app
spec:
containers:
- name: react-app
image: username/myapp:latest
ports:
- containerPort: 80
resources:
requests:
memory: "32Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: react-app-service
spec:
selector:
app: react-app
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancerkubectl apply -f deployment.yaml
kubectl get pods
kubectl get services1. Non-Root User
# ❌ Root User (Default)
FROM nginx:alpine
# Läuft als root
# ✓ Non-Root User
FROM nginx:alpine
RUN addgroup -g 1001 -S nodejs && adduser -S react -u 1001
USER react2. Minimal Base Images
# ❌ Große Attack Surface
FROM ubuntu:latest
# ✓ Minimal
FROM nginx:alpine
# Oder noch minimaler: distroless
FROM gcr.io/distroless/static-debian113. Image Scanning
# Trivy: Vulnerability Scanner
trivy image username/myapp:latest
# Snyk
snyk container test username/myapp:latest4. Secrets nicht im Image
# ❌ Falsch: Secret im Image
ENV API_SECRET=abc123
# ✓ Richtig: Secret zur Runtime
# Über Environment Variable oder Docker Secret
docker run -e API_SECRET=abc123 myapp:latestFehler 1: Dependencies in beiden Stages installieren
# ❌ Falsch: npm install in beiden Stages
FROM node:18 AS builder
RUN npm install
FROM nginx:alpine
RUN npm install # Unnötig! Nginx braucht kein npmFehler 2: .env Files im Image
# ❌ Falsch: .env wird ins Image kopiert
COPY . .
# Inkludiert .env → Secret im Image!
# ✓ Richtig: .dockerignore
# .dockerignore
.env
.env.*Fehler 3: Port-Mismatch
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
server {
listen 8080; # ← Falsch! EXPOSE sagt 80
}Fehler 4: Build ohne Cache-Busting
# ❌ Cache wird nie invalided
COPY . .
RUN npm install
# package.json ändert sich → Aber Layer ist cached!
# ✓ Richtig: Dependencies zuerst
COPY package*.json ./
RUN npm ci
COPY . .Fehler 5: Nginx läuft als daemon
# ❌ Container stoppt sofort
CMD ["nginx"]
# ✓ Foreground Process
CMD ["nginx", "-g", "daemon off;"]Containerisierung transformiert React-Deployments von “funktioniert auf meinem Rechner” zu “funktioniert überall identisch”. Multi-Stage Builds halten Images schlank, Nginx liefert Static Files performant aus, Docker Compose orchestriert lokale Entwicklung. Das Ergebnis: 25 MB Production-Image statt 1 GB, reproduzierbare Builds, simple CI/CD-Integration. Container sind kein Overhead – sie sind Standard.