Inhaltsverzeichnis

Docker

Das Prinzip von Docker ist einfach: Die Anwendung wird mit allem was sie braucht, in einen Container gepackt und läuft so auf jedem System, auf dem Docker läuft.

Aber ist das nicht das, was man mit Virtualisierung erreicht?

Ja, an sich schon. Dockercontainer teilen sich allerdings den Kernel mit dem Hostsystem und sind so um ein Vielfaches schlanker und ressourcensparender. Ein Apache-Container verbraucht so nur unwesentlich mehr Ressourcen als die native Anwendung auf dem Hostsystem.

Aber wenn der Container doch mehr Ressourcen als die native Anwendung verbraucht, warum sollte ich dann Container nutzen?

Darauf gibt es keine universelle Antwort. Wenn die Anwendung z.B. nur aus statischen Inhalten besteht, macht es wenig Sinn, diese in einen Container zu packen. Mit Containern ist es allerdings möglich, einzelne Anwendungen isoliert voneinander laufen zu lassen.

Wenn eine Anwendung z.B. noch PHP 5.4 braucht, die andere aber schon PHP 7.0, könnte man beide in einen Container packen und sie würden sich nicht in die Quere kommen.

Außerdem lassen sich Anwendungen mittels Dockercontainern sehr einfach skalieren, dazu später mehr.

Im Folgenden wollen wir uns damit beschäftigen, wie man eine Anwendung mit PHP und Datenbank in einen Container packt und diese dann auf einem Server laufen lässt.

Schritt 0: Installation

Hier soll es um die Installation von Docker auf einem Debian 8 (bzw. Ubuntu) System gehen. Für die Installation auf anderen Systemen siehe https://docs.docker.com/engine/getstarted/step_one/

Für die Installation braucht man mindestens einen Kernel der Version 3.10 - Überprüfen lässt sich das mittels

uname -r

1. Diverse Pakete installieren, um den GPG-Schlüssel hinzuzufügen:

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

2. Docker's GPG key hinzufügen:

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

3. Docker's „stable“-Repository hinzufügen:

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

4. Einmal updaten und dann Docker CE (Community Edition) installieren:

sudo apt update && sudo apt install docker-ce

5. Docker sollte jetzt installiert sein. Um das zu testen, folgendes Kommando ausführen:

sudo docker run hello-world

Es sollte nun ein Container gestartet werden, welcher eine „Willkommen“ Nachricht anzeigt.

Images

Ein Dockercontainer besteht immer aus einem sogenannten Image. In diesem Image ist alles drin, was man zum starten eines Containers braucht. Diese Images kann man entweder direkt von Docker Hub herunterladen und ausführen, oder selbst erstellen und nur lokal ausführen. Docker Hub ist eine Art Registry, in der man eigene Images hoch- und die anderer Nutzer herunterladen kann (Es ist auch möglich, sein eigenes Registry zu hosten).

Images von Nutzern erkennt man an der Form des Namens „nutzer/image“ wohingegen offizielle Images nur aus dem Namen bestehen. (Offizielle Images werden offiziell von den Entwicklern unterstützt und supportet).

Images starten

Um ein Image herunterzuladen und direkt zu starten, reicht folgendes Kommando:

docker run image

Mit

docker run hello-world

haben wir vorhin also das „hello-world“-Image heruntergeladen und ausgeführt.

Um das ganze mal greifbarer zu machen, hier ein Beispiel (Dieser Container zeigt nur seinen Hostnamen an, für unsere Zwecke ist das im Moment ausreichend):

docker run -d -p 8080:80 --name web tutum/hello-world

Die einzelnen Optionen erklärt:

-d: Der Container läuft im Hintergrund -p: Port 80 aus dem Container wird an den Host auf Port 8080 weitergereicht. Wenn man nun also <Host-IP>:8080 aufruft, bekommt man den Container. –name: Vergibt einen Namen für den Container, um ihn später einfach wiederzufinden. Ohne diese Option vergbt Docker automatisch einen zufälligen Namen.

Mit dem Kommando

docker ps

lassen sich alle Container anzeigen, die momentan laufen, mit

docker ps -a

alle Container inklusive allen, die nicht mehr laufen.

In unserem Beispiel sieht das dann ungefähr so aus:

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
404ccfd08a99        tutum/hello-world   "/bin/sh -c 'php-f..."   2 seconds ago       Up 1 second         0.0.0.0:8080->80/tcp   web

Konkreter

Das ist jetzt ja alles schön und gut, aber wie können wir denn nun mit Docker eine Website laufen lassen?

In Docker Hub gibt es ein fertiges PHP-Image mit Apache. Zum Ausprobieren erstellen wir uns eine index.php Datei mit folgendem Inhalt:

<?php
phpinfo();

Diese speichern wir irgendwo in einem eigenen Ordner ab (das wird noch wichtig).

Um den Apache-Container nun zu starten, führen wir dieses Kommando aus:

docker run -d -p 8080:80 -v /pfad/zum/ordner/mit/index.php:/var/www/html --name web php:7.0-apache

(Den Pfad natürlich entsprechend anpassen)

Die einzelnen Optionen erklärt:

-d: Der Container läuft im Hintergrund -p: Port 80 aus dem Container wird an den Host auf Port 8080 weitergereicht. Wenn man nun also <Host-IP>:8080 aufruft, bekommt man den Container. -v: Volumes. Damit lassen sich Ordner (oder nur einzelne Dateien) vom Host-System in den Container mounten. Ansonsten werden alle Dateien im Container abgelegt und gelöscht, wenn der Container gestoppt wird. –name: Vergibt einen Namen für den Container, um ihn später einfach wiederzufinden. Ohne diese Option vergbt Docker automatisch einen zufälligen Namen.

Mit dem Kommando

docker ps

lassen sich alle Container anzeigen, die momentan laufen, mit

docker ps -a

alle Container inklusive allen, die nicht mehr laufen.

In unserem Beispiel sieht das dann ungefähr so aus:

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
eeb079101976        php:7.0-apache      "docker-php-entryp..."   4 seconds ago       Up 2 seconds        0.0.0.0:8081->80/tcp   web

Wenn wir jetzt im Browser localhost:8080 aufrufen, sollten wir unsere vorhin erstellte phpinfo() sehen. Der Container funktioniert!

Weitere nützliche Kommandos

docker rm <name des containers>

Entfernt den Container. Dieser muss dazu gestoppt sein.

docker stop/start/restart <name des containers>

(neu-)Startet/Stoppt den Container

docker inspect <name des containers>

Zeigt einige nützliche Informationen über einen Container an, z.B. die vorhandenen Volumes.

Proxys

Es ist ja schließlich nicht möglich, mehrere Container auf dem selben Port laufen zu lassen. Die müssen also auf verschiedenen Ports laufen. Um also für verschiedene Domains verschiedene Container anzusteuern, brauchen wir einen Proxy, der dann auf Port 80 (auf dem Hostsystem) lauscht und je nach Domain die Anfrage an den entsprechenden Container durchreicht. Wir machen das jetzt mal Beispielhaft mit NGINX (Apache würde natürlich auch gehen, NGINX ist allerdings schon „von Haus aus“ auf Loadbalancing ausgelegt und daher hier performanter als Apache)

Nach der Installation von NGINX legen wir eine Beispielhafte Konfiguration in /etc/nginx/sites-available/default an:

server {
   listen         80;
   server_name    example.com;

   location / {
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_pass http://localhost:8080;
   }
}

Wenn jetzt jemand example.com aufruft, wird der Request an den Container durchgereicht. Der Websitebesucher merkt davon nichts.

Der Container-Port ist allerdings öffentlich zugänglich - man kann also von außen über jede IP (Und damit auch jede Domain, die auf diese zeigt) des Hostsystems und Port 8080 auf den Container zugreifen. Um das zu verhindern, starten wir den Container mit einem etwas veränderten -p-Flag:

docker run -d -p 127.0.0.1:8080:80 -v /pfad/zum/ordner/mit/index.php:/var/www/html --name web php:7.0-apache

Damit ist der Container immernoch auf Port 8080 erreichbar, aber nur innerhalb des Hostsystem - von Außen kommt man nicht mehr dran. Damit wird der Proxy spätestens zwingend notwendig.

SSL

Da der einzige Weg, den Container zu erreichen, über einen Proxy ist, kann man sich hier mittels SSL dazwischenschalten und die Website so verschlüsslen. Mittels NGINX geht das Beispielsweise so (Ein gültiges Zertifikat vorrausgesetzt - Im Beispiel mit Letsencrypt):

server {
   listen         80;
   server_name    example.com;
   return         301 https://$server_name$request_uri;
}

