Dockerisierung von (Legacy-)PHP-Applikationen

Dockeranfänger aufgemerkt! Es ist häufiger mal nützlich, alte PHP-Anwendungen in einer sicheren Umgebung zum Laufen zu bringen, am besten auch reproduzierbar. All dies ist kein Hexenwerk – und hier zeigen wir euch, wie ihr (Legacy-)PHP-Applikationen in Docker-Container verpackt.

Die Ausgangssituation

Wir hatten uns in einem internen Projekt damit beschäftigt, Tools, Skripte und andere nützliche Dinge zusammenzutragen, die uns bei einer Migration eines PHP-Projektes auf eine neuere PHP-Version helfen können.

Im Laufe dieser Arbeit wollten wir eine Beispiel-Legacy-Applikation aufsetzen. Sie sollte aus der „echten Welt“ kommen, auf Symfony basieren, aber immer noch klein und übersichtlich genug sein, um unsere erarbeiteten Tools auszuprobieren. Außerdem soll sie auch in einem Workshop Anwendung finden können.

Unsere Wahl fiel auf Wallabag, speziell Wallabag 2.0.0-beta.1, basierend auf der PHP-Version 5.6.

— modernize Your PHP —

In unserem Projekt modernizeYourPHP halten wir alle Dinge fest, die beim Migrieren eines PHP-Projektes auf eine neuere Version helfen können.

Docker und (Legacy-)PHP-Apps

Zu Beginn des Projekts benötigten wir dafür erst einmal zwei Dinge:

  • Die Webserver-Konfiguration und
  • alle Befehle, die zum Bauen der Applikation notwendig sind.

In unserem Fall hatten wir bereits eine Konfiguration für nginx; somit fiel auch die Wahl auf einen Webserver nicht schwer.

Erste Anlaufstelle für PHP-Docker-Images ist Docker Hub: https://hub.docker.com/_/php. Außerdem werden auf Docker Hub allerhand Webserver-Images angeboten; in unserem Fall haben wir uns für das nginx-Docker-Image entschieden.

In unserem Setup verwenden wir Docker Compose, um sowohl einen Webserver-Container, als auch einen Container mit unserem PHP-Projekt, aufzusetzen.

Docker Compose

Unsere docker-compose.yml sah (für den Anfang erst mal) so aus:

version: '3'
services:
  nginx:
    image: nginx
    ports:
      - "8080:80"
    volumes:
      - "./nginx.conf:/etc/nginx/nginx.conf"
      - ".:/var/www/html"
  php:
    build:
      context: wallabagContainer/
    volumes:
      - ".:/var/www/html"

Hier machen wir es uns leicht und laden alle Dateien in unserem Verzeichnis per bind mount in beide Container hinein. Außerdem mounten wir unsere Webserver-Konfiguration nginx.conf über die bereits im Parent-Image existierende darüber.

Der Eintrag wallabagContainer/ bezieht sich auf einen von uns angelegten Unterordner, in dem ein (so benanntes) Dockerfile liegt.

├── docker-compose.yml
├── nginx.conf
└── wallabagContainer
    ├── Dockerfile
    └── wallabag.sqlite
 
1 directory, 4 files

Das Dockerfile

Im Dockerfile haben wir die Umgebung für unsere PHP-Applikation eingerichtet. Während unser nginx schon durch seine Konfigurationsdatei fertig eingerichtet wurde, müssen wir in dem Dockerfile noch ein wenig selbst Hand anlegen:

# Image, auf dem wir aufbauen
FROM php:5.6-fpm
 
# Alter Composer für alte Applikation
COPY --from=composer:1 /usr/bin/composer /usr/bin/composer

ENV COMPOSER_MEMORY_LIMIT=-1
ENV SYMFONY_ENV=prod
 
# Zeitzone (wichtig für unsere Applikation)
ARG timezone='Europe/Paris'
RUN echo "date.timezone="$timezone > /usr/local/etc/php/conf.d/date_timezone.ini
 
# Externe Abhängigkeiten werden mit apt (Debian-Paketmanager) installiert
RUN apt-get update && apt-get install -y \
        libmcrypt-dev libicu-dev libpq-dev libxml2-dev git zip libgd3 libpng-dev \
    && docker-php-ext-install \
        iconv mcrypt mbstring intl pdo gd
 
# Skript für nachträgliche Befehle im Container
COPY run.sh /run.sh
RUN chmod +x /run.sh
 
# Befehle zum Bauen der Applikation
RUN mkdir /app && cd /app && composer create-project wallabag/wallabag wallabag "2.0.0-beta.1" --no-dev
 
CMD ["/run.sh"]

Wir gehen also vom vorgefertigten Image aus und installieren unsere Abhängigkeiten.

Docker-Images sind von Haus aus sehr schlank. Wenn man jedoch – wie in unserem Fall – nicht auf ein bereits existierendes, applikationsspezifisches Image zurückgreifen kann, müssen dementsprechend auch banal erscheinende Abhängigkeiten meist selbst installiert werden.

Das Bauen der Applikation

Im Dockerfile sind alle Befehle enthalten, um unsere Applikation zu bauen. In unserem Skript run.sh verschieben wir die Applikation in den Ordner, der für unseren Webserver zugreifbar ist und zugleich mit unserem Host geteilt wird. Beachtenswert ist, dass die Datei erst ausgeführt wird, nachdem der Container bereits läuft und somit der bind mount erfolgt ist. Um das zu erreichen, verwenden wir die CMD-Direktive.

