Einführung in Docker Compose

1. Übersicht

Bei umfangreicher Verwendung von Docker wird die Verwaltung mehrerer verschiedener Container schnell umständlich.

Docker Compose ist ein Tool, mit dem wir dieses Problem lösen und problemlos mehrere Container gleichzeitig bearbeiten können.

In diesem Tutorial werden wir uns die Hauptfunktionen und leistungsstarken Mechanismen ansehen.

2. Die YAML-Konfiguration erklärt

Kurz gesagt, Docker Compose wendet viele Regeln an, die in einer einzigen Konfigurationsdatei von docker-compose.yml deklariert sind .

Diese YAML-Regeln, sowohl für Menschen lesbar als auch maschinenoptimiert, bieten uns eine effektive Möglichkeit, das gesamte Projekt in wenigen Zeilen aus einer Entfernung von zehntausend Fuß zu erfassen.

Fast jede Regel ersetzt einen bestimmten Docker-Befehl, sodass wir am Ende nur Folgendes ausführen müssen:

docker-compose up

Wir können Dutzende von Konfigurationen von Compose unter der Haube anwenden lassen. Dies erspart uns den Aufwand, sie mit Bash oder etwas anderem zu schreiben.

In dieser Datei müssen wir die Version des Compose-Dateiformats, mindestens einen Dienst sowie optional Volumes und Netzwerke angeben :

version: "3.7" services: ... volumes: ... networks: ... 

Mal sehen, was diese Elemente tatsächlich sind.

2.1. Dienstleistungen

Zu allererst Dienstleistungen beziehen sich auf Container Konfiguration .

Nehmen wir zum Beispiel eine Docker-Webanwendung, die aus einem Front-End, einem Back-End und einer Datenbank besteht: Wir würden diese Komponenten wahrscheinlich in drei Bilder aufteilen und sie in der Konfiguration als drei verschiedene Dienste definieren:

services: frontend: image: my-vue-app ... backend: image: my-springboot-app ... db: image: postgres ... 

Es gibt mehrere Einstellungen, die wir auf Dienste anwenden können, und wir werden sie später genauer untersuchen.

2.2. Volumes & Netzwerke

Volumes hingegen sind physische Bereiche des Speicherplatzes, die zwischen dem Host und einem Container oder sogar zwischen Containern gemeinsam genutzt werden. Mit anderen Worten, ein Volume ist ein freigegebenes Verzeichnis auf dem Host , das von einigen oder allen Containern aus sichtbar ist.

In ähnlicher Weise definieren Netzwerke die Kommunikationsregeln zwischen Containern sowie zwischen einem Container und dem Host . Gemeinsame Netzwerkzonen machen die Dienste von Containern für einander erkennbar, während private Zonen sie in virtuellen Sandboxen trennen.

Wir werden im nächsten Abschnitt mehr darüber erfahren.

3. Zerlegen eines Dienstes

Beginnen wir nun mit der Überprüfung der Haupteinstellungen eines Dienstes.

3.1. Ein Bild ziehen

Manchmal wurde das Bild, das wir für unseren Service benötigen, bereits (von uns oder von anderen) in Docker Hub oder einer anderen Docker-Registrierung veröffentlicht.

Wenn dies der Fall ist, verweisen wir mit dem Bildattribut darauf, indem wir den Bildnamen und das Tag angeben:

services: my-service: image: ubuntu:latest ... 

3.2. Ein Bild erstellen

Stattdessen müssen wir möglicherweise ein Image aus dem Quellcode erstellen, indem wir dessen Docker-Datei lesen .

Dieses Mal verwenden wir das Schlüsselwort build und übergeben den Pfad zur Docker-Datei als Wert:

services: my-custom-app: build: /path/to/dockerfile/ ... 

Wir können auch eine URL anstelle eines Pfads verwenden:

services: my-custom-app: build: //github.com/my-company/my-project.git ... 

Darüber hinaus können wir einen Image- Namen in Verbindung mit dem Build- Attribut angeben , das das Image nach seiner Erstellung benennt und es für andere Dienste verfügbar macht:

services: my-custom-app: build: //github.com/my-company/my-project.git image: my-project-image ... 

3.3. Netzwerk konfigurieren

