Einführung in Docker – von 0 auf 50%

Senior PHP Developer @ Chrono24

Vorwort

Docker ist eine Container Plattform, mit der du Applikationen in isolierten Umgebungen starten kannst.
Dies hilft dir und deinem Team bei alltäglichen Herausforderungen wie lokalen Entwicklungsumgebung sowie auch bei Deployments.

Was ist Docker technisch gesehen?
Wie funktioniert das mit dem Kernel?
Wie ist der Vergleich zu herkömmlicher Virtualisierung?

Das alles interessiert uns in diesem Artikel nicht 🙂

Nachfolgend wird beschrieben, wie Docker auf einem lokalen System aufgesetzt und wie schnell Erfolge mit Docker erzielt werden können.

Wer tiefere Einblicke in den technischen Unterbau von Docker erhalten möchte, dem empfehle ich diese Docker-Übersicht.

Lerne jetzt, wie du als Docker-Beginner mit Docker arbeitest.

 

Installation

Docker zu installieren ist ziemlich einfach.
Wie es dir je nach Betriebssystem gelingt, erfährst du im Folgenden:

Ubuntu / Linux

Es ist wichtig, nicht die Docker Engine aus den normalen Sources zu installieren (outdated).
Folge einfach dieser Anleitung: https://docs.docker.com/install/linux/docker-ce/ubuntu/

Mac

Hier findest du die Anleitung für Mac: https://docs.docker.com/docker-for-mac/install/
Kollegen, die einen Mac benutzen, hatten jedoch Probleme mit der Docker-Toolbox und sind daher auf diese Applikation umgestiegen: https://download.docker.com/mac/stable/Docker.dmg

Windows

Wie Docker auf Windows installiert wird, erfährst du hier: https://docs.docker.com/docker-for-windows/install/

 

Erste Schritte – Dein Erster Container

Nachdem du Docker erfolgreich installiert hast, geht es darum, den ersten Container zu starten. Stelle dir vor, ein „Container“ sei ein „Ding“, das gestartet wird, ähnlich wie ein Programm. Dieses „Ding“ läuft dauerhaft oder beendet sich nach erledigter Aufgabe.

Wenn du der Installations-Anleitung (siehe oben) gefolgt bist, hast du bereits den hello-world Container gestartet. Ansonsten: Starte ihn 🙂

docker run hello-world

docker run hello world

Das ist natürlich langweilig!
Deshalb möchten wir nun andere „Dinge“ (Container) starten, die Aufgaben für uns erledigen.

Images sind fertig gebaute Container die aufgrund einer Bauanleitung (einer Art Rezept) angefertigt wurden. Diese Bauanleitung ist das Dockerfile.

Eine Liste von existierenden öffentlichen Images findest du in Docker Hub. Mithilfe der Filterung werden dir nur offizielle Images angezeigt.

Dein erster Anlaufpunkt sollten offizielle Images sein.
Offizielle Images sind Images, die von großen Communities gepflegt werden, gut dokumentiert sind und Sicherheitsrelevant geprüft werden.

Nehmen wir an, du möchtest einen PHP-Befehl ausführen – ohne PHP lokal installiert zu haben.
Führe folgendes Kommando in deinem Terminal aus.

docker run php:cli php -r 'echo date("c");'

Super! Du hast einen Container mit einem PHP Image gestartet und einen Befehl übergeben. Der Befehl wurde ausgeführt und der Container im Anschluss beendet.

Führe das Kommando erneut aus – der zweite Durchlauf ist schneller.

Lasse uns nun das Kommando Stück für Stück aufschlüsseln.

docker Der Befehl um Docker auszuführen.
run Das Kommando, das ausgeführt werden soll. Wir wollen aktuell einen Container „rennen“ lassen.
php Das Image, das mit dem der Container gestartet werden soll. Siehe https://hub.docker.com/_/php.
cli Der Tag, der gestartet werden soll.
Ein Tag ist eine Art Version / Ausprägung / Unterkategorie des PHP-Images.
Siehe https://hub.docker.com/_/php?tab=tags
php -r 'echo date("c");' Dies ist ein Parameter, den wir an den Container übergeben.
In unserem Fall führen die Parameter die PHP Executable aus und übergeben wiederum ein Kommando, das ausgeführt werden soll.

