Zum Hauptinhalt springen
Docker erklärt – Warum Container die Art verändert haben, wie wir Software ausliefern
#Docker #Container #DevOps #Minecraft #Virtualisierung

Docker erklärt – Warum Container die Art verändert haben, wie wir Software ausliefern

Von Auf meinem Rechner läufts zum portablen Standard: Wie Container das Dependency-Hell gelöst haben – erklärt am Minecraft-Server

SerieContainer & Orchestrierung
Teil 1 von 3

Es gibt einen Satz, den jeder Entwickler kennt und hasst: “Aber auf meinem Rechner läuft’s!”

Das klassische Szenario: Eine Anwendung funktioniert perfekt auf dem Entwickler-Laptop, aber sobald sie auf dem Test-Server oder in Production deployed wird, stürzt sie ab. Falsche Bibliotheksversionen, fehlende Dependencies, unterschiedliche Betriebssystemkonfigurationen – die Liste der möglichen Fehlerquellen ist endlos.

Docker hat dieses Problem gelöst. Nicht durch Magie, sondern durch ein einfaches, aber fundamentales Konzept: Verpacke die Anwendung zusammen mit allem, was sie braucht, in eine portable Einheit.

Seit der Veröffentlichung 2013 hat Docker die Art verändert, wie Software entwickelt, getestet und deployed wird. Aber was genau sind Container? Und warum sind sie mehr als nur “leichtgewichtige virtuelle Maschinen”?


Das Problem: Dependency Hell und Umgebungs-Chaos

Bevor wir zu Container-Technologie kommen, schauen wir uns das konkrete Problem an. Nehmen wir als Beispiel einen Minecraft-Server – ein Use Case, den viele kennen und der die Herausforderungen perfekt illustriert.

Szenario: Drei Minecraft-Server auf einem System

Du betreibst einen Gaming-Server und möchtest drei verschiedene Minecraft-Welten hosten:

  1. Survival-Server – Minecraft 1.21.1 mit Paper-Server, benötigt Java 21
  2. Creative-Server – Minecraft 1.20.4 mit Spigot, läuft mit Java 17
  3. Modded-Server – Minecraft 1.12.2 mit Forge und 150 Mods, benötigt Java 8

Alle drei sollen auf demselben physischen Server laufen. Die Probleme stapeln sich sofort:

Java-Version-Konflikt: Du kannst nicht gleichzeitig Java 8, 17 und 21 als Standard-Java auf einem System haben. Zwar gibt es Workarounds mit Umgebungsvariablen und Pfad-Manipulation, aber das ist fehleranfällig und wartungsintensiv.

Plugin-Konflikte: Die Server nutzen unterschiedliche Plugin-APIs. Spigot-Plugins funktionieren nicht mit Forge. Paper hat eigene Erweiterungen, die mit Standard-Bukkit nicht kompatibel sind.

Ressourcen-Isolation: Wenn der Modded-Server mit 150 Mods den gesamten RAM frisst, sollen die anderen Server nicht darunter leiden. Wie begrenzt man Ressourcen pro Server?

Update-Probleme: Ein Update des Survival-Servers auf Minecraft 1.21 darf nicht die anderen Server beeinflussen. Aber wenn das Update eine neue Java-Version benötigt, wird es kompliziert.

Backup und Migration: Du willst den Creative-Server auf einen anderen physischen Server verschieben. Du musst die exakte Java-Version installieren, alle Spigot-Dependencies, die richtigen Konfigurationsdateien kopieren – und hoffen, dass nichts schiefgeht.

Das traditionelle “Lösungs”-Chaos

Ohne Container gibt es nur unbefriedigende Ansätze:

Separate physische Server: Funktioniert, ist aber teuer und ineffizient. Drei Server für drei Minecraft-Instanzen sind Ressourcenverschwendung.