#!/bin/bash
 
# Wenn unsere Applikation noch nicht existiert
if ! [ -e "/var/www/html/wallabag" ]; then
  echo "Copying project to shared volume, please wait..."
  cp -rL /app/wallabag /var/www/html/
  cp -f /var/www/html/wallabagContainer/wallabag.sqlite /var/www/html/wallabag/data/db/wallabag.sqlite
  echo "Project copied, FPM will start now!"
fi
 
# Starte fpm
php-fpm

Hierin verschieben wir beim ersten Start das Wallabag-Projekt nach /var/www/html. Um Wallabags interaktiven Konfigurationsprozess zu vermeiden, spielen wir eine vorgefertigte Sqlite-Datenbank ein. Dazu haben wir einfach die Datenbank einer frisch eingerichteten Wallabag-Installation kopiert.

Docker und nginx

In unserer nginx.conf gibt es noch einen wichtigen, Docker-spezifischen Punkt:

[…]
 
http {
 
[…]
 
    upstream php-upstream {
        server php:9000;
    }
 
[…]

In Docker Compose sehen sich die Docker-Container gegenseitig unter ihrem Namen (in unserem Fall php) und können so miteinander kommunizieren, selbst wenn wir in unserer Docker-Compose.yml nicht explizit ein Netzwerk definiert haben.

Hinweis

Viele Applikationen benötigen spezifische Webserver-Konfigurationen. Hier ist etwas Recherchearbeit (oder das Kopieren einer bestehenden Konfiguration) sehr wertvoll.

Stolperstein Dateiberechtigungen

Theoretisch haben wir nun alles, was wir brauchen.

Allerdings hatten wir in unserem Projekt auf Unix-Systemen noch das Problem, dass wir falsche Dateiberechtigungen bei unserem PHP-Code hatten, was dazu führte, dass die Anwendung nicht lief. Außerdem sollte der Source Code noch weitergenutzt werden und auch außerhalb der Docker-Umgebung die selben Dateiberechtigungen besitzen.

Deshalb gibt es hier noch einen Punkt, der beachtet werden muss: Die korrekten Zugriffsrechte. Das heißt, eine Übereinstimmung der Nutzer-ID von Host und Gast.

Herausforderungen unter Unix entgegenwirken

Auf Unix-Systemen erzeugt Docker wegen bind mount standardmäßig Dateien, die root gehören. Ein schneller, „schmutziger“ Hack (wie chmod -R 777) kann jedoch problematisch sein und dafür sorgen, dass durch die falschen Zugriffsrechte die Anwendung nicht mehr funktioniert.

Um das zu verhindern, bringen wir Docker dazu, den gleichen Benutzer wie auf unserem Host-System für seine Befehle zu benutzen. Dafür muss die Benutzer- und Gruppen-ID ausgelesen und an das Dockerfile durchgereicht werden. Hierfür haben wir die start.sh angelegt:

#!/bin/bash
 
# this is only relevant for unix hosts
export USER_ID=$(id -u ${USER})
export GROUP_ID=$(id -g ${USER})
 
docker-compose up

Durchreichen der Benutzer- und Gruppen-ID an Docker

USER_ID und GROUP_ID geben wir über die docker-compose.yml an den Container weiter …

[…]
php:
    build:
      context: wallabagContainer/
      args:
        USER_ID: ${USER_ID:-www-data}
        GROUP_ID: ${GROUP_ID:-www-data}
[…]

… und werden von dieser als build-args an das Dockerfile weitergereicht. Dort werden sie – falls gesetzt – verwendet, um das Bauen der Applikation sowie späteres Verschieben in den Webserver-Ordner mit dem übergebenen Nutzer in der run.sh auszuführen:

# Das hier muss ganz oben stehen, um als erstes angewendet zu werden
FROM php:5.6-fpm
 
[…]
 
ARG USER_ID
ARG GROUP_ID

# Zum Anlegen der Ordner benötigen wir noch Root-Rechte
RUN mkdir /app && chown $USER_ID:$GROUP_ID /app
RUN mkdir /.composer && chown $USER_ID:$GROUP_ID /.composer

RUN chown $USER_ID:$GROUP_ID /app

# Ab hier verwendet Docker den angegebenen Benutzer und die angegebene Gruppe für weitere Befehle
USER $USER_ID:$GROUP_ID

# Das Erstellen des app-Ordners wird nun weiter oben erledigt
RUN cd /app && composer create-project wallabag/wallabag wallabag "2.0.0-beta.1" --no-dev
 
# Und das hier muss ganz unten stehen
CMD ["/run.sh"]

Unsere endgültige Verzeichnisstruktur sieht dann folgendermaßen aus:

.
├── docker-compose.yml
├── nginx.conf
├── start.sh
└── wallabagContainer
    ├── Dockerfile
    ├── run.sh
    └── wallabag.sqlite
 
1 directory, 6 files

Willkommen in Docker, PHP-Applikation!

Das war es schon! Unter Unix einfach das Skript start.sh (für erneute Starts reicht ein docker-compose up), bei Windows docker-compose up ausführen, und die App ist unter localhost:8080 erreichbar!

In unserem Repo modernizeYourPHP könnt ihr unser Projekt anschauen und selbst ausprobieren. Dort sind außerdem unsere Erfahrungen zu den eingangs erwähnten Tools und weiteren Dingen, die wir für PHP-Migrationen evaluiert haben, festgehalten.

Für neue Blogupdates anmelden:


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.