Docker Grundlagen

https://oer-informatik.de/docker-grundlagen

tl/dr; (ca. 23 min Lesezeit): Container sind soetwas wie ein kleiner Bruder der virtuellen Maschinen: schlanker, agiler, wendiger. Das bekannteste Tool, um Container zu administrieren ist Docker. Dieses Tutorial beschäftigt sich mit den Grundlagen der Containernutzung mit Docker.

Einstieg: Hello World Docker

Container mit Docker bieten die Möglichkeit, Programme in isolierten Bereichen zu nutzen. Hierbei teilen sich alle Container einen gemeinsamen Kernel. Hierdurch sind zwar die Möglichkeiten beschränkter (z.B. können so keine unterschiedlichen OS virtualisiert werden), die Ansprüche an die Ressourcen sind jedoch deutlich geringer. Das unterscheidet Container-Virtualisierung (wie bei Docker) von Hypervisor-Virtualisierung (HyperV, ESXi, VMWare Workstation, Parallels…).

Die Container können auf Knopfdruck erzeugt werden und ermöglichen so innerhalb kurzer Zeit identische reproduzierbare Umgebungen unabhängig von den Gastsystemen zu erstellen.

Einen sehr guten Einstieg bietet die Website Play with Docker classroom, hier werden in einem kurzen Video die Vorteile und Eigenschaften von Containern erklärt. In mehreren Tutorials können dann direkt im Browser die erklärten Docker-Befehle ausprobiert werden.1.

Für ein einfaches “Hello World” Beispiel von Docker muss lediglich Docker installiert und gestartet sein.

Installation

Für Windows ist der einfachste Weg, Docker Desktop zu installieren. Docker Desktop ist zur privaten Nutzung kostenlos, für größere Unternehmen gibt es eigene Pakete. Die Anleitungen zur Installation von Docker Desktop finden sich hier: https://docs.docker.com/get-docker/

Windows

Um Docker unter Windows nutzen zu können, müssen als Voraussetzung die Features HyperV und WSL 2 aktiviert sein. Eine genaue Anleitung findet sich hier2.

  • Windows Subsystem für Linux (WSL 2) zu installieren und zu aktivieren:
  • Docker Desktop laden: https://hub.docker.com/editions/community/docker-ce-desktop-windows/

Endlich: Hello World

Zur Überprüfung kann im Terminal/Powershell/Bash nach der Version gefragt werden. Ich gebe bei Befehlen, die in der Host-Konsole laufen immer ein PS> vorneweg an (so erscheint es in der Powershell, die Befehle funktionieren in der Bash eines Linuxsystema aber genauso). Befehle, die in der Bash des Containers laufen werden mit CO:/$ annotiert.

Wenn sowohl der Client als auch der Server mit einer Versionsnummer antworten hat die Installation und das Starten des Docker-Services geklappt:

Client: Docker Engine - Community
 Cloud integration: 1.0.2
 Version:           19.03.13
...
Server: Docker Engine - Community
 Engine:
  Version:          19.03.13
...

Einen ersten Container startet der folgende Befehl:

Die Antwort von Docker sieht etwa so aus:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:18a657d0cc1c7d0678a3fbea8b7eb4918bba25968d3e1b0adebfa71caddbc346
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
...
Das Hello World ist geschafft!
Das Hello World ist geschafft!

Was passiert im Hintergrund des “Hello World”?

Es folgen noch ein paar Absätze Text, die sich in jedem Fall zu lesen lohnen (und die dort angegebenen Links ebenso). Das folgende UML-Sequenzdiagramm stellt die Nachrichten und Akteure dar, die bei obigem Befehl aktiv werden:

UML-Sequenzdiagramm zum Nachrichtenfluss zwischen User, DockerEngine und DockerRegistry
UML-Sequenzdiagramm zum Nachrichtenfluss zwischen User, DockerEngine und DockerRegistry

Was passiert dabei genau im Hintergrund?

  • das Programm docker wird aufgerufen

  • Docker versucht, einen Container des Images “hello-world” zu starten.

    • Image ist in diesem Fall eine Vorlage des Systemabbilds (ein Bauplan, in OOP-Sprache: eine Klasse)

    • Container eine lauffähige Version (eine konkrete Instanz dieser Vorlage, in OOP-Sprache: ein aus dem Bauplan erzeugtes konkret nutzbares Objekt)

  • Wenn das Image “hello-world” lokal noch nicht vorhanden ist, sucht Docker danach in veröffentlichten Quellen und lädt es (das lässt sich auch per docker image pull IMAGENAME vorneweg erledigen).

  • Aus dem geladenen Image wird schließlich eine Instanz (also ein Container) erstellt und dieser gestartet.

Im Gegensatz zu virtuellen Maschinen benötigen Container kein vollständiges Betriebssystem: Container nutzen den Kernel des Hosts-Systems - unter Windows und Mac OSX nutzen sie einen gemeinsamen Linux-Unterbau (z.B. das Windows Subsystem for Linux WSL).

Wenn etwas nicht klappt…

Docker bringt neue Abstraktionsschichten in das System. Dabei kann natürlich auch einiges schief gehen. Häufige Probleme sind:

  • Unter Windows: HyperV aktiv? WSL aktiv? Wurde Docker Desktop gestartet?

  • Unter Linux: ist die nötige Berechtigung des Users vorhanden?

    • System mit Root-Zugriff: sudo usermod -aG docker ${USER}
    • In Systemen ohne Root-Zugriff muss eine Gruppe “Docker” mit einem Passwort versehen werden

Einen ersten eigenen Server-Container erstellen

Wir können Container nutzen, wie eigene Linux-Maschinen. Als kleines Beispiel wollen wir einen Python-Webserver erstellen, der uns freundlich begrüßt, wenn wir ihn im Browser ansprechen.

Eine Ubuntu-Konsole lässt sich beispielsweise mit dem folgenden Befehl erzeugen:

Die allgemeine Form dieses Befehls lautet:

Im Einzelnen, was passiert hier:

Befehl / Argument Beschreibung
docker run Befehl, der einen Conatainer aus einem Image erstellt und ausführt
it Es soll ein interaktives Terminal des Containers geöffnet bleiben, dem wir weitere Befehel übergeben können
--name hellopython Der Container soll zukünftig über den Namen hellopython identifizierbar sein
-p 5000:5000 Der Port 5000 des Containers soll am Host-Rechner auch über den Port 5000 erreichbar sein (Damit wir unseren Webserver später mit http://localhost:5000 erreichen können)
ubuntu Name des genutzten Images (der Vorlage)
bash Programm, dass zu Beginn ausgeführt werden soll. Wir wollen ein Terminal öffnen (also die bash)

Als Ergebnis des Befehls öffnet sich eine Shell, die wir wie jede andere Linux-Shell nutzen können.

Programme in unseren Container nachinstallieren

In unserem Container läuft ein minimales Ubuntu (abhängig vom oben gewählten Image). Wir können daher die Ubuntu/Debian-Tools nutzen, um allerlei nachzuinstallieren. An allen Zeilen, die mit CO:/$ beginnen, befinden wir uns in der bash des Containers.

Wir benötigen für den Webserver: einen Editor (nano), Python, den Python Installer PIP und das Web-Framework Flask. Das geht mit den folgenden Befehlen, etwas Zeit und gutmütigen Proxy/Firewall-Einstellungen:

Es wird ein bisschen dauern, bis alls Programme geladen sind.

Ein kleines Python/Flask-Programm als Webserver

Jetzt benötigen wir noch ein bisschen Python-Quellcode - ein Server, der auf die Route “/” hören soll und antwortet. Dazu nutzen wir den eben installierten Editor nano:

Dieser Text lässt sich per Zwischenablage in nano einfügen (Markieren, kopieren und mir der rechten Maustaste in Nano einfügen.). Gespeichert wird die Datei per Strg-O, der Editor geschlossen per Strg-X.

Das waren die ganzen Vorbereitungen.

Das Python-Skritp starten

Wenn wir jetzt den Server mit folgendem Befehl starten:

Sollte im Browser unter http://localhost:5000/ bzw. in der Konsole per curl http://localhost:5000/ eine Antwort kommen.

Der Container ist aus dem Gastsystem per http://localhost:5000 erreichbar
Der Container ist aus dem Gastsystem per http://localhost:5000 erreichbar
StatusCode        : 200
StatusDescription : OK
Content           : Der Container lebt!

Container und Images administrieren

Wir haben ein paar Images geladen, ein paar Container gestartet. Ein guter Zeitpunkt, um ein paar Befehle kennenzulernen, wie wir diese administrieren können.

Vorhandene Containern und Images anzeigen, Container starten und stoppen

Welche Images, aus denen Container erzeugt werden können, wurden bislang geladen und sind lokal vorhanden?

Die Antwort sieht in etwa so aus:

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world         latest              feb5d9fea6a5        10 months ago       13.3kB
alpine              latest              a24bb4013296        5 months ago        5.57MB
ubuntu              latest              216c552ea5ba        7 days ago          77.8MB

Das "hello-world-Image sollte sich durch den Aufruf oben finden. Wir könnten daraus also einen neuen Container erzeugen, ohne das Image erneut aus dem Internet laden zu müssen. Mit 13,3 kB ist es auch nicht allzu groß.

Welche Container laufen denn gerade?

CONTAINER ID   IMAGE           COMMAND   CREATED       STATUS       PORTS                    NAMES
2ffd15b9189a   ubuntu          "bash"    3 hours ago   Up 3 hours   0.0.0.0:5000->5000/tcp   hellopython

In meinem Fall läuft noch der Container mit dem Webserver. Aber der “hello -world”-Container fehlt. Was ist da los? Die Antwort ist einfach: er läuft nicht mehr. Der hello-world-Container hat sich nach der Ausgabe des Texts auf der Konsole wieder beendet.

Um herauszufinden, welche inaktiven Container noch vorhanden sind, kann die Option -a angefügt werden. Weitere Optionen können mit der Option --help angezeigt werden.

Mit dieser Option taucht der hello-world-Container in der Liste auf:

CONTAINER ID   IMAGE           COMMAND             CREATED       STATUS                     PORTS                    NAMES
2ffd15b9189a   ubuntu          "bash"              3 hours ago   Up 3 hours                 0.0.0.0:5000->5000/tcp   hellopython
3f290a423ea9   hello-world     "/hello"            4 hours ago   Exited (0) 4 hours ago                              objective_swanson

Wir finden hier eine Reihe von wichtigen Informationen zum Container: Zur Identifizierung können wir Container ID und Name nutzen, zudem lässt sich der aktuelle Status ablesen. Auch unsere Portfreigabe findet sich in der Liste

Den laufenden Container können wir über den Namen oder die ContainerID ansprechen und stoppen. Bei der ContainerID reichen die ersten Zeichen - der Container muss nur eindeutig identifizierbar sein. Die ContainerID ist ein immer wieder neu erzeugter Hashwert - wenn Schritte Automatisiert werden sollen bietet sich also der Name an.

Nach dem Stoppen des Containers dürfte sich auch unter http://localhost:5000/ nichts mehr tun.

Allgemein werden Container mit folgendem Befehlt ausgeschaltet, bleiben aber vorhanden:

Entsprechend reaktiviert man abgeschaltete Container mit:

Den Webserver wieder starten: Kommandos / Programme in Containern ausführen

In meinem Fall startet der Befehl

zwar den Container. Das kann ich mit docker container ls -a sehen. Aber unser Webserver ist noch nicht aktiviert (unter http://localhost:5000 herrscht Stille). Wir müssen unser Script, das ja im Container liegt, von aussen starten. Dabei hilft der container exec Befehl:

Unser COMMAND zum Starten des Webservers oben war python3 hello_python.py. Der konkrete Befehl müsste also lauten:

Die Ausgabe lässt vermuten, dass unser Flask-Server wieder gestartet ist, der Test im Browser (http://localhost:5000) bestätigt das.

* Serving Flask app 'hello_python'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.17.0.2:5000
Press CTRL+C to quit

Der Webserver läuft. Aber das Terminal scheint blockiert. Auch nach Drücken von Ctrl-C läuft der Webserver weiter und ich bin wieder auf dem Gastsystem. Das ist ja schonmal nicht schlecht.

Das Python-Script verändern und andere interaktive Operationen im Container

Können wir denn jetzt das Script verändern? Dazu müssten wir wieder Zugriff auf die Kommandozeile (bash) des Containers erhalten.

Also einfach als COMMAND bash starten?

Scheinbar passiert nichts, ich bleibe auf dem Hostsystem. Weder habe ich ein Terminal zur Befehlseingabe, noch wartet das System auf weiter Eingaben per Konsole.

Ein Blick in die Befehlshilfe von Docker zeigt ein paar interessante Optionen:

  -i, --interactive          Keep STDIN open even if not attached
  -t, --tty                  Allocate a pseudo-TTY

Wunderbar, mit den beiden Optionen müsste es also klappen. (Wer sich die Mühe macht, hochzuscrollen: die -it-Option haben wir beim ersten Befehl dieses Containers bereits verwendet.)

Damit sind wir in wieder in der Konsole. Ich möchte jetzt :

  • den bestehenden Python/Flask Prozess finden

  • den Python/Flask beenden

  • den Ausgabetext unseres Webservers umschreiben

  • und ihn von aussen so starten, dass kein Terminal belegt wird.

Ein bisschen Linux: Prozesse identifizeren und stoppen

Wir brauchen dazu ein paar Linux-Grundlagen. Aktive Prozesse kann ich in Linux mit folgendem Befehl ausgeben:

In der Spalte PID der Ausgabe findet sich die Prozessnummer, mit der ich den jeweiligen Prozess ansprechen - und auch beenden kann:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root        55  0.0  0.4  31724 26372 ?        Ss   06:59   0:00 python3 hello_python.py

Prozesse beenden kann ich mit kill PID, wobei ich als PID die oben erhaltene Nummer eingebe. Um meinen Webserver zu beenden ist also der folgende Befehl nötig:

Jetzt können wir beispielsweise das Script anpassen, bevor wir den Flask-Server neu starten.

Gespeichert wird bei nano wieder mit Strg-O, verlassen mit Strg-X. Wenn es nichts weiter zu tun gibt können wir mit exit die interaktive Container-Shell beenden.

Container-Prozesse in den Hintergrund schicken

Beim letzten Starten des Python-Scripts war die Konsole blockert. Das ist gerade bei Servern, die dauerhaft laufen, nicht sinnvoll. Docker hält als Lösung eine weitere Option bereit:

  -d, --detach               Detached mode: run command in the background

Der detached-Modus schickt den Container in den Hintergrund. Wir probieren das direkt mit dem frisch geänderten Python-Script aus:

Wunderbar, das ist doch genau was wir wollten.

Aufräumarbeiten

Auch wenn Docker-Container deutlich weniger Platz benötigen als VMs benötigen sie mit der Zeit eine ganze Menge Ressourcen. Vor allem Speicherplatz wird belegt von Images und Containern, die irgendwo im verborgenen schlummern. Wie viel Speicherplatz das ist lässt sich mit folgendem Befehl herausfinden:

Welche Container geladen sind und wie viel Speicherplatz sie belegen erhalten wir mit den (synonymen) Befehlen:

docker ps hatten wir bislang nicht benutzt, es ist eine Kurzform von docker container ls. Hier ist die Option -a sehr wichtig, da wir sonst nur laufende Container angezeigt bekommen, der meiste Speicherplatz wird jedoch häufig von alten, nicht mehr benötigten Containern verwendet. Die Option -s gibt uns zusätzlich den jeweilig benötigten Speicherplatz aus.

Für Images erhalten wir selbiges mit dem Befehl:

Alle nicht mehr benötigten Container lassen sich über den Namen oder die ContainerID mit folgendem Befehl löschen:

Wenn keine darauf aufbauenden Container mehr vorhanden sind kann auch das zugehörige Image gelöscht werden:

Falls docker länger nicht genutzt werden soll, kann man auch etwas rabiater vorgehen: system prune löscht alle gestoppten container, images und gepufferte Dateien. Ich bin mit dieser Holzhammer-Methode allerdings etwas vorsichtig…

Fazit

Bis hierhin können wir mit docker schon Container aus vorhandenen Images erstellen, sie interaktiv nutzen, starten, stoppen, löschen. Wir können neue Programme in den Containern installieren und Portfreigaben nach aussen einrichten. Damit lassen sich bereits sehr viele Anwendungsfälle von Containern abbilden.

Selten reicht es aber, vorhandene Images zu nutzen. Die Änderungen, die wir innerhalb eines Containers gemacht haben, sollen häufig wiederverwendet werden. Wir müssen also lernen, eigene Images zu erzeugen. Das wird in einem nächsten Tutorial angegangen.

Leitfragen:

  • Was unterscheidet einen Container von einer virtuellen Maschine?
  • Was unterscheidet einen Container von einem Image?
  • Worin unterscheiden sich die beiden Befehle in ihrem Verhalten?
  • Worin unterscheiden sich die beiden Befehle?
  • Welcher Voraussetzungen müssen erfüllt sein, damit man per docker container exec einen Befehl ausführen kann?

Die Ursprungstexte (als Markdown), Grafiken und zugrunde liegende Diagrammquelltexte finden sich (soweit möglich in weiterbearbeitbarer Form) in folgendem git-Repository:

https://gitlab.com/oer-informatik/devoptools/docker.

Sofern nicht explizit anderweitig angegeben sind sie zur Nutzung als Open Education Resource (OER) unter Namensnennung (H. Stein, oer-informatik.de) freigegeben gemäß der Creative Commons Namensnennung 4.0 International Lizenz (CC BY 4.0).

Creative Commons Lizenzvertrag


  1. Docker-Einstiegskurs Play with Docker classroom: https://training.play-with-docker.com/ops-s1-hello/

  2. Installationsanleitung für Docker unter Windows: https://docs.docker.com/desktop/windows/install/

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: