Apache2.4 mit PHP7 in Docker-Compose performant ausführen

Apache2.4 mit PHP7 in Docker-Compose performant ausführen

Der Apache-Webserver ist aufgrund seiner Vielseitigkeit und Flexibilität bis heute beliebt. Häufig wird er zusammen mit der Skriptsprache PHP eingesetzt. Apache und PHP stellen offizielle Images bereit, die den einfachen Betrieb auf Basis von Docker und Docker-Compose ermöglichen. Dieser Artikel zeigt eine minimalistische Konfiguration.

Beginnen wir mit der Struktur unseres Arbeitsverzeichnisses:

.
├── docker-compose.yml
├── htdocs
│  └── index.php
├── htdpcs
└── httpd.conf

Der Ordner htdocs wird später vom Webserver ausgeliefert. Die index.php Datei liefert beispielhaft phpinfo(); aus. So können wir später die korrekte Konfiguration von PHP testen.

Als Basis wird die docker-compose.yml Datei erstellt:

version: "2.4"
services: 
  apache:
    image: httpd:2.4
    volumes:
      - ./httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ./htdocs:/usr/local/apache2/htdocs
    ports:
      - 80:80

  php:
    image: php:7.3-fpm
    volumes_from:
      - apache

Wir definieren also einen Apache-Container, der sowohl die Konfigurationsdatei, als auch den htdocs-Ordner als Volume gemountet erhält. Durch die Portfreigabe ist der Webserver auf dem Host lokal über Port 80 erreichbar. Abgesehen von kleineren Umgebungen wird man produktiv dafür eher einen Reverse-Proxy wie traefik einsetzen.

Der PHP-Container erhält diese Volumes auch, damit er die PHP-Skripte ausführen kann. Zum Einsatz kommt das jeweils aktuellste PHP 7.3 Image. So erhält der Container die aktuellsten Sicherheitsaktualisierungen. Bei darf kann hier auch eine ältere Version genutzt werden. php:7.2-fpm würde beispielsweise die Vorversion ausführen.

Apache2 Konfiguration

Die Datei httpd.conf füllen wir mit folgender Konfiguration:

Listen 80
LogLevel info
ErrorLog /proc/self/fd/2
User daemon
Group daemon
ServerRoot "/usr/local/apache2"
ServerName localhost
DirectoryIndex index.php index.html

# https://httpd.apache.org/docs/2.4/mod/
LoadModule mpm_event_module modules/mod_mpm_event.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule mime_module modules/mod_mime.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule dir_module modules/mod_dir.so

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so

LoadModule filter_module modules/mod_filter.so
LoadModule deflate_module modules/mod_deflate.so


CustomLog /proc/self/fd/1 common
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b" common

TypesConfig conf/mime.types
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz

SetOutputFilter DEFLATE
AddOutputFilterByType DEFLATE text/html text/plain text/xml 
AddOutputFilterByType DEFLATE text/css 
AddOutputFilterByType DEFLATE application/x-javascript application/javascript application/ecmascript 
AddOutputFilterByType DEFLATE application/rss+xml 
AddOutputFilterByType DEFLATE application/xml 

<Directory />
    AllowOverride none
    Require all denied
</Directory>

<Files ".ht*">
    Require all denied
</Files>

#ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://php:9000/usr/local/apache2/htdocs/$1
<VirtualHost *:80>
    DocumentRoot "/usr/local/apache2/htdocs"
    <Directory "/usr/local/apache2/htdocs">
        Options Indexes FollowSymlinks
        AllowOverride None
        Require all granted

        <FilesMatch "\.php$">
            SetHandler "proxy:fcgi://php:9000/"
        </FilesMatch>
    </Directory>
</VirtualHost>

Hierbei handelt es sich um eine minimalistische Basiskonfiguration für Apache2. Es werden nur die Kernmodule geladen. Sie aktiviert das Logging auf dem stdout Kanal. So lassen sich die Logs auf der Konsole bzw. mit docker logs im Detached-Modus abrufen. Darüber hinaus wird das standardmäßige MIME-Mapping von Apache aktiviert sowie die GZIP-Kompromierung. Und das wichtigste für diesen Stack: Weiterreichen von PHP-Dateien an PFP-FPM mit dem FastCGI-Modul.

Testlauf

Final wird htdocs/index.php angelegt und gefüllt:

<?php phpinfo();

Nun sind alle erforderlichen Dateien vorhanden. Die Container können gestartet werden:

docker-compose up 

Wahlweise kann man auch -d anhängen, damit die Container in Hintergrund gestartet werden. Gerade zum testen finde ich attached besser, hier sind alle Ausgaben auf dem Terminal sichtbar.

Durch den Aufruf von http://localhost im Browser sollte die PHP Infoseite sichtbar werden:

Warum nicht nur die Standard httpd.conf anpassen?

Teils wird in Beispielen keine eigene Apache-Konfigurationsdatei namens httpd.conf erstellt. Sondern ein Docker-Image, worin dann Ersetzungen auf der Standard-Konfigurationsdatei durchgeführt werden. Beispielsweise lässt sich mit sed die Raute vor einer LoadModule Direktive entfernen, um ein Modul zu laden. Oder man fügt einfach eigene Zeilen hinzu. Grundsätzlich funktioniert das, doch ich empfehle dieses Vorgehen aus zwei Gründen nicht:

Potenzielle Fehlerquellen

Je nach Einsatzzweck können manche Standardwerte einem Probleme bereiten. Diese muss man ermitteln und entsprechend anpassen oder entfernen. Danach verlässt man sich auf die verbliebenen Standardwerte. Werden diese irgendwann mal geändert, funktioniert die daraus entstehende Konfiguration u.u. nicht mehr wie erwartet.

Übersicht und Wartbarkeit

Selten bleibt es bei 1 oder 2 Änderungen. Je mehr angepasst werden muss, um so unübersichtlicher wird das Dockerfile. Die Komplexität steigt dadurch sowohl für die Fehlerauche, als auch spätere Erweiterungen.

Performance und Sicherheit

Die Standard-Konfiguration ist darauf ausgelegt, in vielen Fällen möglichst einfach zu funktionieren. Das beugt Frust bei Einsteigern vor, aber ist nicht optimal für die Leistung. Beispielsweise sind standardmäßig viele Module aktiviert, die oft gar nicht alle benötigt werden. Der Apache lädt sie aber trotzdem bei jeder Anfrage. Dies entspricht auch nicht den Best Practices hinsichtlich Sicherheit.

Schlussfolgerung: Eigene Konfiguration

Aus diesen Gründen halte ich eine eigene, explizite Konfiguration für sinnvoller. Darin aktiviert man nur das, was benötigt wird. Unabhängig von Docker hat sich dieses Vorgehen bereits in mehreren Szenarien bewährt. Der Mehraufwand ist überschaubar. Wird mit mehreren Projekten gearbeitet, kann eine Basis-Konfiguration wie aus diesem Artikel als Grundlage genommen werden. Diese passt man dann an und lädt beispielsweise zusätzliche Module, welche im jeweiligen Projekt benötigt werden.

Leave a Reply