Nginx Reverse Proxy Manager – mehrere Web-Server unter einer IP- bzw. Domain-Adresse

Allgemein

Proxy – davon haben sicherlich viele schon einmal gehört, die sich mit dem Internet oder allgemein der Übertragung von IP-Paketen beschäftigen. Aber was ist das überhaupt. Grundsätzlich ist ein Proxy ein Vermittler, also jemand der zwischen Sender und Empfänger steht und Daten (IP-Pakete) nicht einfach nur weiterleitet (das wäre dann ein Router oder einen Layer (OSI) tiefer gedacht ein Switch, der dann allerdings nicht IP-Pakete direkt, sondern Frames nach bestimmten Kriterien weiterleitet). Der Proxy leitet ein IP-Paket stellvertretend für den eigentlichen Absender an das Ziel weiter (oder auch nicht). Dies ermöglicht z.B. einen kontrollierten Zugang von Clients (LAN) in einem Netzwerk zum Internet (WAN). Der Empfänger eines IP-Pakets (Server) kennt dadurch nicht den eigentlichen Absender, sondern nur die IP-Adresse des Proxys. Die Antwort des Servers erfolgt dann zum Proxy, der dann stellvertretend für den Server die Antwort zum Client sendet. So bleibt der Client gegenüber dem Server anonym (außer bei einem transparenten Proxy) und durch diverse Techniken lässt sich auch massiv Bandbreite sparen bzw. der Netzwerkverkehr bis hin zu einer vollständigen Application Firewall (ALG) gezielt kontrollieren. In diesem Fall spricht man von einem Forward-Proxy.

Ein Reverse Proxy hingegen arbeitet genau in die entgegengesetzte Richtung. Dieser Proxy-Server nimmt Anfragen aus dem Internet entgegen und leitet sie als zusätzliche Sicherheitskomponente an im lokalen Netzwerk liegende Serverdienste weiter, z.B. an einem Web-, FTP- oder andere Backend-Server. Dadurch ergeben sich etliche weitere Vorteile wie z.B.:

  • Anonymisierung (der Proxy ist die einzige Schnittstelle des internen Netzwerkes zum Internet, die eigentliche Netzwerkinfrastruktur bleibt nach außen hin unbekannt)
  • Load-Balancing (durch eine smarte interne Netzwerkinfrastruktur lassen sich Server im LAN redundant betreiben und der Proxy verteilt die Anfragen aus dem Internet auf die lokalen Server)
  • Kompression (sowohl ein- als auch ausgehende Verbindungen lassen sich auf dem transportweg zusätzlich komprimieren, dafür wird häufig gzip verwendet, oft auch in Kombination mit Apache oder nginx)
  • Schutz (der Proxy-Server kann bereits direkt am Netzübergang als Paketfilter und Virenscanner dienen, so gelangen ungewünschte Anfragen oder Schadcode gar nicht erst bis zum eigentlichen Server im LAN)
  • Verschlüsselung (SSL-Zertifikatsverwaltung kann vollständig vom Proxy übernommen werden, dies entlastet sämtliche Dienste im LAN, da diese sich nicht mehr um Verschlüsselung kümmern müssen, die Verbindung zwischen Proxy und Server im lokalen Netzwerk kann unverschlüsselt erfolgen)
  • Caching (so wie auch beim Forward-Proxy lassen sich auch hier häufige Anfragen direkt am Proxy zwischenspeichern, was wiederum die dahinterliegenden lokalen Server entlastet (dies führt zu einer verbesserten Performance))

Das Ziel dieses Tutorials ist es, mehrere Dienste (Web-Instanzen), mittels Zertifikate abgesichert (HTTPS), welche unter einer IP im Internet/Intranet gehostet sind, erreichbar zu machen. Ich selbst mache verschiedene Domains als auch mehrere Web-Administrationsoberflächen für andere Dienste auf diese Art von außen erreichbar.

Doch zuerst einmal möchte ich auf die Anleitung vom Entwickler unter https://nginxproxymanager.com/ verweisen. Die erläutert die Installation auch anhand eines Docker-Compose-Files recht gut, geht aber nicht auf die mögliche virtuelle Netzwerktopologie ein. Und genau hier und den möglicherweise auftretenden Problemen möchte ich mit diesem Blogbeitrag ansetzen.

