Willhöft IT-Beratung
  • Zur Startseite
  • Zum Blog-Archiv
Docker
03. Jun 2016

Tipps und Tricks für Docker und docker-compose auf einem Produktions-Server

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 Dienst braucht mehrere Container, z.B. für SSL-Terminierung, Webapp und Datenbank. Diese müssen sich per Netzwerk erreichen und brauchen einen sprechenden Namen.
  • Es müssen lokale Eigenheiten konfiguriert werden, die nicht aus der Versionskontrolle kommen, z.B. IP-Adresse an die gebunden werden soll.
  • Häufig möchte man mehrere Instanzen einer Container-Gruppe nebeneinander laufen lassen, z.B. die produktive Version und eine neuere Version bei einem Blue-Green-Deployment.
  • Dem Betriebssystem muss der Dienst so bekannt gemacht werden, dass er beim Systemstart zur richtigen Zeit hochfährt sowie automatisch zu gegebenen Anlässen und manuell gestoppt und gestartet werden kann. Außerdem sollten Status und Logs mit Betriebsystemmitteln überwacht werden können.

Docker Compose

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

Daten persistieren: Anbindung von Volumes

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.

Lokale Konfiguration

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

Verknüpfung mit systemd

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:

  • Rocket.Chat-Container stoppt
  • Docker-Daemon stoppt
  • Docker wird aktualisiert
  • Docker-Daemon startet
  • Rocket.Chat-Container startet

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.

Zusammenfassung

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.

Impressum



Telefon:
Telefax:
E-Mail:

Vertreten durch

Registereintrag

Registergericht:
Registernummer:

Umsatzsteuer-ID
USt.-Identifikationsnummer 
gemäß §27a USt.-Gesetz:

Angaben zur Berufshaftpflichtversicherung
Name und Sitz der Gesellschaft:


Geltungsraum der Versicherung: