Diese Dokumentation beschreibt den Aufbau der Apache-Konfiguration eines Debian-Webservers mit je einer eigenen Konfigurationsdatei pro Website. Im Mittelpunkt steht eine Umgebung mit mehreren WordPress-Websites und Nextcloud, die hinter einem Reverse Proxy (z. B. Caddy auf OPNsense) betrieben wird und zusätzlich weiterhin direkt über die Server-IP mit Pfad erreichbar sein soll.
Jede Website lässt sich einzeln über a2ensite und a2dissite aktivieren bzw. deaktivieren. Die Datei 000-default.conf enthält nur noch den Default-/Fallback-VirtualHost. Die Konfiguration basiert auf einer tatsächlich aufgebauten und getesteten Umgebung.
Die Konfiguration ist weitgehend domainunabhängig aufgebaut. Bei einem Domainwechsel müssen jedoch die Stellen angepasst werden, an denen der Hostname bzw. der direkte Zugriffspfad fest definiert ist. In den WordPress-wp-config.php-Dateien betrifft dies beispielsweise $direct_path; in nextcloud.conf entsprechend ServerName bzw. ServerAlias. Bei Nextcloud muss die neue Adresse zusätzlich in trusted_domains eingetragen werden.
Alternative: zentrale Konfiguration mit Apache-Makros. Die gesamte Konfiguration kann auch in einer einzigen Datei (000-default.conf) mit mod_macro abgebildet werden. Diese Variante ist in der Dokumentation OPNsense/Caddy: Webserver-Migration mit Apache, WordPress und Nextcloud beschrieben.
Inhaltsverzeichnis
1. Ziel und Funktionsprinzip
1.1 Ziel
Jede Website erhält eine eigene Datei unter /etc/apache2/sites-available/, die alle zugehörigen Apache-Einstellungen enthält (VirtualHost, DocumentRoot, Alias, Directory, Logs). Dabei bleiben beide Zugriffswege erhalten:
- Zugriff über den Hostnamen hinter dem Reverse Proxy, z. B.
https://website1.newdomain.de/undhttps://nextcloud.newdomain.de/ - Direktzugriff über die Server-IP mit Pfad, z. B.
http://192.178.1.100/www.website1.de/undhttp://192.178.1.100/nextcloud/ - Domainunabhängige Zuordnung der WordPress-Sites über
ServerAlias websiteN.* - Unbekannte Hostnamen mit Punkt (FQDN) ohne eigenen VirtualHost erhalten 403
1.2 Komponenten
| Komponente | Rolle | Details |
|---|---|---|
| Reverse Proxy (Caddy) | TLS-Terminierung, Auswahl des Backends anhand des Hostnamens | gibt den ursprünglichen Host-Header und X-Forwarded-Proto an Apache weiter |
| Apache | Webserver, nur HTTP auf Port 80 | Webserver-VM 192.178.1.100; Auswahl des VirtualHosts anhand von Host-Header und Port |
| WordPress | Webanwendung (PHP/MySQL) | je Site ein Verzeichnis /var/www/www.websiteN.de |
| Nextcloud | Webanwendung (PHP) | /var/www/nextcloud |
1.3 Zugriffswege
| Aufruf | Host-Header bei Apache | Zuständiger VirtualHost |
|---|---|---|
https://website1.newdomain.de/ |
website1.newdomain.de |
VirtualHost aus website1.conf (ServerName/ServerAlias) |
http://192.178.1.100/www.website1.de/ |
192.178.1.100 |
Default-VirtualHost aus 000-default.conf mit Alias aus website1.conf |
http://192.178.1.100/ |
192.178.1.100 |
Default-VirtualHost, DocumentRoot /var/www/html |
1.4 Das Problem: Alias gilt nur im eigenen VirtualHost
Ein Alias ist an den VirtualHost gebunden, in dem er steht. Anfragen über die IP tragen den Host-Header 192.178.1.100, passen auf keinen ServerName bzw. ServerAlias und landen deshalb im Default-VirtualHost. Die Alias-Zeilen aller Websites müssen daher im Default-VirtualHost wirksam sein, obwohl sie in den Site-Dateien stehen sollen.
Der naheliegende Ausweg, die Alias-Zeilen außerhalb eines VirtualHosts (auf Server-Ebene) abzulegen, ist nicht geeignet: Server-weite Aliase werden von allen VirtualHosts geerbt. Damit wären die Verzeichnisse anderer Sites unter jeder Subdomain erreichbar (z. B. https://website1.newdomain.de/www.website2.de/).
1.5 Lösung: Define und IfDefine
Der Default-VirtualHost bindet sites-enabled/*.conf ein zweites Mal ein, und zwar mit gesetzter Variable DIRECT_ACCESS. Jede Site-Datei wird dadurch zweimal gelesen:
| Durchlauf | DIRECT_ACCESS |
Wirkung |
|---|---|---|
Normaler Include durch apache2.conf |
nicht gesetzt | Der Block <IfDefine !DIRECT_ACCESS> ist aktiv: Directory und namensbasierter VirtualHost der Site |
| Include innerhalb des Default-VirtualHosts | gesetzt | Nur der Block <IfDefine DIRECT_ACCESS> ist aktiv: die Alias-Zeile der Site landet im Default-VirtualHost |
Define, UnDefine, IfDefine und Include werden beim Einlesen der Konfiguration in der geschriebenen Reihenfolge ausgewertet. Die Datei 000-default.conf bindet sich dabei selbst mit ein; die äußere Klammer <IfDefine !DIRECT_ACCESS> verhindert im zweiten Durchlauf eine Rekursion.
2. Weg einer Anfrage
2.1 Zugriff über den Hostnamen (Reverse Proxy)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Client / Browser URL: https://website1.newdomain.de/category/smart-home/ ↓ DNS website1.newdomain.de → öffentliche IP (Wildcard-Eintrag) ↓ Router / Firewall Portweiterleitung 443 → Reverse Proxy ↓ Caddy TLS-Terminierung, Auswahl des Backends anhand der Domain ↓ (Site-Block "website1.newdomain.de" → reverse_proxy 192.178.1.100) ↓ HTTP, Port 80, Host: website1.newdomain.de, X-Forwarded-Proto: https Apache Adressebene: *:80 → Namensebene: Host-Header gegen ServerName/ServerAlias ↓ VirtualHost aus website1.conf: ServerAlias "website1.*" ↓ → DocumentRoot /var/www/www.website1.de Directory-Block aus website1.conf: Options, AllowOverride All, Require all granted ↓ .htaccess (mod_rewrite) /category/smart-home/ ist weder Datei noch Verzeichnis → index.php ↓ WordPress wp-config.php: HTTPS aus X-Forwarded-Proto, WP_HOME = https://website1.newdomain.de → liefert die Seite /category/smart-home/ |
2.2 Direktzugriff über die Server-IP
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Client im LAN URL: http://192.178.1.100/www.website1.de/category/smart-home/ ↓ Apache Host: 192.178.1.100 → kein ServerName/ServerAlias passt ↓ Default-VirtualHost aus 000-default.conf (erster *:80-VirtualHost); ↓ <If>-Regel: IP-Adresse ist zulässig Alias "Alias /www.website1.de" (Zeile aus website1.conf, im Default-VirtualHost ↓ eingebunden) → /var/www/www.website1.de Directory-Block aus website1.conf: AllowOverride All gilt pfadbasiert auch für den Alias ↓ .htaccess (mod_rewrite) relative RewriteRule → index.php (Apache ergänzt das Alias-Präfix) ↓ WordPress wp-config.php: HTTP_HOST ist eine IP → WP_HOME = http://192.178.1.100/www.website1.de → liefert die Seite /category/smart-home/ |
Das Zielverzeichnis ist in beiden Fällen identisch, der Weg dorthin unterscheidet sich: einmal über den DocumentRoot eines namensbasierten VirtualHosts (URL-Pfad ab /), einmal über einen Alias im Default-VirtualHost (URL-Pfad ab /www.website1.de/). Daraus folgt der Bedarf an der URL-Umschaltung in WordPress (Kapitel 9.1) und an einer relativen RewriteRule (Kapitel 8.2).
2.3 Zwei Zeitpunkte: Konfiguration laden und Anfrage bearbeiten
| Zeitpunkt | Vorgang |
|---|---|
| Start/Reload von Apache | apache2.conf liest sites-enabled/*.conf in alphabetischer Reihenfolge. 000-default.conf steht durch das Präfix 000- zuerst; sein VirtualHost wird damit zum Default-Server. Innerhalb dieses VirtualHosts werden alle Site-Dateien mit gesetzter Variable DIRECT_ACCESS erneut gelesen; dabei entstehen die Alias-Zeilen. |
| Pro Anfrage | Auswahl des VirtualHosts (Port, Host-Header), Zuordnung von URL-Pfad zu Datei (Alias/DocumentRoot), Zusammenführen der Directory-Blöcke, Auswertung der .htaccess, Übergabe an PHP. |
2.4 Zusammenspiel zwischen Reverse Proxy und Apache
Der Reverse Proxy entscheidet, an welchen Backend-Server eine Anfrage weitergeleitet wird. Apache entscheidet anschließend anhand des Host-Headers und des Ports, welcher VirtualHost die Anfrage verarbeitet. Beide Konfigurationen sind unabhängig und müssen aufeinander abgestimmt sein.
Auf der Seite des Reverse Proxys ist je Subdomain ein Eintrag mit dem Webserver als Ziel erforderlich. Beispiel (von OPNsense aus den GUI-Einträgen erzeugter Caddyfile):
|
1 2 3 4 5 6 |
website1.newdomain.de { handle { reverse_proxy 192.178.1.100 { } } } |
Ein Port ist nicht angegeben, es gilt Port 80 (HTTP). Caddy öffnet eine eigene Verbindung zu Apache und gibt dabei folgende Informationen weiter (Standardverhalten von reverse_proxy):
| Information | Verhalten | Verwendung |
|---|---|---|
| URI/Pfad und Query | unverändert | Auswahl von Dateien/Rewrites, WordPress-Routing |
Host |
unverändert mit dem ursprünglichen Hostnamen | Auswahl des Apache-VirtualHosts (ServerName/ServerAlias) |
X-Forwarded-For |
Client-IP | Logs; mod_remoteip (Nextcloud-VirtualHost) |
X-Forwarded-Proto |
https |
HTTPS-Erkennung in WordPress (wp-config.php) und Nextcloud (SetEnvIf) |
X-Forwarded-Host |
ursprünglicher Hostname | in dieser Konfiguration nicht ausgewertet |
Zertifikatsbezug und TLS-Konfiguration des Reverse Proxys sind nicht Gegenstand dieser Dokumentation; Apache lauscht ausschließlich auf Port 80.
2.5 Auswirkungen falscher Host- oder Pfadkonfiguration
| Fehler | Wirkung |
|---|---|
Host-Header passt auf keinen ServerName/ServerAlias |
Default-VirtualHost: 403 (FQDN) oder Inhalt von /var/www/html (IP) |
Site-Datei nicht mit a2ensite aktiviert |
wie oben: 403 am Hostnamen, 404 beim Direktzugriff |
| Backend-Adresse oder Port im Reverse Proxy falsch | 502/Timeout beim Reverse Proxy |
Alias fehlt im IfDefine DIRECT_ACCESS-Teil der Site-Datei |
Direktzugriff per IP/Pfad liefert 404 |
RewriteBase / oder absolute Substitution (/index.php) in der .htaccess |
funktioniert nur bei einem der beiden Zugriffswege |
WP_HOME/WP_SITEURL passen nicht zur aufgerufenen URL |
fehlende CSS/JS, Redirect-Schleifen, falsche Links |
Fehlendes X-Forwarded-Proto-Handling |
WordPress/Nextcloud erzeugen http://-URLs hinter HTTPS |
3. Voraussetzungen und Module
3.1 Installation und Module
|
1 2 3 4 5 |
apt update apt install apache2 a2enmod rewrite headers remoteip env setenvif alias dir mime apache2ctl -M | grep -E 'rewrite|headers|remoteip|env|setenvif|alias|dir|mime' |
Die PHP-Pakete (WordPress, Nextcloud) und der Datenbankserver richten sich nach den Anforderungen der jeweiligen Anwendungen.
| Modul | Verwendung | Folge bei Fehlen |
|---|---|---|
rewrite |
WordPress-Permalinks und Nextcloud-Rewrite-Regeln über .htaccess |
Unterseiten liefern 404 |
headers |
Header always set Strict-Transport-Security in nextcloud.conf; Nextcloud-.htaccess |
Apache startet nicht (Invalid command 'Header') |
remoteip |
RemoteIPHeader/RemoteIPInternalProxy in nextcloud.conf |
Apache startet nicht (AH00526: Invalid command 'RemoteIPHeader') |
env, setenvif |
SetEnv, SetEnvIf (Nextcloud, HTTPS-Erkennung) |
Konfigurationsfehler bzw. fehlende HTTPS-Erkennung |
alias |
Alias-Direktiven für den Direktzugriff |
Direktzugriff per IP nicht möglich |
dir, mime |
Verzeichnis-Index bzw. Dateitypen; von Nextcloud vorausgesetzt | Fehlfunktionen der Nextcloud-.htaccess |
Das Modul macro wird nicht benötigt.
3.2 Aufbau der Debian-Konfiguration
|
1 2 3 4 5 |
/etc/apache2/apache2.conf Hauptdatei; bindet mods-enabled, ports.conf, conf-enabled und sites-enabled ein /etc/apache2/ports.conf Listen-Direktiven /etc/apache2/mods-available/ verfügbare Module → a2enmod / a2dismod → mods-enabled/ (Symlinks) /etc/apache2/sites-available/ verfügbare VHosts → a2ensite / a2dissite → sites-enabled/ (Symlinks) /etc/apache2/conf-available/ verfügbare Zusatzkonfiguration → a2enconf / a2disconf → conf-enabled/ |
apache2.conf enthält am Ende IncludeOptional sites-enabled/*.conf. Die Dateien werden in alphabetischer Reihenfolge eingelesen; darauf beruht die Sonderstellung von 000-default.conf.
3.3 Globale Zugriffsbasis
Die Datei apache2.conf sollte folgende Standardeinstellungen enthalten (Auszug):
|
1 2 3 4 5 6 7 8 9 10 11 |
<Directory /> Options FollowSymLinks AllowOverride None Require all denied </Directory> <Directory /var/www/> Options Indexes FollowSymLinks AllowOverride None Require all granted </Directory> |
Das Dateisystem ist gesperrt; /var/www/ ist freigegeben, jedoch ohne .htaccess-Auswertung (AllowOverride None). Die Freigabe der .htaccess-Auswertung erfolgt je Anwendung im Directory-Block der jeweiligen Site-Datei.
4. Zielstruktur
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/etc/apache2/sites-available/ ├── 000-default.conf Default-/Fallback-VirtualHost, bindet die Alias-Teile der Sites ein ├── nextcloud.conf Nextcloud: Directory, VirtualHost, Alias ├── website1.conf WordPress website1 ├── website2.conf WordPress website2 ├── website3.conf WordPress website3 └── website4.conf WordPress website4 /var/www/ ├── html/ Standard-DocumentRoot des Default-VirtualHosts ├── nextcloud/ Nextcloud ├── www.website1.de/ WordPress – website1.newdomain.de ├── www.website2.de/ WordPress – website2.newdomain.de ├── www.website3.de/ WordPress – website3.newdomain.de └── www.website4.de/ WordPress – website4.newdomain.de |
Die Namen der Site-Dateien sind frei wählbar, solange sie alphabetisch nach 000-default.conf einsortiert werden. Nur dann bleibt der Default-VirtualHost der erste *:80-VirtualHost und damit der Default-Server.
5. Die Datei 000-default.conf
Pfad: /etc/apache2/sites-available/000-default.conf
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# /etc/apache2/sites-available/000-default.conf # Default/fallback vhost. Must stay first in sites-enabled (000- prefix). <IfDefine !DIRECT_ACCESS> <VirtualHost *:80> DocumentRoot /var/www/html # Unknown FQDNs -> 403 (IPs and dotless names stay allowed) <If "%{HTTP_HOST} !~ /^([0-9.]+|[^.:]+)(:[0-9]+)?$/"> Require all denied </If> # Direct access via IP/path: pulls the Alias lines of all enabled sites into this vhost Define DIRECT_ACCESS IncludeOptional /etc/apache2/sites-enabled/*.conf UnDefine DIRECT_ACCESS ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined </VirtualHost> </IfDefine> |
| Bestandteil | Zweck | Folge ohne diese Einstellung |
|---|---|---|
<IfDefine !DIRECT_ACCESS> (außen) |
Verhindert, dass sich die Datei beim Include aus dem eigenen VirtualHost erneut selbst einbindet | Rekursion bzw. verschachtelte VirtualHosts, Apache startet nicht |
DocumentRoot /var/www/html |
Inhalt für http://<IP>/ ohne Pfad |
Kein definierter Standardinhalt |
<If …> Require all denied |
Hosts mit Punkt im Namen ohne eigenen VirtualHost (unbekannte Subdomains) erhalten 403; IP-Adressen und Kurznamen ohne Punkt bleiben zugelassen | Unbekannte Hostnamen erreichen die Alias-Pfade |
Define / IncludeOptional / UnDefine |
Bindet die Site-Dateien mit gesetzter Variable DIRECT_ACCESS ein; UnDefine setzt die Variable danach zurück |
Direktzugriff per IP/Pfad liefert 404 |
Die Datei enthält keinerlei Informationen zu einzelnen Websites. Eine neue Site erfordert keine Änderung an dieser Datei.
6. Die Site-Dateien
6.1 WordPress: website1.conf
Pfad: /etc/apache2/sites-available/website1.conf
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# /etc/apache2/sites-available/website1.conf <IfDefine !DIRECT_ACCESS> <Directory /var/www/www.website1.de> Options +FollowSymLinks AllowOverride All Require all granted </Directory> # Hostname access (reverse proxy -> Host: website1.<domain>), domain-independent <VirtualHost *:80> ServerName website1.newdomain.de ServerAlias website1.* DocumentRoot /var/www/www.website1.de ErrorLog ${APACHE_LOG_DIR}/website1-error.log CustomLog ${APACHE_LOG_DIR}/website1-access.log combined </VirtualHost> </IfDefine> # Direct access via IP: parsed inside the default vhost only <IfDefine DIRECT_ACCESS> Alias /www.website1.de /var/www/www.website1.de </IfDefine> |
| Bestandteil | Zweck | Folge ohne diese Einstellung |
|---|---|---|
<Directory …> mit AllowOverride All |
Auswertung der WordPress-.htaccess; gilt pfadbasiert für DocumentRoot- und Alias-Zugriff |
Permalinks liefern 404; nur index.php-URLs funktionieren |
ServerName |
Primärer Hostname des VirtualHosts | Apache vergibt einen Standardnamen, Warnungen im Log |
ServerAlias website1.* |
Ordnet jede Domain mit dem Subdomain-Label website1 dieser Site zu; die Domain steht nirgends fest |
Bei einem Domainwechsel wäre jede Site-Datei anzupassen |
DocumentRoot |
Zuordnung Subdomain → Verzeichnis | falsche oder keine Site |
ErrorLog/CustomLog |
getrennte Logs je Site | gemeinsame Logs, schwerere Diagnose |
Alias im IfDefine DIRECT_ACCESS |
Direktzugriff http://<IP>/www.website1.de/ über den Default-VirtualHost |
Direktzugriff liefert 404 |
Die weiteren WordPress-Sites unterscheiden sich ausschließlich in vier Werten:
| Datei | ServerName / ServerAlias |
Verzeichnis (Directory, DocumentRoot) |
Alias / Log-Präfix |
|---|---|---|---|
website1.conf |
website1.newdomain.de / website1.* |
/var/www/www.website1.de |
/www.website1.de / website1 |
website2.conf |
website2.newdomain.de / website2.* |
/var/www/www.website2.de |
/www.website2.de / website2 |
website3.conf |
website3.newdomain.de / website3.* |
/var/www/www.website3.de |
/www.website3.de / website3 |
website4.conf |
website4.newdomain.de / website4.* |
/var/www/www.website4.de |
/www.website4.de / website4 |
6.2 Nextcloud: nextcloud.conf
Pfad: /etc/apache2/sites-available/nextcloud.conf
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# /etc/apache2/sites-available/nextcloud.conf <IfDefine !DIRECT_ACCESS> <Directory /var/www/nextcloud/> Options +FollowSymLinks AllowOverride All Require all granted <IfModule mod_dav.c> Dav off </IfModule> SetEnv HOME /var/www/nextcloud SetEnv HTTP_HOME /var/www/nextcloud </Directory> <VirtualHost *:80> ServerName nextcloud.newdomain.de DocumentRoot /var/www/nextcloud/ # access below /nextcloud/ also at the hostname (e.g. passed-through legacy URLs) Alias /nextcloud /var/www/nextcloud/ RemoteIPHeader X-Forwarded-For # reverse proxy IP as seen by the web server RemoteIPInternalProxy 192.178.100.1 SetEnvIf X-Forwarded-Proto "https" HTTPS=on Header always set Strict-Transport-Security "max-age=15552000" ErrorLog ${APACHE_LOG_DIR}/nextcloud-error.log CustomLog ${APACHE_LOG_DIR}/nextcloud-access.log combined </VirtualHost> </IfDefine> # Direct access via IP: parsed inside the default vhost only <IfDefine DIRECT_ACCESS> Alias /nextcloud /var/www/nextcloud/ </IfDefine> |
| Direktive | Zweck | Folge ohne diese Einstellung |
|---|---|---|
ServerName nextcloud.newdomain.de |
Wählt den VirtualHost für den vom Reverse Proxy weitergereichten Host-Header. Der Wert muss dem Hostnamen entsprechen, unter dem Nextcloud aufgerufen wird; er ist domainabhängig und bei einem Domainwechsel anzupassen. | Anfragen landen im Default-VirtualHost (403) |
Alias /nextcloud im VirtualHost |
Zugriff unter dem Pfad /nextcloud/… auch am Hostnamen, z. B. für vom Reverse Proxy durchgereichte alte URLs (WebDAV, CalDAV, CardDAV mit Pfadpräfix). Nextcloud leitet seinen Webroot aus dem Skriptpfad ab. |
404 für Clients mit Pfadpräfix /nextcloud/ am Hostnamen |
Alias /nextcloud im IfDefine DIRECT_ACCESS |
Direktzugriff http://<IP>/nextcloud/ über den Default-VirtualHost |
Direktzugriff per IP liefert 404 |
RemoteIPHeader / RemoteIPInternalProxy |
mod_remoteip setzt die Client-IP aus X-Forwarded-For, sofern die Verbindung von der Proxy-Adresse stammt |
Logs und Nextcloud sehen nur die IP des Proxys |
SetEnvIf X-Forwarded-Proto "https" HTTPS=on |
Nextcloud erkennt die ursprüngliche HTTPS-Anfrage | http://-URLs, Mixed Content, Redirect-Probleme |
Dav off, SetEnv HOME, AllowOverride All |
Nextcloud wickelt WebDAV selbst ab; Home-Verzeichnis für PHP; Auswertung der Nextcloud-.htaccess |
Konflikte mit mod_dav; Rewrite-Regeln wirkungslos |
6.3 Aufbau einer Site-Datei (Muster)
Jede Site-Datei folgt demselben Schema. Die Klammern um Directory und VirtualHost sind zwingend, da die Datei sonst beim Include im Default-VirtualHost einen verschachtelten VirtualHost erzeugt.
|
1 2 3 4 5 6 7 8 |
<IfDefine !DIRECT_ACCESS> # 1. Directory block(s) of the site (global, path based) # 2. <VirtualHost *:80> for hostname access </IfDefine> <IfDefine DIRECT_ACCESS> # 3. Alias line(s) for direct access via IP/path </IfDefine> |
Kommentare müssen in einer eigenen Zeile stehen. Apache erlaubt keine Kommentare hinter einer Direktive; ein Kommentar hinter RemoteIPInternalProxy 192.178.1.1 würde als weiteres Argument gelesen.
7. Einrichtung, Aktivierung und neue Websites
7.1 Einrichtung
Nach dem Anlegen von 000-default.conf, nextcloud.conf und website1.conf … website4.conf werden die Konfigurationen geladen:
|
1 2 3 |
a2ensite 000-default nextcloud website1 website2 website3 website4 apache2ctl configtest && apache2ctl -S systemctl restart apache2 |
Nach Modulaktivierungen (a2enmod) ist ein restart erforderlich, für reine Konfigurationsänderungen genügt systemctl reload apache2. Die Ausgabe von apache2ctl -S muss 000-default.conf als default server für *:80 und je Site einen namevhost mit Datei und Zeile zeigen. Weitere aktivierte Site-Dateien mit VirtualHosts für dieselben Namen (z. B. frühere Einzeldateien) führen zu doppelten VirtualHosts und sind mit a2dissite zu deaktivieren.
7.2 Neue WordPress-Site (Beispiel website5)
- Verzeichnis
/var/www/www.website5.deanlegen bzw. die Site dorthin kopieren. website1.confnachwebsite5.confkopieren und die vier Werte aus Kapitel 6.1 ersetzen (Name, Verzeichnis, Alias, Log-Präfix).- In der
wp-config.phpder Site$direct_pathauf den Alias-Pfad setzen (Kapitel 9.1). - Im Reverse Proxy die Subdomain
website5mit Ziel192.178.1.100anlegen (Kapitel 2.4). a2ensite website5 && apache2ctl configtest && systemctl reload apache2
Die Datei 000-default.conf bleibt unverändert.
7.3 Site vorübergehend deaktivieren
|
1 2 |
a2dissite website3 apache2ctl configtest && systemctl reload apache2 |
Mit der Site-Datei entfallen gleichzeitig Directory, VirtualHost und der Alias für den Direktzugriff. Das ist der wesentliche Vorteil gegenüber der zentralen Makro-Variante.
8. Die .htaccess-Dateien
8.1 Rolle der .htaccess in dieser Konfiguration
Die .htaccess-Dateien liegen im Verzeichnis der jeweiligen Anwendung (/var/www/www.websiteN.de/.htaccess, /var/www/nextcloud/.htaccess) und werden von Apache nur ausgewertet, wenn AllowOverride es für das Verzeichnis erlaubt. Die globale Voreinstellung für /var/www/ ist AllowOverride None (Kapitel 3.3). Deshalb enthält jede Site-Datei einen eigenen Directory-Block mit AllowOverride All.
Die .htaccess wird nach der Auswahl von VirtualHost und Alias/DocumentRoot ausgewertet (Kapitel 2.1) und ist unabhängig vom Zugriffsweg dieselbe Datei. Sie darf deshalb weder Hostnamen noch absolute URL-Pfade enthalten.
| Voraussetzung | Konfigurationsort | Folge bei Fehlen |
|---|---|---|
AllowOverride All |
Directory-Block der Site-Datei |
.htaccess wird ignoriert; Permalinks liefern 404 |
Modul rewrite |
a2enmod rewrite |
Unterseiten liefern 404 bzw. interner Serverfehler |
Lesezugriff für www-data |
Dateirechte der .htaccess |
403/500 für alle Anfragen im Verzeichnis |
Modul headers (Nextcloud) |
a2enmod headers |
interner Serverfehler (Nextcloud-.htaccess setzt Header) |
8.2 WordPress
Pfad je Site: /var/www/www.websiteN.de/.htaccess. WordPress erzeugt den Block beim Speichern der Permalink-Einstellungen. Einheitlicher Block für alle Sites (Subdomain-Zugriff und Alias-Direktzugriff):
|
1 2 3 4 5 6 7 8 9 10 |
# BEGIN WordPress <IfModule mod_rewrite.c> RewriteEngine On RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] RewriteRule ^index\.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . index.php [L] </IfModule> # END WordPress |
| Zeile | Funktion |
|---|---|
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] |
reicht den Authorization-Header an PHP durch (z. B. für die REST-API) |
RewriteRule ^index\.php$ - [L] |
Aufrufe von index.php werden nicht weiter umgeschrieben |
RewriteCond … !-f / !-d |
Regel gilt nur für Pfade, die weder Datei noch Verzeichnis sind |
RewriteRule . index.php [L] |
übergibt alle übrigen Pfade an WordPress, das den Permalink auflöst |
Warum kein RewriteBase und keine absolute Substitution:
| Variante | Subdomain (DocumentRoot /) |
Alias-Direktzugriff (/www.websiteN.de/) |
|---|---|---|
RewriteBase / + RewriteRule . /index.php (WordPress-Standard) |
funktioniert | falsch: /index.php liegt im Default-DocumentRoot, nicht im Alias-Verzeichnis |
RewriteBase /www.websiteN.de/ + RewriteRule . /www.websiteN.de/index.php |
falsch: dieser Pfad existiert im DocumentRoot der Subdomain nicht (404) | funktioniert |
RewriteRule . index.php ohne RewriteBase (relative Substitution) |
funktioniert | funktioniert |
Bei relativer Substitution ergänzt Apache das jeweilige URL-Präfix des Verzeichnisses selbst. Eine Angabe von Host oder Pfad ist nicht erforderlich. Bedingungen auf den Hostnamen (RewriteCond %{HTTP_HOST} …) sind zu vermeiden, da sie die Konfiguration domainabhängig machen. Von Plugins ergänzte Abschnitte außerhalb von # BEGIN WordPress … # END WordPress bleiben unberührt.
Kontrolle:
|
1 2 3 4 5 6 7 |
# via subdomain (Host header) curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: website1.newdomain.de' http://127.0.0.1/category/smart-home/ # via alias (direct access) curl -s -o /dev/null -w '%{http_code}\n' http://192.178.1.100/www.website1.de/category/smart-home/ # redirects in .htaccess files / site files (expected: no output) grep -nEi 'Redirect|RewriteRule.*\[R' /var/www/www.*/.htaccess /etc/apache2/sites-available/*.conf |
Beide Aufrufe müssen den Status 200 liefern, sofern der Permalink existiert; ein 404 kommt dann von WordPress selbst, nicht von Apache. Treffer des grep in der Nextcloud-.htaccess (/.well-known/carddav, /.well-known/caldav) sind regulär.
8.3 Nextcloud
Die .htaccess liegt unter /var/www/nextcloud/.htaccess und gehört zum Lieferumfang von Nextcloud. Sie enthält unter anderem Sicherheits-Header, Rewrite-Regeln sowie die Weiterleitungen für /.well-known/carddav und /.well-known/caldav.
- Voraussetzungen:
AllowOverride AllimDirectory-Block vonnextcloud.confsowie die Modulerewrite,headers,env,dirundmime. - Die Datei wird von Nextcloud verwaltet und kann bei Updates überschrieben werden. Anpassungen gehören daher in die Site-Datei
nextcloud.conf. - Die
.htaccessgilt, wie bei WordPress, pfadbasiert für beide Zugriffswege (Hostname undAlias /nextcloud).
Die .htaccess kann bei Bedarf im Verzeichnis /var/www/nextcloud mit folgendem Befehl neu erzeugt bzw. aktualisiert werden:
|
1 |
sudo -u www-data php occ maintenance:update:htaccess |
9. WordPress- und Nextcloud-Besonderheiten
9.1 WordPress: wp-config.php
Pfad: /var/www/<websiteN>/wp-config.php
Damit WordPress bei beiden Zugriffswegen die richtige Basis-URL verwendet, enthält die Datei je Site folgenden URL-Block. Er steht nach der ABSPATH-Definition und vor require_once(ABSPATH . 'wp-settings.php');; nach dem Laden von wp-settings.php wäre es für define('WP_HOME', …) zu spät.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/* ------------------------------------------------------------------ * URL handling: reverse proxy (any domain) or direct access via IP/short name * Per site only $direct_path needs to be adjusted (alias path of the default vhost). * Insert before: require_once(ABSPATH . 'wp-settings.php'); * ------------------------------------------------------------------ */ $direct_path = '/www' . '.website1.de'; if (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') { $_SERVER['HTTPS'] = 'on'; } $wp_host = $_SERVER['HTTP_HOST'] ?? ''; if ($wp_host !== '') { // not for WP-CLI/cron without HTTP context $wp_name = preg_replace('/:\d+$/', '', $wp_host); $wp_direct = filter_var($wp_name, FILTER_VALIDATE_IP) || strpos($wp_name, '.') === false; $wp_scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://'; $wp_url = $wp_scheme . $wp_host . ($wp_direct ? $direct_path : ''); define('WP_HOME', $wp_url); define('WP_SITEURL', $wp_url); } |
Template einer vollständigen wp-config.php:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
<?php define('WP_CACHE', true); // WP-Optimize Cache /** * The base configuration for WordPress * * @package WordPress */ /** MySQL settings */ define( 'DB_NAME', "<db_name>" ); define( 'DB_USER', "<db_user>" ); define( 'DB_PASSWORD', "<db_password>" ); define( 'DB_HOST', "localhost" ); define( 'DB_CHARSET', 'utf8mb4' ); define( 'DB_COLLATE', '' ); /** Authentication Unique Keys and Salts. */ // Beispielwerte; je Installation eigene Werte verwenden (bei einer Migration die Werte der bestehenden Datei unverändert übernehmen) define( 'AUTH_KEY', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); define( 'SECURE_AUTH_KEY', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); define( 'LOGGED_IN_KEY', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); define( 'NONCE_KEY', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); define( 'AUTH_SALT', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); define( 'SECURE_AUTH_SALT', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); define( 'LOGGED_IN_SALT', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); define( 'NONCE_SALT', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); /** WordPress Database Table prefix. */ $table_prefix = 'wp_'; /** WordPress debugging mode. */ define('WP_DEBUG', false); /** Absolute path to the WordPress directory. */ if (!defined('ABSPATH')) { define('ABSPATH', dirname(__FILE__) . '/'); } /* ------------------------------------------------------------------ * URL handling: reverse proxy (any domain) or direct access via IP/short name * Per site only $direct_path needs to be adjusted (alias path of the default vhost). * Insert before: require_once(ABSPATH . 'wp-settings.php'); * ------------------------------------------------------------------ */ $direct_path = '/www' . '.websiteN'; if (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') { $_SERVER['HTTPS'] = 'on'; } $wp_host = $_SERVER['HTTP_HOST'] ?? ''; if ($wp_host !== '') { // not for WP-CLI/cron without HTTP context $wp_name = preg_replace('/:\d+$/', '', $wp_host); $wp_direct = filter_var($wp_name, FILTER_VALIDATE_IP) || strpos($wp_name, '.') === false; $wp_scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://'; $wp_url = $wp_scheme . $wp_host . ($wp_direct ? $direct_path : ''); define('WP_HOME', $wp_url); define('WP_SITEURL', $wp_url); } /** Sets up WordPress vars and included files. */ require_once(ABSPATH . 'wp-settings.php'); |
$direct_pathist die einzige site-spezifische Angabe und muss exakt demAliasin der Site-Datei entsprechen (Alias /www.website1.de↔'/www' . '.website1.de').- Hostnamen mit Punkt (Zugriff über den Reverse Proxy) erhalten die Basis-URL ohne Pfad; IP-Adressen und Namen ohne Punkt (Direktzugriff) erhalten zusätzlich
$direct_path. X-Forwarded-Proto: httpssetzt$_SERVER['HTTPS']; ohne diese Zeile erzeugt WordPress hinter dem Reverse Proxyhttp://-URLs.WP_HOMEundWP_SITEURLüberschreiben die Datenbankwertehomeundsiteurlzur Laufzeit; die Domain steht nirgends in der Datei.
Kontrolle:
|
1 2 3 |
php -l /var/www/www.website1/wp-config.php curl -s -H 'Host: website1.newdomain.de' -H 'X-Forwarded-Proto: https' http://127.0.0.1/ | grep -o 'href="[^"]*"' | head -5 curl -s http://192.178.1.100/www.website1 | grep -o 'href="[^"]*"' | head -5 |
Die ersten Links müssen mit https://website1.newdomain.de/ bzw. http://192.178.1.100/www.website1.de/ beginnen.
9.2 Nextcloud: config.php
Pfad: /var/www/nextcloud/config/config.php
Die Einstellungen in der config.php hängen unter anderem von den Apache-Direktiven in nextcloud.conf ab. Soll der Server zusätzlich über eine andere Adresse erreichbar sein, muss diese URL in den Trusted Domains von Nextcloud ergänzt werden. Dies kann mit einem einfachen Befehl erfolgen:
|
1 2 3 |
cd /var/www/nextcloud sudo -u www-data php occ config:system:get trusted_domains sudo -u www-data php occ config:system:set trusted_domains 2 --value=nextcloud.newdomain.de |
| Einstellung | Empfehlung | Begründung |
|---|---|---|
trusted_domains |
Hostname (nextcloud.newdomain.de) und Server-IP eintragen |
Fehlt der Hostname, erscheint „Zugriff über eine nicht vertrauenswürdige Domain“; ohne IP funktioniert der Direktzugriff nicht |
trusted_proxies |
IP des Reverse Proxys (optional) | Mit mod_remoteip im VirtualHost ist REMOTE_ADDR bereits die Client-IP; der Eintrag hat dann keine Wirkung |
overwritehost, overwriteprotocol |
nicht setzen | Der Reverse Proxy reicht den Host unverändert durch, SetEnvIf deckt HTTPS ab; unbedingt gesetzt würde der Direktzugriff per IP auf den Hostnamen umgebogen (Login-Schleife) |
overwritecondaddr |
nicht in Kombination mit mod_remoteip |
Wird gegen REMOTE_ADDR geprüft; nach mod_remoteip ist das nicht mehr die Proxy-IP, die Bedingung greift nicht |
overwritewebroot |
nicht setzen | Der Webroot wird aus dem Skriptpfad abgeleitet (leer am Hostnamen, /nextcloud beim Alias) |
overwrite.cli.url |
setzen | Betrifft CLI/Cron-Links; der Webzugriff funktioniert ohne |
10. Prüf- und Testverfahren
Die Befehle sind auf dem Webserver auszuführen. <IP> ist durch die Server-IP zu ersetzen.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# syntax and vhost assignment (default server must be 000-default.conf) apache2ctl configtest apache2ctl -S # hostname access (Host header simulated) for h in website1 website2 website3 website4 nextcloud; do printf '%-12s' "$h"; curl -s -o /dev/null -w '%{http_code}\n' -H "Host: $h.newdomain.de" http://127.0.0.1/ done # direct access via IP/path curl -s -o /dev/null -w '%{http_code}\n' http://<IP>/www.website1.de/ curl -s -o /dev/null -w '%{http_code}\n' http://<IP>/nextcloud/ # unknown FQDN must return 403 curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: foo.newdomain.de' http://127.0.0.1/ # no alias leakage: other site directory must not be served below a subdomain curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: website1.newdomain.de' http://127.0.0.1/www.website2.de/ # Nextcloud below /nextcloud/ at the hostname curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: nextcloud.newdomain.de' http://127.0.0.1/nextcloud/status.php |
| Test | Erwartetes Ergebnis |
|---|---|
apache2ctl -S |
default server aus 000-default.conf; je Site ein namevhost mit Datei sites-enabled/<site>.conf |
| Hostname-Zugriff (Schleife) | kein 403; Nextcloud liefert eine Antwort der Anwendung (z. B. 200 oder 302) |
| Direktzugriff IP/Pfad | kein 404; die Seite der jeweiligen Site wird ausgeliefert |
| Unbekannter FQDN | 403 |
| Alias-Leckage | kein Inhalt der Site website2 unter der Subdomain website1 (Antwort von WordPress der Site website1) |
11. Fehleranalyse und typische Probleme
| Symptom | Ursache | Lösung |
|---|---|---|
Apache startet nicht: AH00526: Invalid command 'RemoteIPHeader' |
Modul remoteip (bzw. headers, setenvif, env) nicht aktiviert |
a2enmod remoteip headers env setenvif, danach systemctl restart apache2 |
| Syntaxfehler in einer Direktive mit Kommentar in derselben Zeile | Apache erlaubt keine Inline-Kommentare; der Kommentar wird als Argument gelesen | Kommentar in eine eigene Zeile verschieben |
<VirtualHost> cannot occur within <VirtualHost> section |
Eine aktivierte Site-Datei besitzt keine IfDefine-Klammer; sie wird beim Include im Default-VirtualHost mitgelesen |
Muster aus Kapitel 6.3 einhalten |
403 am Hostnamen, Site fehlt in apache2ctl -S |
Site-Datei nicht aktiviert; die Anfrage trifft den Default-VirtualHost und die <If>-Regel |
a2ensite <site>, apache2ctl configtest, systemctl reload apache2 |
| Nextcloud: „Zugriff über eine nicht vertrauenswürdige Domain“ | Der aufgerufene Hostname fehlt in trusted_domains, oder ServerName in nextcloud.conf entspricht nicht dem tatsächlich verwendeten Hostnamen |
ServerName korrigieren; Hostname per occ config:system:set trusted_domains ergänzen (Kapitel 9.2) |
| WordPress: Startseite funktioniert, Unterseiten liefern 404 | AllowOverride steht nicht auf All (Directory-Block fehlt oder gehört zu einem anderen Pfad), oder rewrite ist nicht aktiviert |
Directory-Pfad in der Site-Datei prüfen, a2enmod rewrite (Kapitel 8) |
| WordPress: Direktzugriff per IP ohne CSS, Links ohne Alias-Pfad | $direct_path in der wp-config.php fehlt oder weicht vom Alias ab |
$direct_path an den Alias anpassen (Kapitel 9.1) |
| Direktzugriff per IP liefert 404 | Der Include-Block in 000-default.conf fehlt, oder der IfDefine DIRECT_ACCESS-Teil der Site-Datei fehlt bzw. enthält einen falschen Pfad |
Kapitel 5 und 6 prüfen |
| Doppelte VirtualHosts, falsche Site wird ausgeliefert | Weitere aktivierte Dateien enthalten VirtualHosts für dieselben Namen | a2dissite der überzähligen Dateien |
IP-Aufrufe liefern Inhalt einer Site statt /var/www/html |
Eine Datei, die alphabetisch vor 000-default.conf einsortiert wird, enthält den ersten *:80-VirtualHost |
Dateinamen mit anderem Präfix wählen; apache2ctl -S prüfen |
Warnung AH00558: Could not reliably determine the server's fully qualified domain name |
Kein globaler ServerName; unkritisch |
Optional: echo "ServerName webserver" > /etc/apache2/conf-available/servername.conf && a2enconf servername |
12. Vergleich mit der zentralen Makro-Variante
Die zentrale Variante mit mod_macro ist in der Dokumentation OPNsense/Caddy: Webserver-Migration mit Apache, WordPress und Nextcloud beschrieben (Kapitel 2.4).
| Punkt | Zentral mit Makro | Eine Datei pro Website |
|---|---|---|
| Aktivieren/Deaktivieren einzelner Sites | nicht möglich (alles in einer Datei) | a2ensite / a2dissite je Site |
| Apache-Modul | macro erforderlich |
nicht erforderlich |
| Neue Site | eine Zeile Use WPSite plus eine Alias-Zeile in 000-default.conf |
Datei kopieren und vier Werte ersetzen; 000-default.conf bleibt unverändert |
| Umfang je Site | Aufteilung auf Makro, Alias-Block und Directory-Sammelfreigabe |
alle Einstellungen in einer Datei |
| Mechanik | Makro-Expansion | Define/IfDefine-Include im Default-VirtualHost (jede Datei wird zweimal gelesen) |
| Domainunabhängigkeit | ServerAlias $sub.* |
unverändert über ServerAlias websiteN.* |
Hinweis: Bei Paketupdates von apache2 kann dpkg wegen der geänderten Datei 000-default.conf eine Rückfrage zur Konfigurationsdatei stellen. Die lokale Version ist beizubehalten.
Tutorials, Konfigurationen und Open-Source-Projekte sind mit regelmäßigem Aufwand für Tests, Pflege und Dokumentation verbunden. Wer unsere Arbeit unterstützen möchte, trägt dazu bei, dass neue Inhalte entstehen. Wir danken dafür.