Türchen 13: Docker, docker-compose und andere Abkürzungen

Dec
13
2017

Denken wir ein paar Jahre zurück, dann gab es dort nicht nur einen Weihnachtsartikel zu Docker, sondern auch einen Hype um Buzzwords wie “Dockerize your infrastructure” und ähnlichem. Als Stalker von Docker, seit seinem Beginn, kann ich heute sagen: “Es ist mehr als reif für unsere Branche!”. Dank “docker-compose” und diesen Tricks aus meinem Workflow ist es nicht mehr so ein Krampf wie vor Jahren noch.

Wie üblich haben wir im Stammverzeichnis von unserem Projekt eine “docker-compose.yml” liegen und eine “Dockerfile”. Damit alle bescheid wissen, muss ich vielleicht kurz sagen was die docker-compose.yml tolles kann. Kurz an der Oberfläche kratzen als Intro und im Anschluss geht es an die Arbeitserleichterungen / Abkürzungen und das mächtige Yaml.

docker-compose simuliert Cluster

Was das Tool docker-compose macht, fasse ich gern als “Cluster simulieren” zusammen. Fangen wir mit einem einzelnen Server bzw. “Container” an:

services:
  php:
    image: php:7.2-nginx
    volumes:
      - ".:/var/www/html"
    ports:
      - "80:80"

Hier wird ein Container erstellt, welcher mit PHP 7.2 läuft und das Ganze über nginx ausliefert (siehe “image”). Kombinationen mit Apache oder als FPM sind ebenfalls möglich (siehe PHP Repository im Docker Hub).
Dem Container können über “volumes” einzelne Verzeichnisse freigegeben werden. In diesem Beispiel wird das Stammverzeichnis von unserem Projekt komplett in den üblichen Document-Root vom Container geladen. Erreichen können wir das unter http://localhost dank dem Port-Forwarding wie unter “ports” (Host-Port:Container-Port) zu sehen. Wer Port 80 bereits belegt hat, kann per “61337:80” oder anderen Nummern auf so etwas wie http://localhost:61337 umleiten.

docker-compose up -d

Das ist der Befehl, mit dem wir Docker alle Container aus der docker-compose.yml starten lassen. Wer nun seine Seite, egal ob Magento, WordPress oder tolle Symfony-Sachen, unter localhost im Browser aufruft, der läuft damit vor die Wand, denn eines fehlt nocht: ein Datenbankserver. Hier nimmt uns docker-compose viel Arbeit ab mit diesen zusätzlichen Zeilen in der docker-compose.yml:

  db:
    image: mariadb:latest
    links:
      - php

Fertig. Nach einem erneuten `docker-compose up -d` hat die PHP-Instanz einen SQL-Server hinzubekommen.

Jetzt merken wir auch was die Vorteile von docker-compose sind und wie sehr es uns bei Anwendungen hilft, die in einem Cluster sind. Wir könnten noch viele weitere Container hinzufügen, wie zum Beispiel Elasticsearch-Server oder einen Varnish-Server und vor dem Deployment lokal testen, ob unser Magento damit zurecht kommt. Besser noch: Mit der docker-compose.yml und der Dockerfile im Repo nutzt jeder Entwickler die gleiche Grundlage (PHP, DB, …) und simuliert das komplette Cluster bei sich lokal.

Was es jetzt nur noch braucht, ist eine Person mit Docker und docker-compose Expertise im Team, welche weiß wie diese Yaml-Dateien gebaut werden: https://docs.docker.com/compose/compose-file/
Dann sind die Tage von “aber bei mir geht es” vorbei.

Basics für die klassichen Images

Tweaken wir das ganze noch ein wenig. Seit 2 Jahren circa nutze ich ausschließlich Docker in meinem Workflow, um (ähnlich Vagrant) direkt den Zielserver zu simulieren. Heute könnte ich mir kaum etwas anderes vorstellen. Vor allem und gerade erst mit den folgenden Erfahrungen wurde daraus ein brauchbarer Workflow.

