Bei nine.ch entwickeln wir viele Tools zur Automatisierung unseres Unternehmens und zur effizienten Interaktion mit unseren Kunden. Diese Dienste kommunizieren normalerweise mit mindestens einem anderen Dienst. In den meisten Fällen müssen sie auf Daten in einer Datenbank zugreifen oder Daten mit einem anderen Dienst austauschen.

Solche Programme zu entwickeln ist eine Herausforderung, denn idealerweise laufen alle abhängigen Dienste lokal auf dem Gerät der Entwicklerin oder des Entwicklers. Und deren abhängige Dienste wiederum auch, und so weiter.

Bisher setzten wir auf ein eher komplexes Setup basierend auf mit Vagrant verwalteten virtuellen Maschinen, die mit Puppet aufgesetzte wurden, sowie Boxen für lokale Abhängigkeiten wie Datenbanken. Dies einzurichten und zum Laufen zu bringen nahm normalerweise einige Zeit in Anspruch und war fehleranfällig. Zunächst einmal ging häufig vergessen, alle abhängigen Dienste zu starten. Aber auch die virtuellen Maschinen und unsere Puppet-Scripte immer auf dem neuesten Stand zu halten war, gelinde gesagt, kein grosses Vergnügen. Meist dauerte die Konfiguration der VMs viel Zeit. Und all zu oft hatte ein Update von macOS zur Folge, dass danach einige der Dienste nicht mehr recht funktionierten.

All dies führte zu einer Situation, in der es ständig Unterschiede zwischen den Setups der einzelnen Entwickler gab. Ein ums andere Mal wurden diese Unterschiede so gravierend, dass ein Setup, das auf dem Computer eines Entwicklers funktionierte, bei jemand anderem nicht auf die gleiche Weise funktionierte. (Und ganz bestimmt funktionierte es auf dem Jenkins-Server nicht auf die gleiche Weise).

Unser neues Setup basiert jetzt fast ausschliesslich auf Docker und Docker Compose. Obwohl wir diese Technologien gegenwärtig nicht für unsere Produktionsdienste verwenden, konnten wir viele Problempunkte in der Entwicklung beseitigen. In den nächsten Abschnitten wird detailliert beschrieben, wie unsere neue Entwicklungsumgebung aufgebaut ist, sodass unsere Ruby-basierten Anwendungen effizient und zuverlässig entwickelt werden können.

Obwohl wir dabei Ruby im Hinterkopf hatten, beschränkt sich die aktuelle Lösung keineswegs nur auf die Ruby-Entwicklung. Sie lässt sich auch auf die meisten anderen Softwarestacks anwenden, wie z. B. Python, Java oder sogar PHP. Das Setup kann auf Linux mit der nativen Docker-Version implementiert werden und auf macOS mit Docker for Mac und höchstwahrscheinlich auch auf Windows mit Docker for Windows. Wir verwenden jeweils die Docker Community Edition.

Managed Umgebungen

Wir hofften, durch den Aufbau eines neuen Entwicklungssetups u. a. die Einarbeitungszeit für neue Entwickler verkürzen, komplexe Abhängigkeiten zwischen unseren Diensten mit Leichtigkeit testen zu können und die Anzahl der Reibungspunkte zwischen den (normalerweise macOS-basierten) Entwicklergeräten und unseren Staging- und Produktionssystemen (die alle Linux-basiert sind) zu reduzieren.

Um unser Ziel zu erreichen, identifizierten wir drei kritische Stellen, die wir angehen wollten:

  • Sich unterscheidende Softwareversionen und -konfigurationen auf unterschiedlichen Systemen

  • Verwaltung aller notwendiger Abhängigkeiten

  • Unterschiedliche Plattformen

All dies muss umgesetzt werden, ohne die Effizienz und etablierte Entwicklungstechniken zu beeinträchtigen (z. B. Nutzung der Hot Redeployment-Funktion von Frameworks, die dies unterstützen).

Als wir anfingen, nach einer Lösung für diese Herausforderungen zu suchen, hatte Docker den absoluten Höhepunkt seiner Hype-Phase bereits überschritten und entwickelte sich langsam zu einem zuverlässigen und anerkannten Produkt. Kein Mitglied unseres Teams hatte zuvor intensiv und professionell mit Docker gearbeitet. Die meistens hatten noch nicht einmal ein einziges Dockerfile geschrieben. Das war vor ungefähr einem Jahr und sollte sich schnell ändern.

Die Lösung

Wir starteten, indem wir verschiedene Arten und Patterns zum Erstellen unserer Dockerfiles erkundeten und damit experimentierten. Die meisten von uns fingen an, Blogeinträge über Docker zu lesen, in denen dies und jenes geraten oder abgeraten wurde, und schliesslich auch ein oder zwei Bücher über das Thema. Wir hatten dann eine hitzige Diskussion über die “ultimative Art und Weise“, unsere Projekte zu strukturieren. Schliesslich einigten wir uns auf unsere einfache aber leistungsstarke Lösung:

  • docker-compose build --pull muss die aktuelle sich in der Entwicklung befindliche Anwendung in ein Docker Image packen.

  • docker-compose up app startet den aktuellen Dienst und all seine Abhängigkeiten. Es muss kein zusätzlicher Quellcode ausgecheckt werden. Wenn die “app“ gestartet wurde, wird alles, was die Anwendung benötigt, bereits vorhanden und initialisiert sein.

  • docker-compose -f docker-compose.test.yaml run --rm app startet nur das absolute Minimum dessen, was für die Durchführung der Tests erforderlich ist, und führt alle Tests durch. Der Rückgabecode “Null“ deutet auf einen erfolgreich durchgeführten Test hin. Wird ein anderer Beendigungscode als “Null ausgegeben, deutet das auf fehlgeschlagene Tests hin.

  • Wir führen unsere Dienste in der Produktion noch nicht mit Docker aus, “kleine“ und “optimierte“ Docker Images sind also noch nicht unsere Priorität. Aber jeder Dienst muss in einem eigenen Dockerfile eingeschlossen sein und pro Docker-Container ist nur ein laufender Prozess zulässig.

Um die oben aufgeführten grundlegenden Regeln erfolgreich umzusetzen, haben wir uns auf einige andere Regeln verständigt:

  • Die Hauptapplikation, d. h. diejenige, die sich gerade in der Entwicklung befindet, wird stets app genannt.

  • Die app definiert ihre unmittelbaren Abhängigkeiten. Jede dieser Abhängigkeitein gibt wiederum ihre eigenen Abhängigkeiten an, und so weiter.

  • Jenkins sucht jeweils nach einer Datei namens docker-compose.test.yaml und führt, wenn vorhanden, diese anstelle der normalen Tests aus.

  • Besteht ein Dienst aus mehreren Prozessen, wird dasselbe Image mehrmals mit unterschiedlichen Befehlen ausgeführt. Nicht vergessen: Ein Prozess pro Container!

  • Zur Konfiguration sollen Umgebungsvariablen verwendet werden.

  • Falls Datenbanken initialisiert werden müssen, geschieht dies automatisch.

  • Falls der erfolgreiche Start eines Dienstes von einem anderen Dienst abhängt, wie zum Beispiel einer Datenbank oder einem Message Broker, wartet der erste Dienst, bis der andere Dienst verfügbar ist, bevor er sich selbst startet.

Versionen und Konfigurationen

Die grösste Herausforderung für uns war wahrscheinlich, die Versionen der Ausführungsumgebung, Bibliotheken und externen Abhängigkeiten wie Datenbanken zu verwalten.

Bisher verwendeten wir Boxen, um alle Dienste zu verwalten, die auf unseren Rechnern laufen mussten. Unsere eigenen Anwendungen verfügten über ein Procfile um alles zu starten, was zu dieser Anwendung gehörte. Alle weiteren Anwendungen mussten manuell gestartet werden, z. B. jene, die direkt auf unseren Rechnern installiert waren oder jene, die getrennt ausgecheckt werden mussten. Und einige unserer Anwendungen kamen mit einer Vagrant-Maschine, die ggf. ebenfalls gestartet werden musste.

