Einführung in Docker

Docker Logo

Bitte beachten Sie: Die folgende Anleitung ist nicht innerhalb eines Shared Webhosting-Produktes umsetzbar, sondern nur auf einem ungemanagten Server.

Überblick

Moderne Webanwendungen bestehen heute oft aus mehreren Komponenten, die einzeln installiert werden müssen. Dabei sind meist auch Änderungen am darunter liegenden Linux-System nötig. Es müssen zusätzliche Abhängigkeiten der Software auf dem System installiert sein. So können zur Installation einer Anwendung auf einem Linux System schnell mal ein paar Stunden ins Land gehen. Nach Abschluss einer Installation ist eine Anwendung eventuell auch tief in das Linux System integriert und nicht mehr gekapselt. Zur Umgehung dieser Problematik kann man für jede komplexe Webanwendung einen eigenen virtuellen Server einrichten. Hierbei ergeben sich jedoch die folgenden Probleme:

  • Keine Deduplikation: Werden mehrere Images auf einem Server installiert, die z.B. auf einem Debian Linux Image basieren, müssten alle Daten des Images mehrfach gespeichert werden.

  • Kompatibilität: Das Image muss zur verwendeten Virtuallisierungslösung kompatibel sein. Die Portabilität ist somit eingeschränkt.

  • Vollvirtualisierung: Es handelt sich um eine Vollvirtualisierung, bei der auch Gerätetreiber z.B. für Netzwerkkarten vollständig virtualisiert werden. Das führt zu weiterem Overhead.

Zur Auflösung der Problematik wurden Docker entwickelt. Wichtige Konzepte und Begriffe aus dem Docker Ökosystem sind:

  • Ein Image ist die Festplatte des Containers. Ein Image kann aus mehreren Layern bestehen. Diese werden während der Erstellung des Containers erzeugt und können vom laufenden Container nicht mehr verändert werden. In jedem Layer werden nur die Änderungen gegenüber dem vorigen Layer gespeichert. Wird ein Container auf Basis eines Images gestartet, so wird ein sogenannter Container Layer erzeugt, der vom jeweiligen Container individuell beschrieben werden kann. Images können auf andere Images aufbauen. Durch Layer und aufeinander aufbauende Images kann eine Deduplizierung der gespeicherten Daten erzielt werden. Ein Image wird nicht zur persistenten Speicherung von Daten genutzt. Beispielsweise beim Update der Software wird das gesamte Image ersetzt. Es ist daher als zustandslos zu betrachten.

  • Ein Container ist eine Instanz des Images. In ihr steht eine vom Host paravirtualisierte Linux-Umgebung zur Verfügung.

  • Ein Volume dient dazu, persistente Daten eines Containers, wie z.B. die Inhalte einer Datenbank, zu speichern. Soll später ein Update des Containers durchgeführt werden, so kann das Image des Containers ausgetauscht werden, während die Daten erhalten bleiben.

  • Die Engine ist der serverseitige Prozess auf dem die Docker Umgebung ausgeführt wird.

  • Eine Docker Registry erlaubt es Images mit einer Organisation oder auch öffentlich zu teilen. In der Docker Standardinstallation ist Docker Hub als Standard-Registry eingebunden.

  • Ein Docker Repository ist eine Ansammlung von Docker Images, die in einer Registry abgelegt werden können. In einem Repository können auch mehrere unterschiedliche Versionen eines Image abgelegt werden.

  • Das Dockerfile ist das Rezept eines Containers. Hierin wird Anweisung für Anweisung das Image für einen Container erstellt. Mit jeder Anweisung wird ein neuer Layer innerhalb des Containers erzeugt, sodass bei einer Änderung im Dockerfile nicht das gesamte Image neu erstellt werden muss. In einem Debian Container könnten hier z.B. apt Befehle stehen, die Pakete installieren.

Anwendungsfälle für Docker

  • Docker eignet sich als Entwicklungsumgebung. Den Entwicklern kann über Docker Container eine einheitliche Umgebung zur Entwicklung von Linux-basierten Anwendungen bereitgestellt werden. Docker Container, die eigentlich ein Linux beinhalten können auch auf anderen Plattformen, wie Windows oder macOS gleichermaßen gestartet werden.

  • Die entwickelte Software kann im Produktivbetrieb direkt in Form von Containern weiterbetrieben werden.

  • Docker erleichtert die Dokumentation von installierter Software. Im Dockerfile oder Docker Compose sind die Anweisungen für die Erstellung eines Containers hinterlegt. Diese Rezepte lassen sich ein einem Versionskontrollsystem, wie git, verwalten und versionieren.

  • Docker eignet sich gut um auf einzelnen Servern Webanwendungen zu starten, die ansonsten einer aufwändigen Installation bedürften. Diese laufen so in einer abgekapselten Umgebung und können bei Bedarf leicht wieder von jeweiligen Server entfernt werden.

  • In der Continuous Integration können Docker Container in Pipelines mit Hilfe von Tools wie Travis CI, Jenkins oder Wercker eingebaut werden um Anwendungen zu kompilieren und zu testen bevor das Deployment automatisiert über Docker erfolgt.

  • Docker eignet sich auch für das Deployment großer, hochskalierter Anwendungen aus Mikro-Services. Durch die hohe Flexibilität von Docker können Container schnell auf mehreren Servern zusammenhängend gestartet werden.

Wann man Docker nicht verwenden sollte

  • Docker eignet sich nicht für Anwendungen mit graphischer Desktop Oberfläche, da es primär für Web- und Kommandozeilenanwendungen entwickelt wurde.

  • Durch die Verwendung von Docker muss mit leichten Performanz-Einbußen gerechnet werden. Es eignet sich daher nicht für hochperformante Anwendungen.

  • Da es sich um eine Paravirtualisierung handelt, können keine Veränderungen am Linux Kernel vorgenommen werden. Auch das Laden von Kernelmodulen ist in Docker-Containern nicht möglich.

Docker Compose

Um mehrere Container die zu einem Dienst gehören gemeinsam erstellen und verwalten zu können wurde Docker Compose als einfaches Werkzeug erfunden. In einer Datei docker-compose.yml können dabei die einzelnen Container und deren Beziehungen untereinander, also z.B. Volumes, Netzwerke und Portweiterleitungen definiert werden. Docker Compose kann auch Container auf Basis von Dockerfiles erzeugen.

Die Verwendung von Docker Compose wird in einem folgenden Beispiel erläutert. Am Ende des Artikels finden Sie in der Befehlssammlung eine Reihe von wichtigen Befehlen für Docker Compose.

Große Docker Umgebungen

Zur Verwaltung von großen Docker Umgebungen stehen verschiedene Tools und weitere Lösungen zur Verfügung. Hier soll nur ein kurzer Überblick über diese Lösungen gegeben werden. Eine bekannte Lösung ist z.B. Kubernetes. Kubernetes übernimmt die folgenden Aufgaben:

  • Erstellung von Anwendungsservices, die sich über mehrere Container erstrecken
  • Sammlung von Telemetrie-Daten z.B. zur Überwachung der Auslastung und Leistungsfähigkeit
  • Verwaltung von Zugangsberechtigungen zur Verwaltung der Container
  • Verwaltung der Replikation von Diensten zur Erhöhung der Ausfallsicherheit

Rancher ist eine verbreitete Lösung für das Management von Kubernetes.

Ab in die Praxis: Installation von Docker auf einem Debian Server

Es bietet sich an, die Community-Variante von Docker zu verwenden. Die Installation wird hier für Debian 10 “Buster” gezeigt. Eine ausführliche Anleitung findet man auch auf der Docker-Webseite. Docker wird auf dem folgenden Weg nicht aus den offiziellen Debian Repositories, sondern direkt aus dem offiziellen Docker Repository installiert. So kann es stets aktuell gehalten werden.

Verbinden Sie sich bitte per SSH auf Ihren Server. Zunächst sollten alle zuvor eventuell installierten Docker-Versionen entfernt werden:

sudo apt update
sudo apt remove docker docker-engine docker.io containerd runc

Installieren Sie bitte die nötigen Abhängigkeiten aus Debian:

sudo apt install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg2 \
    software-properties-common

und fügen Sie den PGP-Schlüssel von Docker hinzu:

curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -

Fügen Sie dann das Docker-Repository hinzu:

sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/debian \
   $(lsb_release -cs) \
   stable"

Im nächsten Schritt kann nun Docker installiert werden:

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io

Sie können anderen Benutzern auf dem Linux System erlauben, Docker zu verwalten. Dazu muss der Benutzer Mitglied der Gruppe docker sein. Falls die Gruppe nicht existiert, erstellen Sie diese bitte mit:

sudo groupadd docker

und fügen Sie anschließend den Benutzer zur Gruppe hinzu:

sudo usermod -aG docker <BENUTZER>

Um zu überprüfen, ob Docker korrekt installiert wurde, kann man einen “Hallo Welt” Container starten:

docker run hello-world 