server {
   listen         443 ssl;
   server_name    example.com;

   ssl_certificate /etc/letsencrypt/live/domain.tld/fullchain.pem;
   ssl_certificate_key /etc/letsencrypt/live/domain.tld/privkey.pem;

   ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

   ssl_session_cache shared:SSL:10m;
   ssl_session_timeout 30m;

   ssl_prefer_server_ciphers on;
   ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";

   ssl_stapling on;
   ssl_stapling_verify on;

   ssl_dhparam /etc/ssl/certs/dhparam.pem;

#   add_header Strict-Transport-Security "max-age=63072000;";

   # Logs werden auf dem Hostsystem gespeichert
   access_log  /var/log/nginx/docker-webapp_access.log;
   error_log   /var/log/nginx/docker-webapp_error.log;

   location / {
	proxy_buffers 16 4k;
        proxy_buffer_size 2k;

        proxy_read_timeout 300;
        proxy_connect_timeout 300;
        proxy_redirect     off;

        proxy_set_header   Host              $http_host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Frame-Options   SAMEORIGIN;

        client_max_body_size 10G;

        proxy_pass http://127.0.0.1:8080;
   }
}

Im Beispiel wurden noch einige zusätzliche (sichere) SSL-Einstellungen vorgenommen, um die Gesamtsicherheit zu erhöhen, diese haben sich in der Praxis bewährt.

Verbindungen zu anderen Containern

Unsere Website läuft also jetzt in einem Dockercontainer. Die meisten Websites benutzen heutzutage Datenbanken. Da Dockercontainer allerdings mehr oder weniger von der Außenwelt abgeschnitten sind, können sie nicht so ohne weiteres eine Verbindung zu z.B. einem Mysql-Server auf dem Hostsystem herstellen.

Nach dem Dockerprinzip sollten Container nur Sinnvolle Teile enthalten - die Datenbank sollte also in einem seperaten Container laufen

Um diese Verbindung zwischen unserem Website-Container und einem Datenbank-Container zu realisieren, brauchen wir zunächst einen Datenbankcontainer:

docker run -d --name db -v /var/www/webapp/db:/var/lib/mysql mariadb

Das Volume dient dazu, dass die Daten der Datenbank nicht verloren gehen, wenn der Container neugestartet wird.

Um diesen Container nun mit einem anderen zu verlinken, muss dieser mit dem –link Flag gestartet werden:

docker run -d -p 127.0.0.1:8080:80 --link db:db -v /pfad/zum/ordner/mit/index.php:/var/www/html --name web php:7.0-apache

Jetzt können wir aus dem Website-Container heraus auf die Datenbank via db (als Host angegeben) zugreifen.

Neurdings gibt es in Docker das „networks“-Feature. Damit lassen sich verschiedene Container in ein gemeinsames Netzwerk einsperren und können direkt über ihre Namen miteinander kommunizieren - ohne dass man das vorher explizit mit “–link“ angeben muss.

Compose

Im letzten Beispiel waren das einige Flags - bei einer Anwendung, die aus mehreren verschiedenen Containern besteht oder viele Volumes/Umgebungsvariablen (Man kann Umgebungsvariablen an den Container übergeben, um z.B. Standardpasswörter zu ändern) benutzt, kann es sehr schnell unübersichtlich werden.

Um dem entgegenzuwirken, gibt es Docker-Compose.

Installation erfolgt durch folgende Kommandos:

curl -L https://github.com/docker/compose/releases/download/1.12.0/docker-compose-`uname -s`-`uname -m` > ./docker-compose
mv ./docker-compose /usr/bin/docker-compose
chmod +x /usr/bin/docker-compose

Damit definiert man in einer docker-compose.yml-Datei die gesamte Webapp:

version: '2'
services:
  web:
    image: php:7.0-apache
    volumes:
      - /pfad/zum/ordner/mit/index.php:/var/www/html
    ports:
      - "127.0.0.1:8080:80"
    links:
      - db:db
    depends_on:
      - db
    restart: always
  db:
    image: mariadb
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=ganzgeheimespasswort
    volumes:
      - /var/www/webapp/db:/var/lib/mysql

depends_on verhindert, dass der Container ohne die Datenbank startet. restart: always sorgt dafür, dass der Container immer neugestartet wird, wenn z.B. der Server neugestartet wird.

Starten lässt sich diese Kombination mit

docker-compose up -d

Dafür muss man sich im selben Ordner wie die docker-compose.yml-Datei befinden.

Orchestration

Dockercontainer machen erst so richtig Sinn, wenn man sie massenweise einsetzt. Das kann man manuell mit Shell-Skripten machen, oder man nutzt Tools wie Docker Swarm, Kubernetes oder Rancher, welche Containerorchestrierung wesentlich einfacher machen.