Cool, nicht wahr?
Was hat das dir jetzt gebracht?
Du hast einen PHP-Befehl ausgeführt ohne PHP lokal installiert zu haben!

Aber ich kann PHP/Java/Node/Python/… doch einfach lokal installieren
Ja – aber das ist nicht immer gewünscht, praktikabel oder einfach.

Lasse uns nun einen PHP-Befehl in verschiedenen PHP-Versionen ausführen.

docker run php:5.5-cli php -r 'echo PHP_VERSION;'

Und jetzt nochmal mit Python

Und mit Nginx …

… um nur einige Beispiele zu zeigen.

Es ist sehr einfach zwischen verschiedenen Versionen zu wechseln!

Docker Layer

Jedes Mal, wenn du eines dieser Kommandos ausführst, werden Docker-Layer heruntergeladen. Lasse dich davon nicht verunsichern. Dies ist lediglich beim ersten Starten von einem Image notwendig, bis die Layer gecached sind.

Mehr zu Docker Layern, was Docker Layer sind und warum du sie benötigst, lernst du im nächsten Abschnitt.

Ein kurzer Ausflug zum Dockerfile

Über die zuvor genannten Layer möchte ich nun einiges erklären.
Ein Dockerfile (du erinnerst dich, Image = fertiges Gebäude, Dockerfile = Bauanleitung) besteht aus einzelnen Befehlen.
Ein Image besteht aus einem oder mehreren Layern – ein Schichtensystem. Ein Layer enthält Daten, ähnlich wie eine Festplatte. Wird eine Datei im Bauprozess geändert, so wird dies in einem neuen Layer festgehalten.
Befehle im Dockerfile, die eine Änderung hervorrufen, generieren alle einen neuen Layer.
Ein Befehl (innerhalb des Dockerfiles) ist z.B. RUN.
Jedes Mal, wenn RUN im Dockerfile verwendet wird, wird ein neuer Layer erzeugt. Du möchtest darauf achten, möglichst wenige Layer zu erzeugen, d.h. du solltest Befehle zusammenfassen. Dies reduziert die Dateigröße des Images und hat viele weitere Vorteile.
Als Gegenspieler erscheint der Layer-Cache. Du solltest darauf achten, dein Dockerfile so aufzubauen, dass du von dem Layer-Cache profitierst.

# mehrere Kommandos zusammengefasst
RUN set -x \
    && addgroup -g 101 -S nginx \
    && adduser -S -D -H -u 101 -h /var/cache/nginx -s /sbin/nologin -G nginx -g nginx nginx \
    && apkArch="$(cat /etc/apk/arch)" \
    && nginxPackages=" \

Aber solche technischen Feinheiten musst du jetzt noch nicht beachten 😉

Hinweis: Daten persistieren

Sobald ein Docker Container beendet wird, verliert der Container alle seine Daten.
Falls du etwas dauerhaft speichern möchtest, dann musst du das außerhalb des Containers tun.

Mehr Container – etwas Sinnvolles bitte

Die zuvor gestarteten Container waren zwar beeindruckend, jedoch nicht gerade nützlich.
Nun möchten wir Container starten, konfigurieren und benutzen.
Für den Anfang einen MySQL-Server.

Schritt 1: Finde das gewünschte Image in Docker Hub
https://hub.docker.com/search?q=mysql&type=image

Schritt 2: Benutze das „Official Image“ https://hub.docker.com/_/mysql

Schritt 3: Lies die Doku für das Image und verwende es.

