Mehrere PHP-Versionen parallel verwalten unter Linux
$
/etc
Linux · PHP · PHP-FPM · Magento
Mehrere PHP-Versionen
parallel verwalten unter Linux

Wer mehrere Kundenprojekte auf einem Server betreibt, kommt an unterschiedlichen PHP-Versionen nicht vorbei, weil Magento, WordPress und Legacy-Anwendungen selten dieselbe Version verlangen. Mit versionierten FPM-Paketen, update-alternatives und sauber getrennten Pools pro vhost lassen sich mehrere PHP-Versionen konfliktfrei und produktionssicher parallel betreiben.

15 Min. Lesezeit update-alternatives · php-fpm Pools · Ondrej-PPA Ubuntu · Debian · Nginx · Magento

1. Warum ein Server mehrere PHP-Versionen braucht

Agenturen und Freelancer, die mehrere Kundenprojekte auf einem gemeinsamen Server hosten, stoßen fast zwangsläufig auf das Problem unterschiedlicher PHP-Versionsanforderungen. Ein Magento-2.4.6-Shop verlangt PHP 8.1 oder 8.2, ein frisch aufgesetztes Magento 2.4.8 dagegen PHP 8.3, und daneben läuft vielleicht noch ein älteres WordPress-Projekt, das mit PHP 8.3 wegen veralteter Plugins Fehler wirft. Einen einzigen globalen PHP-Interpreter zu betreiben bedeutet in diesem Alltag entweder ständige Kompatibilitätsprobleme oder ein riskantes Downgrade, das andere Projekte gefährdet.

Die Lösung liegt nicht in virtuellen Maschinen oder Containern für jedes einzelne Projekt, sondern in der nativen Fähigkeit von Debian- und Ubuntu-basierten Distributionen, mehrere PHP-Versionen als eigenständige, parallel installierbare Pakete zu führen. Jede Version bekommt ihren eigenen FPM-Daemon, ihre eigene php.ini und ihren eigenen Satz an Extensions, ohne dass sich die Versionen gegenseitig überschreiben. Dieser Ansatz ist ressourcenschonender als vollständige Container pro Projekt und lässt sich mit den Bordmitteln von apt und systemd vollständig abbilden.

2. Versionierte Pakete installieren: php8.1-fpm neben php8.3-fpm

Die Basis für parallele PHP-Versionen ist das PPA von Ondřej Surý, das für Ubuntu und Debian gepflegte, versionierte PHP-Pakete bereitstellt. Anders als das Standard-Repository, das nur eine einzige PHP-Version pro Distributionsversion anbietet, liefert das PPA php8.1, php8.2, php8.3 und weitere Versionen gleichzeitig installierbar, jede mit eigenem Namenspräfix. Das Paket php8.1-fpm installiert einen komplett eigenständigen FPM-Daemon, der parallel neben php8.3-fpm läuft, jeweils mit eigenem systemd-Service und eigenem Unix-Socket.

Wichtig ist, alle benötigten Extensions für jede Version einzeln zu installieren, da apt Pakete wie php8.1-mysql und php8.3-mysql als komplett getrennte Pakete behandelt. Ein häufiger Anfängerfehler ist, nur die Extensions einer Version zu installieren und beim zweiten Interpreter fehlende Module erst im Produktionsbetrieb zu bemerken, wenn eine Magento-Installation mit Class GD not found oder ähnlichen Fehlern abbricht. Ein kurzes Vergleichsskript mit dpkg -l | grep php8.1 gegen dpkg -l | grep php8.3 deckt fehlende Pakete zuverlässig auf.


# Add the Ondrej Sury PPA for versioned PHP packages (Ubuntu/Debian)
sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update

# Install PHP 8.1 and PHP 8.3 side by side, each with its own FPM daemon
sudo apt install -y php8.1-fpm php8.1-cli php8.1-mysql php8.1-xml \
  php8.1-mbstring php8.1-curl php8.1-zip php8.1-gd php8.1-intl php8.1-bcmath

sudo apt install -y php8.3-fpm php8.3-cli php8.3-mysql php8.3-xml \
  php8.3-mbstring php8.3-curl php8.3-zip php8.3-gd php8.3-intl php8.3-bcmath

# Verify both FPM services are active independently
systemctl status php8.1-fpm --no-pager | head -n 5
systemctl status php8.3-fpm --no-pager | head -n 5

# List installed PHP CLI binaries
ls -la /usr/bin/php8.*

3. update-alternatives: Wie der PHP-CLI-Standard verwaltet wird

Nach der Installation existieren die Binärdateien /usr/bin/php8.1 und /usr/bin/php8.3 parallel, aber der einfache Aufruf php -v in der Shell muss auf genau eine davon zeigen. Genau dafür ist update-alternatives gedacht, ein Debian-Mechanismus, der symbolische Links mit Prioritäten verwaltet und einen einzigen generischen Namen wie /usr/bin/php auf eine konkrete Version umbiegt. Jede installierte PHP-Version registriert sich beim Paket-Setup automatisch als Alternative mit einer Priorität, die grob der Versionsnummer entspricht.

Der Befehl sudo update-alternatives --config php öffnet ein interaktives Menü, das alle registrierten PHP-Versionen mit Priorität auflistet und den Nutzer eine Nummer eingeben lässt. Für Automatisierungsskripte, in denen keine Interaktion möglich ist, funktioniert stattdessen update-alternatives --set php /usr/bin/php8.3 ohne Rückfrage. Entscheidend zu verstehen: Dieser Wechsel betrifft ausschließlich den CLI-Aufruf php in der Shell und in Cronjobs, er hat keinerlei Einfluss auf die FPM-Pools, die Webserver über Unix-Sockets ansprechen und komplett unabhängig davon konfiguriert werden.


# List all registered PHP alternatives with their priorities
sudo update-alternatives --list php

# Interactive selection menu
sudo update-alternatives --config php
# Selection    Path             Priority   Status
# 0            /usr/bin/php8.3   83        auto mode
#* 1            /usr/bin/php8.1   81        manual mode
#  2            /usr/bin/php8.3   83

# Non-interactive switch, useful in provisioning scripts
sudo update-alternatives --set php /usr/bin/php8.1

# Confirm the active CLI version
php -v | head -n 1

# Same mechanism also applies to php-config and phpize
sudo update-alternatives --config php-config
sudo update-alternatives --config phpize

4. PHP-FPM Pools: Ein eigener Prozess pro Version

PHP-FPM organisiert Arbeitsprozesse in sogenannten Pools, die jeweils in einer eigenen Konfigurationsdatei unterhalb von /etc/php/8.3/fpm/pool.d/ definiert werden. Jede installierte PHP-Version bringt standardmäßig einen Default-Pool namens www.conf mit, der bereits einen eigenen Unix-Socket unter /run/php/php8.3-fpm.sock öffnet. Für den produktiven Mehrprojektbetrieb reicht der Default-Pool aber selten aus, weil jedes Projekt eigene Ressourcenlimits, einen eigenen Systembenutzer und ein eigenes Prozessmanagement braucht, um sich nicht gegenseitig zu beeinflussen.

Ein sauberes Setup legt für jedes Projekt einen eigenen Pool mit eindeutigem Namen an, etwa shop-kunde-a.conf, mit eigenem user, group und eigenem Socket-Pfad. Die Direktive pm = dynamic mit individuell abgestimmten Werten für pm.max_children, pm.start_servers und pm.max_requests verhindert, dass ein speicherhungriges Projekt die FPM-Worker eines anderen Projekts über PHP-interne Memory-Limits hinaus verdrängt. Nach jeder Pool-Änderung ist ein systemctl reload php8.3-fpm nötig, das laufende Requests zu Ende bedient, bevor die neue Konfiguration greift.


; /etc/php/8.3/fpm/pool.d/shop-kunde-a.conf
; Dedicated pool for a single Magento project on PHP 8.3

[shop-kunde-a]
user = shop-kunde-a
group = shop-kunde-a

; Unique socket path, referenced later in the Nginx vhost
listen = /run/php/php8.3-fpm-shop-kunde-a.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 12
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500

; Per-pool php.ini overrides, isolated from other pools
php_admin_value[memory_limit] = 2G
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M
php_admin_value[error_log] = /var/log/php/shop-kunde-a-error.log
php_flag[display_errors] = off

5. Nginx vhost pro Projekt an den richtigen Pool binden

Der letzte Baustein ist die Verbindung zwischen Nginx und dem passenden FPM-Socket. In der fastcgi_pass-Direktive des jeweiligen Server-Blocks trägt man den exakten Socket-Pfad des zugehörigen Pools ein, nicht den Default-Socket der Version. Dadurch entscheidet allein die Nginx-Konfiguration, welche PHP-Version und welcher Ressourcen-Pool ein bestimmtes Projekt bedient, unabhängig davon, welche Version gerade als globaler CLI-Standard über update-alternatives gesetzt ist.

Diese Trennung ist der entscheidende Vorteil gegenüber einer einzigen globalen PHP-Installation: Ein Update des CLI-Standards für Wartungsskripte auf dem Server beeinflusst keinen einzigen laufenden vhost, weil Nginx ausschließlich über den fest verdrahteten Socket-Pfad kommuniziert. Wer testen will, ob ein Projekt mit einer neuen PHP-Version kompatibel ist, kopiert einfach den vhost, ändert den Socket-Pfad auf den Test-Pool und testet über einen alternativen Hostnamen, ohne den produktiven vhost anzurühren.

# /etc/nginx/sites-available/shop-kunde-a.conf
server {
    listen 443 ssl http2;
    server_name shop-kunde-a.de;
    root /var/www/shop-kunde-a/pub;
    index index.php;

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;

        # Points directly at the dedicated pool socket for PHP 8.3
        fastcgi_pass unix:/run/php/php8.3-fpm-shop-kunde-a.sock;

        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_read_timeout 300;
    }
}

# Reload after every vhost or pool change
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl reload php8.3-fpm

6. CLI-Version sicher pro Projekt umschalten

Für Deployment-Skripte, Composer-Aufrufe und bin/magento-Kommandos innerhalb eines einzelnen Projekts ist der globale update-alternatives-Standard oft zu grob, weil auf demselben Server mehrere Projekte mit unterschiedlichen CLI-Anforderungen gleichzeitig arbeiten müssen. Der sichere Weg führt über die direkte, versionsspezifische Binärdatei statt über den generischen php-Befehl: /usr/bin/php8.1 bin/magento setup:upgrade ruft garantiert PHP 8.1 auf, unabhängig davon, welche Version gerade als globaler Standard gesetzt ist.

Für Composer empfiehlt sich dieselbe Strategie über die Umgebungsvariable COMPOSER_PHP_PATH oder durch direkten Aufruf mit der gewünschten Binärdatei, etwa /usr/bin/php8.3 /usr/local/bin/composer.phar install. In Deploy-Skripten setzt man am Anfang eine Variable wie PHP_BIN="/usr/bin/php8.3" und referenziert diese konsequent für alle folgenden Aufrufe, statt sich auf den zum Ausführungszeitpunkt zufällig aktiven globalen Standard zu verlassen. Das macht das Skript reproduzierbar, unabhängig davon, wer es wann auf welchem Server startet.


#!/usr/bin/env bash
# deploy-shop-kunde-a.sh: pin PHP version explicitly for reproducible deploys
set -euo pipefail

readonly PHP_BIN="/usr/bin/php8.3"
readonly PROJECT_DIR="/var/www/shop-kunde-a"
readonly COMPOSER_BIN="/usr/local/bin/composer.phar"

cd "$PROJECT_DIR"

echo "[INFO] Using $($PHP_BIN -v | head -n 1)"

"$PHP_BIN" "$COMPOSER_BIN" install --no-dev --optimize-autoloader

"$PHP_BIN" bin/magento maintenance:enable
"$PHP_BIN" bin/magento setup:upgrade
"$PHP_BIN" bin/magento setup:di:compile
"$PHP_BIN" bin/magento setup:static-content:deploy de_DE en_US -f
"$PHP_BIN" bin/magento maintenance:disable

sudo systemctl reload php8.3-fpm
echo "[OK] Deploy finished with pinned PHP 8.3"

7. Extensions und php.ini pro Version pflegen

Jede parallel installierte PHP-Version besitzt ihr eigenes Konfigurationsverzeichnis, etwa /etc/php/8.1/ und /etc/php/8.3/, jeweils mit getrennten Unterordnern für cli, fpm und mods-available. Das Aktivieren einer Extension über phpenmod muss deshalb pro Version einzeln erfolgen, ein globales Aktivieren gibt es nicht. Der Befehl sudo phpenmod -v 8.3 redis aktiviert die Redis-Extension gezielt nur für PHP 8.3, während PHP 8.1 davon unberührt bleibt, bis derselbe Befehl auch dort ausgeführt wird.

Für projektspezifische php.ini-Werte wie memory_limit oder max_execution_time ist die Pool-Konfiguration aus Abschnitt 4 der bevorzugte Ort, weil sie ausschließlich das jeweilige Projekt betrifft und bei jedem Deploy versioniert im Repository mitgeführt werden kann. Globale Änderungen an der php.ini unter /etc/php/8.3/fpm/php.ini wirken sich dagegen auf alle Pools dieser Version aus und sollten nur für tatsächlich versionsweite Einstellungen wie opcache.enable oder Zeitzonenwerte verwendet werden.

Ein bewährter Kontrollmechanismus vor jedem größeren Rollout ist ein Diff-Vergleich der installierten Extension-Pakete zwischen zwei Versionen, etwa mit dpkg -l | grep php8.1- gegen dpkg -l | grep php8.3-. Ebenso hilfreich ist php8.3 --ini | grep "Loaded Configuration", das anzeigt, welche php.ini-Datei ein bestimmtes CLI-Binary tatsächlich lädt, denn gerade bei mehreren parallelen Versionen ist das keineswegs immer offensichtlich und eine häufige Fehlerquelle bei Support-Anfragen.

8. Magento-Projekte: Versionsanforderungen konkret zuordnen

Magento veröffentlicht mit jeder Minor-Version klar dokumentierte, unterstützte PHP-Bereiche, und diese Bereiche überschneiden sich zwischen aufeinanderfolgenden Magento-Versionen typischerweise nur teilweise. Magento 2.4.6 unterstützt PHP 8.1 und 8.2, Magento 2.4.7 erweitert das um PHP 8.3, und Magento 2.4.8 verlangt PHP 8.3 als Minimum bei gleichzeitiger Unterstützung neuerer Patch-Releases. Wer mehrere Magento-Stores auf unterschiedlichem Patch-Level betreut, landet damit fast zwangsläufig bei mindestens zwei parallel benötigten PHP-Versionen auf demselben Server.

Vor jedem Versionswechsel lohnt der Blick in die composer.json des jeweiligen Magento-Projekts, in der die Direktive require.php das exakt unterstützte Intervall angibt, etwa ">=8.3.0 <8.4.0-0". Ein Composer-Update mit einer nicht passenden aktiven CLI-Version bricht in solchen Fällen sofort mit einer klaren Fehlermeldung ab, statt inkompatible Pakete zu installieren, was eine der zuverlässigsten Absicherungen gegen versehentliche Versionsverwechslungen im Deploy-Prozess darstellt.

9. Umschalt-Methoden im direkten Vergleich

Die fünf häufigsten Stolperfallen beim parallelen Betrieb mehrerer PHP-Versionen lassen sich klar benennen und ebenso klar durch etablierte Praktiken vermeiden. Die folgende Tabelle stellt jeweils die unsichere Standardlösung dem empfohlenen, produktionssicheren Ansatz gegenüber.

Aufgabe Unsicher / Fehleranfällig Empfohlener Ansatz Vorteil
CLI-Aufruf in Deploy-Skripten php bin/magento ... /usr/bin/php8.3 bin/magento ... Unabhängig vom globalen update-alternatives-Standard
FPM-Zuordnung im vhost Default-Socket aller Projekte gemeinsam Eigener Pool und Socket pro Projekt Keine gegenseitige Ressourcen-Verdrängung
Extension-Verwaltung Nur eine Version bestückt, zweite vergessen phpenmod -v pro Version, Diff-Check Fehlende Module fallen vor dem Deploy auf
CLI-Standard wechseln Manuelles Umbiegen von Symlinks per Hand update-alternatives --set php ... Konsistent, dokumentiert, skriptfähig
Versionskompatibilität prüfen Version raten oder Changelog überfliegen require.php aus composer.json auswerten Composer bricht bei Mismatch sofort sauber ab

In der Praxis zeigt sich, dass die meisten Vorfälle mit falscher PHP-Version aus genau diesen fünf Stellen stammen: unklarer CLI-Aufruf, geteilter FPM-Socket, vergessene Extension, manuell verdrehte Symlinks und ignorierte Composer-Constraints. Wer diese fünf Punkte in Deploy-Skripten und Server-Dokumentation konsequent explizit macht, reduziert versionsbedingte Ausfälle auf ein Minimum, auch wenn gleichzeitig fünf oder mehr Magento-Stores mit unterschiedlichem Patch-Level auf demselben Server laufen.

Mironsoft

Server-Administration, PHP-Infrastruktur und Magento-Hosting

Mehrere Magento-Stores auf einem Server im Griff behalten?

Wir richten saubere Multi-PHP-Umgebungen mit dedizierten FPM-Pools pro Projekt ein und sorgen dafür, dass jede Magento-Version genau die PHP-Version bekommt, die sie tatsächlich braucht, ohne gegenseitige Beeinflussung.

Multi-PHP-Setup

Versionierte FPM-Pakete, saubere Pool-Trennung und dokumentierte CLI-Standards

Deploy-Automatisierung

Versionsgepinnte Deploy-Skripte, die keinen globalen PHP-Standard voraussetzen

Magento-Migration

PHP-Versionswechsel bei Magento-Upgrades ohne Downtime für andere Projekte

10. Zusammenfassung

Das parallele Betreiben mehrerer PHP-Versionen unter Linux ist mit den Bordmitteln von Debian und Ubuntu ohne Container-Overhead vollständig lösbar. Versionierte Pakete aus dem Ondrej-Sury-PPA installieren jede PHP-Version als eigenständigen FPM-Daemon mit eigenem Socket. update-alternatives steuert ausschließlich den globalen CLI-Standard und hat keinen Einfluss auf laufende Webprojekte. Dedizierte FPM-Pools pro vhost sorgen für saubere Ressourcentrennung, und die direkte, versionsspezifische Binärdatei in Deploy-Skripten macht jeden Ablauf unabhängig vom gerade aktiven globalen Standard.

Der größte praktische Hebel liegt darin, jede der fünf typischen Fehlerquellen, unklarer CLI-Aufruf, geteilter FPM-Socket, vergessene Extension, manuell verdrehte Symlinks und ignorierte Composer-Constraints, aktiv im Deploy-Prozess zu adressieren statt sie dem Zufall zu überlassen. Wer diese Struktur einmal sauber aufsetzt, kann beliebig viele Magento-Stores mit unterschiedlichen PHP-Anforderungen auf demselben Server betreiben, ohne dass ein Versionswechsel für ein Projekt jemals ein anderes gefährdet.

Mehrere PHP-Versionen parallel verwalten: Das Wichtigste auf einen Blick

Pakete pro Version

php8.1-fpm und php8.3-fpm laufen als komplett eigenständige Daemons, Extensions müssen pro Version einzeln installiert werden.

update-alternatives

Steuert nur den globalen CLI-Befehl php, hat keinerlei Auswirkung auf FPM-Pools oder laufende vhosts.

Ein Pool pro vhost

Eigener Socket, eigener Systembenutzer, eigene Ressourcenlimits pro Projekt verhindern gegenseitige Beeinflussung.

Deploy-Skripte pinnen

/usr/bin/php8.3 statt php in Deploy-Skripten macht Abläufe reproduzierbar und unabhängig vom globalen Standard.

11. FAQ: Mehrere PHP-Versionen parallel verwalten

1Warum mehrere PHP-Versionen gleichzeitig?
Unterschiedliche Kundenprojekte haben unterschiedliche, oft nicht überschneidende PHP-Versionsanforderungen. Ein globaler Interpreter würde mindestens eines der Projekte unkompatibel machen.
2Was macht update-alternatives genau?
Verwaltet nur, welche Version der globale CLI-Befehl php aufruft. Keine Auswirkung auf FPM-Pools, die über eigene Sockets angesprochen werden.
3Wie installiere ich mehrere Versionen unter Ubuntu?
Über ppa:ondrej/php mit versionierten Paketen wie php8.1-fpm und php8.3-fpm, die als getrennte systemd-Services mit eigenen Sockets laufen.
4Wie ordnet Nginx die richtige Version zu?
Über fastcgi_pass mit dem exakten Socket-Pfad des zugehörigen Pools, unabhängig vom globalen CLI-Standard.
5Warum ein eigener Pool pro Projekt?
Eigener Systembenutzer und eigene Ressourcenlimits verhindern, dass ein Projekt die Worker eines anderen verdrängt.
6Bestimmte PHP-Version im Deploy-Skript?
Direkt über die versionsspezifische Binärdatei wie /usr/bin/php8.3 statt über den generischen php-Befehl aufrufen.
7Extensions pro Version einzeln nötig?
Ja, apt behandelt php8.1-mysql und php8.3-mysql als getrennte Pakete. Ein Diff-Vergleich deckt fehlende Module vorab auf.
8Welche PHP-Version für welches Magento?
Magento 2.4.6 nutzt PHP 8.1/8.2, 2.4.7 erweitert um PHP 8.3, 2.4.8 verlangt PHP 8.3 als Minimum. Details stehen in require.php der composer.json.
9Composer mit falscher PHP-Version?
Composer prüft require.php gegen die aktive Version und bricht bei Mismatch sofort mit klarer Fehlermeldung ab.
10Extension nur für eine Version aktivieren?
Mit phpenmod -v 8.3 redis wird die Extension gezielt nur für PHP 8.3 aktiviert, andere Versionen bleiben unberührt.