Bevor wir eine zu entwickelnde Anwendung ausführen konnten, mussten wir uns also ständig fragen: Ist meine lokale Maschine auf dem neuesten Stand? Habe ich die aktuellste Version aller abhängigen Dienste ausgecheckt und werden sie ausgeführt? Habe ich alle weiteren Abhängigkeiten aktualisiert und vollständig gestartet?

Heute führen wir einfach docker-compose pull aus und alle benötigten Abhängigkeiten werden mit der richtigen Version abgerufen. Wir geben die Version unserer Abhängigkeiten an und kriegen so immer die erwartete Version. Wenn wir zum Beispiel PostgreSQL mit Version 9.5 benötigen, fragen wir nach postgres:*9.5-alpine* und erhalten exakt diese Version. Und wenn jemand entscheidet, auf PostgreSQL 9.6 zu aktualisieren, erhalten alle Entwickler die neue PostgreSQL-Version, sobald sie die letzten Änderungen des docker-compose.yml-Files aus dem Repository holen. Und neu können sogar PostgreSQL 9.5 und 9.6 parallel laufen! Dies nutzen wir beispielsweise für die Integrationstests unseres Tools nine-manage-databases.

Abhängigkeiten

Die wichtigste Verbesserung ist, dass wir jetzt mit docker-compose up app mit einem Befehl alles starten können. Die meisten unserer Tools können nun so gestartet werden. Eine Ausnahme sind beispielsweise CLI-Tools, also Tools, die nicht permanent laufen. Aber selbst diese Tools können von dem neuen Befehl profitieren.

Schauen wir uns ein Beispiel an: Wir haben ein CLI-Tool namens nine-manage-vhosts, mit dem unsere Kunden auf einfache Weise neue Virtual Hosts erstellen und ein Let’s Encrypt-Zertifikat anfordern können. Bisher konnten wir das Anfordern solcher Zertifikate nur testen, indem wir eine neue Version des CLI-Tools auf einem Server bereitstellten, der aus dem Internet erreichbar war. Nun startet docker-compose up boulder, den offiziellen Let’s Encrypt-Server, in einem Docker-Container. Auf einmal konnten wir den gesamten Vorgang des Anforderns und Widerrufens von Let’s Encrypt-Zertifikaten direkt von unseren Entwicklerrechnern testen. Und dies selbst, wenn unsere Rechner nicht mit dem Internet verbunden waren! Wir konnten sogar Integrationstests schreiben, die einen neuen Vhost erstellen und vom Let’s Encrypt-Testserver in Docker ein Zertifikat anfordern. Diese Tests laufen nun auf unserem CI-Server bei jedem Build.

Wenn wir mit dem alten Setup eine Anwendung entwickelten, die von einer unserer Anwendungen abhing, waren wir gezwungen, den Code dieses Tools auf unseren Rechnern auszuchecken, da es keine andere Möglichkeit gab, diese andere Anwendung zu starten. Das bedeutete auch, dass wir alle Repositorys der Abhängigkeiten aktualisieren mussten, bevor wir die Entwicklung einer einzigen Anwendung starten konnten. Heute erstellen wir die Docker Images für die Tests auf unserem CI-Server und laden diese Images dann in unsere private Docker Registry.

Beim Start einer Anwendung sorgt Docker dafür, dass alle Abhängigkeiten aus der Registry heruntergeladen werden. Aufgrund des aggressiven Cachings von Docker müssen wir uns eigentlich nur noch um die Aktualisierung der Abhängigkeiten kümmern. Dies ist jedoch nur noch das Starten von docker-compose pull, anstatt in alle Repos zu gehen und überall git pull --rebase auszuführen.

Hinweis: Es ist wichtig, immer aktuelle Versionen der Images von Docker Hub auf den CI zu verwenden, damit man immer die neuesten Sicherheitsupdates bekommt. Einfach wie folgt den --pull-Parameter beim Bauen hinzufügen: docker-compose build --pull. Aus dem gleichen Grund sollten auch alle Docker Images regelmässig neu gebauen werden!

Plattformen

Wie bereits in der Einleitung erwähnt, war unsere bisherige Konfiguration sehr spezifisch auf macOS ausgerichtet. Wir versuchten durch Verwendung von Vagrant die Unterschiede zwischen der Linux- und der macOS-Umgebung zu überbrücken. Boxen installierte dafür Abhängigkeiten lokal, die für macOS verfügbar waren, wie zum Beispiel PostgreSQL, MariaDB oder Redis.

Wir starten unsere Anwendungen normalerweise einfach direkt auf macOS, da das Starten einer VM pro Anwendung viel Zeit in Anspruch genommen hätte; dies taten wir also nur dann, wenn es absolut notwendig war.

Oftmals führte diese Art des Entwickelns, also das Ausführen der Apps unter macOS, im besten Fall zu Überraschungen auf unserem CI-Server oder im schlimmsten Fall in unseren Staging- oder gar Produktionsumgebungen. All diese Umgebungen sind schliesslich Linux-basiert.

Aufgrund des aggressiven Cachings von Docker ist der Aufwand dafür, dieselbe Anwendung immer und immer wieder neu zu erstellen, sehr gering, sobald eine Anwendung einmal in ein Image gepackt wurde. Ein Build nimmt normalerweise nicht mehr Zeit in Anspruch als die Aktualisierung der Ruby-Abhängigkeiten; etwas, was wir auf unseren Rechnern sowieso hätten tun müssen. Ausserdem mussten wir früher für einige Abhängigkeiten riesige Bibliotheken, wie QT/Webkit, lokal installiert haben, was viel Platz in Anspruch nahm und welche auch regelmässig aktualisiert werden mussten.

Heute führen wir unsere Anwendungen kaum noch direkt auf macOS aus. Es ist einfach zu aufwändig, sicherzustellen, dass alle Abhängigkeiten aktuell und installiert sind und laufen.

Effiziente Entwicklung

Wir haben nun viel darüber geschrieben, wie wir die Vorbereitsungszeit verkürzen können, bevor wir mit der Entwicklung einer neuen Anwendung beginnen (oder diese fortsetzen) können. Aber wie bereits in der Einleitung erwähnt, möchten wir trotz all der Vorzüge, die sich für die Startphase anbieten, die Effizienz bei der Entwicklungsarbeit per se nicht opfern. Der Grund dafür ist simpel: Wir benötigen normalerweise mehr Zeit für das Umsetzen einer neuen Funktion als für die Vorbereitung zum Entwickeln der neuen Funktion.

Eine der wichtigsten Funktionen für uns ist die Möglichkeit, Code zu ändern und die Auswirkungen unmittelbar in unserer Anwendung zu sehen, ohne die Anwendung manuell neu laden oder neu starten zu müssen. Ruby on Rails kann die Anwendung automatisch neu laden, wenn bestimmte Dateien geändert werden. (Solche Konzepte sind ebenfalls bekannt als Hot Reload und auch in anderen Programmiersprachen und Frameworks verfügbar.)

Wenn Ruby on Rails in einem Docker-Container läuft, hat es standardmässig keinen Zugriff auf die Originaldateien und würde somit auch nicht bemerken, wenn sich diese ändern. Um dies zu ermöglichen, müssen wir das Konzept hinter den Volumes von Docker verstehen. Volumes können überall in einem Docker-Container gemountet werden. Es gibt viele Arten von Volumes, aber für diesen Fall bieten sich besonders gemountete Volumes an. Diese sind im Grunde ein Ordner, der aus dem Hostsystem in den Container eingebunden wird. Anders gesagt: Die Dateien in diesen gemounteten Ordnern werden zwischen dem Hostsystem und dem Docker-Container synchronisiert. Wenn sich eine Datei also im Original-Dateisystem änder, wird diese Änderung auch im Docker-Container durchgeführt, und umgekehrt.

Die Nutzung dieser gemounteten Ordner ermöglicht es uns, den Code der Anwendung ausserhalb vom Container zu ändern und die Änderungen im Container unmittelbar zur Verfügung gestellt zu bekommen.

In der Showcase-Anwendung findet man ein Beispiel für solche Mounts.

Stolperfallen

Es wurde bereits erwähnt, dass sich Docker in den letzten Jahren rapide verändert hat und sich erst vor Kurzem langsam stabilisiert hat. Für uns bedeutet das, dass es hier und dort immer noch ein paar kleine Tücken gibt. Ein paar davon sind es wert, erwähnt zu werden:

Vorsichtig Mounten

Oft stiessen wir auf das Problem, das etwas von ausserhalb des Containers im Container gemountet wurde und dort wichtige Dateien überschrieb.

Ein Entwickler startete zum Beispiel auf seinem Rechner Rails. Später startete er dieselbe Rails-Anwendung auch mit Docker. Dabei beachtete er aber nicht, dass das lokale Verzeichnis in den Docker-Container gemountet wird. Die Rails-Anwendung im Docker-Container wurde nicht gestartet, da sie eine PID-Datei der Anwendung fand, die ausserhalb des Containers lief. Dies kam unerwartet für den Entwickler, und er musste die gemounteten Pfade anpassen.

Was wir daraus gelernt haben: Wir müssen vorsichtig mounten und nur das, was wir benötigen!

Probleme bei gemounteten Volumes in Docker für Mac

Wenn ein Mount aus vielen Dateien bestand, z. B. wenn er das log-Verzeichnis enthielt, war die Synchronisierung zwischen dem Hostsystem und dem Container sehr langsam. Dies hatte drastische Auswirkungen auf die I/O-Performance im Container und die Anwendung im Container wurde extrem langsam. (Das Laden einer Seite, für das normalerweise wenige Millisekunden benötigt wurden, dauerte auf einmal mehrere Sekunden.)

Wir lernten daraus, nicht blind alle Ordner in einer Anwendung zu synchronisieren, sondern nur die erforderlichen. (Hauptsächlich die Ordner, die Quellcode und Objekte enthalten.)

Hinweis: Dies ist ein bekanntes Problem von Docker für macOS und Windows, das bei Docker in Linux jedoch nicht vorkommt.

Lernprozess

Für die meisten Teammitglieder war es nicht trivial, sich in Docker und seine Konzepte einzuarbeiten. In ein paar Jahren gilt dies wahrscheinlich nicht mehr, aber gegenwärtig kann man von einem Software-Engineer nicht erwarten, bereits mit Docker gearbeitet zu haben.

Und obwohl die Dokumentation sich erheblich verbessert hat, bleibt es am Anfang immer noch schwierig, die Konzepte zu begreifen.

Best Practices

Es wurde bereits ein paar Mal erwähnt, dass Docker eine junge Technologie ist, die sich immer noch rapide verändert. Wir haben ein paar “Best Practices“ erarbeitet, die sich bewährt haben, die meisten wurden in diesem Artikel erwähnt. Ausserdem pflegt Docker eine Reihe von Best Practices zum Schreiben von Dockerfiles.

Nicht alle Ratschläge aus Blogeinträgen sind gültig bzw. noch gültig. Ratschläge aus Blogeinträgen, die erst vor ein paar Jahren geschrieben wurden, sind heute möglicherweise komplett überholt. (Dieser Blogeintrag ist womöglich keine Ausnahme!). Und es gibt da draussen immer noch eine Menge “Mythen“! Es ist also Vorsicht zu walten und das Gelesene mit der Originaldokumentation abzugleichen oder in eigenen Experimenten zu validieren.

Zu Beachten ist auch, dass wir unsere Anwendungen, für die wir Docker verwenden, noch nicht in der Produktion einsetzen! Dort hin zu gelangen wird zusätzlichen Aufwand sein, und wir werden wohl einige unserer aktuellen Docker-Praktiken noch einmal überarbeiten müssen.

Docker Registry

