Docker – die revolutionäre Container-Technologie

Bei Docker handelt es sich um eine Technologie für die containerbasierte Virtualisierung von Software-Anwendungen. Der von Docker im Mainstream verankerte containerbasierte Ansatz hat in den letzten Jahren die Anwendungsentwicklung umgekrempelt. Dabei wurden alle Bereiche der Entwicklung berührt: Wie man Anwendungen und Komponenten baut, Software-Dienste verteilt und von der Entwicklung in die Produktion geht. All diese Prozesse laufen mit Docker anders ab, als es vormals üblich war.

Aber nicht nur die Entwicklungsprozesse haben sich verändert, sondern auch die Software-Architektur: weg von monolithischen Gesamtlösungen und hin zu Verbünden leichtgewichtiger „Microservices“. Damit einhergehend erhöhte sich auch die Komplexität der resultierenden Gesamtsysteme. In den letzten Jahren hat sich u. a. die Software Kubernetes etabliert, um Muli-Container-Anwendungen zu managen.

Die Entwicklung der containerbasierten Virtualisierung ist bei weitem nicht abgeschlossen; es bleibt also spannend. In diesem Artikel erklären wir, wie Docker als grundlegende Technologie funktioniert. Ferner schauen wir uns an, welche Beweggründe zur Entwicklung von Docker beigetragen haben.

Hinweis

Beachten Sie: Der Name „Docker“ hat mehrere Bedeutungen. Er wird synonym verwendet für die Software an sich, für das ihr zugrundeliegende Open-Source-Projekt sowie für eine US-amerikanische Firma, die verschiedene Produkte und Dienstleistungen kommerziell betreibt.

Neben dem Docker Team sind führende Technologieunternehmen wie Cisco, Google, Huawei, IBM, Microsoft und Red Hat an der Entwicklung von Docker und damit einhergehender Technologien beteiligt. Als neuere Entwicklung kommt neben dem Linux-Kernel mittlerweile auch Windows als native Umgebung für Docker-Container zum Einsatz. Hier einige der wichtigsten Meilensteine in der Docker-Entstehungsgeschichte:

Jahr

Meilensteine der Docker-Entwicklung

2007

cgroups-Technologie im Linux-Kernel integriert

2008

LXC veröffentlicht; baut wie später Docker auf cgroups und Linux-Namespaces auf

2013

Docker als Open Source veröffentlicht

2014

Docker bei Amazon EC2 verfügbar

2015

Kubernetes veröffentlicht

2016

Docker auf Windows 10 Pro via Hyper-V verfügbar

2019

Docker auf Windows Home via WSL2 verfügbar

Was ist Docker?

Die Kernfunktionalität von Docker besteht in der Container-Virtualisierung von Anwendungen. Diese steht im Gegensatz zur Virtualisierung mit virtuellen Maschinen (VM). Mit Docker wird der Anwendungs-Code inklusive aller Abhängigkeiten in ein sogenanntes Image gepackt. Die Docker-Software führt die so verpackte Anwendung in einem Docker-Container aus. Images lassen sich zwischen Systemen bewegen und auf jedem System ausführen, auf dem Docker läuft.

Zitat

„Containers are a standardized unit of software that allows developers to isolate their app from its environment […]“ – Aussage der Docker-Entwickler, Quelle: https://www.docker.com/why-docker

„Container sind eine standardisierte Einheit von Software, die es Entwicklern erlaubt, ihre App von ihrer Umgebung zu isolieren […]“ (Übersetzung: IONOS)

Wie beim Einsatz einer virtuellen Maschine (VM) liegt ein Hauptaugenmerk bei Docker-Containern auf der Isolierung der laufenden Anwendung. Anders als bei VMs wird jedoch kein komplettes Betriebssystem virtualisiert. Stattdessen weist Docker jedem Container gewisse Betriebssystem- und Hardware-Ressourcen zu. Aus einem Docker-Image lassen sich beliebig viele Container erzeugen und parallel betreiben. So werden skalierbare Cloud-Dienste realisiert.

