Basierend auf über 15 Jahren Erfahrung mit der Bereitstellung containerisierter Systeme in großem Maßstab in den Bereichen Full-Stack, KI/ML, IoT und Robotik
Nachdem ich containerisierte Bereitstellungen für alles von Hochfrequenz-Handelsplattformen bis hin zu Flotten autonomer Roboter entworfen habe, habe ich gelernt, dass Docker-Bereitstellungen im Produktionsbetrieb weit mehr erfordern als nur das Schreiben einer Dockerfile. Dieser umfassende Leitfaden fasst die hart erarbeiteten Erkenntnisse aus realen Bereitstellungen zu umsetzbaren Strategien für 2025 und darüber hinaus zusammen.
Mehrstufige Builds sind nicht mehr optional – sie sind für Produktionsbereitstellungen unverzichtbar. Hier erfahren Sie, warum und wie Sie sie effektiv einsetzen:
# ======================================== # Stufe 1: Build-Umgebung # ======================================== FROM node:20-alpine AS builder # Nur Build-Abhängigkeiten installieren RUN apk add --no-cache python3 make g++ WORKDIR /build # Optimierung des Layer-Cachings: Abhängigkeitsdateien zuerst kopieren COPY package*.json ./ COPY yarn.lock* ./ # ALLE Abhängigkeiten installieren (einschließlich devDependencies) RUN npm ci # Quellcode kopieren COPY . . # Anwendung erstellen RUN npm run build && \ npm prune --production # ======================================== # Stufe 2: Produktions-Laufzeit # ======================================== FROM node:20-alpine # Sicherheit: Nicht-Root-Benutzer erstellen RUN addgroup -g 1001 -S nodejs && \ adduser -S nodejs -u 1001 # Nur Laufzeitabhängigkeiten installieren RUN apk add --no-cache dumb-init WORKDIR /app # Nur Produktionsartefakte kopieren COPY /build/dist ./dist COPY /build/node_modules ./node_modules COPY /build/package.json ./ # Zu Nicht-Root-Benutzer wechseln USER nodejs # dumb-init für korrekte Signalverarbeitung verwenden ENTRYPOINT ["dumb-init", "--"] # Zustandsprüfung HEALTHCHECK \ CMD node -e "require('http').get('http://localhost:3000/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })" EXPOSE 3000 CMD ["node", "dist/index.js"]
Für Python-/ML-Anwendungen:
# Build-Phase mit vollständiger Conda-Umgebung FROM continuumio/miniconda3:latest AS builder WORKDIR /build COPY environment.yml . RUN conda env create -f environment.yml && \ conda clean -afy # Produktionsphase mit minimaler Laufzeit FROM python:3.11-slim COPY /opt/conda/envs/myenv /opt/conda/envs/myenv ENV PATH="/opt/conda/envs/myenv/bin:$PATH" WORKDIR /app COPY . . CMD ["python", "app.py"]
Wichtige Erkenntnisse:
latest.dockerignore konsequent ein (node_modules, .git, Tests usw.)Sicherheit muss von Anfang an integriert sein. Hier ist mein praxiserprobter Sicherheitsstack:
# Verwende Trivy für das Scannen auf Schwachstellen trivy image --severity HIGH,CRITICAL myapp:latest # Verwende Grype für zusätzliche Abdeckung grype myapp:latest # In CI/CD integrieren docker build -t myapp:${CI_COMMIT_SHA} . trivy image --exit-code 1 --severity CRITICAL myapp:${CI_COMMIT_SHA}
Toolauswahl (2025):
# FALSCH – Ausführung als Root FROM ubuntu:22.04 COPY app /app CMD ["/app/server"] # RICHTIG – Nicht-Root mit korrekten Berechtigungen FROM ubuntu:22.04 RUN groupadd -r appuser && \ useradd -r -g appuser -u 1001 appuser && \ mkdir /app && \ chown -R appuser:appuser /app COPY app /app USER appuser WORKDIR /app CMD ["./server"]
# docker-compose.yml services: api: image: myapp:latest read_only: true tmpfs: - /tmp:noexec,nosuid,size=100m volumes: - ./data:/app/data security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE
Tun Sie dies NIEMALS:
# FALSCH! ENV DB_PASSWORD=mysecretpassword ENV API_KEY=abc123
Produktionsmuster:
# Verwendung von Docker Swarm-Geheimnissen version: '3.8' services: app: image: myapp:latest environment: - NODE_ENV=production - DATABASE_URL_FILE=/run/secrets/db_url secrets: - db_url - api_key deploy: replicas: 3 secrets: db_url: external: true api_key: external: true
Für Kubernetes:
apiVersion: v1 kind: Secret metadata: name: app-secrets type: Opaque stringData: database-url: "postgresql://..." api-key: "..." --- apiVersion: apps/v1 kind: Deployment metadata: name: myapp spec: template: spec: containers: - name: app envFrom: - secretRef: name: app-secrets
Enterprise-Muster: Externe Secret-Manager verwenden
# Verwendung des External Secrets Operators apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: vault-backend spec: provider: vault: server: "https://vault.company.com" auth: kubernetes: mountPath: "kubernetes" --- apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: app-secrets spec: secretStoreRef: name: vault-backend target: name: app-secrets data: - secretKey: database-url remoteRef: key: secret/data/app/database property: url
# Images mit Cosign signieren (Standard 2025) cosign sign --key cosign.key myregistry/myapp:v1.0 # Vor der Bereitstellung verifizieren cosign verify --key cosign.pub myregistry/myapp:v1.0
Ordnungsgemäße Zustandsprüfungen machen den Unterschied zwischen einer Verfügbarkeit von 99,9 % und 99,99 %.
# Einfacher HTTP-Zustandscheck HEALTHCHECK \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 # Erweiterter Zustandscheck mit Abhängigkeiten HEALTHCHECK \ CMD curl -f http://localhost:8080/health/ready || exit 1
// Express.js-Muster für den Zustandscheck const express = require('express'); const app = express(); let isReady = false; // Liveness: Läuft die Anwendung? app.get('/health/live', (req, res) => { res.status(200).json({ status: 'alive', timestamp: Date.now() }); }); // Bereitschaft: Ist die Anwendung bereit, Datenverkehr zu bedienen? app.get('/health/ready', async (req, res) => { try { // Datenbankverbindung prüfen await db.ping(); // Redis-Verbindung prüfen await redis.ping(); // Externe API-Abhängigkeiten prüfen await checkExternalServices(); res.status(200).json({ status: 'ready', timestamp: Date.now(), dependencies: { db: 'ok', cache: 'ok', apis: 'ok' } }); } catch (error) { res.status(503).json({ status: 'not ready', error: error.message, timestamp: Date.now() }); } }); // Startup: Ist die Initialisierung abgeschlossen? app.get('/health/startup', (req, res) => { if (isReady) { res.status(200).json({ status: 'started' }); } else { res.status(503).json({ status: 'starting' }); } });
apiVersion: apps/v1 kind: Deployment metadata: name: myapp spec: replicas: 3 template: spec: containers: - name: app image: myapp:v1.0 ports: - containerPort: 8080 # Startup-Probe: Gibt der App Zeit zur Initialisierung startupProbe: httpGet: path: /health/startup port: 8080 failureThreshold: 30 periodSeconds: 10 # Liveness-Probe: Neustart bei Fehlerzustand livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 0 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 # Bereitschaftsprüfung: Aus dem Dienst entfernen, wenn nicht bereit readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3
Wichtiger Hinweis: Trennen Sie „Liveness“ von „Readiness“. Fehler bei der Liveness führen zum Neustart von Pods; Fehler bei der Readiness entfernen sie lediglich aus den Load Balancern. Ein Fehler in einer Abhängigkeit sollte sich auf die Readiness auswirken, nicht auf die Liveness.
Konfigurationsmanagement entscheidet über Erfolg oder Misserfolg von Produktionsbereitstellungen. Hier ist die Hierarchie, die ich verwende:
1. Secrets (niemals im Code oder in Konfigurationsdateien)
2. Umgebungsvariablen (deployment-spezifisch)
3. Konfigurationsdateien (als Volumes gemountet)
4. Anwendungsstandardwerte (im Code)
version: '3.8' services: api: image: ${REGISTRY}/myapp:${VERSION} environment: - NODE_ENV=production - LOG_LEVEL=${LOG_LEVEL:-info} - DATABASE_URL=${DATABASE_URL} env_file: - .env.production secrets: - db_password - jwt_secret configs: - source: app_config target: /app/config/production.yml deploy: replicas: 3 restart_policy: condition: on-failure delay: 5s max_attempts: 3 window: 120s update_config: parallelism: 1 delay: 10s failure_action: rollback monitor: 30s resources: limits: cpus: '2' memory: 2G reservations: cpus: '1' memory: 1G secrets: db_password: external: true jwt_secret: external: true configs: app_config: file: ./config/production.yml
apiVersion: v1 kind: ConfigMap metadata: name: app-config data: app.yml: | server: port: 8080 timeout: 30s features: newFeature: true logging: level: info --- apiVersion: apps/v1 kind: Deployment spec: template: spec: containers: - name: app volumeMounts: - name: config mountPath: /app/config readOnly: true - name: secrets mountPath: /app/secrets readOnly: true volumes: - name: config configMap: name: app-config - name: secrets secret: secretName: app-secrets
Protokollierung ist kein optionales Extra. Hier ist mein Produktions-Stack:
// Winston-Konfiguration für die Produktion const winston = require('winston'); const { ElasticsearchTransport } = require('winston-elasticsearch'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), defaultMeta: { service: 'myapp', version: process.env.VERSION, environment: process.env.NODE_ENV }, transports: [ // Konsole für Docker-Protokolle new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ) }), // Elasticsearch für zentralisierte Protokollierung new ElasticsearchTransport({ level: 'info', clientOpts: { node: process.env.ELASTICSEARCH_URL, auth: { username: process.env.ES_USER, password: process.env.ES_PASSWORD } } }) ], exceptionHandlers: [ new winston.transports.File({ filename: 'exceptions.log' }) ], rejectionHandlers: [ new winston.transports.File({ filename: 'rejections.log' }) ] }); // Middleware zur Korrelation von Anfragen app.use((req, res, next) => { req.id = req.headers['x-request-id'] || uuid.v4(); req.logger = logger.child({ requestId: req.id }); next(); });
# docker-compose.yml services: api: image: myapp:latest logging: driver: "json-file" options: max-size: "10m" max-file: "3" labels: "service,environment" labels: service: "api" environment: "production"
version: '3.8' services: # Anwendung myapp: image: myapp:latest environment: - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 - OTEL_SERVICE_NAME=myapp - OTEL_RESOURCE_ATTRIBUTES=environment=production,version=${VERSION} depends_on: - otel-collector # OpenTelemetry Collector otel-collector: image: otel/opentelemetry-collector-contrib:latest command: ["--config=/etc/otel-collector-config.yml"] volumes: - ./otel-collector-config.yml:/etc/otel-collector-config.yml ports: - "4317:4317" # OTLP gRPC-Empfänger - "4318:4318" # OTLP HTTP-Empfänger # Prometheus (Metriken) prometheus: image: prom/prometheus:latest volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus ports: - "9090:9090" # Grafana (Visualisierung) grafana: image: grafana/grafana:latest environment: - GF_SECURITY_ADMIN_PASSWORD=secret volumes: - grafana-data:/var/lib/grafana - ./grafana/dashboards:/etc/grafana/provisioning/dashboards - ./grafana/datasources:/etc/grafana/provisioning/datasources ports: - "3000:3000" # Loki (Protokolle) loki: image: grafana/loki:latest ports: - "3100:3100" Volumes: - ./loki-config.yml:/etc/loki/local-config.yml - loki-data:/loki # Tempo (Traces) tempo: Image: grafana/tempo:latest Befehl: [ "-config.file=/etc/tempo.yml" ] Volumes: - ./tempo.yml:/etc/tempo.yml - tempo-data:/tmp/tempo # Jaeger (Alternative verteilte Tracing-Lösung) jaeger: image: jaegertracing/all-in-one:latest environment: - COLLECTOR_OTLP_ENABLED=true ports: - "16686:16686" # Jaeger-UI - "14268:14268" # Collector HTTP - "4317:4317" # OTLP gRPC Volumes: prometheus-data: grafana-data: loki-data: tempo-data:
// OpenTelemetry-Instrumentierung const { NodeSDK } = require('@opentelemetry/sdk-node'); const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); const sdk = new NodeSDK({ traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + '/v1/traces', }), metricReader: new PrometheusExporter({ port: 9464, }), serviceName: 'myapp', }); sdk.start(); process.on('SIGTERM', () => { sdk.shutdown() .then(() => console.log('Tracing terminated')) .catch((error) => console.log('Error terminating tracing', error)) .finally(() => process.exit(0)); });
Die ewige Frage. Hier ist mein Entscheidungsrahmen, nachdem ich beide in der Produktion eingesetzt habe:
| Faktor | Kubernetes | Docker Swarm |
|---|---|---|
| Teamgröße | 5+ Entwickler | 2–4 Entwickler |
| Komplexität | Hoch (steile Lernkurve) | Niedrig (Docker-nativ) |
| Ökosystem | Riesig (über 70 % Marktanteil) | Begrenzt, aber stabil |
| Multi-Cloud | Hervorragend | Begrenzt |
| Ressourcen-Overhead | Höher | Geringer |
| Erweiterte Funktionen | StatefulSets, Jobs, CronJobs, Custom Resources | Grundlegende Orchestrierung |
| Community-Support | Umfangreich | Begrenzt |
| Am besten geeignet für | Groß angelegte, komplexe Bereitstellungen | Kleine bis mittlere Bereitstellungen |
# Swarm initialisieren docker swarm init --advertise-addr <manager-ip># Worker hinzufügen docker swarm join --token <worker-token> <manager-ip>:2377 # Stack bereitstellen docker stack deploy -c docker-compose.yml myapp # Dienst skalieren docker service scale myapp_api=5 # Rollendes Update docker service update --image myapp:v2 myapp_api # Überwachen docker service ls docker service ps myapp_api
# K3s (Lightweight K8s) installieren curl -sfL https://get.k3s.io | sh - # Anwendung bereitstellen kubectl apply -f deployment.yml # Skalieren kubectl scale deployment myapp --replicas=5 # Rollendes Update kubectl set image deployment/myapp app=myapp:v2 # Überwachen kubectl get pods kubectl top pods kubectl logs -f deployment/myapp
# Edge-K3s-Cluster (ressourcenbeschränkt) apiVersion: v1 kind: Namespace metadata: name: edge-production --- apiVersion: apps/v1 kind: Deployment metadata: name: edge-processor namespace: edge-production spec: replicas: 2 template: spec: containers: - name: processor image: myapp:edge resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "256Mi" cpu: "200m" nodeSelector: node-role.kubernetes.io/edge: "true" tolerations: - key: "node-role.kubernetes.io/edge" operator: "Exists" effect: "NoSchedule"
version: '3.8' services: # Frontend (React/Next.js) frontend: build: context: ./frontend dockerfile: Dockerfile.prod ports: - "80:80" - "443:443" depends_on: - backend volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - certbot-certs:/etc/letsencrypt - certbot-webroot:/var/www/certbot deploy: replicas: 2 resources: limits: cpus: '0.5' memory: 256M # Backend (Node.js/Python/Go) backend: image: ${REGISTRY}/backend:${VERSION} environment: - NODE_ENV=production - DATABASE_URL=postgresql://postgres:5432/mydb - REDIS_URL=redis://redis:6379 depends_on: - db - redis deploy: replicas: 3 resources: limits: cpus: '1' memory: 1G restart_policy: condition: on-failure # Datenbank (PostgreSQL) db: image: postgres:16-alpine Umgebung: POSTGRES_PASSWORD_FILE: /run/secrets/db_password Volumes: - postgres-data:/var/lib/postgresql/data - ./init.sql:/docker-entrypoint-initdb.d/init.sql Geheimnisse: - db_password Bereitstellung: Platzierung: Einschränkungen: - node.labels.db == true # Cache (Redis) redis: Image: redis:7-alpine Befehl: redis-server --appendonly yes Volumes: - redis-data:/data # Hintergrundaufträge (Celery/Bull) worker: image: ${REGISTRY}/backend:${VERSION} command: celery -A app.celery worker --loglevel=info depends_on: - redis - db deploy: replicas: 2 volumes: postgres-data: redis-data: certbot-certs: certbot-webroot: secrets: db_password: external: true
# nginx.conf upstream backend { least_conn; server backend:8080 max_fails=3 fail_timeout=30s; server backend:8080 max_fails=3 fail_timeout=30s; server backend:8080 max_fails=3 fail_timeout=30s; } server { listen 80; server_name example.com www.example.com; # HTTP zu HTTPS umleiten return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name example.com www.example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # Moderne SSL-Konfiguration ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5;</manager-ip></worker-token></manager-ip> ssl_prefer_server_ciphers on; # Sicherheits-Header add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; # Statische Dateien location /static { alias /usr/share/nginx/html/static; expires 1y; add_header Cache-Control "public, immutable"; } # API-Proxy location /api { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 30s; proxy_send_timeout 30s; proxy_read_timeout 30s; } # SPA-Fallback location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } }
# Dockerfile für PyTorch/TensorFlow mit GPU FROM nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04 # Python und Abhängigkeiten installieren RUN apt-get update && apt-get install -y \ python3.11 \ python3-pip \ && rm -rf /var/lib/apt/lists/* WORKDIR /app # ML-Frameworks installieren COPY requirements.txt . RUN pip3 install --no-cache-dir -r requirements.txt # Modell und Anwendung kopieren COPY models/ ./models/ COPY app.py . # Nicht-Root-Benutzer RUN useradd -m -u 1001 mluser && \ chown -R mluser:mluser /app USER mluser # API freigeben EXPOSE 8000 # Mit Gunicorn + Uvicorn-Workern ausführen CMD ["gunicorn", "app:app", \ "--workers", "4", \ "--worker-class", "uvicorn.workers.UvicornWorker", \ "--bind", "0.0.0.0:8000", \ "--timeout", "120"]
apiVersion: apps/v1 kind: Deployment metadata: name: ml-inference spec: replicas: 2 template: spec: containers: - name: model-server image: myregistry/ml-model:v1.0 ports: - containerPort: 8000 resources: requests: memory: "4Gi" cpu: "2" nvidia.com/gpu: 1 limits: memory: "8Gi" cpu: "4" nvidia.com/gpu: 1 env: - name: MODEL_PATH value: "/models/my-model" - name: BATCH_SIZE value: "32" volumeMounts: - name: models mountPath: /models readOnly: true livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 60 periodSeconds: 30 readinessProbe: httpGet: path: /ready port: 8000 initialDelaySeconds: 30 periodSeconds: 10 volumes: - name: models persistentVolumeClaim: claimName: model-storage nodeSelector: accelerator: nvidia-tesla-t4 tolerations: - key: nvidia.com/gpu operator: Exists effect: NoSchedule
from fastapi import FastAPI, HTTPException from pydantic import BaseModel import torch import numpy as np from typing import List import logging app = FastAPI() # Modell beim Start laden model = None @app.on_event("startup") async def load_model(): global model model = torch.load('/models/my-model.pth') model.eval() logging.info("Modell erfolgreich geladen") class PredictionRequest(BaseModel): data: List[List[float]] class PredictionResponse(BaseModel): predictions: List[float] confidence: List[float] @app.post("/predict", response_model=PredictionResponse) async def predict(request: PredictionRequest): try: input_tensor = torch.tensor(request.data, dtype=torch.float32) with torch.no_grad(): output = model(input_tensor) predictions = output.argmax(dim=1).tolist() confidence = torch.softmax(output, dim=1).max(dim=1).values.tolist() return PredictionResponse( predictions=predictions, confidence=confidence ) except Exception as e: logging.error(f"Vorhersagefehler: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/health") async def health(): return {"status": "healthy", "model_loaded": model is not None} @app.get("/metrics") async def metrics(): # Prometheus-Metrik-Endpunkt return {"requests_total": 1000, "avg_latency_ms": 45}
version: '3.8' services: # MLflow für die Experimentverfolgung mlflow: image: ghcr.io/mlflow/mlflow:latest command: mlflow server --host 0.0.0.0 --backend-store-uri postgresql://mlflow:password@db:5432/mlflow --default-artifact-root s3://mlflow-artifacts Ports: - "5000:5000" Umgebung: - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} abhängig von: - db # Modellbereitstellung model-server: image: myregistry/ml-model:${MODEL_VERSION} environment: - MLFLOW_TRACKING_URI=http://mlflow:5000 - MODEL_NAME=my-production-model - MODEL_STAGE=Production depends_on: - mlflow deploy: replicas: 3 resources: limits: nvidia.com/gpu: 1
# Dockerfile für ARM64-Edge-Geräte FROM arm64v8/python:3.11-slim # Systemabhängigkeiten installieren RUN apt-get update && apt-get install -y \ build-essential \ libgpiod2 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # Mit Ressourcenbeschränkungen ausführen CMD ["python3", "edge_processor.py"]
version: '3.8' services: # MQTT-Broker (Eclipse Mosquitto) mqtt: image: eclipse-mosquitto:2 ports: - "1883:1883" - "9001:9001" volumes: - ./mosquitto.conf:/mosquitto/config/mosquitto.conf - mosquitto-data:/mosquitto/data - mosquitto-logs:/mosquitto/log # IoT-Gateway gateway: image: myregistry/iot-gateway:latest environment: - MQTT_BROKER=mqtt://mqtt:1883 - DEVICE_ID=${DEVICE_ID} - CLOUD_ENDPOINT=${CLOUD_ENDPOINT} depends_on: - mqtt devices: - "/dev/ttyUSB0:/dev/ttyUSB0" privileged: true deploy: resources: limits: cpus: '0.5' memory: 256M # Edge Analytics analytics: image: myregistry/edge-analytics:latest environment: - MQTT_BROKER=mqtt://mqtt:1883 - INFLUXDB_URL=http://influxdb:8086 depends_on: - mqtt - influxdb # Zeitreihendatenbank influxdb: image: influxdb:2.7-alpine ports: - "8086:8086" volumes: - influxdb-data:/var/lib/influxdb2 environment: - INFLUXDB_DB=iot_data - INFLUXDB_HTTP_AUTH_ENABLED=true # Grafana zur Visualisierung grafana: image: grafana/grafana:latest ports: - "3000:3000" Volumes: - grafana-data:/var/lib/grafana depends_on: - influxdb Volumes: mosquitto-data: mosquitto-logs: influxdb-data: grafana-data:
# edge_processor.py – Optimiert für Geräte mit begrenzten Ressourcen import paho.mqtt.client as mqtt import json import logging from collections import deque import time class EdgeProcessor: def __init__(self): self.mqtt_client = mqtt.Client() self.buffer = deque(maxlen=1000) # Ringpuffer self.batch_size = 100 self.last_upload = time.time() def process_sensor_data(self, data): # Edge-Verarbeitung: Rauschen filtern, aggregieren, komprimieren if self.is_valid(data): processed = self.preprocess(data) self.buffer.append(processed) # Batch-Upload in die Cloud if len(self.buffer) >= self.batch_size or \ time.time() - self.last_upload > 300: # 5 min self.upload_batch() def preprocess(self, data): # Leichte Inferenz am Edge ausführen return { 'timestamp': data['timestamp'], 'value': data['value'], 'anomaly': self.detect_anomaly(data['value']) } def upload_batch(self): if self.buffer: batch = list(self.buffer) self.mqtt_client.publish('cloud/data', json.dumps(batch)) self.buffer.clear() self.last_upload = time.time()
# Dockerfile für ROS2 Humble FROM ros:humble-ros-base-jammy # Abhängigkeiten installieren RUN apt-get update && apt-get install -y \ ros-humble-navigation2 \ ros-humble-slam-toolbox \ ros-humble-robot-localization \ python3-colcon-common-extensions \ && rm -rf /var/lib/apt/lists/* WORKDIR /ros2_ws # Arbeitsbereich kopieren COPY src/ src/ # ROS2-Arbeitsbereich erstellen RUN . /opt/ros/humble/setup.sh && \ colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release # Einstiegspunkt einrichten COPY ./ros_entrypoint.sh / RUN chmod +x /ros_entrypoint.sh ENTRYPOINT ["/ros_entrypoint.sh"] CMD ["ros2", "launch", "my_robot", "robot.launch.py"]
version: '3.8' services: # ROS-Master / Discovery-Server ros2-discovery: image: ros:humble command: ros2 daemon start network_mode: host environment: - ROS_DOMAIN_ID=0 # Roboter 1 robot1: image: myregistry/robot:v1.0 environment: - ROBOT_ID=robot1 - ROS_DOMAIN_ID=0 - ROBOT_NAMESPACE=/robot1 devices: - /dev/video0:/dev/video0 - /dev/ttyACM0:/dev/ttyACM0 privileged: true network_mode: host # Roboter 2 robot2: image: myregistry/robot:v1.0 environment: - ROBOT_ID=robot2 - ROS_DOMAIN_ID=0 - ROBOT_NAMESPACE=/robot2 devices: - /dev/video1:/dev/video1 - /dev/ttyACM1:/dev/ttyACM1 privileged: true network_mode: host # Flottenmanager fleet-manager: image: myregistry/fleet-manager:latest ports: - "8080:8080" Umgebung: - ROS_DOMAIN_ID=0 Netzwerkmodus: host abhängig von: - ros2-discovery # Visualisierung (RViz) rviz: Image: myregistry/robot:v1.0 Befehl: ros2 run rviz2 rviz2 Umgebung: - DISPLAY=$DISPLAY - ROS_DOMAIN_ID=0 volumes: - /tmp/.X11-unix:/tmp/.X11-unix:rw network_mode: host
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: myapp-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: myapp minReplicas: 3 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 - type: Pods Pods: Metrik: Name: http_requests_per_second Ziel: Typ: Durchschnittswert Durchschnittswert: „1000“ Verhalten: ScaleDown: Stabilisierungsfenster in Sekunden: 300 Richtlinien: - Typ: Prozent Wert: 50 Zeitraum in Sekunden: 60 scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 30 - type: Pods value: 4 periodSeconds: 30 selectPolicy: Max
apiVersion: autoscaling.k8s.io/v1 kind: VerticalPodAutoscaler metadata: name: myapp-vpa spec: targetRef: apiVersion: "apps/v1" kind: Deployment name: myapp updatePolicy: updateMode: "Auto" resourcePolicy: containerPolicies: - containerName: '*' minAllowed: cpu: 100m memory: 128Mi maxAllowed: cpu: 4 memory: 8Gi controlledResources: ["cpu", "memory"]
# AWS EKS-Knotengruppe mit automatischer Skalierung apiVersion: eksctl.io/v1alpha5 kind: ClusterConfig metadata: name: production-cluster region: us-west-2 managedNodeGroups: - name: general-purpose instanceType: t3.xlarge minSize: 3 maxSize: 10 desiredCapacity: 5 volumeSize: 100 ssh: allow: false labels: role: general tags: nodegroup-role: general iam: withAddonPolicies: autoScaler: true cloudWatch: true ebs: true - name: gpu-nodes instanceType: g4dn.xlarge minSize: 0 maxSize: 5 desiredCapacity: 0 volumeSize: 200 labels: accelerator: nvidia-tesla-t4 taints: - key: nvidia.com/gpu value: "true" effect: NoSchedule
# Istio VirtualService für Canary-Bereitstellung apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: myapp spec: hosts: - myapp.example.com http: - match: - headers: user-agent: regex: ".*Mobile.*" route: - destination: host: myapp Teilmenge: v2 Gewichtung: 100 - Route: - Ziel: Host: myapp Teilmenge: v1 Gewichtung: 90 - Ziel: Host: myapp Teilmenge: v2 Gewichtung: 10 --- apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: myapp spec: host: myapp trafficPolicy: connectionPool: tcp: maxConnections: 100 http: http1MaxPendingRequests: 50 http2MaxRequests: 100 outlierDetection: consecutiveErrors: 5 interval: 30s baseEjectionTime: 30s maxEjectionPercent: 50 subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2
# PostgreSQL mit Replikation version: '3.8' services: postgres-primary: image: postgres:16-alpine environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password POSTGRES_REPLICATION_MODE: master POSTGRES_REPLICATION_USER: replicator POSTGRES_REPLICATION_PASSWORD_FILE: /run/secrets/repl_password volumes: - postgres-primary-data:/var/lib/postgresql/data deploy: placement: constraints: - node.labels.db.primary == true postgres-replica1: image: postgres:16-alpine environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password POSTGRES_REPLICATION_MODE: slave POSTGRES_MASTER_SERVICE: postgres-primary POSTGRES_REPLICATION_USER: replicator POSTGRES_REPLICATION_PASSWORD_FILE: /run/secrets/repl_password volumes: - postgres-replica1-data:/var/lib/postgresql/data depends_on: - postgres-primary # Schreibgeschützter Verbindungspooler pgbouncer: image: pgbouncer/pgbouncer:latest environment: - DATABASES_HOST=postgres-primary - DATABASES_PORT=5432 - DATABASES_DBNAME=mydb - PGBOUNCER_POOL_MODE=transaction - PGBOUNCER_MAX_CLIENT_CONN=1000 - PGBOUNCER_DEFAULT_POOL_SIZE=25 ports: - "6432:6432"
# .github/workflows/deploy.yml name: Build and Deploy on: push: branches: [ main, develop ] pull_request: branches: [ main ] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Tests ausführen run: | docker compose -f docker-compose.test.yml up --abort-on-container-exit docker compose -f docker-compose.test.yml down security-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Image erstellen run: docker build -t ${{ env.IMAGE_NAME}}:${{ github.sha }} . - name: Trivy-Schwachstellenscanner ausführen uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.IMAGE_NAME }}:${{ github.sha }} format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' exit-code: '1' - name: Trivy-Ergebnisse auf GitHub Security hochladen uses: github/codeql-action/upload-sarif@v2 if: always() with: sarif_file: 'trivy-results.sarif' build-and-push: needs: [test, security-scan] runs-on: ubuntu-latest Berechtigungen: Inhalte: Lesen Pakete: Schreiben Schritte: - Verwendet: actions/checkout@v4 - Name: Bei Container Registry anmelden Verwendet: docker/login-action@v3 Mit: Registry: ${{ env.REGISTRY }} Benutzername: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Metadaten extrahieren id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix={{branch}}- - name: Docker-Image erstellen und pushen uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - name: Image mit Cosign signieren run: | cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} env: COSIGN_EXPERIMENTAL: "true" deploy-staging: needs: build-and-push if: github.ref == 'refs/heads/develop' runs-on: ubuntu-latest environment: staging steps: - name: Auf Staging bereitstellen run: | kubectl set image deployment/myapp \ app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop-${{ github.sha }} \ --namespace=staging deploy-production: needs: build-and-push if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest environment: production steps: - name: Bereitstellung in der Produktionsumgebung run: | kubectl set image deployment/myapp \ app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \ --namespace=production - name: Auf Rollout warten run: | kubectl rollout status deployment/myapp --namespace=production --timeout=5m - name: Smoke-Tests ausführen run: | curl -f https://api.example.com/health || (kubectl rollout undo deployment/myapp --namespace=production && exit 1)
# argocd-application.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: myapp namespace: argocd spec: project: default source: repoURL: https://github.com/myorg/myapp-k8s-manifests targetRevision: HEAD path: overlays/production kustomize: images: - myregistry/myapp:v1.2.3 destination: server: https://kubernetes.default.svc namespace: production syncPolicy: automated: prune: true selfHeal: true allowEmpty: false syncOptions: - CreateNamespace=true - PrunePropagationPolicy=foreground - PruneLast=true retry: limit: 5 backoff: duration: 5s factor: 2 maxDuration: 3m revisionHistoryLimit: 10
# Blue-Green mit Kubernetes apiVersion: v1 kind: Service metadata: name: myapp spec: selector: app: myapp version: blue # Für die Bereitstellung auf „green“ umschalten ports: - port: 80 targetPort: 8080 --- apiVersion: apps/v1 kind: Deployment metadata: name: myapp-blue spec: replicas: 3 selector: matchLabels: app: myapp version: blue template: metadata: labels: app: myapp version: blue spec: containers: - name: app image: myapp:v1.0 --- apiVersion: apps/v1 kind: Deployment metadata: name: myapp-green spec: replicas: 3 selector: matchLabels: app: myapp version: green template: metadata: labels: app: myapp version: green spec: containers: - name: app image: myapp:v2.0
# prometheus.yml global: scrape_interval: 15s evaluation_interval: 15s alerting: alertmanagers: - static_configs: - targets: - alertmanager:9093 rule_files: - /etc/prometheus/alerts/*.yml scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] - job_name: 'docker' docker_sd_configs: - host: unix:///var/run/docker.sock relabel_configs: - source_labels: [__meta_docker_container_name] target_label: container - job_name: 'kubernetes-pods' kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] action: keep regex: true - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] action: replace target_label: __metrics_path__ regex: (.+) - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] action: replace regex: ([^:]+)(?::\d+)?;(\d+) replacement: $1:$2 target_label: __address__
# alerts.yml groups: - name: application_alerts interval: 30s rules: - alert: HighErrorRate expr: | ( sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) ) > 0.05 for: 5m labels: severity: critical annotations: summary: "Hohe Fehlerrate festgestellt" description: "Die Fehlerrate beträgt {{ $value | humanizePercentage }} (Schwellenwert: 5%)" - alert: Hohe Speicherauslastung expr: | ( container_memory_usage_bytes{name!=""} / container_spec_memory_limit_bytes{name!=""} ) > 0.9 for: 5m labels: severity: warning annotations: summary: "Container {{ $labels.name }} hoher Speicherverbrauch" description: "Der Speicherverbrauch beträgt {{ $value | humanizePercentage }}" - alert: PodCrashLooping expr: | rate(kube_pod_container_status_restarts_total[15m]) > 0 for: 5m labels: severity: critical annotations: summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} befindet sich in einer Crash-Schleife" - alert: DeploymentReplicasMismatch expr: | kube_deployment_spec_replicas != kube_deployment_status_replicas_available for: 10m labels: severity: warning annotations: summary: "Deployment {{ $labels.namespace }}/{{ $labels.deployment }} Replikate stimmen nicht überein"
# grafana/dashboards/app-dashboard.json (vereinfacht) { "dashboard": { "title": "Anwendungsmetriken", "panels": [ { "title": "Anfragefrequenz", "targets": [ { "expr": "sum(rate(http_requests_total[5m])) by (service)" } ] }, { "title": "Fehlerrate", "targets": [ { "expr": "sum(rate(http_requests_total{status=~\"5..\"}[5m])) by (service)" } ] }, { "title": "Antwortzeit (p95)", "targets": [ { "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))" } ] } ] } }
// Einrichtung der OpenTelemetry-Ablaufverfolgung const { NodeSDK } = require('@opentelemetry/sdk-node'); const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); const { Resource } = require('@opentelemetry/resources'); const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions'); const sdk = new NodeSDK({ resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'myapp', [SemanticResourceAttributes.SERVICE_VERSION]: process.env.VERSION, environment: process.env.NODE_ENV, }), traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + '/v1/traces', }), instrumentations: [ getNodeAutoInstrumentations({ '@opentelemetry/instrumentation-fs': { enabled: false, }, }), ], }); sdk.start();
# Wichtige Debugging-Befehle # Protokolle anzeigen docker logs -f <container-id>kubectl logs -f deployment/myapp kubectl logs -f deployment/myapp --previous # Vorheriger Container # Befehle im Container ausführen docker exec -it <container-id>/bin/sh kubectl exec -it deployment/myapp -- /bin/sh # Ressourcenauslastung prüfen docker stats kubectl top pods kubectl top nodes # Ressourcen beschreiben kubectl describe pod <pod-name>kubectl get events --sort-by='.lastTimestamp' # Netzwerk-Debugging kubectl run -it --rm debug --image=nicolaka/netshoot --restart=Never -- /bin/bash # Im Debug-Pod: # nslookup myservice # curl myservice:8080/health # tcpdump -i any port 8080 # Portweiterleitung kubectl port-forward deployment/myapp 8080:8080 # Dateien aus dem Container kopieren kubectl cp <pod-name>:/app/logs ./local-logs # Cluster-Informationen anzeigen kubectl cluster-info dump
1. WebAssembly (Wasm)-Container
# Zukunft: Wasm-basierte Micro-VMs FROM scratch COPY /app/main.wasm / CMD ["/main.wasm"]
2. eBPF für Observability
3. Platform Engineering & Interne Entwicklerplattformen
# Backstage + Kubernetes für Self-Service apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: myapp spec: type: service lifecycle: production owner: team-backend system: core-platform providesApis: - myapi-v1 consumesApis: - auth-api - payment-api
4. Green Computing & CO₂-bewusste Planung
# Workloads basierend auf der CO₂-Intensität planen apiVersion: v1 kind: Pod spec: schedulerName: carbon-aware-scheduler nodeSelector:</pod-name></pod-name></container-id></container-id> Kohlenstoffintensität: niedrig
5. KI-gesteuerte Betriebsabläufe (AIOps)
Produktive Docker-Bereitstellungen im Jahr 2025 erfordern einen ganzheitlichen Ansatz, der weit über das Schreiben von Dockerfiles hinausgeht. Erfolg basiert auf:
Die Landschaft der Container-Orchestrierung entwickelt sich ständig weiter, doch diese Grundprinzipien bleiben unverändert. Ob Sie sich nun wegen seines Ökosystems für Kubernetes oder wegen seiner Einfachheit für Docker Swarm entscheiden – konzentrieren Sie sich darauf, widerstandsfähige, beobachtbare und sichere Systeme aufzubauen.
Denken Sie daran: Die beste Bereitstellungsstrategie ist die, die Ihr Team auch tatsächlich aufrechterhalten kann. Fangen Sie einfach an, messen Sie alles und iterieren Sie auf der Grundlage realer Produktionsdaten.
Bleiben Sie neugierig, lernen Sie weiter und viel Spaß beim Bereitstellen!