Dies ist der erste Teil einer Tutorial-Reihe zur Container-Lösung Docker. Hier erklären wir, was Docker ist, wofür es sich sinnvoll einsetzen lässt und wie Sie es verwenden. Der Workshop basiert auf der aktuellen Docker-Version 1.3.

Im Prinzip ist Docker alter Wein in neuen Schläuchen. Schon lange gibt es auf Unix-Systemen Runtime-Umgebungen, die von dem Host-System abgekapselt sind, auf dem sie laufen. Selbst der gute alte Chroot funktioniert so ähnlich; fortgeschrittenere Lösungen dafür sind die Jails von BSD oder komplexe Lösungen wie Virtuozzo oder OpenVZ. Der Sinn der meisten solchen Ansätze ist es, die in solchen Umgebungen laufenden Anwendungen vom Host-System und voneinander abzukapseln und dabei möglichst wenig Ressourcen zu verbrauchen - also eine Art VM light.

Ähnlich funktioneren auch die Linux Container LXC, die auf die im Linux-Kernel eeingeführten Features Cgroups und Namespaces zurückgreifen. Cgroups dienen der Ressourcenkontrolle, sodass ein Container nicht zuviel Rechenzeit und Speicher vom Hostsystem beansprucht. Die Namespaces helfen dabei, den Blick im Container künstlich einzuschränken, sodass jeder Anwender im Container nur seine eigenen Dinge sieht.

Ähnlich funktioniert auch Docker, das auf die gleichen Kernel-Features zurückgreift, aber seine Popularität hat andere Gründe: Docker hat eine Infrastruktur für den Umgang mit Container-Images geschaffen, die es ermöglicht, leicht Images von anderen Anwendern aus einem Repository zu laden und sie für eigene Zwecke anzupassen. Besonders trickreich ist dabei der schichtweise Aufbau der Images, der mit Hilfe von Overlay-Dateisystemen einen schreibbaren Layer auf die bereits vorhandenen Schichten legt und somit Funktionalität hinzufügt.

Anfangs hat Docker hierzu AUFS verwendet, das aber nicht Teil es Mainline-Kernels ist und deshalb nicht auf allen Linux-Distributionen verfügbar war. Heute unterstützt Docker neben AUFS zu diesem Zweck auch Btrfs, ZFS, Device Mapper, VFS und OverlayFS(2). OverlayFS2 steht nur auf neueren Kerneln zur Verfügung, als gute Wahl gilt derzeit OverlayFS, das etwa auf CentOS auch als Default eingestellt ist. Die Komponente, die sich um die Verwaltung des Schichten-Dateisystems kümmert, heißt im Docker-Jargon “storage driver”, früher “graphdriver”.

Docker wird deshalb meist weniger als Virtualisierungslösung gehandelt, denn als neue Methode einfach und unkompliziert Anwendungen mit allen ihren Abhängigkeiten auszuliefern, eben als Image, das anschließend als Container zum Laufen gebracht wird. Zudem ist es aufgrund der nur rudimentär vorhandenen Isolation per Namespaces und Cgroups keine Containerlösung, die hohe Sicherheit garantiert. Daran, dies auszubauen, wird fieberhaft gearbeitet. Bis dato gilt die Empfehlung, Docker-Container mit AppArmor oder SELinux noch weiter einzuschränken.

Docker ist noch lange nicht fertig. Wieso es die Entwickler für nötig gehalten haben, ihre Software mit der Versionsnummer 1.0 auszuzeichnen, wo sich doch selbst grundlegende Dinge laufend ändern, von der Stabilität mal ganz abgesehen, bleib ihr Geheimnis. Die Antwort lautet vermutlich: Marketing.

Jedenfalls ist vor kurzem Version 1.13 erschienen und hat wieder einmal einiges über den Haufen geworfen. Zum Beispiel das Commandline-Interface. Im Prinzip ist das eine gute Sache, denn es ist nun besser strukturiert. So folgt nun nach dem Docker-Befehl zuerst die Domäne, auf die sich der danach folgende Befehl bezieh. Um installierte Images aufzulisten, verwenden Sie also:

