Docker-Container
Die Virtualisierungs-Lösung Docker hat im Verlauf des letzten Jahrzehnts grundlegend verändert, wie Software gebaut, verteilt und betrieben wird. Anders als bei den zuvor etablierten virtuellen Maschinen (VM) werden mit Docker einzelne Anwendungen virtualisiert. Bei einem Docker-Container handelt es sich also um einen Anwendungs- oder Software-Container.
Der Begriff des Software-Containers ist angelehnt an physische Container, wie sie auf Schiffen eingesetzt werden. In der Logistik haben Container als standardisierte Einheit die modernen Handelsketten erst ermöglicht. So lässt sich ein Container auf jedem dafür ausgelegten Schiff, Lkw oder Zug befördern. Dies funktioniert weitgehend unabhängig vom Inhalt des Containers. Nach außen hin ist der Container mit standardisierten Schnittstellen versehen. Ganz ähnlich ist dies auch bei Docker-Containern der Fall.
- Inklusive Wildcard-SSL-Zertifikat
- Inklusive Domain Lock
- Inklusive 2 GB E-Mail-Postfach
Was ist ein Docker-Container?
Worum genau handelt es sich nun bei einem Docker-Container? Lassen wir dafür die Docker-Entwickler zu Wort kommen:
„Container sind eine standardisierte Einheit von Software, die es Entwicklern erlaubt, ihre App von ihrer Umgebung zu isolieren.“ (Übersetzung: IONOS)
Originalzitat: „Containers are a standardized unit of software that allows developers to isolate their app from its environment.“ – Quelle: https://www.docker.com/why-docker
Anders als ein physischer Container existiert ein Docker-Container im virtuellen Umfeld. Ein physischer Container wird einer standardisierten Spezifikation folgend zusammengebaut. Bei virtuellen Containern finden wir ein ähnliches Muster vor: ein Docker-Container wird aus einer unveränderlichen Vorlage, dem sogenannten Image, erzeugt. Ein Docker-Image enthält die zur Erzeugung eines Containers notwendigen Abhängigkeiten und Konfigurationseinstellungen.
So wie viele physische Container auf eine einzige Spezifikation zurückzuführen sind, lassen sich beliebig viele Docker-Container aus einem Image erzeugen. Damit bilden Docker-Container die Basis für skalierbare Dienste und reproduzierbare Anwendungsumgebungen. Wir können einen Container aus einem Image erzeugen sowie einen existierenden Container in einem neuen Image speichern. Innerhalb des Containers lassen sich Prozesse ausführen, pausieren und beenden.
Anders als bei der Virtualisierung mit virtuellen Maschinen (VM) enthält ein Docker-Container kein eigenes Betriebssystem („Operating System“, OS). Stattdessen greifen alle auf einem Docker-Host laufenden Container auf denselben OS-Kernel zu. Beim Einsatz von Docker auf einem Linux-Host wird der vorhandene Linux-Kernel genutzt. Läuft die Docker-Software auf einem Nicht-Linux-System, kommt ein minimales Linux-Systemabbild via Hypervisor oder virtueller Maschine zum Einsatz.
Jedem Container wird bei der Ausführung eine gewisse Menge an Systemressourcen zugewiesen. Dazu gehören Arbeitsspeicher, CPU-Kerne, Massenspeicher und (virtuelle) Netzwerkgeräte. Technisch wird der Zugriff eines Docker-Container auf Systemressourcen über die sogenannten cgroups („Control Groups“) limitiert. Zum Partitionieren der Kernel-Ressourcen und zur Abgrenzung der Prozesse untereinander kommen sogenannte Kernel-Namespaces zum Einsatz.
Nach außen hin kommunizieren Docker-Container über das Netzwerk. Dafür werden Ports freigeschaltet, auf denen spezifische Dienste lauschen. Oft handelt es sich dabei um Web- oder Datenbankserver. Die Container selbst werden auf dem jeweiligen Docker-Host über die Docker-API gesteuert. Dabei lassen sich Container u. a. starten, stoppen und entfernen. Der Docker-Client stellt ein Kommandozeilen-Interface („Command Line Interface“, CLI) mit den entsprechenden Befehlen bereit.
Wie unterscheiden sich Docker-Container und Docker-Image?
Die Dualität der Begriffe Docker-Container und Docker-Image sorgt oft für Verwirrung. Dies ist wenig überraschend, handelt es sich doch um ein Henne-Ei-Problem: Ein Container wird aus einem Image erzeugt; jedoch lässt sich ein Container auch als neues Image speichern. Schauen wir uns die Unterschiede zwischen den beiden Konzepten im Detail an.
Ein Docker-Image ist eine inerte Vorlage. Das Image belegt lediglich etwas Platz auf der Festplatte und macht ansonsten nichts. Hingegen handelt es sich beim Docker-Container um eine „lebendige“ Instanz. Ein laufender Docker-Container hat ein Verhalten – der Container interagiert mit der Umgebung. Ferner hat ein Container einen Zustand, der sich über die Zeit ändert und dabei eine variable Menge an Arbeitsspeicher belegt.
Vielleicht sind Ihnen die Konzepte „Klasse“ und „Objekt“ aus der objektorientierten Programmierung (OOP) bekannt. Die Beziehung zwischen Docker-Container und Docker-Image ist in etwa vergleichbar mit der Beziehung zwischen Objekt und dazugehöriger Klasse: Eine Klasse liegt nur einmal vor; daraus lassen sich mehrere gleichartige Objekte erzeugen. Die Klasse selbst wird aus einer Quelltext-Datei geladen. Im Docker-Universum findet sich ein ähnliches Muster: Aus einer Quelltext-Einheit, dem „Dockerfile“, wird eine Vorlage erzeugt, aus der wiederum viele Instanzen entstehen:
Quelltext | Vorlage | Instanz | |
Docker-Konzept | Dockerfile | Docker-Image | Docker-Container |
Programmier-Analogie | Klassen-Quelltext | geladene Klasse | instanziiertes Objekt |
Wir bezeichnen den Docker-Container als „laufende Instanz“ des dazugehörigen Images. Dabei sind die Begriffe „Instanz“ und „Instanziieren“ abstrakt. Falls Sie damit nicht viel anfangen können, nutzen Sie eine Eselsbrücke. Ersetzen Sie mental „instanziieren“ durch „stanzen“. Auch wenn keine Verwandtschaft zwischen den Worten besteht, gibt es auf die Informatik bezogen eine hohe Übereinstimmung der Bedeutungen. Stellen Sie sich das Prinzip so vor: Wie wir mit einer Keksform viele gleichartige Kekse aus einer Lage Teig stanzen, instanziieren wir aus einer Vorlage viele gleichartige Objekte. Instanziieren ist also der Zeitpunkt, an dem ein Objekt durch eine Vorlage erzeugt wird.
Wie ist ein Docker-Container aufgebaut?
Um zu verstehen, wie ein Docker-Container aufgebaut ist, hilft ein Blick auf die „Zwölf-Faktor-App“-Methodologie. Dabei handelt es sich um eine Sammlung von zwölf grundlegenden Prinzipien für Aufbau und Betrieb serviceorientierter Software. Sowohl Docker als auch die Zwölf-Faktor-App stammen aus dem Jahr 2011. Die Zwölf-Faktoren-App unterstützt Entwickler dabei, Software-as-a-Service-Apps nach bestimmten Standards zu gestalten. Dazu zählen u. a.:
- Deklarative Formate nutzen, die die Konfiguration automatisieren und neu beteiligten Entwicklern Zeit und Kosten ersparen
- Zugrundeliegendes Betriebssystem beachten und maximale Portierbarkeit zwischen Ausführungsebenen gewährleisten
- Deployment auf Cloud-Plattformen bevorzugen (Server und Serveradministration vermeiden)
- Einheitliche Entwicklung und Produktion, um agiles Continuous Deployment zu ermöglichen
- Skalierbarkeit, ohne dass Tooling, Architektur oder Entwicklungsverfahren geändert werden müssen
Der Aufbau eines Docker-Containers orientiert sich an diesen Grundsätzen. Ein Docker-Container umfasst die folgenden Komponenten, die wir uns nachfolgend im Detail anschauen:
- Container-Betriebssystem und Union-Dateisystem
- Software-Komponenten und -Konfiguration
- Umgebungsvariablen und Laufzeit-Konfiguration
- Ports und Volumen
- Prozesse und Logs
Container-Betriebssystem und Union-Dateisystem
Im Unterschied zu den virtuellen Maschinen enthält ein Docker-Container kein eigenes Betriebssystem. Stattdessen greifen alle auf einem Docker-Host laufenden Container auf einen geteilten Linux-Kernel zu. Im Container ist lediglich eine minimale Ausführungsschicht enthalten. Dazu gehören für gewöhnlich eine Implementierung der C-Standard-Bibliothek sowie eine Linux-Shell zum Ausführen von Prozessen. Hier eine Übersicht der Komponenten des offiziellen „Alpine-Linux“-Image:
Linux Kernel | C-Standard-Bibliothek | Unix-Kommandos |
vom Host | musl libc | BusyBox |
Ein Docker-Image besteht aus einem Stapel schreibgeschützter Dateisystem-Schichten, zu Englisch „Layers“. Ein Layer beschreibt die Änderungen am Dateisystem zum darunter befindlichen Layer. Über ein spezielles Union-Dateisystem wie overlay2 werden die Layers überlagert und zu einer konsistenten Oberfläche vereint. Beim Erzeugen eines Docker-Containers aus einem Image wird den schreibgeschützten Layers ein weiterer, beschreibbarer Layer hinzugefügt. Alle Änderungen am Dateisystem werden per „Copy-on-Write“-Verfahren in den beschreibbaren Layer aufgenommen.
Software-Komponenten und -Konfiguration
Aufbauend auf dem minimalen Container-Betriebssystem werden in einem Docker-Container weitere Software-Komponenten installiert. Für gewöhnlich folgen dann weitere Einrichtungs- und Konfigurationsschritte. Zur Installation kommen die gebräuchlichen Wege zum Einsatz:
- per System-Paketmanager wie apt, apk, yum, brew etc.
- per Programmiersprachen-Paketmanager wie pip, npm, composer, gem, cargo etc.
- via Kompilation im Container mit make, mvn etc.
Hier einige Beispiele für häufig in Docker-Containern eingesetzte Software-Komponenten:
Einsatzgebiet | Software-Komponenten |
Programmiersprachen | PHP, Python, Ruby, Java, JavaScript |
Entwicklungs-Tools | node / npm, React, Laravel |
Datenbank-Systeme | MySQL, Postgres, MongoDB, Redis |
Webserver | Apache, nginx, lighttpd |
Caches und Proxies | Varnish, Squid |
Content-Management-Systeme | WordPress, Magento, Ruby on Rails |
Umgebungsvariablen und Laufzeit-Konfiguration
Der Zwölf-Faktoren-App-Methodologie folgend wird die Konfiguration eines Docker-Containers in Umgebungsvariablen, sogenannten Env-Vars („Environment Variables“), gespeichert. Dabei verstehen wir unter Konfiguration alle Werte, die sich zwischen den verschiedenen Umgebungen, etwa Entwicklungs- vs. Produktionssystem, ändern. Oft gehören dazu Hostnamen und Datenbank-Credentials.
Die Werte der Umgebungsvariablen beeinflussen das Verhalten des Containers. Um Umgebungsvariablen innerhalb eines Containers verfügbar zu machen, kommen zwei primäre Wege zum Einsatz:
- Definition in Dockerfile
Im Dockerfile wird mit der ENV-Anweisung eine Umgebungsvariable deklariert. Dabei lässt sich ein optionaler Default-Wert vergeben. Dieser kommt zum Tragen, falls die Umgebungsvariable beim Starten des Containers leer ist.
- Übergeben beim Starten des Containers
Um auf eine Umgebungsvariable im Container zuzugreifen, die nicht im Dockerfile deklariert wurde, übergeben wir die Variable beim Starten des Containers. Dies funktioniert für einzelne Variablen per Kommandozeilen-Parameter. Ferner lässt sich eine sogenannte Env-Datei übergeben, die mehrere Umgebungsvariablen samt deren Werten definiert.
Hier das Muster, um eine Umgebungsvariable beim Starten des Containers zu übergeben:
docker run --env <env-var> <image-id>
Bei vielen Umgebungsvariablen bietet es sich an, eine Env-Datei zu übergeben:
docker run --env-file /path/to/.env <image-id>
Mit Hilfe des 'docker inspect'-Befehls lassen sich die im Container vorhandenen Umgebungsvariablen samt deren Werten anzeigen. Daher gilt Vorsicht bei der Nutzung geheimer Daten in Umgebungsvariablen.
Beim Starten eines Containers aus einem Image lassen sich Konfigurations-Parameter übergeben. Dazu gehören u. a. die Menge zugewiesener Systemressourcen, die ansonsten unlimitiert sind. Ferner kommen Start-Parameter zum Einsatz, um Ports und Volumen für den Container zu definieren – mehr dazu im nächsten Abschnitt. Die Start-Parameter können evtl. im Dockerfile voreingestellte Werte überschreiben. Nachfolgend einige Beispiele.
Docker-Container beim Start einen CPU-Kern und 10 Megabyte Speicher zuweisen:
docker run --cpus="1" --memory="10m" <image-id>
In Dockerfile definierte Ports beim Starten des Containers freischalten:
docker run -P <image-id>
TCP-Port 80 des Docker-Hosts auf Port 80 des Docker-Containers mappen:
docker run -p 80:80/tcp <image-id>
Ports und Volumen
Ein Docker-Container enthält eine von der Außenwelt isolierte Anwendung. Damit dies nützlich ist, muss die Interaktion mit der Umgebung möglich sein. Daher gibt es Wege, Daten zwischen Host und Container sowie zwischen mehreren Containern auszutauschen. Standardisierte Schnittstellen erlauben dabei den Einsatz eines Containers in unterschiedlichen Umgebungen.
Die Kommunikation mit im Container laufenden Prozessen von der Außenseite läuft über freigeschaltete Netzwerk-Ports. Hierbei kommen die Standardprotokolle TCP und UDP zum Einsatz. Stellen wir uns als Beispiel einen Docker-Container vor, der einen Webserver enthält; dieser lauscht auf TCP-Port 8080. Ferner enthält das Dockerfile des Docker-Images die Zeile 'EXPOSE 8080/tcp'. Wir starten den Container mit 'docker run -P' und greifen unter der Adresse 'http://localhost:8080' auf den Webserver zu.
Ports dienen zur Kommunikation mit im Container laufenden Diensten. In vielen Fällen ist es jedoch sinnvoll, eine zwischen dem Container und dem Host-System geteilte Datei zum Datenaustausch zu nutzen. Zu diesem Zweck kennt Docker verschiedene Typen von Volumen:
- Benannte Volumen – empfohlen
- Anonyme Volumen – gehen beim Entfernen des Containers verloren
- Bind Mounts – historisch bedingt und nicht empfohlen; performant
- Tmpfs Mounts – liegen im Arbeitsspeicher; nur unter Linux
Die Unterschiede zwischen den Volumentypen sind subtil; die Wahl des passenden Typs hängt stark vom jeweiligen Einsatzszenario ab. Eine detaillierte Beschreibung würde den Rahmen dieses Artikels sprengen.
Prozesse und Logs
Ein Docker-Container kapselt für gewöhnlich eine Anwendung oder einen Dienst. Die innerhalb des Containers ausgeführte Software bildet eine Menge laufender Prozesse. Die Prozesse eines Docker-Containers sind isoliert von Prozessen anderer Container oder des Host-Systems. Innerhalb eines Docker-Containers lassen sich Prozesse starten, stoppen und auflisten. Die Steuerung erfolgt über die Kommandozeile, bzw. über die Docker-API.
Laufende Prozesse geben kontinuierlich Statusinformationen aus. Der Zwölf-Faktoren-App-Methodologie folgend werden zur Ausgabe die Standard-Datenströme STDOUT und STDERR verwendet. Die Ausgabe auf diese beiden Datenströme lässt sich mit dem 'docker logs'-Kommando auslesen. Alternativ kann ein sogenannter Logging-Treiber verwendet werden. Der Standard-Logging-Treiber schreibt Logs im JSON-Format.
Wie und wo werden Docker-Container verwendet?
Docker kommt heutzutage in allen Bereichen des Software-Lifecycles zum Einsatz. Dazu gehören Entwicklung, Test und Betrieb. Die auf einem Docker-Host laufenden Container werden über die Docker-API gesteuert. Der Docker-Client nimmt Befehle auf der Kommandozeile entgegen; zur Steuerung von Verbünden aus Docker-Containern kommen spezielle Orchestrierungs-Tools zum Einsatz.
Das grundlegende Muster beim Einsatz von Docker-Containern sieht folgendermaßen aus:
- Docker-Host bezieht Docker-Image von Registry.
- Docker-Container wird aus Image erzeugt und gestartet.
- Im Container enthaltene Anwendung läuft, bis der Container gestoppt oder entfernt wird.
Schauen wir uns zwei Beispiele für den Einsatz von Docker-Containern an:
Einsatz von Docker-Containern in der lokalen Entwicklungsumgebung
Besonders beliebt ist der Einsatz von Docker-Containern in der Software-Entwicklung. Für gewöhnlich wird eine Software von einem Team von Spezialisten entwickelt. Dabei kommt eine als Toolchain (zu Deutsch: „Werkzeugkette“) bezeichnete Sammlung von Entwicklungs-Tools zum Einsatz. Jedes Tool liegt in einer spezifischen Version vor, und die ganze Kette funktioniert nur dann, wenn die Versionen untereinander kompatibel sind. Ferner muss die Konfiguration der Tools stimmen.
Um sicherzustellen, dass die Konsistenz der Entwicklungsumgebung gegeben ist, greift man auf Docker zurück. Es wird einmalig ein Docker-Image erstellt, das die gesamte Toolchain korrekt konfiguriert enthält. Jeder Entwickler des Teams zieht sich das Docker-Image auf die lokale Maschine und startet daraus einen Container. Die Entwicklung erfolgt dann innerhalb des Containers. Ergibt sich eine Änderung an der Toolchain, wird das Image zentral aktualisiert.
Einsatz von Docker-Containern in orchestrierten Verbünden
In Datencentern von Hosting-Providern und PaaS-Anbietern („Platform-as-a-Service“) kommen Verbünde von Docker-Containern zum Einsatz. Load-Balancer, Webserver, Datenbankserver etc.: Jeder Dienst läuft in einem eigenen Docker-Container. Dabei kann ein einzelner Container nur eine gewisse Last stemmen. Docker-Tools überwachen die Container sowie deren Auslastung und Zustand. Bei steigender Last startet der Orchestrator zusätzliche Container. Dieser Ansatz erlaubt die schnelle Skalierung von Diensten als Reaktion auf sich ändernde Bedingungen.
Vorteile und Nachteile der Docker-Container-Virtualisierung
Die Vorteile der Virtualisierung mit Docker sind insbesondere mit Hinsicht auf den Einsatz virtueller Maschinen (VM) zu betrachten. Im Vergleich zu VMs sind Docker-Container deutlich leichtgewichtiger. Sie lassen sich schneller starten und verbrauchen weniger Ressourcen. Auch die den Docker-Containern zugrundeliegenden Images sind um Größenordnungen kleiner: Während VM-Images für gewöhnlich hunderte MB bis einige GB groß sind, beginnen Docker-Images bereits bei wenigen MB.
Jedoch hat die Container-Virtualisierung mit Docker auch einige Nachteile. Da ein Container kein eigenes Betriebssystem enthält, ist die Isolierung der darin laufenden Prozesse nicht ganz perfekt. Beim Einsatz großer Mengen von Containern ergibt sich ein hoher Grad an Komplexität. Ferner handelt es sich bei Docker um ein gewachsenes System. Mittlerweile macht die Docker-Plattform zu viel. Daher werden verstärkt Anstrengungen unternommen, die einzelnen Komponenten aufzuspalten.