Virtuelle Maschinen: Besser, aber jede VM benötigt ein vollständiges Betriebssystem. Für einen Minecraft-Server, der 2 GB RAM braucht, verschwendet man 500-800 MB allein für das Guest-OS. VMs starten langsam (Minuten statt Sekunden) und sind ressourcenhungrig.

Manuelle Isolation mit chroot/systemd: Technisch möglich, aber komplex, fehleranfällig und schwer zu warten. Kein vernünftiger Workflow für Development-to-Production.

Das ist genau das Problem, das Docker löst.


Was sind Container? Die Idee hinter Docker

Ein Container ist eine isolierte Ausführungsumgebung für Prozesse, die den Kernel des Host-Systems mitnutzt, aber ansonsten vollständig abgeschottet ist. Anders als virtuelle Maschinen emulieren Container keine Hardware – sie nutzen Linux-Kernel-Features wie Namespaces und Control Groups (cgroups), um Prozesse voneinander zu trennen.

Container vs. virtuelle Maschinen

Der Vergleich wird oft gemacht, ist aber irreführend. Container sind nicht leichtgewichtige VMs. Der fundamentale Unterschied:

Virtuelle Maschinen emulieren komplette Hardware. Ein Hypervisor (KVM, VMware, Hyper-V) teilt physische Ressourcen auf mehrere virtuelle Maschinen auf. Jede VM hat ein vollständiges Betriebssystem mit eigenem Kernel, eigenen Bibliotheken und allen System-Services. Das bedeutet:

  • Start-Zeit: 30 Sekunden bis mehrere Minuten
  • Overhead: 500 MB bis mehrere GB pro VM nur für das OS
  • Isolation: Komplett – eigener Kernel, eigene Hardware-Abstraktion
  • Portabilität: Begrenzt – abhängig vom Hypervisor

Container teilen sich den Kernel des Host-Systems. Ein Container ist im Grunde ein isolierter Prozess mit eigener Sicht auf Dateisystem, Netzwerk und Prozessliste. Das bedeutet:

  • Start-Zeit: Millisekunden bis wenige Sekunden
  • Overhead: Einige MB für Base-Image, der Rest ist die Anwendung selbst
  • Isolation: Prozess- und Dateisystem-Ebene, kein eigener Kernel
  • Portabilität: Exzellent – läuft überall wo Docker läuft

Die Minecraft-Server-Lösung mit Containern

Zurück zu unserem Beispiel. Mit Docker wird aus dem Chaos eine saubere Architektur:

# Survival-Server (Java 21, Minecraft 1.21.1)
docker run -d -p 25565:25565 \
  -v survival-data:/data \
  --name survival-server \
  minecraft-paper:1.21.1-java21

# Creative-Server (Java 17, Minecraft 1.20.4)
docker run -d -p 25566:25565 \
  -v creative-data:/data \
  --name creative-server \
  minecraft-spigot:1.20.4-java17

# Modded-Server (Java 8, Minecraft 1.12.2)
docker run -d -p 25567:25565 \
  -v modded-data:/data \
  --name modded-server \
  minecraft-forge:1.12.2-java8

Jeder Server läuft in seinem eigenen Container mit exakt der Java-Version, den Bibliotheken und der Konfiguration, die er braucht. Die Container sehen sich nicht gegenseitig. Updates betreffen nur den jeweiligen Container. Und das Beste: Du kannst diese Container auf jedem System starten, das Docker installiert hat – egal ob Ubuntu, CentOS, Windows Server oder macOS.


Wie funktioniert Docker? Die technische Grundlage

Docker nutzt mehrere Linux-Kernel-Features, um Isolation und Ressourcen-Management zu ermöglichen.

Namespaces – Isolation auf Prozess-Ebene

Linux-Namespaces trennen Prozesse voneinander. Es gibt verschiedene Arten:

PID Namespace: Jeder Container sieht nur seine eigenen Prozesse. Der Minecraft-Server in Container 1 mit PID 1 sieht nicht den Server in Container 2.