Voraussetzung ist natürlich ein eigener Server mit einer öffentlichen IP-Adresse/Domain und eine funktionierende Docker-Installation (hier inklusive Portainer). Dazu musst du jetzt wissen, das Docker eine eigene virtuelle Netzwerkinfrastruktur aufbaut, auf die ich in einem anderen Blogbeitrag bereits eingegangen bin.

Was wir brauchen sind natürlich zum einen den Nginx Proxy Manager und in unserem Fall WordPress. Du findest vielleicht etliche Anleitungen im Internet für Nginx und Owncloud, aber wenn du das mit WordPress probierst, klappt das irgendwie nicht (hier und da erntest du eine Fehlermeldung). Dabei soll es doch „eigentlich“ ganz einfach mit Portainer sein – wir klicken uns alles zusammen und fertig – Pustekuchen.

NGINX Reverse Proxy

Fangen wir mit dem ersten Schritt an. Die schnellste und einfachste Installation für den Nginx Proxy Manager (siehe auch Link oben) erfolgt mit einem Docker-Compose-File. Dazu einfach in Portainer auf Stacks klicken, dann +Add stack, bei Name zum Beispiel npm eingeben und in das Texteingabefeld (Web Editor) das Compose-File von der Entwicklerseite hineinkopieren und auf Deploy klicken.

Hier nochmal vollständigkeitshalber den Inhalt des Files:

version: "3"
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      # These ports are in format <host-port>:<container-port>
      - '80:80' # Public HTTP Port
      - '443:443' # Public HTTPS Port
      - '81:81' # Admin Web Port
      # Add any other Stream port you want to expose
      # - '21:21' # FTP
    environment:
      DB_MYSQL_HOST: "db"
      DB_MYSQL_PORT: 3306
      DB_MYSQL_USER: "npm_user"
      DB_MYSQL_PASSWORD: "npm_pass"
      DB_MYSQL_NAME: "npm_db"
      # Uncomment this if IPv6 is not enabled on your host
      # DISABLE_IPV6: 'true'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    depends_on:
      - db

  db:
    image: 'jc21/mariadb-aria:latest'
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: 'npm_root_pass'
      MYSQL_DATABASE: 'npm_db'
      MYSQL_USER: 'npm_user'
      MYSQL_PASSWORD: 'npm_pass'
    volumes:
      - ./data/mysql:/var/lib/mysql

Ich habe in diesem yaml-File (Docker-Compose) statt einfach wie im Original „npm“ „individuelle“ Credentials eingesetzt (hier im Beispiel mit ganz ganz einfachen Passwörtern), so das du erkennen kannst, welches Passwort bei den Umgebungsvariablen des Nginx weiter unten im Abschnitt mit denen der Datenbank (db) übereinstimmen muss. Aber gehen wird das File mal Schritt für Schritt durch:

  1. Das Image jc21/nginx-proxy-manager:latest wird mit folgenden Optionen geladen und gestartet:
    1. es wird ausgeführt, bis es manuell gestoppt wird
    2. Es hört auf den Ports 80, 443 und 81
    3. es werden diverse Umgebungsvariablen für eine MySQL-Datenbank definiert (Name: db, Port 3306, User, Password, Tabellenname)
    4. es werden 2 lokale Verzeichnisse in den Container gemappt (data und letsencrypt)
    5. der Container hat eine Abhängigkeit vom Container db
  2. Das Image jc21/mariadb-aria:latest wird mit folgenden Optionen geladen und gestartet:
    1. es wird ausgeführt, bis es manuell gestoppt wird
    2. es werden diverse Umgebungsvariablen für die MySQL-Datenbank definiert (MySQL-Root-Passwort, Datenbankname, User, Password)
    3. es wird 1 lokales Verzeichnisse in den Container gemappt (data /mysql)

Das Mapping der Verzeichnisse dient dazu, das auch bei Updates von Containern die Konfigurationsdaten von Ngnix stets erhalten bleiben, weil diese auf dem lokalen Host und nicht im Container gespeichert werden.

Nachdem du aber nun auf Deploy geklickt hast, dauert das wenige Sekunden und unter Containers solltest du jetzt neben Portainer zwei weitere Container lauffähig (grün) sehen können (beide im Stack npm).