Composer schneller machen

Composer ist nicht mehr wegzudenken aus der PHP-Welt. Wir wissen auch alle, dass Composer nach dem Auflösen der Pakete diese entweder im Cache hat oder neu herunterladen muss. Was uns auch zu dem Hauptproblem von Docker-Containern bringt. Ein Container ist so kurzlebig, dass Composer sehr sehr oft die Pakete immer wieder neu herunterladen muss. Denn sobald wir unseren Host / den eigenen Rechner oder den Container neu starten ist der Composer Cache weg, außer …

services:
  # ...
  php:
    # ...
    volumes:
      - "$HOME/.cache/composer:/var/composer"
    environment:
      COMPOSER_CACHE_DIR: /var/composer

… wir mounten den Cache vom Host in den Container rein. In unserem Home-Verzeichnis lebt der Cache gefühlt ewig und über “COMPOSE_CACHE_DIR” teilen wir dem Composer im Container mit, wo er den Cache findet. Schon kann “composer install” um einiges schneller im Container durchlaufen.

IPs helfen von Namensauflösung über Workflow bis XDebug

Vorhin haben wir den Port 80 vom Container auf unseren Host übertragen, damit die Seite per “localhost” erreichbar ist im Browser. So leicht das auch ist, so sehr stört es im Arbeitsalltag. Das Erste was stört ist, dass Projekte sich gegenseitig blockieren – über den Tag verteilt arbeite ich an mehreren Projekten und alle wollen an Port 80 ran. Immer brav Container vom vorherigen Projekt abschalten, damit die Container vom nächsten Projekt wieder Port 80 belegen können nervt. Deswegen bekommt jedes Projekt seinen eigenen IP-Raum:

services:
  php:
    # ...
    networks:
      default:
        ipv4_address: 10.16.35.10
  db:
    # ...
    networks:
      default:
        ipv4_address: 10.16.35.20

networks:
  default:
    ipam:
      config:
      - subnet: 10.16.35.0/24

Der letzte Teil ist am Interessantesten, denn dort wird für diese “docker-compose.yml” ein eigenes Netzwerk etabliert. IPs die mit “10.” starten sind per Definition privat, also keine Sorge vor Kollisionen mit Public-IPs. Dann nehme ich gerne die Uhrzeit, es ist also 16:35 während ich diesen Aritkel schreibe, was die IP etwas “zufällig” macht. So ist es selten, dass zwei Projekte miteinander kollidieren. Das 24er Netz erlaubt uns den Containern eine beliebige IP zwischen “10.16.35.0” bis “10.16.35.254” zu vergeben, wie an “php” und “db” zu sehen.

