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.
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.
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).
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
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!
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.
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.
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.
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.
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.
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.