Wenn Sie den bisherigen drei Teilen meiner Artikelreihe zu Docker gefolgt sind, haben Sie bereits gelesen, wie Sie mit Docker auf Ihrem eigenen Computer arbeiten (Einführung in Docker Teil 1 – Interaktive Arbeit mit Docker und Einführung in Docker Teil 2 – Containerdaten und Backups für Docker), und wie Sie selbst wiederverwendbare Docker-Images erzeugen können (Einführung in Docker Teil 3 – Eigene Docker-Images bauen). Für den Einsatz von Docker im Deployment oder auch für komplexere Entwickleraufgaben fehlen nun noch Informationen zur sogenannten Orchestrierung, also der Koordination mehrerer Docker-Container, die gemeinsam aufgesetzt, gestartet und beendet werden, und die untereinander Kontakt aufnehmen können, um bestimmte Aufgabenstellungen zu bewältigen.
Es gibt für die Orchestrierung komplexer Deployment-Umgebungen entsprechend komplexe Software. Dazu zählen außer dem verbreiteten Kubernetes auch die Lösungen der verschiedenen Cloud-Anbieter, die historisch bereits vor Kubernetes als eigenständige Systeme in Betrieb genommen und später mit entsprechenden Kompatibilitätsschichten ausgestattet wurden. In diesem Artikel soll es nicht um die Details dieser unterschiedlichen Lösungen gehen, sondern um die Gemeinsamkeiten der Idee von Orchestrierung, die sich schon auf einer wesentlich einfacheren Ebene erlernen und nachvollziehen lassen: und zwar mit docker-compose.
Erste Schritte an der Kommandozeile
Je nachdem, welches Installationspaket Sie auf dem eigenen Rechner verwenden, um Docker zu installieren, könnten Sie docker-compose bereits verfügbar haben. Testen Sie es einfach einmal, indem Sie an der Kommandozeile docker-compose eingeben. Das Kommando sollte sich dann mit einer Kurzanleitung melden, wenn es auf dem Gerät gefunden wird. Falls Sie es nicht installiert haben, sollten Sie den Installationsanleitungen in der offiziellen Dokumentation folgen, um das Kommando korrekt zu installieren.
In der Vergangenheit war das Kommando grundsätzlich vom Hauptpaket der Docker-Software getrennt, und daher wurde allgemein der Name “docker-compose” mit einem Bindestrich verwendet. Allerdings ist das Kommando heute so mit Docker integriert, dass es auch als Unterkommando von docker selbst verwendet werden kann. Im Internet finden Sie heute ebenfalls Anleitungen, die den Aufruf mit einem Leerzeichen beschreiben: docker compose … – beide Varianten sind möglich, und es gibt keine Unterschiede im Ergebnis.
Wie Sie beim kurzen Testlauf bereits sehen können, bietet docker-compose viele Unterkommandos an. Manche davon sind analog zu denen von docker selbst, aber grundsätzlich benötigen alle Ausführungen eine Konfigurationsdatei, aus der die Struktur des beabsichtigten Containernetzwerks hervorgeht. Mit der Option -f können Sie eine oder mehrere solche Dateien explizit angeben, ansonsten wird nach Konvention im lokalen Verzeichnis nach einer Datei mit dem Namen docker-compose.yml gesucht.
Eine Beispiel-Orchestrierung
Das Format der Konfigurationsdatei ist einfach. Es wird YAML verwendet, das heisst, Einrückung zählt. Um ein Beispielszenario zu demonstrieren, brauchen Sie allerdings mehrere Dateien – keine Sorge, die sind zum einfachen Mitarbeiten gedacht!
Zunächst nehmen wir uns eine kurze JavaScript-Programmdatei, die einen HTTP-Server startet, vor. Dieses Programm soll hier stellvertretend für eine größere Webanwendung sein, oder gar für einen ganzen Webserver. Im folgenden nenne ich diese Datei service.js, und lege sie lokal unter dem Pfad ./service/service.js ab.
Die Struktur dieses kurzen Programms ist einfach: Mithilfe von Node als JavaScript-Laufzeitumgebung wird ein einfacher HTTP-Server gestartet, der auf jeden Zugriff mit der Meldung “Hello from the Node service” reagiert. Der Server ist auf Port 8080 TCP ansprechbar. Falls Sie kein Programmierer sind, können Sie diese Details gern ignorieren und sich stattdessen jede andere netzwerkfähige Applikation vorstellen!
Die zweite Datei für unser Beispiel ist Dockerfile. Sie sollte ebenfalls im Verzeichnis ./service abgelegt werden.
Die Anweisungen in diesem Dockerfile, wie in vorherigen Artikeln bereits beschrieben, erzeugen ein Image auf Basis eines Node-Standardimages, kopieren die Programmdatei in das Image, und starten den Prozess.
Konfiguration im YAML-Format
Nachdem diese Elemente vorbereitet sind, darf jetzt endlich eine docker-compose.yml Konfigurationsdatei erzeugt werden. Für den Anfang ist auch diese sehr simpel:
Beachten Sie bei der Erstellung der Datei die Einrückung, das ist bei YAML wichtig. Es ist übrigens Zufall, dass der Name service, den ich für den HTTP-Server im Beispiel gewählt habe, derselbe ist wie die Bezeichnung services, die als Standard-Schlüsselwort in docker-compose.yml verwendet wird. Natürlich dürfte Ihr eigener Eintrag einen beliebigen anderen Namen haben.
Der Block services beschreibt die Dienste, die im Rahmen der Orchestrierung für Ihr Anwendungssystem erzeugt werden. Derzeit ist das nur einer: service. Dieser Dienst ist mit dem Schlüsselwort build so eingerichtet, dass er zunächst gebaut wird, und zwar einfach per Angabe des Unterverzeichnisses, in dem sich das Dockerfile befindet. Dies ist eine komfortable Lösung für den Umgang mit selbstgebauten Images. Natuerlich – das werden Sie bald sehen – kann eine docker-compose-Konfiguration auch auf Images über deren Namen zugreifen, und so könnten Sie auch ein selbst erzeugtes Image unabhängig bauen oder aktualisieren.
Starten Sie das erste eigene Anwendungsmodul
Wenn Sie nun diese Datei unter dem Namen docker-compose.yml im lokalen Verzeichnis abgelegt haben (diesmal außerhalb von ./service!), können Sie Ihr erstes kleines Anwendungssystem starten. Beachten Sie bitte, dass eventuell für das Starten von docker-compose ein sudo erforderlich ist, genau wie beim Start des Kommandos docker selbst – das hängt von Ihrer Installation und Systemumgebung ab.
Beim Start arbeitet Docker die verschiedenen Schritte ab. Zuerst wird das Image gebaut, da es noch nicht existiert. Dazu muss das Basisimage node:lts-alpine heruntergeladen werden, dann wird die JavaScript-Datei kopiert. Nach erfolgreichem Bau startet der Dienst, und der Prozess läuft auf der Konsole weiter. Das ist das Standardverhalten von docker-compose up. Wenn Sie die gestarteten Container gerne im Hintergrund laufen haben möchten, müssen sie docker-compose up -d starten.
Mit Ctrl-C können Sie den Vorgang für den Moment wieder abbrechen. Derzeit wäre es nämlich unmöglich, von außen auf den HTTP-Server zuzugreifen. Dieser macht zwar intern seinen Port 8080 verfügbar – was Sie auch der EXPOSE-Anweisung im Dockerfile entnehmen können – aber da der Port nicht zum Host hin abgebildet ist, kann nicht darauf zugegriffen werden.
Orchestrierte Systeme unterstützen die bekannten Startoptionen
Es ist möglich, mit detaillierten Konfigurationsanweisungen in docker-compose.yml die Parameter anzugeben, die Sie bereits aus vorherigen Artikeln dieser Reihe sowie vom Umgang mit dem Kommandozeilentool docker kennen. Zum Beispiel können sie mit der Option -p 8080:8080 den Port 8080 vom laufenden Container auf den Host abbilden (in diesem Fall unter Beibehaltung der Portnummer). Entsprechend gibt es ein Schlüsselwort ports für die docker-compose.yml, mit der Sie dasselbe erreichen können. Hier sind die verschiedenen Syntaxvarianten, die im YAML-Format unterstützt werden, um diesen Wert zu setzen. Sie können eine Liste übergeben, indem Sie eckige Klammern verwenden:
Alternativ können Sie die Liste mit Spiegelstrichen formatieren:
Diese beiden Varianten sind typische Optionen, die sich aus der YAML-Formatierung ergeben. Zusätzlich unterstützt docker-compose in diesem und vielen anderen Fällen ein kurzes und ein langes Format. Damit ist auch die folgende Variante möglich:
Es würde zu weit führen, die unterschiedlichen Varianten für andere Optionen zu erwähnen, während das Beispiel weiter entwickelt wird. Daher möchte ich an dieser Stelle auf die detaillierte Dokumentation verweisen, die alle Möglichkeiten auflistet. Für den weiteren Verlauf gehe ich davon aus, dass der direkte Zugriff auf Container-Port 8080 nicht aktiv ist.
nginx als zweite Anwendungskomponente
Um das Beispiel nun zu einem echten Anwendungssystem auszubauen, brauchen Sie mindestens eine weitere Komponente. In echten Anwendungen empfiehlt es sich gewöhnlich nicht, HTTP-Server direkt für den Zugriff von aussen zu öffnen, sonders es werden vorgeschaltete Proxy-Server verwendet. Daher möchte ich nun beschreiben, wie Sie nginx als Proxy für den Server im Beispiel aktivieren können. Zunächst wird dafür eine weitere Konfigurationsdatei benötigt, aber zuvor gilt es, eine Frage zu klären: Wie wird der Proxy Anfragen an den Server-Container weiterleiten, bzw. wie greift ein Container in einem System auf einen anderen Container zu?
Die Antwort ist: Das geht fast von selbst. Es ist nämlich so, dass docker-compose automatisch alle Container eines Systems in dasselbe virtuelle Netzwerk einordnet. Selbstverständlich können Sie das ändern, aber der Einfachheit halber können sich immer alle Container in einem Netzwerk gegenseitig ansprechen bzw. anhand eines Namens finden.
Erzeugen Sie nun einen neuen Unterordner namens nginx, und darin eine Datei app.conf. Dies ist der Inhalt für die Datei:
Die Einstellungen im Detail zu beschreiben ist Aufgabe der nginx-Dokumentation, speziell zum Betrieb von nginx als Reverse Proxy finden Sie hier verschiedene Hinweise. Im Wesentlichen handelt es sich bei den Einstellungen im Beispiel um “Best Practice”-Settings. Der interessanteste Punkt ist die Zeile proxy_pass, denn hier wird nginx angewiesen, Anfragen an einen Server namens service weiterzuleiten. Dies ist deswegen möglich, weil docker-compose, wie beschrieben, den Dienst namens service unter demselben Namen im virtuellen Netzwerk verfügbar macht.
Um beide Dienste im Anwendungssystem gleichzeitig zu starten, muss nun noch die Datei docker-compose.yml modifiziert werden. Damit auch die korrekte Einrückung deutlich wird, hier noch einmal die ganze Datei auf dem aktuellen Stand:
Neu ist der Block nginx. Darin wird mit der Anweisung image angegeben, welches Image dem Dienst zugrunde liegt. Die Bezeichnung hat dieselbe Struktur, die Sie bereits vom Containerstart an der Kommandozeile kennen, oder vom FROM im Dockerfile. Mit depends_on wird docker-compose bekannt gemacht, dass der Dienst service von nginx benötigt wird. Dabei dient dies der automatischen Feststellung der richtigen Startreihenfolge, und die Abhängigkeit wird auch beim selektiven Start von Diensten verwendet. Es wird allerdings nicht auf den vollständigen Start des anderen Dienstes gewartet. Dies ist ein Trugschluss, auf den mancher docker-compose-Anwender hereingefallen ist, daher sei es hier erwähnt. Es gibt eine gesonderte Dokumentationsseite zum Thema “Kontrolle der Startreihenfolge”, auf der auch Methoden zum Abwarten bestimmter Startzustände beschrieben werden.
Die letzte neue Zeile in docker-compose.yml ist die für das Volume. Hier wird der lokale Pfad ./nginx (in dem die Konfigurationsdatei abgelegt ist) im Container unter /etc/nginx/conf.d abgebildet. Das ist der Pfad, in dem der laufende nginx im Container seine Konfigurationsdateien erwartet. Das Postfix :ro bedeutet, dass der Pfad “read only” abgebildet wird, so dass die Konfiguration nicht aus dem Container heraus geändert werden kann.
Nun ist es an der Zeit, das Anwendungssystem zu starten. Das Kommando lautet wie vorher:
Die Ausgaben vom nginx-Container enthalten einige Startmeldungen, und dann ist das System bereit. Nun können Sie mit einem Browser auf den URL http://localhost zugreifen und Sie werden dann die Meldung vom HTTP-Server sehen, die auf dem Weg über den Proxy ausgegeben wird: “Hello from the Node service”.
Die Realität darf komplexer sein
Um das Bild abzurunden, möchte ich noch einige Optionen ansprechen, die bisher im Beispiel nicht zum Einsatz gekommen sind. Der erste wichtige Punkt ist, dass Docker-Volumen oder lokale Verzeichnisse in mehr als einem Container gleichzeitig abgebildet sein können. Sie haben im Beispiel gesehen, dass ein Volume “read only” verwendet wurde, und das darf auch in mehreren Containern geschehen – nur ein Container sollte schreibend auf das Volumen zugreifen. Achtung: Diese Koordination wird nicht erzwungen, es ist Ihnen überlassen, potentielle Konflikte bei gleichzeitigem Zugriff zu vermeiden. Dieser Ansatz wird oft gewählt, um Informationen zwischen Containern zu teilen. Zum Beispiel gibt es ein Projekt zur Integration von Let’s Encrypt mit nginx, das so die Zertifikatsdateien an den richtigen Stellen verfügbar macht.
Weitere Elemente, die oft in docker-compose.yml erscheinen, sind Volumen, Netzwerke und Umgebungsvariablen. Benannte Volumen kennen Sie bereits von Docker selbst, und diese können auch mit docker-compose verwendet werden, wenn Sie lieber der Docker-Engine die Verwaltung überlassen, statt Volumen direkt im Dateisystem abzulegen. Netzwerke können im einfachsten Fall mit spezifischen Namen erzeugt werden, um etwa einige Container logisch von anderen zu trennen. In komplexen Fällen ist es auch möglich, bestimmte Adressbereiche zuzuweisen, oder gar Brücken von Containern zu Host-Netzwerkschnittstellen zu bauen. Und zuletzt: Die Nutzung von Umgebungsvariablen zur Konfiguration kennen Sie ebenfalls bereits von Docker. In docker-compose.yml können Sie solche Variablen in einem Block namens environment pro Dienst eintragen, es werden zusätzlich externe Variablen gelesen (standardmässig aus der Datei .env, wenn sie existiert), und es gibt Mechanismen zur Übernahme von Variablenwerten in andere Teile der Konfiguration.
Grundlagen der Orchestrierung – Fazit
Damit sind die Grundlagen von docker-compose bereits abgedeckt! Zur Konfiguration einfacher Anwendungssysteme mit mehreren Komponenten genügen die Fähigkeiten von docker-compose bereits – natürlich ist ein Studium der Dokumentation zu weiteren Details empfehlenswert. Für komplexere Deployment-Szenarien können Sie eventuell mit Docker Swarms arbeiten, oder natürlich gleich Kubernetes nutzen und von dessen Skalierungs- und Verwaltungsfeatures profitieren. Es ist auch möglich, eine docker-compose.yml direkt in verschiedene Clouds hochzuladen und damit ein Deployment zu automatisieren. In jedem Fall werden Sie von den Kenntnissen der Grundlagen profitieren, und in meiner alltäglichen Arbeit als Entwickler, Softwarearchitekt und Admin mehrerer Linuxserver nutze ich die beschriebenen Funktionen von docker-compose regelmäßig.
Ich wünsche gutes Gelingen für Ihre Docker-Projekte!
Hier finden Sie weitere Artikel zum Thema:
- Interaktive Arbeit mit Docker – Eine Einführung in den Betrieb von Docker-Containern Teil 1
- Containerdaten und Backups für Docker – Eine Einführung in den Betrieb von Docker-Containern Teil 2
- Eigene Docker-Images bauen – Einführung in den Betrieb von Docker-Containern – Teil 3
- Docker im Entwickleralltag
- Docker Debugging: Diagnose und Behebung von gängigen Container-Fehlern
Titelmotiv: Photo by Timelab Pro on Unsplash
- Eine Docker-Anwendung auf einem Virtual Ubuntu-Server von Host Europe betreiben – Teil 2 - 4. November 2022
- Eine Docker-Anwendung auf einem Virtual Ubuntu-Server von Host Europe betreiben – Teil 1 - 19. Oktober 2022
- Vorbereitung eines Anwendungssystems für ein Deployment mit Docker – Tutorial - 19. Mai 2022