Network Namespace: Jeder Container hat sein eigenes virtuelles Netzwerk-Interface, eigene IP-Adresse, eigene Ports. Das ist der Grund, warum drei Minecraft-Server alle auf Port 25565 lauschen können – aus Container-Sicht ist es immer Port 25565, Docker mapped das auf unterschiedliche Host-Ports (25565, 25566, 25567).

Mount Namespace: Jeder Container hat sein eigenes Dateisystem. Änderungen in einem Container beeinflussen andere nicht. Ein /tmp-Verzeichnis in Container 1 ist völlig getrennt von /tmp in Container 2.

UTS Namespace: Jeder Container kann einen eigenen Hostname haben.

IPC Namespace: Inter-Process Communication ist isoliert.

User Namespace: User-IDs können zwischen Container und Host gemapped werden. Root in einem Container muss nicht Root auf dem Host sein.

Control Groups (cgroups) – Ressourcen-Limitierung

cgroups begrenzen und überwachen Ressourcennutzung:

CPU: Ein Container kann auf z.B. 2 CPU-Kerne oder 50% einer CPU limitiert werden.

RAM: Der modded Minecraft-Server bekommt maximal 4 GB RAM, der Survival-Server nur 2 GB. Wenn ein Container sein Limit erreicht, wird er nicht den Rest des Systems lahmlegen.

Disk I/O: Limitierung von Lese- und Schreiboperationen pro Sekunde, um I/O-intensive Container zu drosseln.

Network Bandwidth: Begrenzen von Netzwerk-Traffic pro Container.

Union Filesystems – Layer-basierte Images

Docker-Images bestehen aus mehreren schreibgeschützten Layern, die übereinander gestapelt werden. Das ist eine der Killer-Features:

Layer 1: Ubuntu Base (80 MB)
Layer 2: Java 21 Installation (200 MB)
Layer 3: Paper-Server Binary (50 MB)
Layer 4: Custom Plugins (10 MB)
Layer 5: Konfigurationsdateien (1 MB)

Diese Layer werden geteilt. Wenn du 10 Container mit demselben Ubuntu-Base-Image startest, wird dieser Layer nur einmal gespeichert und von allen Containern genutzt. Nur die Unterschiede (z.B. Container-spezifische Config) werden separat gespeichert.

Beim Start eines Containers wird ein beschreibbarer Layer oben drauf gelegt. Alle Änderungen landen in diesem Layer. Das Base-Image bleibt unverändert.


Dockerfile – Der Bauplan für Container-Images

Ein Dockerfile ist die Definition, wie ein Container-Image gebaut wird. Schauen wir uns ein konkretes Beispiel für unseren Minecraft-Server an:

# Base-Image: Alpine Linux mit Java 21
FROM eclipse-temurin:21-jre-alpine

# Metadaten
LABEL maintainer="[email protected]"
LABEL version="1.0"

# Arbeitsverzeichnis erstellen
WORKDIR /minecraft

# Paper-Server herunterladen
RUN wget https://api.papermc.io/v2/projects/paper/versions/1.21.1/builds/119/downloads/paper-1.21.1-119.jar \
    -O server.jar

# EULA akzeptieren (in Production: via Volume mount)
RUN echo "eula=true" > eula.txt

# Port exponieren
EXPOSE 25565

# Volume für persistente Daten
VOLUME /minecraft/world

# Startup-Command
CMD ["java", "-Xmx2G", "-Xms2G", "-jar", "server.jar", "nogui"]

Die Dockerfile-Instruktionen erklärt

FROM: Definiert das Base-Image. Hier nutzen wir ein offizielles Eclipse Temurin Java 21 Image auf Alpine-Basis. Alpine ist eine minimal Linux-Distribution (5 MB statt 100+ MB bei Ubuntu).

LABEL: Metadaten für das Image. Nützlich für Dokumentation und Filterung.

WORKDIR: Setzt das Arbeitsverzeichnis. Alle folgenden Befehle laufen in /minecraft.

RUN: Führt Befehle während des Image-Builds aus. Jeder RUN-Befehl erstellt einen neuen Layer. Deshalb werden mehrere Befehle oft mit && kombiniert, um Layer zu minimieren.

EXPOSE: Dokumentiert, welcher Port genutzt wird. Ändert nichts am Netzwerk, ist reine Dokumentation.

VOLUME: Definiert einen Mount-Point für persistente Daten. Die Minecraft-Welt soll Container-Neustarts überleben.

CMD: Der Befehl, der beim Container-Start ausgeführt wird. Hier starten wir den Minecraft-Server mit 2 GB RAM.

Image bauen und starten

# Image bauen
docker build -t minecraft-paper:1.21.1 .

# Container starten
docker run -d \
  --name survival-server \
  -p 25565:25565 \
  -v minecraft-survival:/minecraft/world \
  minecraft-paper:1.21.1

# Logs ansehen
docker logs -f survival-server

# Container stoppen
docker stop survival-server

# Container entfernen
docker rm survival-server

Das Volume minecraft-survival ist persistent. Selbst wenn der Container gelöscht wird, bleiben die Welt-Daten erhalten.


Docker Compose – Mehrere Container orchestrieren

Für unser Minecraft-Szenario mit drei Servern wäre es mühsam, jeden Container einzeln per docker run zu starten. Docker Compose löst das Problem elegant:

version: '3.8'

services:
  survival:
    image: minecraft-paper:1.21.1
    container_name: survival-server
    ports:
      - "25565:25565"
    volumes:
      - survival-data:/minecraft/world
    environment:
      - JAVA_OPTS=-Xmx2G -Xms2G
    restart: unless-stopped

  creative:
    image: minecraft-spigot:1.20.4
    container_name: creative-server
    ports:
      - "25566:25565"
    volumes:
      - creative-data:/minecraft/world
    environment:
      - JAVA_OPTS=-Xmx1G -Xms1G
    restart: unless-stopped

  modded:
    image: minecraft-forge:1.12.2
    container_name: modded-server
    ports:
      - "25567:25565"
    volumes:
      - modded-data:/minecraft/world
    environment:
      - JAVA_OPTS=-Xmx4G -Xms4G
    restart: unless-stopped

volumes:
  survival-data:
  creative-data:
  modded-data:

Starten und Verwalten

# Alle Container starten
docker compose up -d

# Status ansehen
docker compose ps

# Logs aller Container
docker compose logs -f

# Einen spezifischen Service neu starten
docker compose restart creative

# Alles stoppen und entfernen
docker compose down

Mit einer einzigen Datei ist die komplette Infrastruktur definiert. Versionierbar, reproduzierbar, portabel.


Warum Container die Software-Entwicklung verändert haben

1. Portabilität – “Build once, run anywhere”

Ein Docker-Image läuft auf jedem System mit Docker. Entwickler-Laptop (macOS), CI/CD-Server (Ubuntu), Production (CentOS) – überall identisch. Das “Auf meinem Rechner läuft’s”-Problem ist gelöst.

2. Konsistenz über Umgebungen

Development, Staging und Production nutzen exakt dieselben Images. Keine Überraschungen mehr beim Deployment, weil eine Library-Version unterschiedlich ist.

3. Schnelle Entwicklungszyklen

Ein Container startet in Sekunden. Neue Version testen? Image bauen, Container starten, fertig. Kein Warten auf VM-Boots oder lange Provisioning-Prozesse.

4. Ressourcen-Effizienz

Auf einem Server, der 10 VMs hosten könnte, laufen problemlos 50-100 Container. Weniger Overhead bedeutet bessere Hardware-Auslastung.

5. Microservices-Architektur

Kleine, unabhängige Services – jeder in einem eigenen Container. Updates einzelner Services ohne Downtime. Das ist die Grundlage moderner Cloud-Native-Anwendungen.

6. Immutable Infrastructure

Container werden nicht gepatcht oder upgedatet – sie werden ersetzt. Neue Version? Neues Image bauen, alte Container stoppen, neue starten. Das eliminiert Configuration Drift und macht Rollbacks trivial.


Die Grenzen von Docker

Docker ist mächtig, aber kein Allheilmittel. Es gibt Grenzen:

Linux-Kernel-Abhängigkeit

Container teilen sich den Host-Kernel. Das bedeutet: Ein Windows-Container läuft nicht auf einem Linux-Host und umgekehrt. Für Windows-Container brauchst du einen Windows-Host. (Docker Desktop nutzt auf macOS/Windows eine Linux-VM, um Linux-Container zu ermöglichen, aber das ist eine Abstraktionsschicht.)

Nicht für alle Workloads geeignet

Anwendungen mit sehr spezifischen Kernel-Anforderungen oder Anwendungen, die direkten Hardware-Zugriff brauchen (manche GPU-Workloads, spezialisierte Netzwerkkarten), sind in Containern schwieriger zu betreiben.

Security-Überlegungen

Container sind isoliert, aber teilen sich den Kernel. Eine Kernel-Schwachstelle kann alle Container betreffen. VMs bieten hier stärkere Isolation. Für hochsensible Workloads kombiniert man oft beides: Container innerhalb von VMs.

Persistenz erfordert Volumes

Container sind ephemeral – zustandslos. Daten gehen verloren, wenn der Container gelöscht wird, außer sie liegen in Volumes. Das ist beabsichtigt (Immutable Infrastructure), erfordert aber ein Umdenken.

Orchestrierung bei Scale

Docker allein reicht nicht für hunderte oder tausende Container auf dutzenden Hosts. Hier braucht es Orchestrierung – was uns direkt zum nächsten Artikel führt.


Von Docker zu Kubernetes – Die natürliche Entwicklung

Docker Compose funktioniert großartig für eine Handvoll Container auf einem einzelnen Host. Aber was passiert, wenn du skalieren musst?

Stell dir vor, dein Minecraft-Server-Netzwerk wächst:

  • 50 verschiedene Server-Instanzen
  • Verteilt über 10 physische Maschinen
  • Automatisches Failover, wenn eine Maschine ausfällt
  • Load Balancing zwischen Server-Instanzen
  • Automatische Skalierung bei Community-Events

Docker allein kann das nicht leisten. Hier kommt Container-Orchestrierung ins Spiel – und das führt uns direkt zu Kubernetes, dem Thema des dritten Artikels dieser Serie.


Praktische Erkenntnisse

Docker hat bewiesen, dass Container-Technologie nicht kompliziert sein muss. Ein simples Dockerfile, docker build, docker run – mehr braucht es nicht für die Grundlagen. Die Komplexität kommt erst bei Scale, bei verteilten Systemen, bei Hochverfügbarkeit.

Für die meisten Anwendungsfälle – lokale Development-Umgebungen, CI/CD-Pipelines, kleine bis mittlere Produktions-Deployments – reicht Docker vollkommen aus. Die Kubernetes-Welt ist mächtig, aber oft Overkill.

Die Minecraft-Server-Metapher zeigt das Kernprinzip: Isolation, Portabilität, Reproduzierbarkeit. Ob Minecraft, eine Node.js-App, eine .NET-Anwendung oder ein Rust-Microservice – das Konzept bleibt gleich.

Im nächsten Artikel der Serie schauen wir uns an, wie das in der Praxis für verschiedene Technologie-Stacks aussieht: Production-ready Dockerfiles für Node.js, .NET Core, Java und Rust, Integration mit Datenbanken und die Kommunikation zwischen Containern.


Weiterführende Links: