Docker – Teil 2: Das Arbeiten mit Containern

Nachdem wir im vorherigen Artikel eine Grundlage für Docker geschaffen haben schauen wir uns diesmal die Container näher an. Die Container sind die magischen kleinen Einheiten, die Docker so bekannt und verbreitet gemacht haben. Doch was heißt das genau? Was ist so ein Docker Container und wie arbeitet man damit?

Was ist ein Docker Container?

Die Container sind die genau das, warum sich Docker immer mehr verbreitet. Sie stellen einfach konfigurierbare, abgeschlossene Einheiten dar, in denen wir Anwendungen ausführen können. Klingt kompliziert? Kein Problem, machen wir ein Beispiel.

Wir stellen uns einfach einen Frachtcontainer vor (praktisch, dass die genau gleich heißen). In diesem Frachtcontainer sitzt ein Männchen – oder auch mehrere – und macht irgendwas. In unserem Docker Container könnte dieses Männchen zum Beispiel ein Apache Webserver sein. Da es innerhalb dieses Frachtcontainers immer gleich aussieht, egal ob dieser jetzt in München, New York oder Sydney steht, arbeitet unser Männchen immer unter denselben Bedingungen. Dasselbe gilt auch für den Docker Container.

Für diesen Frachtcontainer können wir natürlich auch einen Bauplan schreiben und diesen an unsere Filiale ans andere Ende der Welt senden. Dort kann dann genau derselbe Frachtcontainer gebaut werden ohne dass die Bauarbeiter jemals das Original gesehen haben und ohne, dass wir das Material für den Bau mitschicken müssen. Auch hier haben wir wieder eine Gemeinsamkeit mit dem Docker Container. Ein Dockerfile beschreibt unseren Container so, dass er auf einem anderen Rechner wieder genau gleich aufgebaut werden kann. Eine weitere Möglichkeit ist es, den Frachtcontainer aufzubauen und dann komplett zusammengebaut zu versenden. Auch das bietet unser Dockerversum natürlich an – mit den sogenannten Docker Images.
Diese können über das zentrale Docker Hub mit anderen Entwicklern geteilt werden – privat oder öffentlich. Aus den Images werden dann die jeweiligen Container gestartet – vergleichbar ist das mit der objektorientierten Programmierung wo aus einer Klasse mehrere Instanzen erzeugt werden. Dabei ist das Image die Klasse und jeder Container wäre eine Instanz davon.

Nun stellen wir neben unseren ersten Frachtcontainer noch einen zweiten Container auf, in dem ein weiteres Männchen arbeitet – nämlich ein MySQL Server. Unsere Männchen können jetzt zwar fleißig vor sich hin werkeln aber leider durch die Containerwände nicht miteinander kommunizieren. Dieses Hindernis können wir aber mit dem sogenannten Linking lösen. Wenn wir zwei Docker Container linken können wir Kommunikationswege wie zum Beispiel offene Ports oder gemeinsame Ordner festlegen – als würden wir Löcher in die Frachtcontainerwände schneiden.

Dies funktioniert nicht nur zwischen mehreren Containern, auch in die Außenwände der Frachtcontainer können wir natürlich Löcher schneiden. Doch wofür brauchen wir diese?
Ein Docker Container lebt nur so lange wie er etwas tut und existiert auch nur so lange. Alles, was innerhalb des Containers passiert, verschwindet sobald er beendet wird. Doch oft wollen wir mit persistenten Daten arbeiten. Wir wollen schließlich nicht jedes mal die PHP Dateien, die ein Webserver innerhalb des Containers ausliefern soll, in diesen Container beim Start reinladen. Dies wird dadurch erreicht, dass zwischen dem Host Rechner – also dem Rechner auf dem der Docker Container gestartet wird – und dem Container Ordner geteilt werden. Wird auf einer Seite dieser Verbindung etwas am Inhalt der Ordner geändert spiegelt sich diese Änderung auf beiden Seiten automatisch wieder.
Dadurch können wir zum Beispiel unsere PHP Anwendung auf unserem Windows / OSX / Linux Rechner entwickeln und gleichzeitig ganz gemütlich einen Webserver im Docker Container laufen lassen, um das Ergebnis zu überprüfen.

 

 

Die technische Seite

Nachdem die Metapher im letzten Abschnitt den Zusammenhang von Docker Containern mit unserer Arbeitsumgebung hoffentlich etwas klarer gemacht hat kümmern wir uns nun um die technische Seite. Wie starten und stoppen wir Docker Container? Wie linken wir sie? Wie teilen wir Verzeichnisse zwischen Host und Container? Eins nach dem anderen, Schritt für Schritt.

 

Container starten

Fangen wir simpel an und starten erstmal einen Container. Container basieren immer auf einem Docker Image. Auch selbst erstellte Container basieren auf einem Basisimage, welches von den Entwicklern zur Verfügung gestellt wird. Im Docker Hub finden wir hunderte verschiedene Images für alle möglichen Situationen. So ein Basisimage könnte dabei zum Beispiel einfach die Grundfunktionalität von Ubuntu sein, was den Namen ubuntu:14.04 trägt.
Die 14.04 ist dabei ein sogenanntes Tag, also eine Art Version. Neben dem Namen und dem Tag gibt es noch die Möglichkeit, die Images zu gruppieren wenn wir eigene Images hochladen. Ein von mir erstelltes Image könnte dann zum Beispiel unter dem Namen ftiersch/mein-server:2.4 verfügbar sein – Autor, Image und Tag.

Aber wir wollten einen Docker Container starten – los gehts. Führt einfach den folgenden Befehl aus (wenn ihr unter Windows arbeitet müsst ihr davor noch boot2docker mit dem Befehl boot2docker up starten und euch dort mit boot2docker ssh einloggen):

docker run ubuntu echo „Hello World“

Wie langweilig – ein Hello World Beispiel. Aber irgendwo muss man ja anfangen. Was passiert jetzt eigentlich bei diesem Befehl?

  1. Docker überprüft lokal, ob es das Image ubuntu gespeichert hat. Falls nein wird es automatisch heruntergeladen (da kein Tag angegeben wurde wird automatisch das aktuellste Image genommen)
  2. Docker startet einen Container aus dem angegebenen Image und führt den Befehl echo „Hello World“ innerhalb des Containers aus, welcher einfach nur eine Ausgabe Hello World erzeugt
  3. Docker beendet den Container wieder, da er seinen Zweck erfüllt hat

Das Ganze hat jetzt doch verdammt lang gedauert nur um ein Hello World auf dem Bildschirm zu sehen oder? Klar, schließlich musste zuerst das Image runtergeladen werden. Führen wir den Befehl jetzt ein zweites Mal aus ist die Ausgabe innerhalb eines Moments da.

 

 

Direkt nach dem Ausführen des Befehls wurde der Container auch schon beendet. Das ist das Standardverhalten, das die Docker Container aufweisen. Möchten wir dieses Standardverhalten verändern gibt es diverse Parameter für den docker run Befehl, die wir benutzen können. Sehen wir uns mal ein paar davon an:

  • -i sorgt dafür, dass der Container interaktiv ist. Das bedeutet, wir können während der Laufzeit damit interagieren
  • -t bindet unsere Ein- und Ausgaben an diesen interaktiven Container
  • -d sorgt dafür, dass der Container im Hintergrund weiterläuft (Daemonmode). Das ist nützlich für Container, die dauerhaft laufen sollen (beispielsweise Container, die Server enthalten oder Aufgaben, die einige Minuten oder Stunden dauern können)
  • -p bindet Ports von unserem Host an den Container. Ein Beispiel wäre -p 80:5000 welches dann unseren Hostport 80 auf den Containerport 5000 mappt. Das kann sinnvoll sein, wenn wir beispielsweise denselben Container mehrfach aufrufen und die Ports andernfalls kollidieren würden.
  • -v linkt ein Verzeichnis auf unserem Hostrechner zu einem Verzeichnis innerhalb des Containers. So können Daten persistent gespeichert werden, da diese normal mit dem Auslaufen des Containers verschwinden. Ein Beispiel wäre hier -v ./folder:/var/folder. Auch hier steht vor dem Doppelpunkt das Verzeichnis auf dem Hostrechner und danach das Verzeichnis innerhalb des Containers – genau wie bei den Ports.
  • –name gibt unserem Container einen Namen. Dieser wird andernfalls automatisch vergeben, ist aber nicht immer leicht zu merken. Namen können als Ersatz für die (sehr kryptische) ContainerID verwendet werden, machen also durchaus Sinn. Hier ist noch zu beachten, dass die Namen auf dem Hostrechner eindeutig sein müssen – bevor wir also einen neuen Container mit diesem Namen anlegen können (falls wir zum Beispiel etwas am Image geändert haben) müssen wir den vorherigen löschen. Wie das geht zeige ich euch später.

Das ist ein Auszug der Möglichkeiten. Um mit diesen Optionen etwas rumzuspielen könnt ihr beispielsweise einen interaktiven Container mit folgendem Befehl starten:

docker run -i -t ubuntu /bin/bash

Dieser Befehl startet einen Ubuntucontainer, führt den /bin/bash Befehl aus (gibt euch also eine Konsole) und mappt unsere Tastatureingaben in diese Konsole. Dadurch können wir nun innerhalb des Containers ausprobieren bis wir diesen mit dem Befehl exit wieder verlassen.

Container auflisten und stoppen

Um aktuell laufende Docker Container – und einige Informationen – zu bekommen können wir den Befehl

docker ps

nutzen. Hier sehen wir wichtige Eigenschaften wie beispielsweise die ContainerID, das Image, gemappte Ports oder auch den Namen des Containers.
In dieser Form listet der Befehl nur Container auf, die aktuell laufen. Möchten wir alle Container sehen, die auf unserem Host zur Verfügung stehen, hängen wir einfach den Parameter -a an.

Wollen wir einen laufenden Container stoppen geht das ganz einfach mit

docker stop NAME/ID

Dadurch wird der Container quasi ausgesetzt – er existiert aber weiterhin und behält auch den Zustand, den das Docker Image hatte, als wir docker run ausgeführt haben. Updaten wir also unser Image müssen wir erneut docker run ausführen um die Änderungen wirksam zu machen.

Um den Container wieder zu starten genügt ein einfaches

docker start NAME/ID

 

 

Container entfernen

Wie oben erwähnt muss ein Container erst entfernt werden, wenn er erneut durch den docker run Befehl mit demselben Namen ausgeführt werden soll. Das funktioniert ganz einfach mit

docker rm NAME

Davor muss der Container natürlich gestoppt werden.

 

Ich hoffe, diese Übersicht hat euer Verständnis von Docker etwas verbessert. Die Artikelreihe ist natürlich noch nicht zu Ende, nächstes mal geht es mit den Docker Images weiter.

 

Möchtest du automatisch mit Informationen zum Thema Docker versorgt werden? Dann folge mir einfach bei Twitter unter @AbHeuteCoden.

3 Kommentare zu “Docker – Teil 2: Das Arbeiten mit Containern

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

*