Auch wenn wir von Docker als einer Software sprechen, handelt es sich tatsächlich um mehrere Software-Komponenten, die über die Docker-Engine-API kommunizieren. Ferner kommt eine Handvoll spezieller Docker-Objekte zum Einsatz; z. B. gehören dazu die bereits erwähnten Images und Container. Die Docker-spezifischen Workflows setzten sich aus den Software-Komponenten und Docker-Objekten zusammen. Schauen wir uns dieses Zusammenspiel im Detail an.

Docker-Software

Grundlage für die Docker-Software bildet die sogenannte Docker-Engine. Diese dient hauptsächlich zur Verwaltung und Steuerung der Container und der ihnen zugrundeliegenden Images. Für darüber hinausgehende Funktionalitäten kommen spezielle Docker-Tools zum Einsatz. Diese werden vor allem für die Verwaltung von Anwendungen benötigt, die aus Gruppen von Containern bestehen.

Docker-Engine

Die Docker-Engine läuft auf einem lokalen System oder einem Server und besteht aus zwei Komponenten:

  1. Dem Docker-Daemon dockerd: Dieser läuft dauerhaft im Hintergrund und lauscht auf Zugriffe über die Docker-Engine-API. Auf entsprechende Befehle hin verwaltet dockerd Docker-Container und weitere Docker-Objekte.
  2. Dem Docker-Client docker: Dabei handelt es sich um ein Kommandozeilenprogramm. Der Docker-Client dient zur Steuerung der Docker-Engine und stellt Befehle für Erstellung und Aufbau von Docker-Containern sowie Erstellung, Bezug und Versionierung von Docker-Images bereit.

Docker-Engine-API

Bei der Docker-Engine-API handelt es sich um eine REST-API. Sie bildet die Schnittstelle zum Docker-Daemon. Zur Integration der Docker-Engine-API in Software-Projekte stehen offizielle „Software-Development-Kits“ (SKDs) für die Programmiersprachen Go und Python bereit. Daneben existieren vergleichbare Bibliotheken für mehr als ein Dutzend weiterer Programmiersprachen. Der Zugriff auf die API erfolgt auf der Kommandozeile mit dem docker-Befehl. Ferner lässt sich die API direkt mittels cURL oder vergleichbaren Tools ansprechen.

Docker-Tools

Beim Einsatz virtueller Maschinen kommen oft aus mehreren Software-Komponenten bestehende Systeme zum Einsatz. Im Gegensatz dazu begünstigt die Container-Virtualisierung mit Docker Verbünde lose gekoppelter Microservices. Diese eignen sich für verteilte Cloud-Lösungen, die ein hohes Maß an Modularität und Hochverfügbarkeit bieten. Jedoch wächst die Komplexität derartiger Systeme schnell an. Um diese effizient zu verwalten, werden spezielle, als „Orchestrator“ bekannte Software-Tools eingesetzt.

Mit Docker Swarm und Docker Compose stehen zwei offizielle Docker-Tools zur Orchestrierung von Container-Verbünden bereit. Mit dem 'docker swarm'-Kommando lassen sich mehrere Docker-Engines zu einer virtuellen Engine zusammenfassen. Dabei können die einzelnen Engines über mehrere Systeme und Infrastrukturen verteilt betrieben werden. Der 'docker compose'-Befehl dient zum Erzeugen von als „Stack“ bezeichneten Multi-Container-Anwendungen.

Komfortabler im Einsatz als Swarm und Compose ist der ursprünglich von Google entwickelte Orchestrator Kubernetes. Dieser hat sich als Standard etabliert und wird von der Industrie breitflächig eingesetzt. Hosting-Firmen und weitere Anbieter von „Software as a Service“- und „Platform as a Service“-Lösungen setzen verstärkt auf Kubernetes als grundlegende Infrastruktur.

Docker-Objekte

Die Arbeitsabläufe im Docker-Ökosystem ergeben sich aus dem Zusammenspiel der Docker-Objekte. Verwaltet werden diese durch Kommunikation mit der Docker-Engine-API. Schauen wir uns die einzelnen Arten von Objekten im Detail an.

Docker-Image

Bei einem Docker-Image handelt es sich um eine schreibgeschützte Vorlage zum Erzeugen eines oder mehrerer identischer Container. Docker-Images sind quasi die Samen des Systems; sie werden eingesetzt, um Anwendungen zu bündeln und auszuliefern.

Für den Bezug von Docker-Images kommen verschiedene Repositories zum Einsatz. Es gibt sowohl öffentliche als auch nichtöffentliche Repositories. Auf dem beliebten „Docker Hub“ stehen zum Zeitpunkt der Artikelerstellung mehr als fünf Millionen verschiedene Images zu Download bereit. Über die Docker-Kommandos 'docker pull' und 'docker push' wird ein Image von einem Repository bezogen bzw. dort abgelegt.

Docker-Images sind in Schichten – sogenannten Layers – aufgebaut. Jeder Layer repräsentiert eine spezifische Änderung am Image. Damit ergibt sich eine durchgehende Versionierung der Images, was ein Rollback zu einem früheren Zustand ermöglicht. Zur Erzeugung eines neuen Images kann ein existierendes Image als Grundlage genutzt werden.

Docker-File

Beim Docker-File handelt es sich um eine Textdatei, die den Aufbau eines Docker-Images beschreibt. Ein Docker-File ist vergleichbar mit einem Stapelverarbeitungs-Skript; die Datei enthält Befehle, die ein Image beschreiben. Bei der Ausführung des Docker-Files werden die Befehle nacheinander abgearbeitet. Für jeden Befehl wird ein neuer Layer auf dem Docker-Image angelegt. Sie können sich ein Docker-File also auch als eine Art Rezept vorstellen, auf dessen Basis ein Image kreiert wird.

Docker-Container

Kommen wir nun zum zentralen Konzept im Docker-Universum: dem Docker-Container. Während ein Docker-Image eine inerte Vorlage ist, handelt es sich beim Docker-Container um eine aktive, laufende Instanz eines Images. Ein Docker-Image liegt lokal in einer einzigen Kopie vor und belegt lediglich etwas Speicherplatz. Demgegenüber lassen sich aus demselben Image mehrere Docker-Container erzeugen und parallel betreiben.

Jeder Docker-Container enthält für die Ausführung eine gewisse Menge an Systemressourcen wie CPU-Nutzung, Arbeitsspeicher, Netzwerk-Schnittstellen etc. Ein Docker-Container lässt sich erzeugen, starten, beenden und zerstören. Ferner kann man den Zustand eines laufenden Containers als neues Image speichern.

Docker-Volume

Wie wir gesehen haben, wird ein laufender Docker-Container aus einem nicht veränderbaren Image erzeugt. Doch wie verhält es sich mit Daten, die innerhalb des Containers zum Einsatz kommen und über dessen Lebensdauer hinaus erhalten bleiben sollen? Für diesen Anwendungsfall kommen Docker-Volumen zum Einsatz. Ein Docker-Volumen existiert außerhalb eines spezifischen Containers. So können sich mehrere Container ein Volumen teilen. Die im Volume enthaltenen Daten werden auf dem Dateisystem des Hosts gespeichert. Damit ist ein Docker-Volume vergleichbar mit dem Shared Folder einer virtuellen Maschine.

Wie funktioniert Docker?

Das grundlegende Wirkprinzip von Docker ist in der Funktionsweise ähnlich wie bei der vorher entwickelten Virtualisierungs-Technologie LXC: Beide bauen auf dem Linux-Kernel auf und realisieren die containerbasierte Virtualisierung. Sowohl Docker als auch LXC vereinen zwei für sich genommen gegenläufige Ziele:

  1. Laufende Container teilen sich denselben Linux-Kernel und sind damit im Vergleich zu virtuellen Maschinen leichtgewichtig.
  2. Laufende Container sind voneinander isoliert und haben nur Zugriff auf ein limitiertes Maß an Systemressourcen.

Um diese Ziele zu erreichen, setzen sowohl Docker auch als LXC auf die Technologien „Kernel-Namespaces“ und „Control Groups“. Schauen wir uns an, wie dies im Detail funktioniert.