Am Anfang dachten wir, es wäre nicht erforderlich, unsere eigene Docker Registry zu haben. Doch bald merkten wir, dass wir damit falsch lagen. Eine Docker Registry zu pflegen ist eigentlich recht einfach, je nachdem, was benötigt wird. Da wir aus unserer Registry nichts für produktive Systeme beziehen, ist eine hohe Verfügbarkeit kein Muss, ebenso wenig wie Backups. (Wir können unsere Registry jederzeit aus dem Quellcode wieder ganz neu befüllen.)

Dennoch können Docker Images sehr gross werden und es empfiehlt sich sehr, die Registry von Zeit zu Zeit zu bereinigen!

Mehr Ignoranz

Dieser Tipp wird sehr hilfreich sein, sollte er nicht bereits bekannt sein! Beim Erstellen eines docker build nimmt Docker den gesamten Inhalt aus dem Build-Verzeichnis und sendet ihn an den Docker-Daemon. Wenn sich in der Anwendung viele Dateien befinden (z. B. node_modules, Log-Dateien oder kompilierte Objekte), die nicht Teil des finalen Docker Images sein sollen, sollten die Pfade zu den jeweiligen Dateien und Ordner zum .dockerignore-File hinzugefügt werden. So werden Docker Builds im Allgemeinen wesentlich schneller.

Showcase

Ich habe ein kleines Showcase Projekt erstellt, das eine einfache Ruby On Rails Anwendung enthält, die eine Datenbank verbindet.