docker image ls

Analog funktioniert das mit Containern:

docker container ls

Die alten Subbefehle list funktionieren auch noch, werden aber vielleicht irgendwann abgeschafft. Auch docker ps zeigt noch die laufenden Container an - dieser Befehl war bisher Standard und ist noch in vielen Tutorials und bestimmt auch in der Docker-Dokumentation zu finden.

Auch das Universum rund um Docker expandiert ständig: Da sich Docker-Container auch übers Netzwerk zu komplexen Anwendungen verschalten lassen, gibt es schon eine Vielzahl von “Orchestrierungslösungen”, um solche Container-Cluster zu managen.

Doch zurück zur Praxis und damit zur Docker-Installation. Für die gängigen Linux-Distributionen gibt es Pakete respektive Paketquellen auf der Docker-Homepage, die Sie einbinden und dann das neueste stabile Docker mit dem Paketmanager installieren. Auch die Distributoren liefern in ihren Repositories Docker aus, aber das sind meisten sehr alte Versionen, die Sie nicht verwenden sollten (Docker entwickelt sich rasend schnell weiter).

Zumindest auf Ubuntu ist es halbwegs gut möglich, von der Distro-Version auf die von Docker gelieferte Software-Version zu wechseln. Auf Red Hat und somit CentOS funktioniert das nicht, da Red Hat ein eigenes Docker mit einer Reihe von Änderungen vertreibt. Wenn Sie also da s distributionseigene Docker installiert haben, müssen Sie die installierten Images und so weiter wegschmeißen. Am besten Sie fangen noch einmal von vorne an.

Laden Sie also etwa unter CentOS von der Docker-Downloadseite die Repository-Infos herunter und speichern sie unter /etc/yum.repos.d/docker.repo. Analog funktioniert das mit Fedora, Debian (7.7, 8.0, Testing) und Ubuntu (14.04, 16.04, 16.10). Auch für Windows und macOS gibt es Docker-Pakete, die zusätzlich zur Container-Umgebung noch VMs verwenden, in denen ein Container-Linux läuft. Die Windows-Version setzt Hyper-V voraus (also 64 Bit Win 10 Pro etc.), die macOS-Version bringt eine angepasste Variante des XHyve-Hypervisors mit.

Ist Docker installiert, läuft (auf Linux) im Hintergrund ein Daemon, der vom Commandline-Tool docker seine Befehle entgegennimmt, normalerweise auf dem lokalen Rechner über einen Unix-Socket. Der Docker-Daemon wird über das Init-System der Distribution gestartet, auf CentOS 7 und Ubuntu 16.10 also per Systemd:

 # systemctl start docker
 # ps auxw | grep docker
 root     13356  4.0  0.1 572328 37132 ?        Ssl  16:40   1:20 /usr/bin/dockerd
 root     13366  0.0  0.0 571056  8728 ?        Ssl  16:40   0:01 docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --shim docker-containerd-shim --runtime docker-runc

Wie man sieht, läuft nicht nur der Docker-Daemon, sondern noch ein zweiter Daemon, der wiederum mit dem Docker-Daemon kommuniziert. Der Grund dafür ist, dass Docker immer stärker modularisiert wird. So griff Docker am Anfang auf LXC zurück, um damit über Cgroups und Namespaces Container zu erzeugen. In einer späteren Version wurde diese Funktion in der Libcontainer selbst implementiert. Als sich dann bei der Linux Foundation Arbeitsgruppen zur Standardisierung von Containern gründete, lagerte Docker sein Container-Runtime-Interface “runc” aus und spendierte es der Open Container Initiative.

Docker lässt sich ohne Root-Rechte als gewöhnlicher User verwenden, wenn dieser der Gruppe “docker” angehört. Mit dem Befehl docker info zeigen Sie die Basisinformation der Docker-Installation an, die etwa den verwendeten Storage Driver und das verwendete Runtime-Plugin (eben runc) anzeigt:

$ docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 1
Server Version: 1.13.0
Storage Driver: overlay
 Backing Filesystem: xfs
 Supports d_type: false
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins: 
 Volume: local
 Network: bridge host macvlan null overlay
Swarm: inactive
Runtimes: runc
Default Runtime: runc
...

Wie erwähnt ist ein Grund für den Erfolg von Docker das gut gefüllte Repository, in dem sich Images von beinahe jeder freien Software finden - und die mehr oder weniger gut gepflegt werden. Ein Beispiel sind die MySQL-Images, die von Oracle selbst gebaut werden und die in den Versionen 5.5 bis 8.0 vorliegen. Um die Funktion von Docker zu demonstrieren, soll nur ein einfaches Image installiert werden, das den Umgang mit Images und Containern demonstriert, die minimalistische Linux-Distribution Alpine. Das Image laden Sie aus dem Repository mit dem folgenden Befehl herunter:

docker image pull alpine

(Es funktioniert auch noch der alte Aufruf docker pull ..., aber in der neuen Version ist klarer, worum es geht.) Da das Image nur ein paar MByte groß ist, ist der Download schnell erledigt. Ein Aufruf von docker image ls zeigt, dass das Image nun im lokalen Repository vorhanden ist:

$ docker image ls 
REPOSITORY     TAG         IMAGE ID        CREATED             SIZE
alpine         latest      88e169ea8f46    6 weeks ago         3.98 MB

Nun können Sie von diesem Image einen Container starten:

docker container run -it alpine /bin/ash

Wenn Sie den Run-Befehl ausführen, ohne vorher das passende Image heruntergeladen zu haben, erledigt Docker das übrigens automatisch. Haben Sie den obigen Aufruf eingegeben, finden Sie sich in einer Root-Shell von Alpine Linux wieder. Dazu ist zu sagen, dass der Befehl nicht unbedingt die typische Art ist, einen Container starten, denn mit den Optionen “-i” und “-t” wird zum einen ein Pseudoterminal angelegt und zum anderen die Standardeingabe geöffnet, also mit anderen Worten der Container in einem interaktiven Modus gestartet. Typischerweise werden Container, die mit Anwendungen fertig konfektioniert sind, stattdessen mit “-d” im Hintergrund gestartet. Außerdem steht an letzter Stelle des Docker-Aufrufs noch der Befehl, der nach dem Start des Containers ausgeführt wird; das ist hier die Almquist Shell, die Standard-Shell von Alpine Linux. Typische Anwendungscontainer haben stattdessen das auszuführende Kommando fest konfiguriert.

Führen Sie nun in einem anderen Terminal docker container ls aus, sehen Sie den Alpine-Container:

docker container ls 
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
ac882c4e7173        alpine              "/bin/ash"          6 minutes ago       Up 6 minutes                            sleepy_sinoussi

An erster Stelle steht ein (abgekürzt wiedergegebener) Hash, der den Container eindeutig identifiziert. An letzter Stelle ist der besser lesbare Name zu sehen, den Docker automatisch vergibt. Stattdessen können Sie beim Start eines Container auch einen eigenen Namen vergeben:

docker container run --name alpine1 -it alpine /bin/ash

Wenn Sie nun in der Alpine-Shell mit ps die Prozesse anzeigen, sehen Sie neben dem ps-Befehl nur die Shell. Das ist eben, was innerhalb des Containers sichtbar ist. Auch im Dateisystem werden Sie nicht allzuviele Dateien finden. Allerdings ist dort das Proc-Dateisystem des auf dem Host-System(!) laufenden Kernels zu sehen, der auch als Kernel des Containers dient - nur eben einschränkt durch die Namespaces: So gibt es auch in “/proc” nur den einen Prozess “/bin/ash” mit der PID 1!

Über den Namen oder den Hash, die Sie mit docker container ls erfahren, stoppen Sie den Container wieder:

docker container stop alpine1

Alternativ würde in unserem Beispiel auch das Verlassen der Shell dazu führen, dass der Container beendet wird. Wenn Sie nun den Container mit dem Namen noch einmal starten wollen, bekommen Sie eine Fehlermeldung präsentiert:

docker: Error response from daemon: Conflict. The container name "/alpine1" is already in use by container 1c08e339d0742b463b02b9ddcf0c776f7badb0e7e44036151db8a9fa31dd4a7d. You have to remove (or rename) that container to be able to reuse that name..

Der Grund dafür ist, dass Container nicht automatisch gelöscht werden. Der Aufruf docker container ls -a zeigt alle vorhandenen Container an, auch die, die nicht mehr laufen. Sie löschen einen Container mit docker container rm ..., entweder gefolgt vom Namen oder dem Hash. Jetzt können Sie einen neuen Container mit dem alten Namen starten. Sie können den Vorgang auch automatisieren, also festlegen, dass Docker den Container nach dem Beenden löscht, wenn Sie ihn mit der Option “–rm” starten:

docker container run --name alpine1 -it --rm alpine /bin/ash

Generell gilt als Best Practice, dass Container am besten als flüchtig anzusehen sind, dass sie also jederzeit verworfen werden und aus dem Image neu gestartet werden können. Hier stellt sich natürlich die Frage, wie man hier sinnvoll mit User- oder Anwendungsdaten umgehen soll, aber dafür gibt es eine Reihe von Lösungen, auf die dieses Tutorial noch eingehen wird.

Analog zum Löschen eines Containers löschen Sie ein Image aus dem Repository mit docker image rm ..., gefolgt vom Hash oder von dem “Repository:Tag” (siehe oben die Ausgabe von docker image ls), mit dem das Image versehen ist. Im Beispiel von Alpine Linux sieht das so aus:

docker image rm alpine:latest 

Besonders praktisch sind auch die neuen Prune-Kommandos, die beim Aufräumen helfen. So löscht docker container prune alle nicht mehr laufenden Container, während docker image prune -a alle Images löscht, von denen es keine Container mehr gibt.

Zum Abschluss noch eine kleines Worst-Practice-Beispiel, das die Fähigkeiten von Docker demonstriert. Haben Sie einen Alpine-Container gestartet und sind in der Root-Shell gelandet, aktualisieren Sie zunächst mit apk update die Paketquellen. Mit apk info zeigen Sie die installierten Pakete an. Installieren Sie nun zum Beispiel mit apk add nginx den Nginx-Webserver. Nun wechseln Sie in ein anderes Terminal auf dem Docker-Host und geben den folgenden Befehl ein:

docker container commit alpine alpine-nginx:0.1

Wenn Sie jetzt mit docker image ls einen Blick in das lokale Repository werfen, finden Sie dort ihr erstes eigenes Image, das auf dem Alpine-Image basiert, aber zusätzlich den Nginx-Webserver enthält. Wie angedeutet, gilt dieser Weg nicht als Best Practice, da das interaktive Hinzufügen eines Pakets nur ein Ad-hoc-Verfahren ist, das nicht reproduzierbar und nicht dokumentiert ist. Um Docker-Images strukturiert zu erzeugen, hält Docker mit den sogenannten Dockerfiles einen besseren Weg bereit, mit denen sich dieses Tutorial noch ausführlich beschäftigen wird. Allerdings demonstriert unser Beispiel dennoch, wie sich Docker-Image schichtenweise zusammensetzen lassen.

Nun können Sie sich in Ruhe weiter mit den Docker-Befehlen beschäftigen und Container und Images anlegen und löschen – kaputtmachen können Sie ja nichts. docker help zeigt eine Übersicht der verfügbaren Befehle; docker Befehl help verrät mehr zu einem einzelnen Befehl.

In der kommenden Folge unseres Docker-Tutorials wird es darum gehen, wie man mit Volumes flüchtige Container mit dauerhaften Nutzerdaten verwendet.