Docker-Container kommunizieren untereinander in Netzwerken, die implizit oder durch Konfiguration von Docker Compose erstellt wurden . Ein Dienst kann mit einem anderen Dienst im selben Netzwerk kommunizieren, indem er einfach nach Containername und Port referenziert wird (z. B. Netzwerkbeispieldienst: 80 ), vorausgesetzt, wir haben den Port über das Schlüsselwort expose zugänglich gemacht :

services: network-example-service: image: karthequian/helloworld:latest expose: - "80" 

In diesem Fall würde es übrigens auch funktionieren, ohne es verfügbar zu machen , da sich die Anweisung expose bereits in der Image-Docker-Datei befindet.

Um einen Container vom Host zu erreichen , werden die Ports müssen deklarativ durch die freiliegenden Anschlüsse Schlüsselwort , das auch wir , wenn Aussetzen des Port anders in der Host auswählen kann:

services: network-example-service: image: karthequian/helloworld:latest ports: - "80:80" ... my-custom-app: image: myapp:latest ports: - "8080:3000" ... my-custom-app-replica: image: myapp:latest ports: - "8081:3000" ... 

Port 80 ist jetzt vom Host aus sichtbar, während Port 3000 der beiden anderen Container auf den Ports 8080 und 8081 im Host verfügbar ist. Dieser leistungsstarke Mechanismus ermöglicht es uns, verschiedene Container auszuführen, die dieselben Ports ohne Kollisionen freigeben .

Schließlich können wir zusätzliche virtuelle Netzwerke definieren, um unsere Container zu trennen:

services: network-example-service: image: karthequian/helloworld:latest networks: - my-shared-network ... another-service-in-the-same-network: image: alpine:latest networks: - my-shared-network ... another-service-in-its-own-network: image: alpine:latest networks: - my-private-network ... networks: my-shared-network: {} my-private-network: {} 

In diesem letzten Beispiel können wir sehen, dass ein anderer Dienst im selben Netzwerk in der Lage sein wird, Port 80 des Netzwerkbeispieldienstes zu pingen und zu erreichen , während ein anderer Dienst in seinem eigenen Netzwerk gewonnen hat 't.

3.4. Einrichten der Volumes

Es gibt drei Arten von Volumes: anonyme , benannte und Host- Volumes .

Docker verwaltet sowohl anonyme als auch benannte Volumes und stellt sie automatisch in selbst generierte Verzeichnisse auf dem Host bereit. Während anonyme Volumes mit älteren Versionen von Docker (vor 1.9) nützlich waren, sind benannte Volumes heutzutage der empfohlene Weg. Mit Host-Volumes können wir auch einen vorhandenen Ordner im Host angeben.

Wir können Host-Volumes auf Service-Ebene und benannte Volumes auf der äußeren Ebene der Konfiguration konfigurieren, um letztere für andere Container sichtbar zu machen und nicht nur für den, zu dem sie gehören:

services: volumes-example-service: image: alpine:latest volumes: - my-named-global-volume:/my-volumes/named-global-volume - /tmp:/my-volumes/host-volume - /home:/my-volumes/readonly-host-volume:ro ... another-volumes-example-service: image: alpine:latest volumes: - my-named-global-volume:/another-path/the-same-named-global-volume ... volumes: my-named-global-volume: 

Hier haben beide Container Lese- / Schreibzugriff auf den freigegebenen Ordner my-named-global-volume , unabhängig von den unterschiedlichen Pfaden, denen sie ihn zugeordnet haben. Die beiden Host-Volumes stehen stattdessen nur dem Volume-Beispieldienst zur Verfügung .

Der Ordner / tmp des Host-Dateisystems wird dem Ordner / my-volume / host-volume des Containers zugeordnet.

Dieser Teil des Dateisystems ist beschreibbar, dh der Container kann auf dem Hostcomputer nicht nur Dateien lesen, sondern auch schreiben (und löschen).