Der erste Schritt ist immer, ein Dockerfile zu schreiben. Es ist nicht sehr spektakulär, aber es gibt noch ein paar Leckerbissen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# /Dockerfile
FROM ruby:2.4

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -q &&  apt-get -qq install -y \
  # required by our "waitforpg" script
  postgresql-client \
  # required by rails, see https://github.com/rails/execjs
  nodejs nodejs-legacy npm \
  # always clean up after the work is done
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# Copy helper scripts
COPY docker/* /usr/bin/

# Get ready for the app
RUN mkdir -p /app
WORKDIR /app

# Copy Gemfile & Gemfile.lock separately,
# so that Docker will cache the 'bundle install'
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs 2

# Copy the application
COPY . ./

# We've copied the entrypoint script to /usr/bin above
ENTRYPOINT ["/usr/bin/entrypoint"]

# Finally, we'll start the development server by default
CMD ["bin/rails server"]
  • Auf Zeile 14 werden alle unsere Helfer-Scripte direkt nach /usr/bin kopiert. Dies macht sie auf dem ganzen System verfügbar, ohne den konkreten Pfad anzugeben. Es muss einfach sichergestellt sein, dass das “Executable“-Flag gesetzt wurde, d.h. chmod +x scriptname!

  • Auf Zeile 22 werden die Gemfile und Gemfile.lock Dateien kopiert, so dass bundler ausgeführt werden kann (auf Zeile 23), bevor die gesamte Applikation in das Docker Image kopiert wird. Docker wird diesen Schritt zwischenspeichern, damit der Schritt nicht wieder durchführt werden muss. Es sei denn, Gemfile oder Gemfile.lock ändern sich. Das spart viel Zeit beim Wiederaufbau des Docker Images.

  • Auf Zeile 29 definieren wir ein spezielles Entrypoint Script. Dieses stellt sicher, dass unser Haupt-CMD nur startet, wenn der PostgreSQL Server bereits Verbindungen akzeptiert und die Datenbank auf die neueste Version migriert wurde.

Es gibt nur eine Sache, die im Entrypoint-Script wichtig ist, und das ist die exec Anweisung auf der letzten Zeile. Damit wird sichergestellt, dass der Entrypoint-Prozess durch den Prozess ersetzt wird, der in CMD angegeben ist. Wenn diese Anweisung fehlt, wird der in CMD angegebene Prozess als Unterprozess gestartet und die Signalverarbeitung funktioniert möglicherweise nicht wie erwartet.

1
2
3
4
5
6
7
8
#!/bin/sh
# entrypoint.sh

waitforpg
setup_pg postgres

# Prevent zombies
exec [email protected]

Das meiste unserer “Magie” passierte dennoch im docker-compose.yml File:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
version: '3'
services:
  app:
    build: .
    image: docker-registry.internal/ninech/docker-example
    volumes:
      - .:/app
    ports:
      - 3000
    depends_on:
      - postgres
    environment:
      - POSTGRES_PASSWORD=frankenstein
  postgres:
    image: postgres:9.5-alpine
    environment:
      - POSTGRES_PASSWORD
  • Auf Zeile 4 sagen wir docker-compose die app aus dem aktuellen Verzeichnis heraus zu erstellen.

  • Auf Zeile 7 sagen wir docker-compose, das aktuelle Verzeichnis als /app Verzeichnis im Container zu mounten. So werden alle Änderungen an den Dateien ausserhalb des Containers in den Container hinein synchronisiert.

  • Auf Zeile 10 listen wir alle Abhängigkeiten von app auf. docker-compose startet automatisch alle diese Abhängigkeiten.

  • Auf Zeile 17 legen wir fest, dass das POSTGRES_PASSWORD als Umgebungsvariable übergeben wird. Da wir jedoch keinen Wert festlegen, wird docker-compose nach einem Wert suchen und schliesslich den Wert auf Zeile 13 verwenden.

Im Vergleich dazu ist die Docker-compose.test.yaml-Datei ganz einfach:

1
2
3
4
5
6
7
version: '3'
services:
  app:
    build: .
    image: docker-registry.internal/ninech/docker-example
    entrypoint: "/usr/bin/test_entrypoint"
    command: "bin/rails test"

Sie ist so kurz, weil die Anwendung zum Starten der Tests sqlite benutzt und daher PostgreSQL nicht notwendig ist.

Die ganze Showcase-Anwendung kann auf Github angesehen werden.

Fazit

Unser neues Setup ermöglicht es uns, schnell mit der Arbeit zu beginnen. Wir können neue Kollegen schneller einarbeiten, da auf ihren Rechnern im Grunde nur Docker laufen muss. Ausserdem funktioniert das Entwickeln nun sogar plattformübergreifend und jeder Engineer kann jetzt ohne Einschränkungen auf seinem bevorzugten Betriebssystem arbeiten.

Dorthin zugelangen kostete Zeit. Es ist ein schrittweiser Prozess: Jedes Mal, wenn wir anfingen, eine Anwendung zu entwickeln, die noch nicht auf Docker umgestellt war, war diese Umstellung das erste, was wir taten (und ist es nach wie vor). Wir wurden ziemlich gut darin, aber am Anfang benötigten wir für jede einzelne Anwendung Tage!

Es ist von entscheidender Bedeutung, ein gemeinsames Verständnis dafür zu entwickeln, wie mit Docker gearbeitet werden soll, denn es gibt noch keine allgemein akzeptierten Standards. Um dies zu erreichen, mussten wir häufig noch einmal auf zuvor getroffene Entscheidungen zurück kommen. Schliesslich erreichten wir den Punkt, an dem wir uns einigen konnten, wie “wir Docker machen möchten“, zumindest vorerst.

Dass wir über das Problem stolperten, dass gemountete Volumen mit vielen Dateien Anwendungen in Containern in Docker für Mac verlangsamen, erinnerte uns auf schmerzliche Weise daran, dass Docker sich noch in den Kinderschuhen befindet. Aber die Dinge verbessern sich schnell und Docker reift mit stetiger Geschwindigkeit.

Ausblick

Wir möchten Docker definitiv bald auch in der Produktion einsetzen. Wir haben bereits begonnen, uns Containermanagement-Plattformen anzuschauen, wobei OpenShift momentan unser Favorit ist.

Da Docker Images grundsätzlich sehr statisch sind, suchen wir auch nach Möglichkeiten, um unsere Images automatisch nach Sicherheitsrisiken zu durchsuchen. Aber momentan achten wir einfach darauf, unsere Images häufig neu zu bauen.


Christian Mäder ist Software Engineer bei nine.ch und spezialisiert auf die Architektur von verteilten Systemen. Folge Christian auf Twitter.