Linux-Kernel

Der Linux-Kernel ist die Kernkomponente des Open-Source-Betriebssystems GNU/Linux. Der Kernel verwaltet die Hardware und steuert Prozesse. Beim Betrieb von Docker außerhalb von Linux wird ein Hypervisor oder eine virtuelle Maschine benötigt, um die Funktionalität des Linux-Kernels bereitzustellen. Unter macOS kommt der vom BSD-Hypervisor bhyve abgeleitete xhyve zum Einsatz. Unter Windows 10 setzt Docker auf dem Hyper-V-Hypervisor auf.

Kernel-Namespaces

Namespaces sind ein Feature des Linux-Kernels. Sie partitionieren Kernel-Ressourcen und gewährleisten damit die Abgrenzung von Prozessen untereinander. Ein Prozess eines Namespace kann nur Kernel-Ressourcen desselben Namespace sehen. Hier eine Übersicht der in Docker zum Einsatz kommenden Namespaces:

Namespace

Beschreibung

Erklärung

UTS

Systemidentifikation

Containern eigene Host- und Domain-Namen zuweisen

PID

Prozess-IDs

Jeder Container verwendet einen eigenen Namensraum für Prozess-IDs; PIDs aus anderen Containern sind nicht sichtbar; so können zwei Prozesse in verschiedenen Containern dieselbe PID benutzen, ohne dass es zu einem Konflikt kommt.

IPC

Interprozess-Kommunikation

IPC-Namespaces isolieren Prozesse in einem Container, sodass diese nicht mit Prozessen in anderen Containern kommunizieren können.

NET

Netzwerkressourcen

Einem Container separate Netzwerkressourcen wie IP-Adressen oder Routing-Tabellen zuweisen

MNT

Mountpoints des Dateisystems

Beschränkt das Dateisystem des Hosts aus Sicht des Containers auf einen eng definierten Ausschnitt

Control Groups

Control Groups, meist als cgroups abgekürzt, dienen zur hierarchischen Organisation von Linux-Prozessen. Einem Prozess (oder einer Gruppe von Prozessen) wird ein begrenztes Maß an Systemressourcen zugewiesen. Dazu zählen Arbeitsspeicher, CPU-Kerne, Massenspeicher und (virtuelle) Netzwerk-Geräte. Während Namespaces Prozesse voneinander isolieren, limitieren Control Groups den Zugriff auf Systemressourcen. So wird beim Betrieb mehrerer Container die Funktionsfähigkeit des Gesamtsystems sichergestellt.

Welche Vorteile hat Docker?

Schauen wir uns die Geschichte der Software-Entwicklung an, um die Vorteile von Docker nachzuvollziehen. Wie wird und wurde Software gebaut, ausgeliefert und ausgeführt? Was hat sich dabei grundlegend verändert? Software bildet das Gegenstück zur Hardware, dem physischen Computer. Ohne Software ist der Computer nur ein Klumpen Materie. Während die Hardware fest und unveränderbar ist, lässt sich Software neu erschaffen und anpassen. Durch das Zusammenspiel der beiden Ebenen ergibt sich die digitale Wunderwelt.

Software auf der physischen Maschine

Traditionell wurde eine Software mit dem Ziel erschaffen, auf einer physischen Maschine ausgeführt zu werden. Dabei stoßen wir schnell an Grenzen. Eine Software läuft ggf. nur auf einer bestimmten Hardware, benötigt z. B. einen bestimmten Prozessor.

Ferner läuft komplexere Software meist nicht komplett autark, sondern in ein Software-Ökosystem eingebunden: Dazu gehören Betriebssystem, Bibliotheken und Abhängigkeiten. Damit das Zusammenspiel funktioniert, müssen alle Komponenten in den richtigen Versionen vorliegen. Hinzu kommt die Konfiguration, die beschreibt, wie die einzelnen Komponenten untereinander verknüpft sind.