Wie du siehst, hat Nginx in der Tat die drei Ports 443, 80 und 81 geöffnet, sprich diese sind jetzt bereits über die öffentliche IP des Docker-Hosts zu erreichen. Port 80 (HTTP) und 443 (HTTPS) werden später benötigt, da nginx auf diesen beiden Anfragen entgegen nehmen wird und diese an dahinter liegende, in diesem Fall Web-Server, Docker-Container weiterreichen wird. Port 81 dient der Konfiguration von Nginx, das testen wir gleich, in dem wir die öffentliche IP unseres Servers, gefolgt von :81 aufrufen – in meinem Fall ist das hier die http://178.254.24.175:81. Dann begrüßt uns auch umgehend der Login-Dialog von Nginx. Die Credentials nach der Erstinstallation sind User admin@example.com und das Passwort changeme.

Nach der erfolgreichen Anmeldung wirst du aufgefordert, neue Credentials (vollständiger Name, Nickname, eMail und Passwort) festzulegen.

Und dann ist Nginx auch schon fertig vorbereitet:

Großartig, oder? Rufen wir im Browser doch mal unsere Domain auf (bei mir hier im Beispiel locarbo.de).

Nett, das uns gratuliert wird, aber wir wollen ja eigentlich unseren WordPress-Blog sehen. Also installieren wir uns den als nächstes.

WordPress

Im nächsten Schritt scrollst du unter App Templates ganz nach unten und klickst WordPress an.

Im folgenden Dialogfenster vergibst du einen passenden Namen sowie Datenbank-Passwort für den Blog wie z.B. kwellkorn_blog und klickst abermals auf Deploy the stack.

Und schon hast du 2 Stacks erstellt:

2 Stacks

Des Weiteren solltest du unter Container dann folgende 5 aktive Container sehen

5 Container

Du siehst also, das sowohl für WordPress als auch für Nginx (npm) je zwei Container erstellt wurden, denn jeder muss Daten irgendwie speichern und beide erledigen das über je eine eigene Datenbank (mariadb bzw. mysql). Weiterhin siehst du, das WordPress den Port 80 bei mir unter 49155 (das kann bei dir ein anderer Port sein) für den Docker-Host erreichbar macht (und somit auch unter der öffentlichen IP-Adresse des Servers).

Probieren wir das doch mal aus. Wenn du lowcarbo.de im Browser einfach aktualisierst, bleibt es bei der Gratulation (siehe oben). Danke, aber das ist nicht das gewünschte Ziel. Also versuchen wir so wie bei Nginx mit Port 81, in diesem Fall jedoch die IP-Adresse gefolgt von Port 49155. Also in meinem Beispiel http://178.254.24.175:49155

Ah, sehr gut! Das scheint zu klappen. Aber STOPP! Warte mit der Ersteinrichtung deines Blogs noch, bis dieser auch unter seiner gewünschten Domain erreichbar ist, wenn du späteren Ärger vermeiden möchtest (siehe weiter unten Troubleshooting – Teil 2 (HTTP Statuscode 500)).

Nginx – Konfiguration einer Domain

