rjjy5hpnifk-erwan-hesry

Docker Tutorial für Einsteiger

Was ist Docker?

Docker ist ein Open-Source Projekt zur Virtualisierung von Betriebssystemen in gekapselte Abschnitte, so genannten Container. Dabei basiert es auf einer Linux-Umgebung und erweitert die schon existierende LxC (Linux Container). Sie isolieren von Prozessen und Prozessgruppen. Sprich im Gegensatz zu einer vollwertigen VM, die sozusagen ein komplett anderes Betriebssystem im Vergleich zum Hostsystem über Hardwarevirtualisierung emuliert, sind Container abgeschottete Bereiche auf der Hostmaschine.

Von VMs und Vagrant zu Docker

Stellt euch vor ihr benutzt eine lauffähige Linux Distribution. Ihr seid Entwickler und benötigt jetzt für zwei Projekte unterschiedliche Versionen ein und derselben Abhängigkeit. Ein treffendes Beispiel ist da natürlich sowas wie die PHP- oder NodeJS-Version. Natürlich könnte wir für NodeJS das Tool nvm installieren und dann immer zur jeweiligen Version springen. Dies ist aber lästig und wird vielleicht auch einfach mal vergessen.

Im Normalfall kommen jetzt die meisten sicher auf die Idee einfach eine VM (Virtual Machine) einzurichten, um auf oder mit dieser zu arbeiten. Dort richten wir alles mühselig ein, um dann geraume Zeit später damit zu arbeiten. Soll die VM dann auch noch als Basis für die Entwicklung des Projektes im Unternehmen dienen, dann werden manchmal auch ganze VMs kopiert und dann angepasst, damit jeder diese Nutzen kann. In diesem Zusammenhang spricht man auch oft von Image.

Als Abstraktion dazu entstand das Vagrant Projekt. Mit Vagrant kann eine VM einfach über eine Konfigurationsdatei verwaltet werden und als Basis für ein Projekt dienen. Anhand dieser Datei, kann eine VM dann ohne nochmaliges Eingreifen oder Kopieren von Dateien mit anderen geteilt werden. Zusätzlich lassen sich durch die Nutzung verschiedener Konfigurationen Test-, Entwicklungs- und Produktivsystem einfach erstellen und verwalten.

Dabei sollte jedoch immer beachtet werden, dass wir mit Vagrant immer noch mit normalen VMs arbeiten. Sprich wir virtualisieren die Hardware unserer Hostmaschine, um darauf ein komplett eigenständiges Betriebssystem auszuführen.

hardware_virtualization

Linux Container und somit auch Docker wollen die Hardwarevirtualisierungsschicht umgehen, um Beispielsweise die langen Startzeiten von VMs zu umgehen, welche vor allem im Deployment von Software im Live-Betrieb eine sehr wichtige Rolle spielen. Anstatt die Hardware der Maschine virtuell zu trennen, wird nur versucht die einzelnen Prozesse zu kapseln und voneinander abzuschotten. In diesem Zusammenhang spricht man daher auch von Softwarevirtualisierung. Dies ist vor allem für die immer beliebter werdenden Cloud-Services und -Hoster interessant. Ähnlich wie Vagrant für VMs, bietet Docker eine einheitliche API zum Verwalten von Containern. Dabei soll ein Container jedoch nur jeweils eine bestimmte Aufgabe erfüllen, wodurch ein Projekt meist aus mehreren Docker-Containern besteht. Beispielsweise lebt die MySQL Datenbank in einem anderen Container als die eigentliche PHP oder NodeJS Anwendung.

Ähnlich zu den VMs wird mit Hilfe einer Docker-Konfiguration ein Image erstellt, welches dann als Ausgangspunkt zur Erstellung von Docker Containern genutzt wird. Ist einmal das Image vorhanden, ist das Erstellen und Hochfahren eines Containers eine Sekundenangelegenheit.

ansatz_der_betriebssystemvirtualisierung_zur_schaffung_virtueller_betriebsumgebungen

Große Unternehmen, wie Google, setzen für ihre Cloud-Services sehr erfolgreich auf den Container-Ansatz mit Docker.

Installation

Obwohl Docker eigentlich für Linux-Systeme entwickelt wurde, hat das Team um Docker viel Zeit in die einfache Nutzung auf MacOS und Windows investiert. War die Einrichtung zu Beginn auf Windows und MacOS eher eine Qual, ist es heute in wenigen Minuten und Schritten erledigt. Dabei wird für die nicht Linux-Maschinen eine minimale Virtuelle Maschine mit einem Linux und Docker installiert, in der später alles weitere im Zusammenhang mit Docker abläuft. Die Installation unterscheidet sich aber nicht wirklich von einem normalen Softwaresetup. Die frühere Alternative mit dem Titel Boot2Docker sollte nicht mehr genutzt werden.

Für die einzelnen Systeme gibt es auf der Webseite des Docker-Projektes einfache Anleitungen.
Hier geht es zum Docker Installations-Guide.

Nutzung

Hat die Installation geklappt, kann in einem Terminal/Kommandozeile/Bash folgender Befehl ausgeführt werden.

docker run hello-world

Hat alles geklappt, erhaltet ihr nach einiger Zeit diese Ausgabe.

Hello from Docker!<br>
This message shows that your installation appears to be working correctly.

Der Ausgangspunkt für Docker ist häufig ein bestehendes Image, welches hier mit hello-world benannt ist. Es ist aber möglich alles von Grund auf selbst einzurichten.
Das angeforderte Image wird auf unserem Rechner/Maschine gesucht, aber nicht gefunden, da es dieses noch nicht gibt.

Unable to find image 'hello-world:latest' locally

Danach sucht Docker automatisch in der eigenen Image-Registry nach dem entsprechenden Abbild. Die Image-Registry findet ihr unter hub.docker.com. Ist dieses gefunden, wird dieses heruntergeladen.

c04b14da8d14: Pull complete<br>
Digest: sha256:0256e8a36e2070f7bf2d0b0763dbabdd67798512411de4cdcf9431a1feb60fd9<br>
Status: Downloaded newer image for hello-world:latest

Ist dies abgeschlossen, wird der eigentlich Container des Images ausgeführt. Wie bereits beschrieben, führt ein Container immer nur ein Befehl aus. Ist dieser abgeschlossen, wird der Container automatisch beendet. In unserem Fall gibt der Container einfach die Information aus, dass alles funktioniert und wir mit weiteren Schritten fortfahren können.

Damit ist auch das Grundprinzip bereits erklärt und verstanden. Die Basis ist ein Image und die eigentliche Ausführung der Aufgabe/Prozesses geschieht im Container, welcher anhand des Images erstellt wird.

Eine Liste von bereits auf dem System vorhandenen Images erhaltet ihr mit:

docker images

Was diese Ausgabe erzeugt:

REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
hello-world              latest              c54a2cc56cbb        4 months ago        1.848 kB

Das Pendant für Container lautet:

docker ps -all

Die Ausgabe in unserem Fall sollte dann so aussehen:

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
f6e88d908823        hello-world         "/hello"            23 minutes ago      Exited (0) 23 minutes ago                       gigantic_sammet

Tipp: Ihr könnt beim Start eines Containers auf einen eigenen Namen vergeben, denn sonst wird einer generiert, wie z.B. gigantic_sammet.

Natürlich haben wir nicht immer Container, die direkt wieder beendet werden, sondern z.B. wie eine Webanwendung dauerhaft laufen sollen, bis wir sie manuell wieder stoppen.
Gestoppt werden kann ein laufender Container mit dieser Zeile:

docker stop [container_name]

Um Container oder Images zu löschen, können folgende Befehle genutzt werden:

docker rm [container_name]
docker rmi [image_name]

Darüber hinaus existieren noch zahlreiche und sinnvolle weitere Befehle, doch sollten die bereits vorgestellten für uns reichen. Als letzten wichtigen Punkt sollt ihr natürlich auch erfahren, wie ihr euch eigene Images bauen könnt. Dieses geschieht in der Regel über sogenannte Dockerfiles.

Zu diesem Zweck könnt ihr einfach eine neue Datei mit dem Namen Dockerfile und ohne Dateiendung anlegen. Hier folgt nun die Definition und Konfiguration der späteren Container mit dem Befehl der ausgeführt werden soll. In den meisten Fällen benutzt ihr ein Basis-Image auf dem ihr aufbauen wollt. Als Beispiel wird die PHP-Anwendung später auf einem Ubuntu-Server laufen, dann bietet es sich an direkt mit einem Ubuntu-Abbild zu starten. Dafür können wir in der Image-Registry schauen, ob es überhaupt ein passendes Image gibt.

Siehe da, unter https://hub.docker.com/_/ubuntu/ findet ihr eine Liste von offiziellen Ubuntu Abbildern.

Aber natürlich gibt es auch bereits vorhandene Images für bekannte Technologien, wie NodeJS oder PHP

Als Beispiel möchten wir einfach ein PHP-Projekt auf Basis von Docker entwickeln. Dafür benötigen wir zuallererst ein Projektverzeichnis mit einem leeren Dockerfile.
In der ersten Zeile der Dockerfile Datei setzen wir das Basis-Image mit Hilfe des FROM keywords.

FROM [REPO]:[VERSION]

In unserem Fall mit PHP könnte das dann wie folgt aussehen.

# use PHP v7.0-apache as source
FROM php:7.0-apache

Wir können mit unserem Dockerfile bereits ein eigenes Image bauen lassen und einen Container damit starten. Das Erzeugen eines neuen Images funktioniert mit diesem Befehl:

docker build [Order|Pfad]

Befinden wir uns bereits in unserem Projektverzeichnis, starten wir den Vorgang wie folgt.

docker build .

Nun lässt schön verfolgen, was Docker macht.

Sending build context to Docker daemon 9.728 kB
Step 1 : FROM php:7.0-apache
7.0-apache: Pulling from library/php
386a066cd84a: Pull complete
269e95c6053a: Pull complete
6243d5c57a34: Pull complete
872f6d38a33b: Pull complete
e5ea5361568c: Pull complete
f81f18e77719: Pull complete
f9dbc878ca0c: Pull complete
195935e4100b: Pull complete
935d0c2409b2: Pull complete
d14786710093: Pull complete
b7dff268d83a: Pull complete
d1083150956d: Pull complete
9284aa2927a6: Pull complete
Digest: sha256:05fe69944d513bd618ad81cf6160e2f0e8237a3abf8383c816b8bbbc5ff83418
Status: Downloaded newer image for php:7.0-apache
---> 336e2be8a343

Es wird erkannt, dass wir das Abbild von PHP als Version 7.0 mit Apache benötigen. Dieses muss erst noch heruntergeladen und entpackt werden. Danach wird ein neues Image gebaut.
Nun taucht das Image auch in unserer Liste auf.

REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
php                      7.0-apache          336e2be8a343        13 days ago         402.6 MB
hello-world              latest              c54a2cc56cbb        4 months ago        1.848 kB

Starten wir nun einen neuen Dockercontainer von diesem Image, läuft ein Apache Webserver und PHP in unserem Container. Jetzt stellt sich die Frage, wie wir denn dann auf unseren Inhalt zugreifen können. Dazu in Kürze mehr.
Euch ist vielleicht aufgefallen, dass unser Image als php und 7.0-apache aufgetaucht ist. Das liegt daran, dass wir in unseren Dockerfile nichts anderes machen als ein anderes Image zu benutzen. Dadurch wird Name und Tag dieses einfach übernommen. Haben wir einmal ein vorhandenes Image, können wir eine beliebige Anzahl Container starten ohne das Image nochmal bauen zu müssen.

Unsere Anwendung besitzt aber natürlich auch eigene Dateien, die der Container nutzen soll. In der Regel findet die Entwicklung eines Projektes in einem src Ordner statt. Aus diesem Grund legen wir einen solchen in unserem Projektverzeichnis an.
Der Einfachheit halber fügen wir diesem Ordner direkt eine index.html oder index.php hinzu. Dort könnt ihr einfach Testinhalte eintragen.

src/index.php

<?php
echo "Hello, World"
?>

Um das noch ausstehende zu verstehen, schauen wir uns noch kurz an, was der Apache Webserver macht. In der Regel stellt er Dateien so bereit, dass sie über ein Netzwerk aufgerufen werden können. Welche Dateien aufgerufen werden können, hängt von der Konfiguration des Webservers ab.
Standardmäßig stellt der Apache Webserver alle Dateien bereit, die unter dem Pfad /var/www/html zu finden sind. Für uns bedeutet dies, dass wir unsere Dateien im src Ordner irgendwie in den Container unter /var/html/www bekommen müssen.

Zum Glück geht das mit Docker und unserem Dockerfile recht einfach. Der COPY Befehl erlaubt es uns Dateien an eine bestimmte Stelle im Container zu kopieren.

# use PHP v7.0-apache as source
FROM php:7.0-apache


COPY src/ /var/www/html/

Nach dem Ändern des Dockerfiles müssen wir das Image neu erstellen! Als nächstes wollen wir ja sehen, ob alles geklappt hat. Daher benötigen wir Zugriff auf unsere Daten im Container.
Dafür müssen wir noch angeben unter welchem Port wir den Apache Webserver auf unserer lokalen Maschine erreichen können. Standardmäßig läuft ein Apache Webserver auf Port 80. Aber das auch nur in unserem Container. Die Außenwelt weiß davon sozusagen nichts.
Dies können beim Start/Erstellen eines Container jedoch ändern. Hier haben wir die Möglichkeit noch spezielle Konfigurationen für den zu startenden Container vorzunehmen.
Über den Parameter -p kann beispielsweise ein Portmapping durchgeführt werden. Wir wollen jetzt den internen Port 80 auf den Port 8080 unseres Rechners mappen.

docker run -p 80:80 [image_id]

Läuft der Container könnt ihr nun im Browser eures Vertrauens localhost:8080 aufrufen und euer Werk bestaunen.

Einziger Nachteil beim Entwickeln ist, dass ihr den Container jedes Mal neu starten müsst, falls ihr etwas ändert. Das ist mühselig und kostet Zeit. Aus diesem Grund könnt ihr auch in diesem kleinen Beispiel statt einem eigenen Dockerfile euer src Verzeichnis als Volume mounten.
Hört sich kompliziert an, ist es aber nicht. Durch die Nutzung von Volumes können einerseits persistente Daten gespeichert werden, die auch nach dem Stoppen eines Containers bestehen bleiben oder aber bestehende Dateien in den Container verlinken. So könnt ihr eure Quelldatei ändern und die Änderungen werden automatisch in eurem Container landen.

Dafür gibt es beim erstellen des Containers den Parameter -v mit der Bedeutung Volume. Hier könnt ihr festlegen, ob der Container ein Volume besitzt oder einen Ordner auf eurer Maschine als Volume in den Container gemounted werden soll.

docker run [image_id] -p 80:8080 -v "/host/pfad/:/container/pfad"

Dabei müssen die Pfade absolut sein. Entscheidet ihr euch in unserem Fall für ein Volume, dann benötigt ihr gar kein eigenes Dockerfile mehr, denn alles für die Entwicklung kann über die Kommandozeile direkt konfiguriert werden.
Beachtet aber, dass ihr im Produktionsbetrieb den Quellcode nicht als Volume einbinden solltet!

Zum Schluss noch ein Hinweis, dass ihr Container auch miteinander verknüpfen könnt. Das werdet ihr spätestens dann brauche, wenn eure PHP Anwendung noch eine Datenbank benötigt.
Ihr würdet hier dann auf ein bestehendes Image mit eurem Wunsch-Datenbanksystem zurückgreifen. Den Datenbankcontainer mit einem eigenen Volume versehen, damit die Daten der Datenbank auch nach dem Stoppen des Containers vorhanden sind.

Damit jetzt der Anwendungs- mit dem Datenbankcontainer kommunizieren kann, existiert wieder ein simpler Parameter für das Erstellen des Containers. Dieser nennt sich link und nimmt als Wert den zu verlinkenden Dockercontainer Namen oder die ID und ein Mapping auf einen eigenen internen Alias entgegen.

docker run [image_id] -p 80:8080 --link mysqlserver:mysqldb

Euch ist vielleicht schon aufgefallen, dass ihr mittlerweile eine Vielzahl von nicht benannten Images und automatisch benannten Container habt. Um das totale Chaos zu vermeiden, könnt ihr Container auch benamen. Dazu vergebt ihr beim Erzeugen eines Images oder einem Container über den Parameter --name einen eindeutige und aussagekräftige Namen.

Zur Vollständigkeit sollte auch erwähnt werden, dass ihr euch auch auf einen Dockercontainer verbinden, Logs einsehen und weitere Statusinformationen abrufen könnt.
Ein entsprechendes Cheatsheet findet ihr hier.

Damit habt ihr nun einen Einblick und vor allem einen Einstieg in das Thema Docker erhalten. Es empfiehlt sich nicht direkt den kompletten Deploymentprozess von vornherein umzustellen. Testet erstmal alles aus, z.B. für die Entwicklungsumgebung des nächsten oder aktuellen Projekts.

Fazit

Alles in allem ist Docker am Anfang vielleicht ein wenig viel für den ein oder anderen. Doch eure Geduld wird sich auszahlen. Statt stundenlanges Einrichten der Arbeitsumgebung für jeden Mitarbeiter oder Aufsetzen von Test-, Staging- oder Produktivumgebungen in mühseliger Kleinstarbeit kann hier viel Zeit und vor allem zahlreiche Nerven gespart werden. Das soll jetzt nicht bedeuten, dass ihr euch über das Thema Sicherheit oder initiales Konfigurieren der Dockerumgebung keine Gedanken machen solltet. In unserem Tutorial erhaltet ihr auch nur einen kleinen Einblick in die Möglichkeiten, die Docker bietet.

Natürlich ist auch das Thema VMs und Virtualisierung für komplexe System viel ausgereifter, jedoch existieren auch viele Ansätze die beide Herangehensweisen verbinden. So kann eine Vagrant-VM auch in Verbindung mit Docker genutzt werden.