Docker statt Vagrant: dank Boot2docker auch auf dem Mac

Vagrant ist ja sowas von 2014. Naja, nicht wirklich, ist ja immernoch ein sehr cooler Ansatz um eine VM versionierbar zu provisionieren. Dies ist auch kein Rant gegen Vagrant! Ich habe ja selbst erst Ende letzten Jahres über den Wechsel von MAMP zu Vagrant gebloggt. Aber wenn man lokal mehrere Projekte mit mehreren verschieden bestückten VMs aufzieht und miteinander reden lässt, ist fix auch 8 GB RAM verkonsumiert. Und da springt Docker durch den anderen Ansatz in die Presche. Angefixt durch zwei Artikel im aktuellen PHPMagazin 2/2015 hab ich das ganze auf meinen Mac mit Hilfe von boot2docker mal angetestet.

TL;DR

Docker ermöglicht änderbare Anwendungs-Container zu erstellen und zu betreiben, die auf nicht änderbaren Images basieren. Änderungen können commitet werden zu einen neuen Image. Diese werden lokal in einen Repository verwaltet.

Docker selbst betreibt keine VMs, sondern hat immer das Host-OS als Unterbau. Auf dem Mac ist es die  Boot2docker VM (VirtualBox). Es ist wunderbar möglich verschiedene Linux-Distros mit verschiedenen Versionen von Diensten nebeneinander Speicherplatz-schonen zu betreiben.

Wenn man das Grundkonzept verstanden und ein bisschen herumprobiert hat, kommt man relativ fix auch als Nicht-Admin dahinter.

Installation boot2docker

Flux von boot2docker.io heruntergeladen bekommt man nach der Installation die ersten Hinweise wie es weitergeht.

  • Quick-Start: Run Boot2Docker (located in Applications), which will open a terminal window. Then, start a test container with: 
docker run hello-world
  • To save and share container images, automate workflows, and more sign-up for a free Docker Hub account.
  • You can upgrade your existing Boot2Docker VM without data loss by running: 
boot2docker upgrade

boot2docker intialisieren

boot2docker gestartet gings dessen Initialisierung los.

bash-3.2$ unset DYLD_LIBRARY_PATH ; unset LD_LIBRARY_PATH
bash-3.2$ mkdir -p ~/.boot2docker
bash-3.2$ if [ ! -f ~/.boot2docker/boot2docker.iso ]; then cp /usr/local/share/boot2docker/boot2docker.iso ~/.boot2docker/ ; fi
bash-3.2$ /usr/local/bin/boot2docker init
Generating public/private rsa key pair.
Your identification has been saved in /Volumes/Daten/ronny/.ssh/id_boot2docker.
Your public key has been saved in /Volumes/Daten/ronny/.ssh/id_boot2docker.pub.
The key fingerprint is:
... 
The key's randomart image is:
+--[ RSA 2048]----+
|                 |
|                 |
..
|             oo+ |
+-----------------+
/usr/local/bin/boot2docker up
$(/usr/local/bin/boot2docker shellinit)
docker version

bash-3.2$ /usr/local/bin/boot2docker up
<b>Waiting for VM and Docker daemon to start...</b>
............................oooooooooooooooooooo
Started.
Writing /Volumes/Daten/ronny/.boot2docker/certs/boot2docker-vm/ca.pem
Writing /Volumes/Daten/ronny/.boot2docker/certs/boot2docker-vm/cert.pem
Writing /Volumes/Daten/ronny/.boot2docker/certs/boot2docker-vm/key.pem

To connect the Docker client to the Docker daemon, please set:
export DOCKER_CERT_PATH=/Volumes/Daten/ronny/.boot2docker/certs/boot2docker-vm
export DOCKER_TLS_VERIFY=1
export DOCKER_HOST=tcp://192.168.59.103:2376

bash-3.2$ $(/usr/local/bin/boot2docker shellinit)
Writing /Volumes/Daten/ronny/.boot2docker/certs/boot2docker-vm/ca.pem
Writing /Volumes/Daten/ronny/.boot2docker/certs/boot2docker-vm/cert.pem
Writing /Volumes/Daten/ronny/.boot2docker/certs/boot2docker-vm/key.pem
bash-3.2$ docker version
Client version: 1.4.1
Client API version: 1.16
Go version (client): go1.3.3
Git commit (client): 5bc2ff8
OS/Arch (client): darwin/amd64
Server version: 1.4.1
Server API version: 1.16
Go version (server): go1.3.3
Git commit (server): 5bc2f

Fehlermeldung “dial unix /var/run/docker.sock: no such file or directory” vermeiden

Schnell den Anweisungen gefolgt und die obigen export DOCKER_* durchgeführt ..

export DOCKER_CERT_PATH=/Volumes/Daten/ronny/.boot2docker/certs/boot2docker-vm
export DOCKER_TLS_VERIFY=1
export DOCKER_HOST=tcp://192.168.59.103:2376

Sofern man das unter Mac nicht in jeden Terminalfenster macht, führen Aufrufe von docker zu Fehlern wie dial unix /var/run/docker.sock.

Hello World Beispiel starten

.. und das Hello World per docker run hello-world Beispiel angetriggert.

bash-3.2$ docker run hello-world
Unable to find image 'hello-world:latest' locally
hello-world:latest: The image you are pulling has been verified
511136ea3c5a: Pull complete
31cbccb51277: Pull complete
e45a5af57b00: Pull complete
Status: Downloaded newer image for hello-world:latest
Hello from Docker.
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (Assuming it was not already locally available.)
3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal.

To try something more ambitious, you can run an Ubuntu container with:

$ docker run -it ubuntu bash

For more examples and ideas, visit:

<a href="http://docs.docker.com/userguide/">http://docs.docker.com/userguide/</a>

Scheint zu klappen

Ubuntu mit Bash starten

Also gleich der nächsten Empfehlung gefolgt und docker run -it ubuntu bash ausgeführt.

bash-3.2$ docker run -it ubuntu bash
Unable to find image 'ubuntu:latest' locally
ubuntu:latest: The image you are pulling has been verified
53f858aaaf03: Pull complete
837339b91538: Pull complete
615c102e2290: Pull complete
b39b81afc8ca: Pull complete
511136ea3c5a: Already exists
Status: Downloaded newer image for ubuntu:latest
2385c5a:/#

Schon blinkt der Cursor in einer Bash innerhalb eines Ubuntu-Docker-Containers! Damit ist die Ausgangsbasis für alle weiteren Docker-Versuche geschaffen.  Mensch mensch, das sind echt Profis am Werk, wenn sogar einer wie ich das ans Laufen bekommt.

Grundlagen anlesen

Bevor ich selber losgelegt habe, hab ich erstmal ein bisschen herumgelesen – zusätzlich zum PHP Magazin Artikeln…

Verstanden hab ich die ganze Geschichte über die Use-Cases-Bilder in der Slideshare-Präsentation „How to deploy PHP projects with docker“ von Ruoshi Ling.

image02

image03

image05

image04

image00

Alles klar? Nicht? Dann den ganzen Vortrag durcharbeiten und die Artikel nochmal lesen.

Mounten von Verzeichnissen: Mac OSX > Boot2docker VM > Docker-Container

Bevor ich irgendwelche MySQL oder Apache hochziehen kann, muss erstmal geklärt werden, wie überhaupt die Daten sauber in die Docker-Container reinkommen. Läuft Docker nativ auf der eigenen Linux-Büchse, sind alle Verzeichnisse ja (im Rahmen von SELinux) erreichbar. Aber auf Mac gibt es mit der Boot2docker VM noch eine weitere Schicht dazwischen.

Seit Docker 1.3 und “signed images” ist zumindest das nicht mehr so schwer. Das Verzeichnis /Users ist standardmäßig eingebunden, siehe auch boot2docker Hilfe.
Bei mir liegen aber die Vhosts und Mysql-Daten auf einer anderen Festplatte und damit unterhalb von /Volumes/Daten. Eigentlich könnten diese ganz bequem über die VirtualBox Oberfläche dem laufenden boot2docker-vm hinzugefügt werden.

image01

Danach boot2docker restart durchführen und per boot2docker ssh schauen ob die Daten nun dort auch verfügbar sind. Funktioniert leider nicht. Aber in den Release-Infos von Docker 1.3 ist ja auch davon die Rede, dass nur /Users funktioniert. Naja, künftig vielleicht irgendwann mal. Ich hab dann einfach meine Daten nach /Users verschoben – Platz ist genug gewesen.

MySQL

Ich habe als Grundlage tutum/mysql verwendet.

Mysql Server versuchsweise starten:

docker run -d --name my-mysql -p 3306:3306 -v /Users/mysql:/var/lib/mysql \
   tutum/mysql:5.6

Funktioniert nicht lt. docker logs mysql. Rechteprobleme, da das gemountete Verzeichnis „1000:staff“ gehört, Mysql wird aber über Nutzer „mysql“ gestartet. Hilfe habe ich hier gefunden.

Also bash im Image starten

docker run -it -v /Users/mysql:/var/lib/mysql tutum/mysql:5.6 bash

.. und darin dann ausführen ..

usermod -u 1000 mysql
chown mysql.staff /var/run/mysqld

.. das commiten als neues Image zu deinnutzer/mysql

docker commit [container-id] deinnutzer/mysql

.. nun Image nochmal starten – jetzt allerdings mit „mysqld“ Aufruf damit der Server startet

docker run -d --name mysql -p 3306:3306 -v /Users/mysql:/var/lib/mysql \
  deinnutzer/mysql mysqld

Per docker logs mysql sollte nun ein vor sich hin schnurrender Mysql-Server-Daemon zu sehen sein.

Als Dockerfile sieht obiges dann so aus:

FROM tutum/mysql:5.6
MAINTAINER Ronny Hartenstein <>

# https://github.com/boot2docker/boot2docker/issues/581
RUN usermod -u 1000 mysql
RUN chown mysql.staff /var/run/mysqld

EXPOSE 3306

CMD ["/usr/sbin/mysqld"]

Apache + www + PHP

Zunächst habe ich das Image eboraas/apache-php verwendet. Als Beispiel dient meine Auslandsreisen-Homepage (basiert auf phpwcms, allerdings in einer sehr alten Version)

docker run -p 80:80 --name vhost-ausland -v /Users/vhosts/ausland:/var/www \
  -d eboraas/apache-php

Läuft wacker einfach los. Toll. Ein Aufruf der Seite über http://[boot2docker ip]/ schlägt aber fehl, da die MySQL nicht erreicht werden kann.

Leider ist meine Auslands-Webseite so alt, dass sie noch mysql_connect etc. verwendet und damit nicht mit PHP 5.4 out-of-the-box losläuft. Weiteres dazu siehe weiter unten bei “Ubuntu 10.04 + PHP 5.3”

Heirat von MySQL und Apache

Innerhalb der boot2docker VirtualBox Instanz sind die Ports 80 und 3306 offen und können untereinander reden. Allerdings weiß ja der eine Container nicht welche IP der andere hat. Und meist braucht man auch die Ports von MySQL nicht nach außen geben, wenn nur der Apache/PHP zugreifen soll. Dafür ist Verlinkung gut. Eine gute Anleitung zum Verlinken von Containern gibts in der offiziellen Docker-Doku.

Verlinken von vhost mit mysql:

docker run -p 80:80 --name vhost-ausland -v /Users/vhosts/ausland:/var/www \
   --link mysql:mysql -d eboraas/apache-php

Testen per Bash

docker exec -it vhost-ausland bash

Die hosts:

2:/# <b>cat /etc/hosts</b>
172.17.0.22    0d9b36837ab2
127.0.0.1    localhost
::1    localhost ip6-localhost ip6-loopback
fe00::0    ip6-localnet
ff00::0    ip6-mcastprefix
ff02::1    ip6-allnodes
ff02::2    ip6-allrouters
172.17.0.14    mysql

Ein „ping mysql“ würde also zu 172.17.0.14 auflösen. Super!

Die Environments:

2:/# <b>set|grep MYSQL</b>
..
MYSQL_PORT=tcp://172.17.0.14:3306
MYSQL_PORT_3306_TCP=tcp://172.17.0.14:3306
MYSQL_PORT_3306_TCP_ADDR=172.17.0.14
MYSQL_PORT_3306_TCP_PORT=3306
MYSQL_PORT_3306_TCP_PROTO=tcp

Und diese Werte sind im PHP mittels $_ENV verfügbar, wobei $db_host="mysql" völlig reicht.

Ports von Mac OSX erreichbar?

Bloß wie die Ports nach außen zu OSX bringen? boot2docker gibt Antworten: Gar nicht, die Ports sind an der laufenden boot2docker-VirtualBox erreichbar.

mysql -h$(boot2docker ip 2>/dev/null) -uroot

Voraussetzung ist, dass für root Zugriff von jeder IP möglich ist, also „root@%„. In abgeschlossenen Netzen auf Entwickler-Rechnern eher kein Problem.

Legacy-LAMP-Stack: Ubuntu 10.04 + PHP 5.3

Da das Auslands-Projekt schon sehr alt ist, soll es weiter auf Ubuntu Lucid und PHP 5.3 laufen. Auch dafür ist Docker gut – nicht nur den neuesten Hipsterkram antesten, sondern auch hornaltes zum laufen bringen!

Dockerfile für Apache2 unter Ubuntu 10.04:

FROM ubuntu:lucid
MAINTAINER Ronny Hartenstein <>

RUN apt-get update && apt-get -y install apache2 apache2-utils apache2.2-bin apache2.2-common apache2-mpm-prefork && apt-get clean

ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_LOG_DIR /var/log/apache2

#RUN /bin/ln -sf ../sites-available/default-ssl /etc/apache2/sites-enabled/001-default-ssl
#RUN /bin/ln -sf ../mods-available/ssl.conf /etc/apache2/mods-enabled/
#RUN /bin/ln -sf ../mods-available/ssl.load /etc/apache2/mods-enabled/

EXPOSE 80
#EXPOSE 443

CMD ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]

Build:

docker build ronnyhartenstein/lucid-apache

Dockerfile für PHP-Aufsatz

FROM ronnyhartenstein/lucid-apache
MAINTAINER Ronny Hartenstein <>

RUN apt-get update && apt-get -y install php-pear php5 php5-cgi php5-cli php5-common php5-curl php5-gd php5-imap php5-mcrypt php5-mysql php5-sqlite php5-xdebug php5-xsl mysql-client && apt-get clean

EXPOSE 80
#EXPOSE 443

CMD ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]

Build:

docker build ronnyhartenstein/lucid-apache-php .

Wie zu sehen ist, ist SSH zwar vorgesehen, aber nicht aktiviert. Ich brauche es dafür derzeit schlicht nicht.

Gestartet und verlinkt gegen MySQL …

docker run -p 80:80 --name vhost-ausland -v /Users/vhosts/ausland:/var/www \
   --link mysql:mysql -d ronnyhartenstein/lucid-apache-php

.. läuft das ganze los und bringt sogar korrekte Ergebnisse. Toll!

Ein paar hilfreiche Docker-Kommandos für den Alltag

Alle Container auflisten – inklusive der bereits beendeten – gut zum aufräumen

docker ps --all

Container anhalten

docker stop [hash|name]

Container löschen

docker rm [hash|name]

Lokal vorhandenen Images listen

docker images

Image löschen

docker rmi [name]

bash starten in laufenden container mit Namen “vhost-ausland”

docker exec -it vhost-auslandbash

 Fazit

Wie schon der Wechsel zu Vagrant ein Game-Changer war, ist Docker noch ein ganzes Stück mehr fetzig. Super ist, dass man alte Linux-Distros zusammen mit den neuesten Distros laufen lassen kann, sorgt für insgesamt geringere Schmerzen in der Wartung. Man muss für „Privatzwecke“ ja noch nicht mal Dockerfiles bauen oder selber zum Docker Hub pushen. Es genügt auch einfach ein Ubuntu (o.ä.) zu starten und mittels bash darin herumzuinstallieren. Am Ende ein commit zu einen eigenen Image und gut. Das kann dann für verschiedene Projekte isoliert aufgerufen werden.

Die ganze Recherche und Probiererei hat mich die letzten beiden Wochen abends und am Wochenende vor den Bildschirm gefesselt. Anfangs ist die Lernkurve flach, aber sobald man das Konzept von Containern und Images grundlegend verstanden hat, kann man sich den Rest durch Herumprobieren erschließen. Da auch keine VMs ewig booten müssen, ist man tatsächlich viel mit Doing beschäftigt und weniger mit warten.

Ich kann euch nur empfehlen, wenn ihr als Devs eure Admins oder DevOps verstehen wollt, oder aber als Dev eure Fullstack skills leveln wollt (oh mann…) – schauts euch an, macht Spaß!