Ziel: Es soll ein MySQL-Server in Docker laufen. Der Server soll vom Host-Rechner (deinem PC) erreichbar sein. Wenn der Container beendet wird und neu gestartet wird, sollen die Datenbanken noch vorhanden sein. Zugangsdaten für den MySQL Server werden vorgegeben.

docker run --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=test -e MYSQL_USER=user1 -e MYSQL_PASSWORD=password1 -v /var/tmp/test-mysql-data:/var/lib/mysql mysql:8

Das aufgeschlüsselte Kommando:

docker Du führst Docker aus.
run Du startest einen Container.
--rm Entferne den Container wenn er gestoppt wird.
-p 3306:3306 Route den Port von innerhalb des Containers (3306) auf außerhalb des Containers 3306.
-e MYSQL_ROOT_PASSWORD=root Setze das root Passwort auf „root“.
-e MYSQL_DATABASE=test Erstelle die Datenbank „test“.
-e MYSQL_USER=user1 Erstelle einen Benutzer mit login „user1„.
-e MYSQL_PASSWORD=password1 Setze das Passwort „password1“ für user1.
-v /var/tmp/test-mysql-data:/var/lib/mysql Mounte den Ordner „/var/lib/mysql“ in den lokalen Ordner “/var/tmp/test-mysql-data”.
mysql:8 Image + Tag. MySQL in der Version 8.

Du gibst kein Kommando an, das ausgeführt werden soll. Damit verwendest du das Standard-Kommando des Images (das wiederum den Server startet).

Falls du eine Meldung erhältst, die ungefähr so aussieht,
„Bind for 0.0.0.0:3306 failed: port is already allocated.“,
dann ist bereits der Port 3306 lokal belegt.
Verwende einfach einen anderen Port z.B. „-p 3355:3306“

Toll! MySQL läuft.
Schau dir die Daten an, die der Server generiert hat.

ls -l /var/tmp/test-mysql-data

Verbinde dich zum Server.

mysql -h 127.0.0.1 -P3306 -uroot -proot test

Erstelle eine Tabelle.

Stoppe den Server (stoppe den laufenden MySQL Prozess im Terminel mit STRG+4).

Starte den Server neu mit docker run ... (siehe Kommando von oben).

Prüfe, ob die erstellte Tabelle noch da ist (ob sie persistiert wurde, trotz Container-Neustart).

mysql -h 127.0.0.1 -P3306 -uroot -proot test -e "show tables;"

bzw.

mysql -h 127.0.0.1 -P3306 -uuser1 -ppassword1 test -e "show tables;"

Du möchtest wissen, welche weiteren Optionen es für „docker run“ gibt? Dann befrage das Hilfe-Kommando.

docker help run

Anwendungsfall: nano Editor in Docker

Aufgabe: Du möchtest den Konsolen-Editor „nano“ verwenden – ihn jedoch nicht lokal installieren.
Als Grundlage nimmst du ein kleines und schnelles Betriebssystem: Alpine.
Natürlich möchtest du die Möglichkeit haben, Dateien permanent auf deinem eigenen PC zu speichern.

docker run --rm -it -v /tmp/mydata:/app alpine sh -c "apk add nano; nano /app/myfile.txt"

Viele der Optionen und Parameter kommen dir bestimmt bekannt vor.
Neu sind -itaka -i -t: Interactive Terminal

Als Kommando benutzt du „sh“. Dieses Kommando bekommt wiederum „-c“ als Argument und die Befehle, die ausgeführt werden sollen. In diesem Fall installierst du nano und öffnest eine Datei in nano.
Die Datei wird bei dir lokal als /tmp/mydata/myfile.txtgespeichert.

Führe das Kommando mehrfach aus.
Dir fällt bestimmt auf, dass nano bei jedem Start neu installiert werden muss. Wie das behoben wird, zeige ich dir im nächsten Kapitel.

Anwendungsfall: Linux testen mit Docker

Aufgabe: Du möchtest eine Linux Version testen/benutzen/ausführen ohne sie lokal bei dir zu installieren.

Hier die Kommandos dafür.

docker run --rm -it debian:10-slim bash
docker run --rm -it alpine sh
docker run --rm -it fedora bash

Und schon bist du in einem Terminal in Debian/Alpine/Fedora – egal, ob du gerade mit Linux, Windows oder MacOS arbeitest.
Großartig!

Anwendungsfall: SASS compile mit Docker

Aufgabe: Du möchtest mit SASS deine scss-Dateien kompilieren (scss -> css).

1. Erstelle die Datei file.scss

/* file.scss */
#hui {
    .pfui {
        color: #ff0000;
    }
}

2. Öffne ein Terminel und wechsle in den Ordner, in dem die Datei liegt.

3. Führe dieses Kommando aus

docker run --rm -it -v "$PWD:/app" -w /app node:slim bash -c "npm install -g sass; sass ./file.scss > ./file.css"

4. Prüfe den Inhalt

cat file.css
npm install dauert zu lange?
Lösung: Siehe Nächstes Kapitel.

 

Weiteres

Ich hoffe, es wird klar, wie flexibel Docker ist.
Fix mal einen Container starten, nur das notwendige persistieren und danach den Container stoppen (wegwerfen). Toll!

Offizielle Images sind meist gut dokumentiert und flexibel konfigurierbar (z.B. MySQL Credentials per Environment Variablen übergeben).

Als nächstes geht es darum, dir dein eigenes Image zu bauen.

Eigenes Image bauen – dein erstes Dockerfile

Nun lernst du, wie du dein eigenes Image baust, welches mithilfe deiner Bauanleitung erstellt wird. Wenn ein Image einmal gebaut ist, kann es schnell gestartet werden ohne die Bauanleitung erneut (bei jedem Start) ausführen zu müssen.

Als Beispiel verwendest du das nano-Szenario aus dem vorherigen Kapitel und verbesserst es.

Erstelle eine Datei „Dockerfile“ (am besten in einem neuen Ordner). Nun befülle diese Datei.

# Dockerfile
FROM alpine

RUN apk update \
    && apk add nano

Dann führe die Bauanleitung aus (in dem Ordner in dem die Dockerfile-Datei liegt).

docker build -t my-local-nano .

Starte den Container

docker run --rm -it -v /tmp/mydata:/app -w /app my-local-nano nano testfile.txt

Schreibe nun etwas mit nano, speichere die Datei und prüfe die erstellte Datei auf deinem Rechner

cat /tmp/mydata/testfile.txt

Du hast jetzt

  • ein wiederverwendbares Image mit Namen my-local-nano erstellt
  • einen Container damit gestartet
  • eine Datei gespeichert und auf deinem lokalen Rechner persistiert

Du kannst jetzt

  • den docker run ... Befehl erneut ausführen, ohne das Image erneut zu bauen

Dockerfile’s helfen dir dabei ausgeführte Kommandos in Layern zu cachen. Wenn du das Dockerfile nicht änderst, dann muss das Image nicht erneut gebaut werden. Das beschleunigt die Ausführung von docker run.

Was kann ein Dockerfile noch?

Stelle dir vor, du verbindest dich mit SSH auf einen Server. Du konfigurierst Services, legst Dateien an, veränderst Parameter usw.
Damit du das Ganze nicht andauernd wiederholen musst, schreibst du ein Skript dafür.
Ähnlich wie dieses Skript (Kommandos sequentiell abarbeiten) funktioniert das Dockerfile.

Gängige Inhalte für ein Dockerfile sind

  • System updaten (apt-get update && apt-get upgrade -y)
  • Pakete installieren (apt-get install -y php-cli)
  • Konfigurationen / Source-Code Dateien in den Container kopieren (COPY)
  • Den ausführenden Systemuser setzen (USER)

Eine vollständige Übersicht findest du in der Dockerfile reference und in den Best Practices für Dockerfiles.
Es hilft dir, wenn du dir Dockerfiles von Open-Source-Projekten ansiehst, um zu lernen, welche Best Practices angewendet werden sollten. Docker Best Practices, Guide Best Practices.