We can mount a volume in read-only mode by appending :ro to the rule, like for the /home folder (we don't want a Docker container erasing our users by mistake).

3.5. Declaring the Dependencies

Often, we need to create a dependency chain between our services, so that some services get loaded before (and unloaded after) other ones. We can achieve this result through the depends_on keyword:

services: kafka: image: wurstmeister/kafka:2.11-0.11.0.3 depends_on: - zookeeper ... zookeeper: image: wurstmeister/zookeeper ... 

We should be aware, however, that Compose will not wait for the zookeeper service to finish loading before starting the kafka service: it will simply wait for it to start. If we need a service to be fully loaded before starting another service, we need to get deeper control of startup and shutdown order in Compose.

4. Managing Environment Variables

Working with environment variables is easy in Compose. We can define static environment variables, and also define dynamic variables with the ${} notation:

services: database: image: "postgres:${POSTGRES_VERSION}" environment: DB: mydb USER: "${USER}" 

There are different methods to provide those values to Compose.

For example, one is setting them in a .env file in the same directory, structured like a .properties file, key=value:

POSTGRES_VERSION=alpine USER=foo

Otherwise, we can set them in the OS before calling the command:

export POSTGRES_VERSION=alpine export USER=foo docker-compose up 

Finally, we might find handy using a simple one-liner in the shell:

POSTGRES_VERSION=alpine USER=foo docker-compose up 

We can mix the approaches, but let's keep in mind that Compose uses the following priority order, overwriting the less important with the higher ones:

  1. Compose file
  2. Shell environment variables
  3. Environment file
  4. Dockerfile
  5. Variable not defined

5. Scaling & Replicas

In older Compose versions, we were allowed to scale the instances of a container through the docker-compose scale command. Newer versions deprecated it and replaced it with the scale option.

On the other side, we can exploit Docker Swarm – a cluster of Docker Engines – and autoscale our containers declaratively through the replicas attribute of the deploy section:

services: worker: image: dockersamples/examplevotingapp_worker networks: - frontend - backend deploy: mode: replicated replicas: 6 resources: limits: cpus: '0.50' memory: 50M reservations: cpus: '0.25' memory: 20M ... 

Under deploy, we can also specify many other options, like the resources thresholds. Compose, however, considers the whole deploy section only when deploying to Swarm, and ignores it otherwise.

6. A Real-World Example: Spring Cloud Data Flow

While small experiments help us understanding the single gears, seeing the real-world code in action will definitely unveil the big picture.

Spring Cloud Data Flow is a complex project, but simple enough to be understandable. Let's download its YAML file and run:

DATAFLOW_VERSION=2.1.0.RELEASE SKIPPER_VERSION=2.0.2.RELEASE docker-compose up 

Compose will download, configure, and start every component, and then intersect the container's logs into a single flow in the current terminal.

It'll also apply unique colors to each one of them for a great user experience:

We might get the following error running a brand new Docker Compose installation:

lookup registry-1.docker.io: no such host

While there are different solutions to this common pitfall, using 8.8.8.8 as DNS is probably the simplest.

7. Lifecycle Management

Let's finally take a closer look at the syntax of Docker Compose:

docker-compose [-f ...] [options] [COMMAND] [ARGS...] 

While there are many options and commands available, we need at least to know the ones to activate and deactivate the whole system correctly.

7.1. Startup

We've seen that we can create and start the containers, the networks, and the volumes defined in the configuration with up:

docker-compose up

After the first time, however, we can simply use start to start the services:

docker-compose start

In case our file has a different name than the default one (docker-compose.yml), we can exploit the -f and file flags to specify an alternate file name:

docker-compose -f custom-compose-file.yml start

Compose can also run in the background as a daemon when launched with the -d option:

docker-compose up -d

7.2. Shutdown

Um die aktiven Dienste sicher zu stoppen, können wir stop verwenden , wodurch Container, Volumes und Netzwerke sowie alle an ihnen vorgenommenen Änderungen erhalten bleiben:

docker-compose stop

Um den Status unseres Projekts zurückzusetzen, laufen wir einfach herunter , wodurch alles mit Ausnahme der externen Volumes zerstört wird :

docker-compose down

8. Fazit

In diesem Tutorial haben wir etwas über Docker Compose und dessen Funktionsweise gelernt.

Wie üblich finden wir die Quelldatei docker-compose.yml auf GitHub sowie eine hilfreiche Reihe von Tests, die im folgenden Bild sofort verfügbar sind: