Anwendungen mit Docker transportabel machen

Grundlagen

Images und Container zusammen bilden die sogenanten "Layer": Jeder Layer in Docker verfügt über ein eigenes Dateisystem, wobei lediglich jenes des Containers schreibbar ist. Es ist folglich nicht möglich, ein zu Grunde liegendes Image zu ändern. Bei Lesevorgängen hangelt sich Docker durch die Kette von Images, bis es die gewünschte Datei gefunden hat. Dieses Vorgehen dürfte Entwicklern, die mit einer Prototyp-basierten Sprache vertraut sind, bekannt vorkommen: Letztlich verhält sich das Dateisystem auf die gleiche Art wie die Prototypkette von Objekten in Sprachen wie JavaScript oder Io.

Will man die an einem Container vorgenommenen Änderungen bewahren, lässt sich der Container wiederum in ein Image umwandeln: Auf die Weise kann man es zu einem späteren Zeitpunkt als Basis für einen neuen Container verwenden. Es ist wichtig, den Unterschied zwischen Images und Container zu verstehen, um die Funktionsweise von Docker nachvollziehen zu können.

Zum Herunterladen eines Images dient das Kommando pull, das als Parameter den Namen des gewünschten Images erwartet, beispielsweise ubuntu:

$ docker pull ubuntu

Der Aufruf überträgt die angegebene Datei aus der öffentlichen Registry von Docker auf das lokale System. Neben den offiziellen Images enthält das Verzeichnis auch von der Community bereitgestellte Varianten. Sie lassen sich leicht daran erkennen, dass ihr Name als Präfix den Namen jenes Benutzers enthält, von dem das Image stammt: Beispielsweise handelt es sich bei "ubuntu" um eine offizielle Veröffentlichung, bei "goloroden/nodejs" hingegen um eine von dem Benutzer "goloroden" bereitgestellte.

Die Webseite der Registry bietet eine Suchfunktion an, mit deren Hilfe man nach vorgefertigten Images stöbern kann (siehe Abbildung 1). Die gleiche Suche lässt sich auch über die Kommandozeile verwenden, indem man den Befehl search angibt:

$ docker search nodejs
Die Registry von Docker enthält vorgefertigte Images, die sich zur lokalen Verwendung herunterladen lassen. (Abb. 1)

Sobald eine passende Datei gefunden und heruntergeladen wurde, lässt sich mit dem Kommando run ein neuer Container starten. Es erwartet außer dem Namen des zu verwendenden Images die auszuführende Anwendung:

$ docker run ubuntu echo Hello world
Hello world

Sobald Letztere endet, stoppt Docker den Container. Daher liefert ein Aufruf des Kommandos ps, das alle derzeit ausgeführten Container auflistet, lediglich eine leere Liste:

$ docker ps

Startet man hingegen eine Anwendung, die dauerhaft läuft, zeigt ps die zugehörigen Daten an. Dazu zählt unter anderem die eindeutige ID des Containers, die für die Umwandlung in ein Image benötigt wird.

Versucht man, run mit einem der Images auszuführen, das lokal nicht vorliegt, schlägt der Vorgang übrigens nicht fehl: Stattdessen versucht Docker zunächst, das gewünschte Image per pull zu beziehen. Daher ist es nicht zwingend erforderlich, ein Image stets von Hand herunterzuladen.

Gelegentlich kann es interessant sein, eine Liste aller lokal vorliegenden Images zu erhalten: Den Zweck erfüllt der images-Befehl. In dessen Ausgabe fällt auf, dass ein Image durchaus auch in verschiedenen Versionen existieren kann. Sie bezeichnet Docker als "Tag":

$ docker images
REPOSITORY TAG IMAGE ID ...
ubuntu 13.10 9f676bd305a4 ...
ubuntu saucy 9f676bd305a4 ...
ubuntu 13.04 eb601b8965b8 ...
... ... ... ...

Optional lässt sich ein solches Tag bei der Ausführung angeben, indem man seinen Namen mit einem Doppelpunkt an den des Images anschließt:

$ docker run ubuntu:13.04 echo Hello world
Hello world

Das bisherige Vorgehen ist geeignet, um eine Anwendung in einem Container auf Grundlage eines Images auszuführen. Allerdings ist es unmöglich, einen solchen fortzusetzen: Da das Erzeugen eines angepassten Containers in der Regel mehr als einen Aufruf auf der Kommandozeile erfordert, benötigt man hierfür einen anderen Weg. Er besteht darin, als Anwendung eine Shell zu übergeben und das run-Kommando zusätzlich um die Parameter -i und -t zu ergänzen. Letztere bewirken, dass der Standardeingabestrom nicht geschlossen wird und ein Pseudo-TTY zur Verfügung steht:

$ docker run -i -t ubuntu bash
root@97132245c370:/#

Ergebnis des Aufrufs ist eine Kommandozeile innerhalb des Containers, auf der man über administrative Rechte verfügt: Nun lässt sich der Container nach Belieben anpassen. Beispielsweise kann der Nutzer ein Personal Package Archive (PPA) hinzufügen und daraus Node.js installieren:

$ docker run -i -t ubuntu bash
root@bf43dab1247c:/# apt-get update
root@bf43dab1247c:/# apt-get install -y software-properties↵
-common python-software-properties
root@bf43dab1247c:/# add-apt-repository -y ppa:chris-lea/node.js
root@bf43dab1247c:/# apt-get update
root@bf43dab1247c:/# apt-get install -y nodejs

Als Bestandteil der Eingabeaufforderung erhält man die ID des Containers. Um sie nach dessen Beenden nochmals abzurufen, lässt sich das ps mit dem Parameter -l nutzen, der die Details des zuletzt ausgeführten Containers ausgibt:

$ docker ps -l
CONTAINER ID IMAGE COMMAND ...
bf43dab1247c ubuntu:12.04 bash ...

Die ID des Containers ist erforderlich, um ihn in ein Image umwandeln zu können. Das erledigt das Kommando commit, das außer der Kennung den gewünschten Namen und optional ein Tag erwartet:

$ docker commit bf43dab1247c goloroden/nodejs:0.10.25
c126f8d31ead2889163f6dd72c887272c4b7205bd0cfccf59165cfbb59176efa

Als Ergebnis zeigt das Kommando die ID des neu erzeugten Images an. Ruft man im Anschluss images auf, findet es sich in der Liste der hinterlegten Abbilder.

$ docker images
REPOSITORY TAG IMAGE ID ...
goloroden/nodejs 0.10.25 c126f8d31ead ...
ubuntu 13.10 9f676bd305a4 ...
ubuntu saucy 9f676bd305a4 ...
ubuntu 13.04 eb601b8965b8 ...
... ... ... ...