Dockerisierung von (Legacy-)PHP-Applikationen

Avatar von Jenny, Alex & Eric

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.

[smartblock id=“10413″]

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.

Goodies von Mayflower

 

Das klingt nach einem Thema, dass Dich in Deinem Alltag bei euch beschäftigt? Das Dich mit vielen Fragen zurück lässt?

Keine Sorge – Hilfe ist nah! Melde Dich unverbindlich bei uns und wir schauen uns gemeinsam an, ob und wie wir Dich unterstützen können.

Docker Compose

Unsere

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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"
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"
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
nginx.conf über die bereits im Parent-Image existierende darüber.

Der Eintrag 

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
├── docker-compose.yml
├── nginx.conf
└── wallabagContainer
├── Dockerfile
└── wallabag.sqlite
1 directory, 4 files
├── docker-compose.yml ├── nginx.conf └── wallabagContainer ├── Dockerfile └── wallabag.sqlite 1 directory, 4 files
├── docker-compose.yml
├── nginx.conf
└── wallabagContainer
    ├── Dockerfile
    └── wallabag.sqlite
 
1 directory, 4 files

Das Dockerfile

Im 

Dockerfile
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
Dockerfile noch ein wenig selbst Hand anlegen:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 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"]
# 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"]
# 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
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#!/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
#!/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
#!/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
/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
nginx.conf gibt es noch einen wichtigen, Docker-spezifischen Punkt:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[]
http {
[]
upstream php-upstream {
server php:9000;
}
[]
[…] http { […] upstream php-upstream { server php:9000; } […]
[…]
 
http {
 
[…]
 
    upstream php-upstream {
        server php:9000;
    }
 
[…]

In Docker Compose sehen sich die Docker-Container gegenseitig unter ihrem Namen (in unserem Fall

php
php) und können so miteinander kommunizieren, selbst wenn wir in unserer
Docker-Compose.yml
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
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
start.sh angelegt:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#!/bin/bash
# this is only relevant for unix hosts
export USER_ID=$(id -u ${USER})
export GROUP_ID=$(id -g ${USER})
docker-compose up
#!/bin/bash # this is only relevant for unix hosts export USER_ID=$(id -u ${USER}) export GROUP_ID=$(id -g ${USER}) docker-compose up
#!/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
docker-compose.yml an den Container weiter …

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[]
php:
build:
context: wallabagContainer/
args:
USER_ID: ${USER_ID:-www-data}
GROUP_ID: ${GROUP_ID:-www-data}
[]
[…] php: build: context: wallabagContainer/ args: USER_ID: ${USER_ID:-www-data} GROUP_ID: ${GROUP_ID:-www-data} […]
[…]
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
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
run.sh auszuführen:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 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"]
# 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"]
# 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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
.
├── docker-compose.yml
├── nginx.conf
├── start.sh
└── wallabagContainer
├── Dockerfile
├── run.sh
└── wallabag.sqlite
1 directory, 6 files
. ├── docker-compose.yml ├── nginx.conf ├── start.sh └── wallabagContainer ├── Dockerfile ├── run.sh └── wallabag.sqlite 1 directory, 6 files
.
├── 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
start.sh (für erneute Starts reicht ein
docker-compose up
docker-compose up), bei Windows
docker-compose up
docker-compose up ausführen, und die App ist unter
localhost:8080
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.

Goodies von Mayflower

Keine Sorge – Hilfe ist nah! Melde Dich unverbindlich bei uns und wir schauen uns gemeinsam an, ob und wie wir Dich unterstützen können.

Unsere Data-Webinar-Reihe

Avatar von Jenny, Alex & Eric

Kommentare

Eine Antwort zu „Dockerisierung von (Legacy-)PHP-Applikationen“

  1. […] wir uns im ersten Post mit der Dockerisierung von (Legacy-)PHP-Applikationen auseinandergesetzt haben, wollen wir in diesem Artikel einen Blick auf verschiedene Tools werfen […]

Schreibe einen Kommentar

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

Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.