Nach Ausführung des Befehls wird das benötigte Image automatisch heruntergeladen und gestartet:

max@demoserver ➜  ~ docker run hello-world 
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete 
Digest: sha256:fc6a51919cfeb2e6763f62b6d9e8815acbf7cd2e476ea353743570610737b752
Status: Downloaded newer image for hello-world:latest

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

Wenn Sie diese Meldung sehen, dann haben Sie Docker erfolgreich installiert.

Portainer als Beispiel für eine Webanwendung

Um zu zeigen wie in Docker eine Webanwendung gestartet werden kann, ziehen wir hier als Beispiel Portainer heran. Hierbei handelt es sich um ein webbasiertes Verwaltungswerkzeug für Docker.

Wir erstellen ein Volume, in dem die persistenten Daten von Portainer abgelegt werden mit folgendem Befehl:

docker volume create portainer_data

Die vorhandenen Volumes können mit dem Befehl docker volume ls angezeigt werden, was in unserem Fall dann so aussieht:

root@demoserver:~# docker volume ls
DRIVER              VOLUME NAME
local               portainer_data
root@demoserver:~# 

Der folgende Befehl startet dann Portainer:

docker run -d -p 9000:9000 --name=portainer --restart=unless-stopped -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer

Die -p Optionen legen dabei eine Portweiterleitung vom Host auf den Container an. Hierbei wird zunächst die Port-Nummer des Hosts angegeben. Mit der --name Flag lässt sich ein Name für den Container festlegen. Mit der --restart Option kann festgelegt werden, wann der Container gestartet werden soll. Hier empfiehlt sich die Option unless-stopped, die den Container nur dann neu startet, wenn er nicht gestoppt wurde. Mit der -v Option wird angegeben, welches Volume wo im Container eingehängt werden soll. Für Portainer reichen wir noch das Unix Socket /var/run/docker.sock durch, welches dem Container den Zugriff auf das auf dem Host laufende Docker ermöglich um dieses verwalten zu können. Schlussendlich muss man noch angeben, welches Image verwendet werden soll. In unserem Falle ist es das Image portainer/portainer.

Durch Ausführen des Befehls, werden die benötigten Images automatisch vom Server heruntergeladen und der Container gestartet. Mit dem Befehl docker pskönnen die laufenden Container angezeigt werden:

root@demoserver:~# docker ps
CONTAINER ID        IMAGE                 COMMAND             CREATED             STATUS              PORTS                                            NAMES
e12e5ebbd96a        portainer/portainer   "/portainer"        6 minutes ago       Up About a minute   0.0.0.0:8000->8000/tcp, 0.0.0.0:9000->9000/tcp   portainer
root@demoserver:~# 

Hieraus ist die ID des Containers ersichtlich, sowie das Image auf dem der Container basiert.

Über den Webbrowser kann nun auf das Portainer Webinterface zugegriffen werden, z.B. über http://demoserver.mustermann-domain.de:9000/. Hier muss jetzt ein Passwort für den Admin-Benutzer von Portainer festgelegt werden:

Portainer

Für die Verbindung zu Docker wählt man dann die Option “Local” aus, sodass das durchgereichte Socket verwendet werden wird:

Portainer

In Portainer kann man nun graphisch die Docker-Instanz verwalten:

Portainer

Möchte man den laufenden Container nun stoppen kann das anhand dessen ID mit dem Befehl docker stop <ID> geschehen:

root@demoserver:~# docker stop 36fc433abcef
36fc433abcef
root@demoserver:~# 

Analog kann der Container auch mit docker start <ID> wieder gestartet werden. Mit dem Befehl docker rm <ID> wird der Container gelöscht.

Problematisch ist in diesem Beispiel noch, dass der Webserver unter Port 8000 in diesem Beispiel nur einen HTTP-Server bereitstellt. Für eine sichere Verbindung zu Portainer wäre eine SSL-Verschlüsselte HTTPS Verbindung nötig. An dieser Stelle ist es üblich auf dem Host-Rechner hierfür einen HTTP-Reverse Proxy, z.B. nginx einzurichten, der die verschlüsselte Verbindung vom Client entgegennimmt und an den Container weiterreicht.

Eigene Container erstellen

Zu besseren Verständnis von Docker wird hier gezeigt, wie Sie ein eigenes Image mit Hilfe von Docker Compose erstellen können. Das ist in diesem Beispiel ein kleiner Webserver auf Basis des Python Frameworks Flask und ein Redis Datenbankserver. Mit Hilfe von Docker Compose ist es möglich mehrere Docker Container gemeinsam zu erstellen.

Für die neue Webanwendung erstellen wir ein Verzeichnis, z.B. mit dem Namen mein-erster-container. In diesem Verzeichnis erstellen wir die Datei app.py mit dem folgenden Inhalt:

# mein-erster-container/app.py

from flask import Flask
from redis import Redis

app = Flask(__name__)
redis = Redis(host='redis', port=6379)

@app.route('/')
def hello():
    redis.incr('hits')
    return 'Meine Beispielanwendung wurde bereits %s mal angezeigt. ' % redis.get('hits').decode("utf-8")


if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True, port=8080)

Für Python brauchen wir noch eine Datei mit dem Namen requirements.txt in der die Python-Abhängigkeiten festgehalten werden:

Flask==1.1.1
redis==3.4.1

Das eigentliche “Rezept” für den Docker-Container wird in der Datei Dockerfile hinterlegt:

FROM ubuntu:latest

MAINTAINER Max Mustermann "max@mustermann-domain.de"

RUN apt-get update -y && \
    apt-get install -y python3 python3-pip python3-dev

# We copy just the requirements.txt first to leverage Docker cache
COPY ./requirements.txt /app/requirements.txt

WORKDIR /app

RUN pip3 install -r requirements.txt

COPY . /app

ENTRYPOINT [ "python3" ]

CMD [ "app.py" ]

Die Container können dann mit dem Befehl

docker-compose up --build

erstellt und gestartet werden. Die Flag --build sorgt dafür, dass der Container mit allen Schritten neu erzeugt wird. Es werden automatisch die benötigten Images heruntergeladen und darauf basierend der Container erstellt und gestartet. Mit der Option -d, also z.B. docker-compose up -d kann der Container im Hintergrund gestartet werden.

Ist der Beispiel-Container gestartet, kann hierrauf über das Webinterface über den Port 8080 zugegriffen werden, z.B. über https://demoserver.mustermann-domain.de:8080/. Hier wird dann der Text aus dem Beispielprogramm angezeigt: “Meine Beispielanwendung wurde bereits 5 mal angezeigt”.

Mit Hilfe des Befehls

docker-compose exec web /bin/bash

kann man eine bash Shell innerhalb des web Containers starten.

Mit docker-compose down kann der Container wieder beendet werden.

Sammlung von wichtigen Befehlen für Einsteiger

Container

  • docker container ls -a - Anzeigen der vorhandenen Container. Mit der Zusatzoption -a werden auch nicht laufende Container angezeigt.
  • docker ps - zeigt laufende Container an
  • docker run -d <IMAGENAME> - Erstellen und Starten eines Containers basierend auf einem Image. Die -d Option startet den Container im Hintergrund.
  • docker start <CONTAINERID> - Startet einen Container anhand einer Container-ID an
  • docker stop <CONTAINERID> - Beendet einen laufenden Container anhand einer Container-ID an
  • docker kill <CONTAINERID> - Bricht einen laufenden Container ab und beendet ihn
  • docker rm <CONTAINERID> - löscht einen gestoppten Container
  • docker log -f <CONTAINERID> - Zeigt die Log-Ausgabe eines Containers anhand einer Container-ID an.
  • docker attach <CONTAINERID> - hier kann mit dem laufenden Prozess innerhalb des Containers über die Standard Ein- und Ausgabe interagiert werden. Wird dieser Hauptprozess des Containers beendet, so wird auch der Container beendet. Das heißt: das Tastenkürzel Strg+ C oder der Befehl exit beendet eventuell den Container.
  • docker exec -it <CONTAINERID> /bin/sh - der Befehl startet eine Shell innerhalb des Containers

Images

  • docker image ls - Anzeigen der vorhandenen Images
  • docker image prune - Entfernt nicht mehr benötigte Images
  • docker image pull <NAME> und docker image push <NAME> holen oder kopieren Images von oder zu einer Registry anhand eines Namens
  • docker image rm <NAME> löscht ein nicht verwendetes Image

Docker Compose

  • docker-compose up - startet die Container, die in der Datei docker-compose.yml im aktuellen Verzeichnis definiert wurden
  • docker-compose down - beendet eine laufende Sammlung von Containern, die in einer docker-compose.yml Datei definiert wurden
  • docker-compose ps - zeigt die Container an, welche gerade von Docker Compose verwaltet werden
  • docker-compose start <NAME> und docker-compose stop <NAME> starten oder beenden Container die Anhand des in der docker-compose.yml hinterlegten namens identifiziert werden

Referenzen