Mit Docker automatisiert Anwendungscontainer erstellen

Seite 2: Standardanwendungen und Verbinden von Containern

Inhaltsverzeichnis

Um beispielsweise jenes Verzeichnis, das das Dockerfile enthält, in das Image zu integrieren, genügt prinzipiell die Angabe eines ADD-Schritts. Allerdings ist es ratsam, das Zielverzeichnis zuvor anzulegen:

[...]
RUN mv ./node-v0.10.26-linux-x64 /opt/node

RUN mkdir /app
ADD . /app

ENV PATH $PATH:/opt/node/bin

Um die Anwendung nun tatsächlich auszuführen, ist zunächst ein Container zu starten. Die gewünschte Applikation lässt sich danach mit dem bekannten run-Kommando anwerfen:

$ docker run goloroden/nodejs node /app/app.js

Solch ein Vorgehen funktioniert zwar, ist aber unpraktisch. Schließlich sollen von diesem Image erzeugte Container stets Node.js ausführen. Um das zu implementieren, lässt sich im Dockerfile das CMD-Schlüsselwort angeben:

[...]
ENV PATH $PATH:/opt/node/bin

CMD [ "node", "/app/app.js" ]

Nun genügt es, den Container mit run zu starten. Durch das CMD-Schlüsselwort weiß Docker, welche Anwendung auszuführen ist:

$ docker run goloroden/nodejs

Für Verwirrung sorgt gelegentlich das Schlüsselwort ENTRYPOINT, das dem CMD-Schlüsselwort auf den ersten Blick zu gleichen scheint. Allerdings besteht ein feiner Unterschied zwischen beiden: Während das mit CMD definierte Kommando überschreibbar ist, ist das über ENTRYPOINT festgelegte fix. Das tatsächlich ausgeführte Kommando ergibt sich dabei stets aus der Kombination der beiden Schlüsselworte.

Der serienmäßig vorgegebene ENTRYPOINT ist "/bin/sh -c", das tatsächlich ausgeführte Kommando lautet daher:

/bin/sh -c node /app/app.js

Sobald man den Unterschied begriffen hat, lassen sich flexible Konfigurationen erstellen. Beispielsweise führt die Angabe von

ENTRYPOINT [ "node" ]
CMD [ "/app/app.js" ]

dazu, dass bei einem Aufruf des Containers ohne zusätzliche Angaben die Anwendung wie gehabt ausgeführt wird. Da man den Wert von CMD allerdings durch die Angabe eines Kommandozeilenparameters überschreiben kann, lassen sich auch andere Node.js-Anwendungen starten. Das Ausführen von Node.js selbst ist jedoch fest vorgegeben.

Schließlich lässt sich durch Angabe von EXPOSE noch definieren, welche Ports des Containers zugänglich sein sollen. In Verbindung mit Node.js läuft das häufig auf Port 3000 hinaus. Das Binden des containereigenen Ports auf den des Wirts lässt sich allerdings nicht innerhalb des Dockerfiles abbilden, sondern obliegt stets dem Aufrufer. Das gesamte Dockerfile enthält nun folgenden Inhalt:

# goloroden/nodejs
# VERSION 0.0.1

FROM ubuntu
MAINTAINER Golo Roden <webmaster@goloroden.de>

RUN apt-get update
RUN apt-get install -y wget git
RUN wget http://nodejs.org/dist/v0.10.26/node-v0.10.26-linux-x64.tar.gz
RUN tar xvfz node-v0.10.26-linux-x64.tar.gz
RUN rm node-v0.10.26-linux-x64.tar.gz
RUN mv ./node-v0.10.26-linux-x64 /opt/node

RUN mkdir /app
ADD . /app

ENV PATH $PATH:/opt/node/bin

EXPOSE 3000

CMD [ "node", "/app/app.js" ]

Besonders interessant an Dockerfiles ist die Möglichkeit, sogenannte "Trusted Builds" für die zentrale Registry zu erzeugen: Bei einem derartigen Build verbindet man ein GitHub-Repository, das ein Dockerfile enthält, mit der Docker-Registry. Wenn eine neue Version des Dockerfiles an GitHub gesendet wird, benachrichtigt ein Webhook automatisch die Docker-Registry, die die geänderte Datei daraufhin abruft und das Image neu erzeugt. Auf diesem Weg können Anwender der Docker-Registry sicher sein, dass ein dort veröffentlichtes Image tatsächlich aus dem angegebenen Dockerfile hervorgegangen ist.

Da sich Images in Docker ausgesprochen leicht erzeugen und starten lassen, liegt es nahe, für jeden Dienst einer Anwendung einen gesonderten Container zu verwenden: Das überträgt das Single-Responsibility-Prinzip auf die Infrastruktur und vereinfacht die Entwicklung und Wartung der einzelnen Container. Selbst verhältnismäßig kleine Anwendungen lassen sich auf unterschiedliche Container verteilen, indem man beispielsweise die Datenbank auslagert.

Das Starten eines gesonderten Containers für die Datenbank ist dabei der einfachste Schritt, für den sich beispielsweise auf ein vorgefertigtes Image wie dockerfile/mongodb zurückgreifen lässt. Nachdem man den Container mit

$ docker run -p 27017:27017 dockerfile/mongodb

gestartet hat, besteht die einzige Herausforderung darin, eine Verbindung zwischen ihm und dem Container herzustellen, der Node.js ausführt. Natürlich lässt sich zu dem Zweck der freigegebene Port verwenden, aber dann ist die Konfiguration eines Containers von dem konkreten Aufruf eines anderen abhängig. Einen solchen Bezug gilt es in der Regel zu vermeiden. Zudem funktioniert diese Herangehensweise ausschließlich bei fest verdrahteten Ports: Überlässt man Docker die Wahl eines freien Ports auf dem Wirtssystem, weiß man bis zum Aufruf des Datenbankcontainers nicht, welcher Port überhaupt verwendet wird.

Docker bietet daher die Möglichkeit an, Container zu verbinden. Dazu muss man zunächst dem Datenbankcontainer bei dessen Start einen Namen zuweisen, zudem kann man auf die explizite Port-Weiterleitung verzichten:

$ docker run -name mongodb dockerfile/mongodb

Anschließend lässt sich der Name beim Start des Node.js-Containers angeben, um eine Verbindung zwischen beiden herzustellen. Dazu dient der Parameter -link. Das folgende Beispiel koppelt den Node.js- an den MongoDB-Container, wobei letzterer unter dem Namen "db" bereitgestellt wird:

$ docker run -link mongodb:db goloroden/nodejs

Docker stellt die Verbindungsdaten zum MongoDB-Container durch unterschiedliche Umgebungsvariablen zur Verfügung, die sich mit process.env aus Node.js auslesen lassen. Allen Umgebungsvariablen ist gemein, dass sie das Präfix "DB" verwenden, weil dem MongoDB-Container über den Parameter -link ebendieser Name zugewiesen wurde. Mit ihrer Hilfe, insbesondere durch DB_PORT, kann Node.js nun eine Verbindung zu MongoDB herstellen, ohne dass man dem Container explizit von Hand mitteilen müsste, unter welcher Adresse die Datenbank auffindbar ist.