Geht es um Docker, ist immer die Rede von Containern. Doch das ist in etwa so ausagekräftig, wie beim Programmieren von Objekten zu sprechen. "Containers don’t contain" liest man des öfteren im Internet, vor allem wenn die Rede auf die mangelnde Sicherheit von Containern kommt.
Im Gegensatz zu älteren Containerlösungen wie Virtuozzo und OpenVZ ist die Sicherheit eines Docker-Containers nicht das primärze Ziel. Jedenfalls bisher, denn mit stärkerer Verbreitung (vor allem im "Enterprise") steigt auch das Sicherheitsbewusstsein und die Forderung nach mehr Sicherheit wird lauter. Bisher gilt die Empfehlung, Docker-Container mit AppArmor oder SELinux einzuschränken.
Der Erfolg von Docker-Containern beruht auf einem anderen Aspekt: Docker hat eine Infrastruktur für Container-Images entwickelt, die es ermöglicht, Images von anderen Anwendern aus einem Repository zu laden und sie für eigene Zwecke anzupassen. Von Vorteil 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. Dies spart potenziell Platz bei der Übertragung übers Netz sowie beim Speicher im lokalen Respository und beschleunigt die Entwicklung eigener Images. Docker gilt deshalb weniger als Virtualisierungslösung gehandelt, denn als neuer Weg, einfach und unkompliziert Anwendungen samt ihren Abhängigkeiten auszuliefern.
Vor kurzem ist Docker 1.13 erschienen und hat einiges über den Haufen geworfen, zum Beispiel das Commandline-Interface, das nun besser strukturiert ist. 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. Die folgende Tabelle stellt die wichtigsten der alten und neuen Aufrufe gegenüber:
Table 1. Neue Befehlssyntax in Docker 1.13
früher | Docker 1.13 |
---|---|
docker attach |
docker container attach |
docker commit |
docker container commit |
docker exec |
docker container exec |
docker logs |
docker container logs |
docker inspect |
docker {container,image} inspect |
docker ps |
docker container ls |
docker rm |
docker container rm |
docker run |
docker container run |
docker start |
docker container start |
docker stop |
docker container stop |
docker top |
docker container top |
docker build |
docker image build |
docker history |
docker image history |
docker images |
docker image ls |
docker import |
docker image import |
docker pull |
docker image pull |
docker rmi |
docker image rm |
docker tag |
docker image tag |
docker deploy |
docker stack deploy |
docker events |
docker system events |
Um Docker zu installieren, greifen Sie am besten auf die Pakete der hinter der freien Software stehenden Firma zurück. Zwar liefern auch die Linux-Distributoren in ihren Repositories Docker aus, aber meist nur alte Versionen.
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 Befehle vom Commandline-Tool "docker" entgegennimmt. 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 Sie sehen, läuft nicht nur der Docker-Daemon, sondern noch ein zweiter Daemon, der 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 normaler User verwenden, wenn er der Gruppe "docker" angehört. Der Befehl "docker info" zeigt die Basisinformation der Docker-Installation an, die etwa den Storage Driver und das Runtime-Plugin (eben runc):
$ 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 beinahe jeder freien Software finden - die mehr oder weniger gut gepflegt werden. 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 starten Sie von diesem Image einen Container:
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.
Der obige Befehl ist nicht unbedingt die typische Art, einen Container starten, denn mit den Optionen -i und -t wird ein Pseudoterminal angelegt und die Standardeingabe geöffnet, also 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 in unserem Beispiel an letzter Stelle des Docker-Aufrufs noch der Befehl, der nach dem Start des Containers ausgeführt wird; das ist hier die Almquist Shell /bin/ash, die Standard-Shell von Alpine Linux. Anwendungscontainer haben normalerweise das auszuführende Kommando fest konfiguriert. Die obigen Aufrufoptionen werden allerdings typischerweise verwendet, um einen schon laufenden Container zu "betreten". Vorausgesetzt, es handelt sich um eine mehr oder weniger vollständige Linux-Distribution, dient dazu der folgenden Befehl:
docker container exec -it Containername/Hash /bin/bash
Doch zurück zu unserem Alpine-Beispiel: 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 (hier "sleepy_sinoussi"). Stattdessen können Sie beim Start eines Container einen eigenen Namen vergeben:
docker container run --name alpine1 -it alpine /bin/ash
Wenn Sie in der Alpine-Shell mit ps die Prozesse anzeigen, sehen Sie neben dem ps-Befehl nur die Shell. Die Prozesse des Host-Systems sind dank der Namespaces verborgen, auch wenn Container und Host den gleichen laufenden Kernel verwenden.
Ü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 Docker beendete Container aufbewahrt, sodass Sie sie später wieder starten können. Der Aufruf docker container ls -a zeigt alle – auch nicht mehr laufenden – Container an. Sie löschen einen Container mit docker container rm …, entweder gefolgt vom Namen oder dem Hash. Jetzt dürfen Sie einen neuen Container mit dem alten Namen starten. Sie können auch beim Start festlegen, dass Docker den Container nach dem Beenden löscht, indem Sie ihn mit der Option --rm starten:
docker container run --name alpine1 -it --rm alpine /bin/ash
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 Bad-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 mit apk add nginx den Nginx-Webserver, wechseln in ein anderes Terminal auf dem Docker-Host und geben den folgenden Befehl ein:
docker container commit alpine1 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 ist und den Weg zum Image nicht dokumentiert. Um Docker-Images strukturiert zu erzeugen, hält Docker mit den sogenannten Dockerfiles einen besseren Weg bereit, mit denen sich dieses Workshop noch ausführlich beschäftigen wird. Allerdings demonstriert das Beispiel bereits, wie sich Docker-Image schichtweise zusammensetzen.
Mehr Informationen über die verfügbaren Befehle gibt das Docker-Kommando selbst mit docker help; docker Befehl --help verrät mehr zu einem einzelnen Befehl. Wer die Bash-Shell verwendet, kann sich die Arbeit mit einem von Docker geschrieben Autocompletion-File vereinfachen.
Im Docker-Repository sind die meisten anderen Linux-Distributionen in unterschiedlichen Versionen vorhanden, die mit dem Docker-Befehl herunterladen und damit experimentieren können. Wie gesagt ist das nicht der typische Einsatzzweck für Container, aber auf der anderen Seite sind Container auf diese Weise eingesetzt auch eine gute und schnelle Alternative zu den typischen Wegwerf-VMs, um eben mal etwas auszuprobieren.
In der zweiten Folge unseres Docker-Tutorials geht es darum, wie man mit Volumes flüchtige Container mit dauerhaften Nutzerdaten verwendet.