Lass mich rein Container – docker exec

Es ist durchaus üblich, Container zu starten und diese später zu „betreten“ (per Terminal in den Container springen).
Das tust du jetzt mit folgenden Kommandos.

Schritt 1: Container starten.

docker run php:apache

Schritt 2: Container ID (oder Name) rausfinden (zweites Terminal).

docker ps

Schritt 3: Mit Container verbinden.

docker exec -it bdd5c095c543 sh

Damit kannst du jederzeit in bereits gestartete Container springen und Aktionen ausführen.
Beispiel Aktionen:

  • Konfigurationen ändern
  • Service neustarten
  • Dateien editieren
  • Logfiles anschauen

Helfer-Snippets – docker aliases

Shell aliases helfen dir dabei, häufig benutzte Befehle komfortabel zwischenzuspeichern und wiederverwendbar zu machen.
Ein beliebtes Beispiel (quasi Standard), um sich den Inhalt eines Verzeichnisses anzeigen zu lassen, ist ll(kurz für z.B. ls -l).

Alias für Composer in Docker

alias composer='docker run --rm -it -u "$(id -u):$(id -g)" -w /app -v $PWD:/app composer'

Mit diesem Alias wird das Kommando composer bereitgestellt. Du kannst es gerne anders benennen.
Das Kommando startet einen Container mit dem offiziellen Composer-Image, benutzt deine lokale User-ID und mountet deinen aktuellen Ordner als /app innerhalb des Containers.

Prüfe, ob dein erstellter Alias funktioniert.

Tipp: Öffne dein Terminal erneut, damit der Alias wirksam wird.

 

Alias für PHP in Docker

alias php72='docker run --rm -it -u "$(id -u):$(id -g)" -w /app -v $PWD:/app --entrypoint=php php:7.2-cli'

Mit diesem Alias wird der Befehl php72 definiert.

Du kannst diesen Befehl wie den Standard-PHP-Befehl benutzen.

Da du dich mittlerweile schon gut mit Docker auskennst, hast du sicherlich bemerkt, dass du im Alias einfach 7.2 mit 7.3 austauschen kannst, um die Version zu ändern.

Verschiedene PHP-Versionen auf einem System installieren – das war einfach!

 

Worauf kommt es bei Docker Images an?

Damit du effektiv und schnell arbeiten kannst, solltest du bei Docker Images Folgendes beachten:

  • Best-Practices befolgen.
  • Anzahl der Image-Layer minimieren.
  • Keine unnötigen (dev) Tools in Images installieren die deployed werden.
  • Image-Dateigröße beachten (lieber slim anstatt full, lieber Alpine anstatt Debian).
  • Wiederverwendbarkeit (Environment Variablen zur Konfiguration benutzen).

Hier ein kleiner Vergleich zur Dateigröße von Images:

REPOSITORY TAG SIZE
node slim 178MB
python latest 932MB
mysql latest 456MB
php cli 398MB
php apache 414MB
composer latest 165MB
debian 10-slim 69MB
debian latest 114MB
fedora latest 194MB
ubuntu latest 64MB
alpine latest 6MB

 

Docker Compose

docker-compose ist ein Tool, mit dem du mehrere Container verknüpfen kannst. Das hilft dir, Applikationen zu starten, die mehrere Services benötigen z.B. einen LAMP Stack (Linux, Apache, MySQL, PHP).

Docker Compose installieren

docker-compose ist einfach zu installieren. Bitte folge dieser Anleitung.

Prüfe mit docker-compose -v, ob die Installation funktioniert hat.

docker-compose.yml Datei

Docker Compose benötigt eine Konfigurationsdatei, die beschreibt, welche Container gestartet werden sollen und wie diese miteinander kommunizieren. Dies ist die docker-compose.yml Datei.

Diese Konfiguration verknüpft mehrere Container – egal, ob fertige Images oder selbst gebaute Images (mit deinem eigenen Dockerfile).

Docker LAMP Stack

Nun startest du mehrere Container die dem LAMP Stack entsprechen.
L(inux) wird indirekt verwendet.
A(pache) und P(HP) laufen innerhalb von einem Container.
M(ySQL) läuft in einem separaten Container, kann jedoch von PHP verwendet werden.

docker-compose.yml File

version: "3.7"
services:
  php:
    container_name: app-php
    image: php:apache
    volumes:
      - "./src:/var/www/html/"
    ports:
      - "8081:80"
    environment:
      MYSQL_USER: user1
      MYSQL_PASSWORD: password1
    links:
      - mysql
    depends_on:
      - mysql
    networks:
      app:


  mysql:
    container_name: app-mysql
    command: --default-authentication-plugin=mysql_native_password --sql-mode=""
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: app
      MYSQL_USER: user1
      MYSQL_PASSWORD: password1
    volumes:
      - "/var/tmp/app-mysql:/var/lib/mysql"
    ports:
      - "3307:3306"
    networks:
      app:

networks:
  app:

Starte die Container

docker-compose up

Springe in den MySQL Container

docker-compose exec mysql mysql -u root -proot

Springe in den PHP Container und prüfe, ob die MySQL Zugangsdaten in den Umgebungsvariablen ankommen.

docker-compose exec php bash # zu Container connecten
apt-get update && apt-get install -y default-mysql-client # MySQL Client installieren
env | grep -i mysql # Environment Variablen checken

Wenn all das geklappt hat, kannst du dich von innerhalb des PHP-Containers zum MySQL-Container verbinden.

mysql -h mysql -u ${MYSQL_USER} -p${MYSQL_PASSWORD} app
Was fehlt noch zur vollständigen Applikation?

Wie du sicher bemerkt hast, werden in der docker-compose.yml Datei nur offizielle Images verwendet. Du wirst aber sehr schnell eigene Extensions, Konfigurationen, Behaviours usw. in deinen Images benötigen.
Hierzu kannst du, wie gewohnt, deine eigenen Dockerfile’s schreiben und diese in Docker Compose verwenden. Siehe docker-compose file build Referenz.

Wie geht es weiter? – Der Weg zu 100%

Super, jetzt kennst du die Grundlagen von Docker. Doch der Weg endet hier noch nicht, es gibt noch viel zu lernen.

Nächste möglichen Themen sind:

Wer sind wir? Warum verwenden wir Docker?

Als Online-Marktplatz für hochwertigen Luxus-Schmuck bringt FineJewels24 Händler und Verkäufer direkt zusammen. Egal ob Ringe, Armbänder, Halsketten oder Ohrringe, Markenschmuck bekannter Designer oder seltenes Einzelstück – FineJewels24 bietet eine außergewöhnliche Schmuck-Vielfalt von dezent bis ausgefallen. Lass dich von unseren atemberaubenden Angeboten verzaubern!

Das FineJewels24-Core Team besteht aus 10 Personen.
Unsere 3 Entwickler (2x Backend, 1x Frontend) verwenden Docker für verschiedenste Zwecke, z.B. für

  • lokale Entwicklung,
  • Container für Deployment Skripte,
  • GitLab CI/CD Pipelines,
  • Komprimierung von CSS, Javascript und Typescript Dateien,
  • Aliase / Snippets, um produktiver zu sein.

Uns hat die lokale Entwicklung mit Docker geholfen, um den Onboarding-Aufwand für neue Entwickler zu reduzieren. Außerdem gewährleistet Docker, dass alle Entwickler in der gleichen Umgebung arbeiten. Um eine bestmögliche Developer-Experience zu gewährleisten, wurden viele Prozesse mit Scripten abgedeckt. Einige Probleme der Kompatibilität zwischen Mac und Linux haben etwas mehr Aufmerksamkeit benötigt 😉

Wir sind zufrieden mit Docker und werden die Nutzung zukünftig ausbauen.