Möchte man mehrere Anwendungen auf einer Maschine parallel betreiben, ergeben sich schnell Versionskonflikte. Ggf. benötigt eine Anwendung eine Version einer Komponente, die mit einer anderen Anwendung nicht kompatibel ist. Im schlimmsten Fall müsste jede Anwendung auf einer eigenen physischen Maschine betrieben werden. Dabei gilt: Physische Maschinen sind teuer und lassen sich nicht einfach skalieren. Wächst also der Ressourcen-Bedarf einer Anwendung, muss diese unter Umständen auf eine neue physische Maschine migriert werden.

Ein weiteres Problem ergibt sich aus dem Umstand, dass Software in der Entwicklung in verschiedenen Umgebungen eingesetzt wird. Ein Entwickler schreibt Code auf dem lokalen System und führt diesen dort zum Testen aus. Die Anwendung durchläuft vor dem Produktiveinsatz mehrere Stufen. Dazu gehören z. B. eine Test-Umgebung zur Qualitätssicherung oder eine Staging-Umgebung für Tests durch das Produkt-Team.

Die verschiedenen Umgebungen existieren oft auf verschiedenen physischen Maschinen. Fast immer gibt es Unterschiede in den Versionen von Betriebssystem, Bibliotheken und Konfiguration. Wie diese alle in Einklang bringen? Denn unterscheiden sich die Umgebungen voneinander, sind Tests wenig aussagekräftig. Ferner muss bei Ausfall eines Systems selbiges ersetzt werden – wie dabei die Konsistenz sicherstellen? Mit physischen Maschinen ist diesen Problemen nur schwer Herr zu werden.

Virtuelle Maschinen als Schritt in die richtige Richtung

Aus der beschriebenen Problematik beim Einsatz physischer Maschinen entsprang die Beliebtheit virtueller Maschinen (VMs). Die Grundidee besteht darin, eine Schicht zwischen Hardware und Betriebssystem bzw. Host-Betriebssystem und Gast-Betriebssystemen einzubinden. Eine VM entkoppelt die Anwendungsumgebung von der darunterliegenden Hardware. Die spezifische Kombination von Betriebssystem, Anwendung, Bibliotheken und Konfiguration ist aus einem Image reproduzierbar. Neben der kompletten Isolation einer Anwendung erlaubt dies das Bündeln mehrerer Anwendungen in einer sogenannten Appliance.

VM-Images lassen sich zwischen physischen Maschinen bewegen, zudem können mehrere virtualisierte Betriebssysteme parallel betrieben werden. Damit ist die Skalierbarkeit der Anwendung sichergestellt. Jedoch ist die Betriebssystem-Virtualisierung ressourcenintensiv und bedeutet für simple Anwendungsfälle einen Overkill.

Die Vorteile der Container-Virtualisierung mit Docker

Die bei der Container-Virtualisierung genutzten Images kommen ohne Betriebssystem aus. Die Container-Virtualisierung ist leichtgewichtiger und bietet annähernd denselben Grad an Isolierung wie VMs. Ein Container-Image vereint Anwendungscode mit allen benötigten Abhängigkeiten und der Konfiguration. Images sind zwischen Systemen portabel, die darauf aufbauenden Container reproduzierbar. Container lassen sich in verschiedenen Umgebungen wie Entwicklung, Produktion, Test und Staging einsetzen. Mit der Layer- und Image-Versionskontrolle ist zudem ein hoher Grad an Modularität gegeben.

Fassen wir die wichtigsten Vorteile der Docker-basierten Virtualisierung von Anwendungen im Gegensatz zum Einsatz einer VM zusammen. Ein Docker-Container:

  • enthält kein eigenes Betriebssystem und keine simulierte Hardware
  • teilt sich einen Betriebssystem-Kernel mit anderen, auf demselben System gehosteten Containern
  • ist im Vergleich zu einer VM-basierten Anwendung in Bezug auf Ressourcen-Nutzung leichtgewichtig und kompakt
  • startet schneller als eine virtuelle Maschine
  • kann in mehreren Instanzen desselben Images parallel betrieben werden
  • lässt sich über Orchestrierung im Verbund mit weiteren containerbasierten Services nutzen
  • eignet sich optimal für die lokale Entwicklung