Das bringt einige Vorteile mit sich:

  • Die WebApp ist per http://10.16.35.10 erreichbar
  • Eine feste IP können wir in unsere Hosts-Datei schreiben (für schöne URLs wie “http://mein-magento.local”)
  • Die Datenbank kann über 10.16.35.20 erreicht werden (supertoll mit dem DB-Tool von phpStorm!)
  • Ich brauche den “links”-Eintrag in “docker-compose” nicht mehr (vgl. oben)
  • XDebug funktioniert endlich vernünftig

XDebug per ENV einrichten

Letzteres aus der Liste kann nun mit nur 1-2 Umgebungsvariablen eingerichtet werden:

services:
  php:
    environment:
      XDEBUG_CONFIG: remote_host=10.16.35.1 idekey=PHPSTORM remote_connect_back=1 remote_enable=1 profiler_enable_trigger=1
      PHP_IDE_CONFIG: "serverName=localhost"

Hier wird deutlich, dass der Host auch immer im Netz drin hängt und die “1” belegt. Deswegen darf kein Container die “1”(also: 10.16.35.1) belegen. Wohin PHP (hier: 10) und DB (hier: 20) kommt ist Geschmacksache.

Und das beste zum Schluss: Wenn wir per `docker-compose exec php bash` in den PHP Container rein gehen, dann findet ein `ping db` den anderen Container nun. Das macht das Kommunizieren der Container untereinander viel einfacher! Der “links”-Eintrag aus der anfänglichen docker-compose.yml kann dadurch komplett entfallen, weil alle Container in diesem Netzwerk sich bereits untereinander kennen.

Mehr Kontrolle über die Daten(-bank)

Etwas anderes fieses an Docker ist, dass es über Zeit viel Platz wegnimmt. Üblicherweise wird ein Projekt aus dem GIT geklont und dessen Verzeichnis wieder gelöscht, wenn es nicht mehr benötigt wird. Docker hortet jedoch noch an ganz anderer Stelle lauter Daten über dieses Projekt und ich möchte nicht immer aus dem Labyrinth von /var/docker diese Daten herauspopeln und so das Projekt final von der Platte löschen. Da sich hier schnell einige GigaByte ansammeln ist es besser alle aufgeblähten Daten im Projektverzeichnis kontrollieren zu können:

services:
  # ...
  db:
    volumes:
    - ".docker/db:/var/lib/mysql"

Es ist immer eine gute Idee ein Unterverzeichnis “.docker” zu haben, in dem Daten liegen die mit dem Ende vom Projekt ebenfalls (lokal) weggeschmissen werden können. So lohnt es sich auch noch andere Daten zwischen Neustarts am Leben zu erhalten oder diese mit dem Container zu teilen:

und was sonst noch je Projekt wichtig erscheint.

Die docker-compose.yml kann mehr (nicht)

Docker Compose bringt noch ein zusätzliches Feature in die Yaml-Dateien rein: Variablen. Das gab es bereits bei “environment” zu sehen und kann überall in der Yaml genutzt werden.

services:
  php:
    image: $CI_IMAGE
    environment:
      COMPOSER_CACHE_DIR: $COMPOSER_CACHE_DIR
      SYMFONY_ENV: "$CI_ENV"

Besonders mächtig für alle, die Ihren Workflow bereits auf Dotenv (.env) aufgebaut haben. Das beste Beispiel hierfür ist das angesprochene Composer-Cache-Verzeichnis, was wir als “$HOME/.cache/composer” festgelegt haben. An dieser Stelle ist bestimmt aufgefallen, dass es für unsere Windows-Kollegen nicht funktioniert, da deren Pfade oft “C:\…” sind. Stattdessen könnten wir das Volume per “$COMPOSER_CACHE_DIR:/var/composer” einbinden, was jeder Entwickler in seiner Shell oder seiner “.env” individuell verwalten kann.

Einige Features von Yaml-Dateien vermisse ich allerdings noch, wie zum Beispiel Merge, was sehr hilfreich für mehrere Container wäre, die sich im Environment gleichen. Auch muss ich leider sagen, dass die Variablen in der Yaml keine Prameter-Substitution (z.B. “${VAR-defaultWertWennNichtGesetzt}” und Co) mitmachen.

IMHO

Der Yaml-Parser von Docker hat also noch einiges nachzuholen. Was sich früher in umständlichen Docker-Befehlen verlief, kann seit docker-compose recht leicht gelöst werden. Für das Entwickeln von WebApps ist es reif genug und vor allem im Team lohnt es sich richtig, um dieses lästige “Aber bei mir geht’s” loszuwerden. Vor allem beim Deployment möchte keiner den Live-Server abschießen.

Mich hat es Wochen gekostet, um diese alltäglichen Probleme in den Griff zu bekommen und hier gibt es dieseinnerhalb von Minuten hinter dem 13ten Türchen. Es bleibt nie aus, dass der Anfang etwas schwer ist und vereinzelt Probleme auftauchen, bei denen `docker-compose logs` hoffentlich hilft. Wenn nicht dann einfach Bescheid sagen 🙂

Schöne Feiertage!

Leave a Reply

Your email address will not be published. Required fields are marked *