Mit Docker-Containern bringt man die Konfiguration beliebiger Dienste unter Versionskontrolle und sowohl auf einem Entwicklungs-Notebook als auch auf einem produktiven Server in Windeseile zum Laufen. Tools wie docker-compose erleichtern dabei die Handhabbarkeit und den Betrieb mehrerer zusammenhängender Docker-Dienste. In diesem Artikel zeigen wir, am Beispiel von Rocket.Chat wie es funktioniert und wie man die üblichen Schwierigkeiten überwindet, speziell beim Dienst-Manager systemd.
Um einen mit Docker entwickelten Dienst zu starten, braucht man in der Regel nur eine Arbeitskopie der Quellen und zwei Befehle, z.B.
git clone myService@github.com; cd myService
docker build --tag myServiceImage .
docker run --name myServiceContainer myServiceImage
Im Einzelfall ergeben sich jedoch meistens folgende Herausforderungen:
Der Wunsch mehrere Container zu orchestrieren, also komfortabel und kontrollierbar miteinander zu verknüpfen, ist sicher so alt wie Container selbst. Daher haben zahlreiche Firmen eigene Framework umgesetzt. Insbesondere ist hier Googles Kubernetes zu nennen, aber auch weniger ambitionierte Ansätze wie MaestroNG und DEIS oder noch ambitioniertere wie CoreOS. Mittlerweile existiert jedoch mit Docker Compose ein Werkzeug, das direkt von den Docker-Entwicklern kommt, und das zahlreiche Anwendungsfälle abdeckt.
Für die meisten Betriebssysteme muss Compose separat installiert werden, es reicht dafür der Download des entsprechenden Binaries.
Zunächst kann man sämtliche Build- und Run-Parameter für alle Container, die
zusammen eine Einheit bilden, in eine übersichtliche YAML-Konfigurationsdatei
docker-compose.yml
eintragen. Im diesem Beispiel setzen wir einen
Chat-Dienst aus einem SSL-Proxy, der eigentlich NodeJS-Anwendung und der
Persistenz-Schicht MongoDB zusammen, jeweils in einem eigenen Container. Dabei
können Images aus einem Repository wie dem DockerHub verwendet werden, genauso
wie Images die erst aus einem lokalen Dockerfile gebaut werden (speziell bei
einer selbst entwickelten Anwendung).
version: "2"
services:
ssl_proxy:
build: ./caddy # Das Image für diesen Container wird aus ./caddy/Dockerfile gebaut
ports:
- "80:80" # Diese Ports sind von AUSSERHALB der Komposition erreichbar.
- "443:443"
volumes: # Volume-Pfade können hier relativ zum docker-compose.yml
# angegeben werden!
- ./caddy/Caddyfile:/etc/Caddyfile
rocketchat: # Auch fertige Images vom Dockerhub können verwendet werden:
image: rocketchat/rocket.chat:0.20.0
environment:
- PORT=3000
- MONGO_URL=mongodb://mongo:27017/rocketchat # Hostname = Names des anderen Containers!
mongo:
image: mongo
command: mongod --smallfiles --oplogSize 128 # Überschreibt das Default-Kommando des Images
volumes:
- ./mongo-configdb:/data/configdb
Die Struktur des Compose-File hat mit docker-compse
1.6 im Februar 2016
einen Versionssprung auf 2
gemacht. Erst jetzt können Volumes und Netzwerke direkt verwaltet werden. Wir beziehen uns durchgehend auf Format-Version 2.
Ein einziger Befehl reicht, um die gesamte Komposition zum Laufen zu bringen:
docker-compose up # ctrl-c beendet alle Container!
Docker Compose lädt ggf. Images nach (wie docker pull
) und baut fehlende
Images (wie docker build
), dann werden drei Container angelegt und
gestartet. Die Ausgabe aller Container wird im Vordergrund angezeigt.
Alternativ kann man die Komposition im Hintergrund hochfahren und Ausgabe die
separat betrachten, auch von einzelnen Containern:
docker-compose up -d
docker-compose logs # ctrl-c beendet die Loganzeige
docker-compose logs rocketchat # Ausgabe nur eines Containers
docker-compose down # Container stoppen UND löschen
In der oben skizzierten Komposition sind die Anwendungsdaten genauso flüchtig wie die Container. Um den Dienst tatsächlich zu betreiben, müssen die Daten stattdessen in ein Volume geschrieben werden, das von der Komposition unabhängig ist. Sog. Named Volumes werden angelegt mit:
docker volume create --name rocketchat-app-data
docker volume create --name rocketchat-mongo-data
In docker-compose.yml
werden die Volumes auf oberste Ebene deklariert und dann je Container eingebunden:
version: "2" # ACHTUNG Volumes erst ab Version 2 so einbinden!
services:
ssl_proxy:
build: ./caddy
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/Caddyfile # Datei wird vom Host gemountet, kein Volume!
rocketchat:
image: rocketchat/rocket.chat:0.20.0
environment:
- PORT=3000
- MONGO_URL=mongodb://mongo:27017/rocketchat
volumes:
- app-data:/app/upload # Kompositions-interner Bezeichner
mongo:
image: mongo
command: mongod --smallfiles --oplogSize 128
volumes:
- ./mongo-configdb:/data/configdb
- mongo-data:/data/db # Kompositions-interner Bezeichner
volumes:
app-data: # Kompositions-interner Bezeichner (s. services:*:volumes)
external:
name: rocketchat-app-data # Systemweiter Bezeichner (s. zuvor docker volume create)
mongo-data:
external:
name: rocketchat-mongo-data
Um Named Volumes in einem normalen, Compose-unabhängigen, Container zu mounten, z.B. während eines Backups, können normale Docker Bordmittel verwendet werden. Grundsätzlich kann ein Volume von mehreren Containern gleichzeitig gemountet werden. Man sollte sich aber im Einzelfall über die Konsequenzen im Klaren sein.
$ docker volume ls
DRIVER VOLUME NAME
local rocketchat-mongo-data
$ docker run -it --rm --volume rocketchat-mongo-data:/mnt busybox ls /mnt
Im übrigen gilt auch für die von Compose verwalteten Container, dass sie mit
den normalen Docker Befehlen exec
, stop
usw. manipuliert werden können.
Es ist nur zu berücksichtigen, dass Compose dem Containernamen Präfix und
Suffix hinzufügt.
Häufig, gerade auf dem Server möchte man einige Parameter ändern, ohne die
versionskontrollierte Datei docker-compose.yml
zu bearbeiten. Eine sehr
flexible Möglichkeit, beliebige Werte überschreibbar zu machen, sind die guten alten Umgebungsvariablen, die wir in Compose-Konfigurationen verwenden können. Wir erweitern docker-compose.yml
einmalig um
einige Variable-Auswertungen:
version: "2"
services:
ssl_proxy:
build: ./caddy
ports:
- "${IP}:80:80"
- "${IP}:443:443"
volumes:
- ./caddy/Caddyfile${CADDYFILE_SUFFIX}:/etc/Caddyfile
rocketchat:
image: rocketchat/rocket.chat:0.20.0
environment:
- PORT=3000
- ROOT_URL=https://chat.willhoeft-it.com
- MONGO_URL=mongodb://mongo:27017/rocketchat
- MAIL_URL=${MAIL_URL}
volumes:
- app-data:/app/upload
mongo:
image: mongo
command: mongod --smallfiles --oplogSize 128
volumes:
- ./mongo-configdb:/data/configdb
- mongo-data:/data/db
volumes:
mongo-data:
external:
name: ${VOLUME_PREFIX}rocketchat-mongo-data
app-data:
external:
name: ${VOLUME_PREFIX}rocketchat-app-data
Jetzt können IP-Adresse, Verbindung zum ausgehenden Mailserver, verwendete Konfigurationsdatei und insbesondere die gemounteten Datenvolumes je Aufruf übersteuert werden:
IP=192.168.178.26 \
CADDYFILE_SUFFIX=-prod \
MAIL_URL=smtp://rocketchat%40example.com:topsecretPassWd@smtp.example.com" \
VOLUME_PREFIX=green- \
docker-compose up -d
An dieser Stelle fehlt nur noch ein Baustein um mehrere Kompositions-Instanzen
nebeneinander laufen zu lassen, die unterschiedlich aber natürlich auch ähnlich
konfiguriert sein können. Mit der Umgebungsvariable COMPOSE_PROJECT_NAME
kann der Projekt-Bezeichner, der normalerweise gleich dem aktuellen
Verzeichnisnamen ist, explizit gesetzt werden. Alle Compose-Kommandos beziehen
sich dann auf die so benannte Instanz. Im Rahmen eines Blue-Green-Deployments
könnte man also eine zweite Instanz z.B. basierend auf einem neu gebauten Image
starten, die eine separate Datenbasis besitzt, z.B. aus einem Backup,
und diese an eine andere IP binden:
COMPOSE_PROJECT_NAME=deploy2016-04-04 \
IP=192.168.178.46 \
CADDYFILE_SUFFIX=-prod \
MAIL_URL=smtp://rocketchat%40example.com:topsecretPassWd@smtp.example.com" \
VOLUME_PREFIX=blue- \
docker-compose up -d
Die großen Serverdistributionen Debian/Ubuntu, RedHat/Fedora/CentOS und SUsE
haben ihre Init-System weitgehend auf systemd umgestellt (mit unterschiedlichem
Fortschritt und/oder Zähneknirschen). Daher soll hier kurz gezeigt werden, wie
man mit diesem Prozessmanager Container-Kompositionen konfigurieren sowie
automatisch und manuell starten und stoppen kann. Zentral ist dabei das
Unit-File, hier docker-rocketchat.service
:
[Unit]
Description=Rocket.chat Docker Container
After=network.target docker.service
Requires=docker.service
[Service]
# override this section in
# /etc/systemd/system/docker-rocketchat.service.d/setup.conf
RestartSec=10
Restart=always
Environment="IP=127.0.0.1"
Environment="VOLUME_PREFIX=test-"
Environment="CADDYFILE_SUFFIX="
Environment="MAIL_URL=smtp://user:pass@smtp.mailprovider.org"
Environment="PROJECT_PATH=/path/to/local/clone/of/wit-rocketchat"
ExecStart=/usr/local/bin/docker-compose --file "${PROJECT_PATH}/docker-compose.yml" up
ExecStop=/usr/local/bin/docker-compose --file "${PROJECT_PATH}/docker-compose.yml" stop
[Install]
WantedBy=docker.service
Wie zu erwarten war, hängt unser Dienst vom Docker-Daemon ab. Etwas
überraschend wirkt die letzte Zeile, da sie wie eine zyklische Abhängigkeit
aussieht. Docker erwünscht den Start der Rocket.Chat-Komposition? In Foren liest
man hier häufig WantedBy=multi-user.target
, was problematisch ist, da hier
ein Neustart des Docker-Daemon nicht die Komposition neustartet! Im
Gegensatz dazu sorgt WantedBy=docker.service
z.B. bei einem Docker-Update
für folgenden Ablauf:
Die Umgebungsvariablen für die Container könnten direkt im Unit-File bearbeitet
werden, wir ziehen es jedoch vor, Anpassung in
/etc/systemd/system/docker-rocketchat.service.d/setup.conf
zu machen, so
dass das Unit-File normal versionskontrolliert und verteilt werden kann.
[Service]
Environment="IP=192.168.178.34"
Environment="VOLUME_PREFIX=prod-"
Environment="CADDYFILE_SUFFIX=-prod"
Environment="MAIL_URL=smtp://rocket:passwd@smtp.example.com"
Environment="PROJECT_PATH=/opt/rocketchat"
Sind die Umgebungsvariablen angepasst, muss das Unit-File noch mit bekannt gemacht werden:
sudo systemctl enable /pfad/von/docker-rocketchat.service
sudo systemctl daemon-reload
Anschließend kann die Komposition als Einheit per systemd gesteuert werden:
sudo systemctl start docker-rocketchat.service
sudo journalctl -fu docker-rocketchat # Ausgabe anzeigen
Anmerkung: Mit Hilfe von Service-Instanzen lassen sich sogar wie oben skizziert mehrere Kompositions-Instanzen mit systemd verwalten.
Docker Compose ist ein sehr praktisches Werkzeug um mehrere Container zu verwalten die als Einheit einen Dienst anbieten. Für sehr große Produktionssysteme sollte man sich sicher Kubernetes oder Docker Swarm anschauen. Aber für kleinere und mittelgroße Installationen oder in einer Microservices-Umgebung kommt man mit Compose sehr weit, ohne sich an zusätzlich Drittframeworks zu binden.