Docker Compose

Orchestrierung mit Docker Compose

Bereits in unserem kleinen Beispiel bräuchten wir normalerweise mehr als einen Container. Es wäre schön, wenn wir neben der Datenbank, auch eine Web-basierte UI für die Verwaltung der Datenbank zur Hand hätten (gibt es ja fast für alle Datenbanken). Damit müssten wir nicht für jede Datenbank auch noch ein Tool installieren, mit dem man die Daten anschauen kann.

Aber bereits 2 Container bringen eine gewisse Komplexität. Zum Beispiel müssen wir die UI für die Datenbank erst starten, wenn die Datenbank bereits hochgefahren und verfügbar ist. Mit mehr Containern (z.B. Redis für Caching, Prometheus für Monitoring, Logging und Tracing, Grafana für Visualisierung dieser Daten, usw.). Alle haben eigene Voraussetzungen, Abhängigkeiten usw.

Wäre es nicht schön, so eine “Laufzeitumgebung” ein Mal zu definieren und immer wieder (sauber) ausführen zu können? Eventuell diese Konfiguration direkt mit dem Projekt auch mit den Kollegen teilen?

So eine Orchestrierung von unterschiedlichen Containern können wir mit “Docker Compose” umsetzten. Das ist praktisch ein kleiner Bruder von Kubernetes, das für die professionelle, hoch skalierbare, Orchestrierungen gedacht ist.

Orchestrierung mit Docker Compose

Die Container, die mit “Docker Compose” orchestriert werden sollen, werden in einer YAML Datei definiert und konfiguriert. Die CLI dafür wird mit “Docker Desktop” mitgeliefert. Ist diese auf Ihrem System nicht installiert, können Sie den Installationsanweisungen von der offiziellen Docker Compose Seite folgen.

Die Datei wird standardmäßig docker-compose.yml genannt und befindet sich in dem gleichen Verzeichnis wie das Projekt. Hier ist der Grundaufbau der Datei:

 1# Liste der Services / Container
 2services:
 3  # Datenbank Container
 4  db:
 5    # Container image
 6    image: postgres:18-alpine
 7    # Umgebungsvariablen
 8    environment:
 9      POSTGRES_PASSWORD: super-secret-password
10    # Port Mapping
11    ports:
12      - "5432:5432"
13  dbAdmin:
14    # Container image
15    image: dpage/pgadmin4
16    # Umgebungsvariablen
17    environment:
18      PGADMIN_DEFAULT_EMAIL: admin@example.com
19      PGADMIN_DEFAULT_PASSWORD: super-secret-password
20    # Port Mapping
21    ports:
22      - "8080:80"

Die Konfiguration enthält min. ein Service (Container) das gestartet werden soll.

  • In der Zeile 4 starten wir die Konfiguration von Postgres Datenbank. Der Service enthält den Namen db.
  • Zeile 6 definiert das Image, das verwendet wird (Postgres in diesem Fall).
  • In der Zeile 8 definieren wir die Umgebungsvariablen, die beim Starten des Containers gesetzt werden sollen. In diesem Fall POSTGRES_PASSWORD wird auf super-secret-password gesetzt.
  • Zeile 10 definiert das Port Mapping, mit dem Containerport 5432 mit dem Hostport 5432 verbunden wird.

Der zweite Service ist eine Admin-UI für Postgres. Sie wird mit dem Namen dbAdmin gestartet und verwendet die Image dpage/pgadmin4. Die Umgebungsvariablen werden ebenfalls gesetzt, um den Benutzername und das Passwort für die Admin-UI zu definieren.

Da bei der Definition des Images for dbAdmin keine Version angegeben wird (kein :tagName am Ende), wird der Tag automatisch auf latest gesetzt (Docker Konvention).

Starten der Orchestrierten Container

Um nun alle konfigurierten Services zu starten, nutzen wir die “Docker Compose” CLI. Die Aufrufe müssen in dem Ordner erfolgen, in dem die docker-compose.yml Datei liegt.

docker compose up -d

Gestarteten Container über Compose

Unter http://localhost:8080 können Sie sich nun mit den Zugangsdaten (aus der Konfiguration) anmelden und eine Verbindung zur Datenbank herstellen (Hostname db, wie der Service Name).

Postgres Admin Web-UI

Anpassung der Orchestrierung an die Applikation

Damit passt die aktuelle minimalistische Konfiguration aber noch nicht zu unseren Anforderungen der App.

  • Wir haben vorgegebene Anmeldedaten für die Datenbank
  • Wir haben einem, vom Standard abweichenden Datenbanknamen
  • Es wäre gut, wenn pgAdmin bereits die Verbindung zu unseren Datenbank hätte, ohne dass wir es jedes Mal nach Neustart eingeben müssten
  • pgAdmin sollte erst starten, wenn die Datenbank einsatzbereit ist (“health check”)

Die Konfiguration der Datenbank können wir leicht anpassen. Die entsprechenden Umgebungsvariablen-Werte kennen wir bereits aus dem direkten Start mit Docker.

services:
  db:
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: devdb

Für die Prüfung, ob der Container wirklich einsatzbereit ist, müssen wir einen “health check” umsetzen. Dieser ist immer abhängig vom Service. Für unsere Postgres Datenbank könnte dieser wie folgt aussehen.

1services:
2  db:
3    healthcheck:
4      test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
5      interval: 2s
6      retries: 10
7      start_period: 5s
8      timeout: 30s
  • Zeile 3 startet die Definition für “health check” (Health Check Doku)
  • Zeile 4 beschreibt den Befehl, mit dem die “Bereitschaft” geprüft werden kann. Bei Postgres können wir dazu pg_isready Tool nutzen. (Check Doku)
  • Zeile 5 gibt an, in welchen Zeitabständen der Check durchgeführt wird, nach dem Ablauf der start_period
  • Zeile 6 gibt an die Anzahl der Versuche, bis der Start als fehlerhaft markiert wird
  • Zeile 7 gibt die üblich Startzeit an. In dieser Zeit wir der Container zwar getestet, die Fehler zählen in dieser Zeit aber nicht zu Wiederholungen.
  • Zeile 8 gibt an, wie lange ein Check max. dauern soll, bis der als fehlerhaft erkannt wird

In pgAdmin können wir nun die Datenbank als Abhängigkeit eintragen, so dass der Container erst dann startet, wenn die Datenbank bereits verfügbar (auf Verbindungsversuche antwortet) ist.

services:
  db:
    ...
  dbAdmin:
    depends_on:
      db:
        condition: service_healthy

Nun fehlt uns nur noch die Konfiguration von pgAdmin, so dass unsere Datenbank da bereits “hinterlegt” ist. Dazu müssen wir (leider) die Dokumentation von pgAdmin genauer ansehen.

Damit wir uns nicht jedes Mal anmelden müssen (wir sind ja auf unseren eigenen Rechner und betreiben keinen Server), können wir pgAdmin ich einen “nicht” Server Modus starten

services:
  dbAdmin:
    environment:
      PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
      PGADMIN_CONFIG_SERVER_MODE: 'False'
  • PGADMIN_CONFIG_SERVER_MODE schaltet pgAdmin in single User Mode, so dass wir uns nicht mehr anmelden müssen
  • PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED erlaubt uns im nächsten Schritt die Konfiguration des Servers durchzureichen

Im letzten Schritt legen wir eine Konfigurationsdatei für die Serverkonfiguration, die wir an pgAdmin durchreichen werden. Diese können wir indirekt für uns erstellen lassen, in dem wir den Server zuerst in pgAdmin konfigurieren und diese Konfiguration dann als JSON-Datei exportieren. Die exportierte Datei muss dann über Docker Desktop aus dem Container exportiert werden.

 1{
 2  "Servers": {
 3    "1": {
 4      "Name": "DB",
 5      "Group": "Servers",
 6      "Host": "db",
 7      "Port": 5432,
 8      "MaintenanceDB": "devdb",
 9      "Username": "devuser",
10      "PasswordExecCommand": "echo \u0027devpassword\u0027"
11    }
12  }
13}

In Zeile 10 übergeben wir das Passwort über ein echo Befehl, da die Konfiguration die Speicherung von unverschlüsselten Passwörtern nicht erlaubt.

1services:
2  dbAdmin:
3    # Datei und Orner Mappings
4    volumes:
5      - ./db_server.json:/pgadmin4/servers.json

In Zeile 5 Mappen wir unsere Konfigurationsdatei servers.json in den Container hinein. pgAdmin erwartet die Serverkonfiguration im Ordner /pgadmin4 mit dem Dateinamen servers.json.

Nun können wir unsere Konfiguration starten. In der Kommandozeile sehen wir auch, dass pgAdmin erst starten, wenn Postgres in den “healthy” Status wechselt. Wenn wir nun die pgAdmin Web-Oberfläche öffnen, gelangen wir direkt, ohne Anmeldung, hinein. Auch der Postgres Server ist für uns bereits konfiguriert.

docker compose up -d
dotnet run

pgAdmin ohne Anmeldung mit Serverkonfiguration

docs