Mehrere virtuelle Server mit nginx und PHP-FPM für Wordpress (Teil 3 / 3)

Im letzten Teil geht es um die Einrichtung von PHP-FPM und ich gebe eine kleine Zusammenfassung bzw. Überblick über die Struktur des gesamten Setups. Wenn alles korrekt eingerichtet ist, solltet ihr nun einen gut funktionierenden Webserver auf Basis von nginx haben, der PHP-FPM nutzt und gut mit Wordpress laufen sollte. Der Server arbeitet für mehrere unterschiedliche Domains, die so gut wie möglich im System getrennt sind.

Die Einrichtung der PHP-Pools

Um den ganzen Bums zum Laufen zu bringen fehlt jetzt nur noch PHP. Den Großteil haben wir schon geschafft, weshalb ich die Einrichtung von PHP nicht in einen neuen Beitrag gepackt habe.

Wie im ersten Teil schon angedeutet, nutze ich PHP-FPM. Die Einstellungen jedes einzelen virtuellen Servers befinden sich demnach in /etc/php/7.3/fpm/pool.d/ und hat die Endung .conf.

Mit dem Parameter listen stellst du die Verbindung zu nginx her. Es wird ein Socket erstellt, über den nginx und PHP-FPM Informationen austauschen. Die Variable $pool enthält den Namen des Pools. Mit prefix legst du Standard-Ordner dieses Pools fest.

# der Namen des Pools (kann mit $pool referenziert werden
[example_com]
listen = /run/php/php-fpm-$pool.sock
prefix = /var/nginx/$pool

Jetzt gibt ein paar wichtige Sicherheitsfeatures: Jeder Pool hat seinen eigenen Benutzer. Hierzu muss man nicht viel erklären: Der Vorteil hier ist, dass sich die PHP-Prozesse verschiedener Server, da sie ja unterschiedlichen Nutzer “gehören” grundsätzlich erstmal nicht in die Quere kommen können:

user = $pool-php
group = www-data
listen.owner = $pool-php
listen.group = www-data

Mit chdir und chroot schließt du diesen Pool in einen bestimmten Ordner ein. Ich hatte oben ja bereits $prefix definiert. Diese Parameter arbeiten eng mit den FastCGI-Einstellungen von nginx zusammen und sind eine beliebte Fehlerquelle. Mit chroot denkt PHP, dass dieser Ordner der Root-Ordner ist. Warum ist das wichtig? Unsere Root-Ordner liegen (siehe Teil 1) alle in einem eigenen Unterordner. So kann PHP nicht ausbrechen und z.B. auf sensible Systembereiche oder die Unterordner anderer Pools / Server zugreifen. Der Parameter chdir legt lediglich fest, dass root auch wirklich root ist. Hier könnte man htdocs als Root festlegen. Da wir in der nginx-Einstellung aber htdocs als Pfad voranstellen, kann das hier so bleiben. Bedenke, dass sich alle folgenden Pfadangaben immer relativ zu den hier festgelegten Einstellungen stattfinden.

chdir = /
chroot = /var/nginx/example1

Weiter geht es mit der Konfiguration der PHP-Prozesse. Mit pm=dynamic legen wir fest, dass der Prozess-Manager prozesse dynamisch starten kann. Mit pm=static startest du immer eine feste Anzahl von Prozessen. (Bei Servern mit hoher Last kann das durchaus Sinn machen, wie hier beschrieben wird). Bei einer kleineren Seite reicht pm=ondemand völlig aus. Wir erinnern uns: Die Prozesse dienen als Interpretor für unsere PHP-Scripte. Ein Prozess bearbeitet eine Anfrage. Wenn du mehr Traffic erwartest, solltest du diese Werte also erhöhen.

max_children gibt die Obergrenze dafür fest. start_servers=1 besagt, dass mindestens 1 Prozess sofort gestartet wird. Mit min_spare_servers legst du fest, wieviele Prozesse mindestens “vorrätig” sind, max_spare_servers legt dafür die Obergrenze fest. Wie viele Prozesse du maximale starten solltest, errechnest du ganz einfach folgender maßen:

Rufe den folgenden Code auf um den Speicherverbrauch deines PHP-Services zu erhalten:

ps --no-headers -o "rss,cmd" -C php-fpm7.3 | awk '{ sum+=$1 } END { printf ("%d%s\n", sum/NR/1024,"Mb") }'

Den freien Speicher lässt du folgendermaßen anzeigen:

free -h

Angenommen, du hast 4.096 MByte freien Speicher zur Verfügung und ein PHP-Prozess verbraucht 4 MByte, dann kannst du insgesamt 1.024 Prozesse starten. Wenn du mehrere virtuelle Server betreibst, teilen diese sich natürlich dieses Kontingent. Im folgenden ein Beispiel: Es werden maximal 1.024 Prozesse gestartet. 100 Prozesse sind immer aktiv, auch wenn sie ungenutzt sind. Sind alle 100 Prozesse beschäftigt, werden mindestens 50 Prozesse gestartet, aber niemals mehr als 200 - das Spiel funktioniert so lange, bis das Kontingent von 1.024 ausgeschöpft ist.

pm = dynamic
pm.max_children = 1024
pm.start_servers = 100
pm.min_spare_servers = 50
pm.max_spare_servers = 200

Wenn dein Server relativ klein ist, solltest du den On-Demand-Modus nutzen. Hier wird ein Prozess nur dann gestartet, wenn der Bedarf da ist. Das spart Speicher und ist in der Regel auch nicht merkbar langsamer.

Der Parameter catch_workers_output steuert die Ausgabe des PHP-Prozesses. Wie alle Log-Einstellungen, kann dieser erhebliche Auswirkungen auf die Performance haben. Falls du also noch ein paar Millisekunden mehr herausholen willst, setze diesen Wert auf no.

catch_workers_output = yes

Die folgenden Einstellungen werden eigentlich in der php.ini vorgenommen. Ich will sie hier aber auf Server-Ebene definieren, da die virtuellen Server ja durchaus unterschiedliche Ansprüche haben.

Sessions, Cookies und Referrer

Wenn du eine zusätzliche Sicherheitshürde einbauen willst, kannst du den Pfad der PHP-Sessions hier ändern. Wenn du in deiner Web-Anwendung nicht mit JavaScript auf Cookies zugreifen willst, kannst du den Cookie-Zugriff außerdem nur auf HTTP einschränken. Und schließlich macht es Sinn, dass eine Session nur vom eigenen Server genutzt werden kann, wenn also dein Server im Referrer übermittelt wird.

php_value[session.save_path] = /sessions
php_value[session.cookie_httponly] = 1
php_value[session.referer_check] = example.com

Sehr nützlich und ein wichtiges Sicherheitsfeature ist disable_functions. Es gibt eine nicht geringe Anzahl von PHP-Funktionen, mit denen sich Systemfunktionen steuern lassen. Diese solltest du grundsätzlich nicht zulassen. Ein weiteres Sicherheitsfeature sind allow_url_fopen und allow_url_include. Damit unterbindest du das Einbinden von schadhaften Code.
Die Einstellungen zum Log werde ich nicht weiter erläuter, da sie wie so oft selbsterklärend sind. Beachte, dass das Logging immer auch gewisse Auswirkungen auf die Performance haben. Andererseits kann die regelmäßige Log-Analyse aber auch rechzeitig wichtige Hinweise auf (Sicherheits-)Probleme deines Systems liefern!

php_admin_value[disable_functions] = php_uname, getmyuid, getmypid, passthru, leak, listen, diskfreespace, tmpfile, link, ignore_user_abord, shell_exec, dl, set_time_limit, exec, system, high$
php_admin_flag[allow_url_fopen] = on
php_admin_flag[allow_url_include] = off

# das Speicherlimit pro Script-Aufrufeinstellen
php_admin_value[memory_limit] = 256M

# Logging-Einstellung
php_flag[display_errors] = off
php_admin_value[error_log] = /var/log/php-fpm/$pool.log
php_admin_flag[log_errors] = on

Die Einstellung von PHP ist damit abgeschlossen. Zum Abschluss gönne PHP und nginx noch einen Neustart. Danach sollte dein System rund laufen.

Zusammenfassung

Wenn du es bis hierhin geschafft hast, unterstützt dein Setup nun einen relativ performanten Server für mehrere Domains (aka virtuelle Server, virtual Hosts), der PHP-FPM nutzt und eine ziemlich solide Sicherheits-Grundeinstellung mitbringt.

Jeder einzelne virtuelle Server hat seine eigene Umgebung im Dateisystem, aus der er kaum ausbrechen kann. Die PHP-Prozesse sind voneinander getrennt, genauso wie die Speicherbereiche für den Cache. Außerdem ist das ganze darauf ausgerichtet, möglichst gut mit Wordpress zu laufen. Um das ganze System für Wordpress perfekt abzurunden, gibt es noch eine Handvoll Möglichkeiten, die ich gesondert vorstellen werden.

Grobe schematische Übersicht des Setups für mehrere virtuelle Server mit nginx und PHP-FPM

Nachtrag

Wenn du noch ein paar zusätzliche Informatioen benötigst, sei dir der der ähnlich ausgerichtete Artikel auf binary-butterfly.de empfohlen. Die Einstellungen für nginx und PHP unterscheiden sich kaum, dafür erfährst du dort auch, wie du zusätzlich mehrere SSH-Nutzer mit ins Boot holen kannst.