Um deinen Blog jetzt auch über die Domain erreichbar zu machen, musst du den Reverse Proxy konfigurieren. Klicke dazu unter Hosts auf Add Proxy Hosts, gib unter Domain Names deinen Domain-Namen an. Ganz wichtig ist dann Scheme (es bleibt bei HTTP, es sei denn, du willst im lokalen Netz auch via SSL kommunizieren, aber dann kommt noch deutlich mehr Konfigurationsaufwand auf dich zu, auf den ich hier nicht näher eingehe), Forward Hostname (das ist der Container-Name (siehe hierzu im Portainer die Container-Übersicht (bei mir kwellkorn_blog_wordpress_1)) und den Port 80 (der Port innerhalb des Containers, der bei mir derzeit mit extern 49155 verbunden ist).

Als nächstes klickst auf den Reiter SSL, wählst „Request a new SSL Certificate“ aus, wählst du gewünschten Sicherheitsparameter, bestätigst die AGB und speicherst die Konfiguration ab.

Sofort holt der Nginx bei Let’s encrypt ein Zertifikat (Gültigkeit 3 Monate) und ist soweit fertig konfiguriert.

Sobald du im Browser die Domain aufrufst (ohne irgendwelche Portnummern, etc.) bekommst du jedoch ein 502 Bad Gateway.

nginx reverse proxy – 502 Bad Gateway

Na schön, zumindest ist das kleine Schloss neben der Domain geschlossen, ein gültiges Zertifikat haben wir also. Aber was soll der Fehler 502?

Troubleshooting – Teil 1

Was ist passiert. Alles nach den beiden Standard-Anleitung gemacht und doch nicht funktioniert. Ich mache es kurz, werfe mal einen Blick in deine Netzwerk-Konfiguration. Das geht in Portainer mit einem Klick.

Wie du siehst, hat Docker jetzt neben seinen Standard-Netzwerken bridge, host und none (siehe hierzu auch meinen passenden Blogbeitrag Netzwerkverwaltung unter Docker) zwei weitere Netzwerke – kwellkorn_blog_default und npm_default. Beide, als auch die Standard-Bridge, liegen in verschiedenen Subnetzen (hier 172.17.0.0/16, 172.21.0.0/16 und 172.18.0.0/16). Und leider werden diese Netzwerke durch die Docker-Host-Firewall iptables sauber voneinander getrennt. Das soll auch grundsätzlich so sein. Ohne irgendwelche Vorgaben (also z.B. bei einem Standard-Pull/Run-Verfahren für Docker-Images/Container) verbinden sich dessen Netzwerkschnittstelle mit der Standard-bridge. Bei deinem Stack wird automatisch auch eine neue Netzwerkschnittstelle eingerichtet.

Wenn du jetzt sowohl bei dem Container npm_app_1 als auch kwellkorn_blog_wordpress_1 ganz nach unten scrollst (also zuerst den jeweiligen Container anklicken), siehst du, das npm_app_1 mit npm_default und der kwellkorn_blog_wordpress_1 mit der Bridge kwellkorn_blog_default verbunden ist. Deshalb kann der Nginx auch nicht auf den WordPress-Container zugreifen.

Exkurs iptables

Wenn du mit iptables nicht näher vertraut bist, kannst du den Abschnitt auch einfach überspringen.

Werfen wir kurz einen Blick in unsere aktuelle Netzwerkkonfiguration:

# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether c4:37:72:a3:fe:69 brd ff:ff:ff:ff:ff:ff
    altname enp0s3
    inet 178.254.24.175/23 brd 178.254.25.255 scope global noprefixroute ens3
       valid_lft forever preferred_lft forever
    inet6 2a00:6800:3:19f:5a1c:bb40:5b4c:228a/64 scope global temporary deprecated dynamic
       valid_lft 68509sec preferred_lft 0sec
    inet6 2a00:6800:3:19f:c637:72ff:fea3:fe69/64 scope global deprecated dynamic mngtmpaddr noprefixroute
       valid_lft 68509sec preferred_lft 0sec
    inet6 fe80::c637:72ff:fea3:fe69/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:94:ec:f1:41 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:94ff:feec:f141/64 scope link
       valid_lft forever preferred_lft forever
122: veth99a2051@if121: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether be:13:c1:67:69:a0 brd ff:ff:ff:ff:ff:ff link-netnsid 3
    inet6 fe80::bc13:c1ff:fe67:69a0/64 scope link
       valid_lft forever preferred_lft forever
129: br-17ea2b02688b: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:fc:e9:c6:78 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-17ea2b02688b
       valid_lft forever preferred_lft forever
    inet6 fe80::42:fcff:fee9:c678/64 scope link
       valid_lft forever preferred_lft forever
131: veth79e1c9d@if130: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-17ea2b02688b state UP group default
    link/ether 36:2a:2a:ae:9e:56 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::342a:2aff:feae:9e56/64 scope link
       valid_lft forever preferred_lft forever
135: veth39e301f@if134: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-17ea2b02688b state UP group default
    link/ether da:23:62:d9:c6:d5 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::d823:62ff:fed9:c6d5/64 scope link
       valid_lft forever preferred_lft forever
146: br-bb8d8116bb85: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:e8:55:c8:b6 brd ff:ff:ff:ff:ff:ff
    inet 172.21.0.1/16 brd 172.21.255.255 scope global br-bb8d8116bb85
       valid_lft forever preferred_lft forever
    inet6 fe80::42:e8ff:fe55:c8b6/64 scope link
       valid_lft forever preferred_lft forever
148: veth80fdc64@if147: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-bb8d8116bb85 state UP group default
    link/ether 3e:49:60:88:e4:56 brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet6 fe80::3c49:60ff:fe88:e456/64 scope link
       valid_lft forever preferred_lft forever
152: veth553daf6@if151: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-bb8d8116bb85 state UP group default
    link/ether 5e:88:d1:cd:b0:22 brd ff:ff:ff:ff:ff:ff link-netnsid 4
    inet6 fe80::5c88:d1ff:fecd:b022/64 scope link
       valid_lft forever preferred_lft forever

# ip r
default via 178.254.24.1 dev ens3 proto static metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
172.18.0.0/16 dev br-17ea2b02688b proto kernel scope link src 172.18.0.1
172.21.0.0/16 dev br-bb8d8116bb85 proto kernel scope link src 172.21.0.1
178.254.24.0/23 dev ens3 proto kernel scope link src 178.254.24.175 metric 100

Wie du erkennen kannst, existiert neben dem Loopback eine physikalische Netzwerkschnittstelle (die mit der öffentlichen IP) und daneben 3 Bridges und damit verbunden virtuellen Ethernet-Schnittstellen. Spezielle Routen existieren nicht, neben der default route nur die 3 directly connected zu den Bridges.

Werfen wir jetzt noch ein Blick in die Filtertabellen:

# iptables -L -t filter -v
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain FORWARD (policy DROP 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
68520   30M DOCKER-USER  all  --  any    any     anywhere             anywhere
68520   30M DOCKER-ISOLATION-STAGE-1  all  --  any    any     anywhere             anywhere
  915  108K ACCEPT     all  --  any    br-bb8d8116bb85  anywhere             anywhere             ctstate RELATED,ESTABLISHED
    9   492 DOCKER     all  --  any    br-bb8d8116bb85  anywhere             anywhere
  106  110K ACCEPT     all  --  br-bb8d8116bb85 !br-bb8d8116bb85  anywhere             anywhere
    3   180 ACCEPT     all  --  br-bb8d8116bb85 br-bb8d8116bb85  anywhere             anywhere
46616   26M ACCEPT     all  --  any    br-17ea2b02688b  anywhere             anywhere             ctstate RELATED,ESTABLISHED
 2683  133K DOCKER     all  --  any    br-17ea2b02688b  anywhere             anywhere
22400 7337K ACCEPT     all  --  br-17ea2b02688b !br-17ea2b02688b  anywhere             anywhere
   12   720 ACCEPT     all  --  br-17ea2b02688b br-17ea2b02688b  anywhere             anywhere
  19M 2753M ACCEPT     all  --  any    docker0  anywhere             anywhere             ctstate RELATED,ESTABLISHED
 122K 7556K DOCKER     all  --  any    docker0  anywhere             anywhere
  23M 4641M ACCEPT     all  --  docker0 !docker0  anywhere             anywhere
    0     0 ACCEPT     all  --  docker0 docker0  anywhere             anywhere

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain DOCKER (3 references)
 pkts bytes target     prot opt in     out     source               destination
 9814  505K ACCEPT     tcp  --  !docker0 docker0  anywhere             172.17.0.4           tcp dpt:9000
 6768  360K ACCEPT     tcp  --  !docker0 docker0  anywhere             172.17.0.4           tcp dpt:8000
  735 37381 ACCEPT     tcp  --  !br-17ea2b02688b br-17ea2b02688b  anywhere             172.18.0.3           tcp dpt:https
  155  7938 ACCEPT     tcp  --  !br-17ea2b02688b br-17ea2b02688b  anywhere             172.18.0.3           tcp dpt:81
 1777 86804 ACCEPT     tcp  --  !br-17ea2b02688b br-17ea2b02688b  anywhere             172.18.0.3           tcp dpt:http

Chain DOCKER-ISOLATION-STAGE-1 (1 references)
 pkts bytes target     prot opt in     out     source               destination
  106  110K DOCKER-ISOLATION-STAGE-2  all  --  br-bb8d8116bb85 !br-bb8d8116bb85  anywhere             anywhere
22400 7337K DOCKER-ISOLATION-STAGE-2  all  --  br-17ea2b02688b !br-17ea2b02688b  anywhere             anywhere
  23M 4641M DOCKER-ISOLATION-STAGE-2  all  --  docker0 !docker0  anywhere             anywhere
  50M   14G RETURN     all  --  any    any     anywhere             anywhere

Chain DOCKER-ISOLATION-STAGE-2 (3 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DROP       all  --  any    br-bb8d8116bb85  anywhere             anywhere
    0     0 DROP       all  --  any    br-17ea2b02688b  anywhere             anywhere
    0     0 DROP       all  --  any    docker0  anywhere             anywhere
  25M 5246M RETURN     all  --  any    any     anywhere             anywhere

Chain DOCKER-USER (1 references)
 pkts bytes target     prot opt in     out     source               destination
  83M   30G RETURN     all  --  any    any     anywhere             anywhere

Innerhalb der FORWARD-Chain siehst du alle auf dem Host verfügbaren Bridges und auch, wie die der Zugriff von irgendwo (any to br…) zu den jeweiligen Bridges erstens nur bei bereits bestehenden Verbindungen (ctstate RELATED,ESTABLISHED) erlaubt ist und ansonsten auf die DOCKER-Chain verwiesen wird. Und dort wird dann später ein Subnetz gegen das andere abgeschottet.

Alles in allem ein gut verschachtelter Paketfilter, der Verbindungen der Bridges untereinander verbietet.

Bevor du jetzt aber manuell neue Filtereinträge schreibst, nutzen wir lieber die Möglichkeiten von Docker/Portainer und geben dem WordPress-Container einfach ein „Füßchen“ im Netzwerk der Bridge npm_default. Ich habe mal versucht, das grafisch darzustellen. Der Container kwellkorn_blog_wordpress_1 bekommt also eine zweite (virtuelle) Netzwerkkarte, die allerdings ihre Verbindung zur Bridge hat, welche auch mit npm_app_1 verbunden ist. Dadurch befinden sich beide in der selben Broadcast-Domäne (Layer 2) und können an jedem IP-Filter (Layer 3) vorbei Daten austauschen.

Dazu klickst du auf den WordPress-Container, scrollst ganz nach unten, wählst bei Join a network „npm_default“ aus und klickst Join Network.

Dann sollte das so aussehen:

Dann versuchst du erneut, im Browser die Domain deines Servers aufzurufen und tada!

Der Rest ist dann die übliche WordPress-Installation:

IT-Sicherheit

Jetzt aufgepasst! Dein Blog ist jetzt über den Reverse Proxy Nginx (Port 443/80) erreichbar. Aber erinnere dich bitte daran, dass per default der interne Port 80 des WordPress-Container auch extern über (in meinem Fall) 49155 erreichbar ist – sprich das geht am Proxy vorbei und stellt ein Sicherheitsproblem dar. Während dein Proxy den internen Container sauber nach außen abschottet, besteht derzeit noch ein Bypass über den Port 49155 (wie gesagt, das kann bei dir ein anderer sein). Dieser Port (oder besser gesagt dieser Bypass) sollte zwingend entfernt werden. Dazu klickst du in Portainer deinen WordPress-Container an klickst oben auf Duplicate/Edit. Dann und scrollt herunter bis zu dem Abschnitt Network ports configuration.

Dort klickst du in der Zeile mit dem Port 80 auf das rote Kästchen mit dem weißen Mülleimer, dann auf Deploy und im folgenden Dialog auf Replace.

Anschließend sollte deine Containeransicht wie folgt aussehen:

Wie du erkennen kannst, sind jetzt von außen nur noch Port 443, 80 und 81 über Nginx erreichbar, sowie Portainer selbst (8000/9000).

Beherzige meinen Rat und öffne nur wirklich notwendige Ports nach außen, du machst dich nur angreifbar. Für noch mehr Sicherheit empfehle ich dir meinen Blogbeitrag zu fail2ban.

Troubeshooting – Teil 2

Sollte die deine WordPress-Instanz einen HTTP-Statuscode 500 hinknallen – Error establishing a database connection – kein Panik. Das kann zum Beispiel passieren, wenn du dem WordPress-Container oder der zugehörigen WordPress-Datenbank die falsche Netzwerkkarte zuweist oder ggf. gelöscht hast. Im Grunde sagt der Fehlercode nichts anderes aus, das der eine Container (WordPress) seine Datenbank, die ja auf der selben Bridge erreichbar sein sollte, nicht finden kann.

Bleib ruhig und mach dir bewusst, das es in den meisten Fällen „lediglich“ ein Netzwerkproblem ist (das wird umso deutlicher, wenn du versuchst, den „defekten“ Blog unter der URL /wp-admin zu administrieren (can’t contact the database at db:3306). Du kannst jetzt lange und gezielt auf Fehlersuche gehen. Was mit Sicherheit nicht der schlechteste Weg ist.

Der einfachere Weg (wie ich eher zufällig herausgefunden habe) ist jedoch, die beiden WordPress-Container einfach zu löschen, den Stack zu stoppen und anschließend erneut zu starten. Dann fügst du den WordPress-Container (NICHT die WP-Datenbank) einfach wieder dem Netzwerk des Nginx (npm_default) zu und tada, dein Blog ist wieder da. Das liegt daran, das erstens deine Daten lokal auf dem Host liegen und zweitens die Datenbank ja nicht wirklich weg ist, nur die Netzwerkverbindung dahin.

So, das war es auch schon. Im letzten Schritt erstellen wir uns einen zweiten Blog, allerdings im Schnelldurchlauf 🙂

Der zweite Blog

Das Problem an mehreren Webseiten hinter einer IP-Adresse ist ja der Umstand, das der Port 80 (http) oder 443 (https) nur einmal an eine IP/Domain gebunden werden kann. Wie also jetzt eine zweite Webseite ebenfalls unter Port 80/443 erreichbar machen?

Und genau da hilft dir der bereits installierte Reverse Proxy. Denn dieser nimmt stellvertretend Anfragen auf Port 80/443 entgegen und leitet diese an dahinter liegende lokale Server weiter. Woher weiß der Reverse Proxy jetzt aber, welche Website der Client abrufen möchte? Na ganz einfach – anhand des Domainnamens! Während unser erster Blog bei mir unter lowcarbo.de erreichbar war, soll der 2. Blog unter lifehacks.lowcarbo.de erreichbar sein. Es handelt sich also um eine Subdomain, die du natürlich bei deinem Provider einrichten musst, aber das sollte kein Problem sein. Der Reverse Proxy nimmt dann die Anfrage entgegen und unterscheidet an Hand des Domainnamens, an welchen Server (Container) weitergeleitet werden soll. Schauen wir uns das in der Praxis an:

1. Unter Portainer auf App Templates klicken, dann Ghost anklicken (ja wir probieren mal etwas anderes)

2. Name und….ja was ist das denn?….hier kann man sogar das Netzwerk auswählen. Du ahnst es sicher schon, wenn das WordPress-Template das auch gehabt hätte, wären uns ein paar Probleme erspart geblieben. Wir wählen das Netzwerk npm_default aus, damit Nginx dann auch direkt auf den neu erstellten Container zugreifen kann.

3. Wie du erkennst, ist der Container (in meinem Fall) direkt über Port 49173 von außen erreichbar.

4. Im Browser schaut das dann unter der IP des Servers, gefolgt von der Portnummer so aus (bei mir also http://178.254.24.175:49173). Wie du auch erkennst, ist die Verbindung als „nicht sicher“ eingestuft, da läuft also noch kein HTTPS.

5. Dann kümmern wir uns als nächstes um SSL und konfigurieren Nginx (Add Proxy Host), beachte hier bitte die interne Portnummer 2368 (die kannst du leicht der Containerübersicht aus Schritt 3 entnehmen).

Wie du in folgendem Screenshot sehen kannst, existieren jetzt 2 Weiterleitungen (Proxy Hosts) für die jeweilige Domain. Genau das ist die Kernaufgabe eines Reverse Proxys.

6. Im Browser ist jetzt der Block unter https://lifehacks.lowcarbo.de erreichbar.

7. Wir entfernen den Port 49173 unter Containers, lifehacks, Duplicate/Edit, Port 2368 entfernen, Deploy und Replace

So einfach kann Containerverwaltung hinter einem Reverse Proxy Manager sein:

Und jetzt viel Erfolg bei der Umsetzung. 🙂

3 thoughts on “Nginx Reverse Proxy Manager – mehrere Web-Server unter einer IP- bzw. Domain-Adresse

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert