Was "mehrere Websites auf einem VPS" bedeutet
Mehrere Websites auf einem VPS zu hosten ist nicht nur eine Frage der Nginx-Konfiguration. Drei Punkte sollten Sie vorab bedenken:
- RAM: Jede aktive Website verbraucht Arbeitsspeicher (PHP-FPM-Worker, Datenbank, Cache). Ein VPS mit 4 GB RAM kann mehrere Websites mit geringem Traffic hosten, aber nicht Dutzende WordPress-Seiten mit gleichzeitigem Traffic. Dimensionieren Sie nach dem tatsächlichen Traffic, nicht nach der Anzahl der Websites.
- Isolierung: Auf ein und derselben PHP-FPM-Instanz ohne spezielle Konfiguration sind die Websites nicht wirklich voneinander isoliert. Eine kompromittierte Website kann die Dateien der anderen lesen. Die Lösung besteht darin, getrennte PHP-FPM-Pools mit eigenen Systembenutzern zu verwenden (siehe den entsprechenden Abschnitt weiter unten).
- Backups: Bei mehreren Websites werden manuelle Backups schnell unübersichtlich. Automatisieren Sie von Anfang an mit einer Schleife über die Verzeichnisse und die Datenbanken.
Empfohlene Verzeichnisstruktur
Verwenden Sie ein Verzeichnis pro Website unter /var/www/, mit dem Domainnamen als Bezeichner. Diese Konvention macht Backup-Skripte und Nginx-Konfigurationen lesbar und vorhersehbar.
/var/www/
site-a.fr/
site-b.fr/
site-c.fr/ Verzeichnisse anlegen:
sudo mkdir -p /var/www/site-a.fr
sudo mkdir -p /var/www/site-b.fr
sudo mkdir -p /var/www/site-c.fr Die Berechtigungen werden beim Anlegen der PHP-FPM-Pools angepasst (nächster Abschnitt).
Ein Nginx-vhost pro Website
Eine Konfigurationsdatei pro Website erstellen
Jede Website hat ihre eigene Datei in /etc/nginx/sites-available/. Packen Sie nicht alles in eine einzige Datei: Eine Datei pro Website erleichtert das Deaktivieren, das Debuggen und das Lesen.
Beispiel für site-a.fr:
sudo nano /etc/nginx/sites-available/site-a.fr server {
listen 80;
listen [::]:80;
server_name site-a.fr www.site-a.fr;
root /var/www/site-a.fr;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/site-a.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(ht|git|env) {
deny all;
}
} Wiederholen Sie das für jede Website und ersetzen Sie site-a.fr sowie den PHP-FPM-Socket (site-a.sock) durch die jeweiligen Werte.
Hinweis: Der Socket /run/php/site-a.sock existiert zu diesem Zeitpunkt noch nicht, er wird im Schritt zu den PHP-FPM-Pools weiter unten erstellt. nginx -t prüft nicht, ob der Socket vorhanden ist, die Konfiguration wird also als gültig bestätigt, aber die Website liefert einen Fehler 502, solange der zugehörige Pool nicht eingerichtet ist.
Websites aktivieren
sudo ln -s /etc/nginx/sites-available/site-a.fr /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/site-b.fr /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/site-c.fr /etc/nginx/sites-enabled/ Testen Sie die Konfiguration immer, bevor Sie neu laden:
sudo nginx -t
sudo systemctl reload nginx Quelle: nginx.org - Direktive server_name
Die Falle des zuerst ausgelieferten vhosts und der Catch-all-vhost
Was ohne expliziten Standard-vhost passiert
Wenn Nginx eine Anfrage für eine Domain oder eine IP erhält, die in keinem server_name deklariert ist, liefert es den ersten in alphabetischer Reihenfolge geladenen vhost aus. Dieses Verhalten ist dokumentiert und vorhersehbar, kann aber ungewollt eine Website preisgeben: Eine Anfrage an die nackte IP des VPS oder an eine Domain, die versehentlich auf den VPS zeigt, landet auf der zuerst konfigurierten Website.
Quelle: nginx.org - How nginx processes a request
Der Catch-all-vhost mit default_server
Erstellen Sie einen Standard-vhost, der alle nicht erkannten Anfragen abfängt und eine leere Antwort zurückgibt (Code 444, der die Verbindung ohne HTTP-Antwort schließt):
sudo nano /etc/nginx/sites-available/default-catchall server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
# Selbstsigniertes Zertifikat, um nicht erkannte HTTPS-Anfragen abzufangen
ssl_certificate /etc/nginx/ssl/self-signed.crt;
ssl_certificate_key /etc/nginx/ssl/self-signed.key;
return 444;
} Erzeugen Sie ein selbstsigniertes Zertifikat für den SSL-Block des Catch-all (Certbot kann kein Zertifikat für _ ausstellen):
sudo mkdir -p /etc/nginx/ssl
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/self-signed.key \
-out /etc/nginx/ssl/self-signed.crt \
-subj "/CN=localhost" Catch-all aktivieren und neu laden:
sudo ln -s /etc/nginx/sites-available/default-catchall /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx Der Code 444 schließt die TCP-Verbindung, ohne eine Antwort zu senden. Automatische Scanner und Anfragen an die nackte IP erhalten keinerlei Information über die gehosteten Websites.
Getrennte PHP-FPM-Pools pro Website
Warum getrennte Pools?
Ohne getrennte Pools laufen alle Websites unter demselben Systembenutzer (häufig www-data). Eine kompromittierte Website kann die Konfigurationsdateien der anderen lesen (.env-Dateien, wp-config.php, API-Schlüssel). Getrennte Pools bringen:
- Isolierung der Berechtigungen: Jede Website läuft unter ihrem eigenen Benutzer
- Prozesslimits pro Website: Eine Website mit einer Traffic-Spitze verbraucht nicht alle verfügbaren PHP-Worker
- Getrennte Logs: Schnell erkennen, welche Website Fehler erzeugt
Quelle: php.net - PHP-FPM-Konfiguration
Einen Systembenutzer pro Website anlegen
sudo useradd -r -s /usr/sbin/nologin site-a
sudo useradd -r -s /usr/sbin/nologin site-b
sudo useradd -r -s /usr/sbin/nologin site-c Verzeichnisberechtigungen anpassen:
sudo chown -R site-a:site-a /var/www/site-a.fr
sudo chown -R site-b:site-b /var/www/site-b.fr
sudo chown -R site-c:site-c /var/www/site-c.fr Nginx (das unter www-data läuft) muss die Dateien lesen können. Fügen Sie www-data den Gruppen der Website-Benutzer hinzu oder passen Sie die Verzeichnisberechtigungen an:
sudo chmod 750 /var/www/site-a.fr
sudo usermod -aG site-a www-data Gut zu wissen: www-data der Gruppe jeder Website hinzuzufügen erlaubt Nginx, die statischen Dateien auszuliefern, bedeutet aber auch, dass der Nginx-Prozess die Dateien aller Websites lesen kann. Die durch die Pools erreichte Isolierung betrifft die PHP-Ausführung, nicht den Lesezugriff durch den Webserver. Für eine strengere Isolierung führt kein Weg an Containern oder getrennten virtuellen Maschinen vorbei.
Einen PHP-FPM-Pool pro Website erstellen
Kopieren Sie den Standard-Pool als Grundlage:
sudo cp /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/site-a.conf
sudo nano /etc/php/8.4/fpm/pool.d/site-a.conf Ändern Sie diese Direktiven in der Datei (ersetzen Sie [www] durch [site-a]):
[site-a]
user = site-a
group = site-a
listen = /run/php/site-a.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3 Wiederholen Sie das für jede Website (site-b.conf, site-c.conf) und passen Sie Pool-Name, Benutzer und Socket an.
Deaktivieren Sie den Standard-Pool, wenn alle Websites ihren eigenen Pool haben:
sudo mv /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/www.conf.disabled Prüfen Sie vorher, dass kein vhost mehr den Standard-Socket /run/php/php8.4-fpm.sock referenziert: Diese Websites würden nach dem Deaktivieren des Pools einen Fehler 502 liefern.
PHP-FPM neu starten:
sudo systemctl restart php8.4-fpm Der Socket jeder Website (/run/php/site-a.sock) entspricht der Direktive fastcgi_pass im zugehörigen Nginx-vhost.
Getrennte Datenbanken pro Website
Eine eigene MariaDB-Datenbank und ein eigener MariaDB-Benutzer pro Website. Verwenden Sie niemals einen Benutzer, den sich mehrere Websites teilen: Die Kompromittierung einer Website gibt sonst Zugriff auf alle Datenbanken.
sudo mariadb -u root -- Website A
CREATE DATABASE site_a_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'site_a_user'@'localhost' IDENTIFIED BY 'starkes_passwort_a';
GRANT ALL PRIVILEGES ON site_a_db.* TO 'site_a_user'@'localhost';
-- Website B
CREATE DATABASE site_b_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'site_b_user'@'localhost' IDENTIFIED BY 'starkes_passwort_b';
GRANT ALL PRIVILEGES ON site_b_db.* TO 'site_b_user'@'localhost';
FLUSH PRIVILEGES;
EXIT; SSL-Zertifikate für mehrere Domains
Certbot verwaltet die Zertifikate jeder Domain unabhängig voneinander. Stellen Sie ein Zertifikat pro Domain (oder pro Paar Domain/www) aus:
sudo certbot --nginx -d site-a.fr -d www.site-a.fr
sudo certbot --nginx -d site-b.fr -d www.site-b.fr
sudo certbot --nginx -d site-c.fr -d www.site-c.fr Certbot ändert die Nginx-vhosts automatisch, um die SSL-Direktiven und die Weiterleitung von HTTP auf HTTPS hinzuzufügen.
Die automatische Erneuerung übernimmt ein von Certbot installierter systemd-Timer. Prüfen Sie, ob er aktiv ist:
sudo systemctl status certbot.timer Vollständige Anleitung (Erneuerung, Wildcard-Zertifikate, Fehlerbehebung): Certbot-Anleitung von OuiHeberg.
Dimensionierung: wie viele Websites bei welchem RAM?
Es gibt keine allgemeingültige Antwort: Die Anzahl der hostbaren Websites hängt vom Traffic jeder Website, vom eingesetzten CMS und von den aktiven Plugins ab. Einige realistische Größenordnungen:
- Eine WordPress-Website mit aktiviertem OPcache und wenig gleichzeitigem Traffic verbraucht bei normaler Last zwischen 128 MB und 256 MB RAM.
- Eine statische Website oder eine leichtgewichtige Anwendung verbraucht deutlich weniger.
- Eine WooCommerce-Website mit Traffic oder schweren Plugins kann 512 MB oder mehr verbrauchen.
Auf einem VPS mit 4 GB RAM ist es realistisch, mehrere Websites mit geringem Traffic zu hosten. Überwachen Sie den tatsächlichen Verbrauch nach dem Produktivstart mit free -h und top und passen Sie die Werte pm.max_children der PHP-FPM-Pools entsprechend an.
Lasten Sie den verfügbaren RAM nicht vollständig aus: Lassen Sie eine Reserve für das Betriebssystem, MariaDB und unvorhergesehene Lastspitzen.
Backups für mehrere Websites
Bei mehreren Websites sollten Sie die Backups von Anfang an automatisieren. Eine Schleife über die Verzeichnisse und die Datenbanken deckt alles in einer einzigen Cron-Aufgabe ab.
Backup-Skript
#!/bin/bash
BACKUP_DIR="/root/backups"
DATE=$(date +%F)
mkdir -p "$BACKUP_DIR"
# Backup der Dateien jeder Website
for SITE in /var/www/*/; do
SITE_NAME=$(basename "$SITE")
tar -czf "$BACKUP_DIR/${SITE_NAME}-files-${DATE}.tar.gz" "$SITE"
done
# Backup der Datenbanken
for DB in site_a_db site_b_db site_c_db; do
mysqldump -u root "$DB" > "$BACKUP_DIR/${DB}-${DATE}.sql"
done
# Backups löschen, die älter als 7 Tage sind
find "$BACKUP_DIR" -type f -mtime +7 -delete Speichern Sie dieses Skript unter /usr/local/bin/backup-sites.sh, machen Sie es ausführbar und tragen Sie es in die crontab ein:
sudo chmod +x /usr/local/bin/backup-sites.sh
sudo crontab -e # Tägliches Backup um 3 Uhr morgens
0 3 * * * /usr/local/bin/backup-sites.sh Backups auslagern
Backups, die auf demselben VPS liegen, schützen nicht vor einem Hardwareausfall oder einer Kompromittierung des Servers. Übertragen Sie die Archive regelmäßig an einen entfernten Speicherort (Objektspeicher, Drittserver, lokaler Rechner) per rsync oder scp.
Häufige Fragen
Kann man Websites mit unterschiedlichen PHP-Versionen auf demselben VPS hosten?
Ja. Installieren Sie mehrere PHP-FPM-Versionen parallel (zum Beispiel php8.1-fpm und php8.4-fpm) und weisen Sie dann jedem Pool die passende Version zu. Der Socket jedes Pools zeigt auf die gewünschte PHP-Version, und der Nginx-vhost jeder Website referenziert den richtigen Socket.
sudo apt install php8.1-fpm php8.4-fpm Jede Version hat ihr eigenes Pool-Verzeichnis: /etc/php/8.1/fpm/pool.d/ und /etc/php/8.4/fpm/pool.d/.
Kann eine abstürzende Website die anderen beeinträchtigen?
Mit getrennten PHP-FPM-Pools und Limits pm.max_children pro Pool verbraucht eine Website mit einer Traffic-Spitze oder mit PHP-Fehlern nicht die Worker der anderen Websites. Ist dagegen MariaDB ausgelastet (zu viele gleichzeitige Verbindungen), können alle davon abhängigen Websites beeinträchtigt werden. Überwachen Sie die MariaDB-Verbindungen mit SHOW PROCESSLIST;.
Wie deaktiviert man eine Website vorübergehend, ohne sie zu löschen?
Löschen Sie den symbolischen Link in sites-enabled und laden Sie Nginx neu:
sudo rm /etc/nginx/sites-enabled/site-a.fr
sudo nginx -t && sudo systemctl reload nginx Die Konfigurationsdatei in sites-available bleibt erhalten. Reaktivieren Sie die Website, indem Sie den symbolischen Link neu anlegen.
Wie fügt man nach der Ersteinrichtung eine neue Website hinzu?
Legen Sie Verzeichnis, Systembenutzer, PHP-FPM-Pool, Datenbank, Nginx-vhost und SSL-Zertifikat an, indem Sie die Abschnitte dieser Anleitung der Reihe nach durchgehen. Jede Ergänzung ist unabhängig und beeinträchtigt die bestehenden Websites nicht.
Sind getrennte PHP-FPM-Pools Pflicht?
Nein, aber sie sind dringend zu empfehlen, sobald mehrere Websites auf demselben VPS koexistieren. Ohne getrennte Pools laufen alle Websites unter www-data und können auf die Dateien der anderen zugreifen. Für einen privaten VPS mit vertrauenswürdigen Projekten kann ein einziger Pool genügen. Für Kundenwebsites oder Projekte Dritter sind getrennte Pools unverzichtbar.
