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!
...

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:

Was passiert dabei genau im Hintergrund?
das Programm
docker
wird aufgerufenDocker 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
- System mit Root-Zugriff:
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:
PS> docker run [OPTIONEN] IMAGENAME [COMMAND]
# startet eine neue Instanz des Dockerimages IMAGENAME und führt darauf COMMAND aus
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
.
from flask import Flask # Installiert das Framework
app = Flask(__name__) # Erzeugt eine Instanz des Frameworks
@app.route('/') # legt fest, dass auf localhost:xxxx/ gelauscht wird
def hello_world():
return "Der Container lebt!" # Gibt die eigentliche Systemantowrt zurück
if __name__ == '__main__': # Falls die App eigenständig läuft
app.run(host='0.0.0.0') # ...führe das Framework aus und reagiere auf alle Adressen
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.

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:
PS> docker container stop (CONTAINERID | CONTAINERNAME)
# stoppt einen bestimmten laufenden Container. Die CONTAINERID erhält man
# über den $ docker container ls -a Befehl
Entsprechend reaktiviert man abgeschaltete Container mit:
PS> docker container start (CONTAINERID | CONTAINERNAME)
# startet einen bestimmten gestoppten Container. Die CONTAINERID erhält man
# über den $ docker container ls -a Befehl
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:
PS> docker container exec CONTAINERID COMMAND
# führt auf dem aktiven Container mit der CONTAINERID den Befehl COMMAND aus
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.
@app.route('/') # legt fest, dass auf localhost:xxxx/ gelauscht wird
def hello_world():
return "Die Wiederkehr des Containers!" # Gibt die eigentliche Systemantowrt zurück
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:
PS> docker container rm CONTAINERID
# löscht einen bestimmten Container. Die CONTAINERID oder der Namen erhält man
# über den "docker ps -sa" Befehl
Wenn keine darauf aufbauenden Container mehr vorhanden sind kann auch das zugehörige Image gelöscht werden:
PS> docker image rm IMAGENAME
# löscht einen bestimmten Container. Die CONTAINERID oder der Namen erhält man
# über den $ docker container ls -sa Befehl
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?
PS> docker container run alpine echo "hello from alpine"
PS> docker container run -it alpine echo "hello from alpine"
- Worin unterscheiden sich die beiden Befehle?
- Welcher Voraussetzungen müssen erfüllt sein, damit man per
docker container exec
einen Befehl ausführen kann?
Links und weitere Informationen
Sehr schöne visuelles Tutorial von Aurelie Vache: Understanding docker ## Quellen und offene Ressourcen (OER)
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).
Docker-Einstiegskurs Play with Docker classroom: https://training.play-with-docker.com/ops-s1-hello/↩
Installationsanleitung für Docker unter Windows: https://docs.docker.com/desktop/windows/install/↩