Webpage im Homelab

23. Nov. 2024

Webseite im Homelab

Zwar gibt es eine Vielzahl von Anbietern, mit denen man seine eigene Webseite ins Netz stellen kann. Die dafür benötigten Bestandteile (Domäne, Webserver und die Verknüpfung der beiden) werden dann vom Anbieter verwaltet und konfiguriert. Das erspart Zeit, bedeutet aber auch, dass alle Daten auf fremder Hardware liegen.

In diesem Beitrag erkläre ich, wie man mit Docker Containern seinen eigenen Webserver betreibt und über Cloudflare online stellt. Um sicherzustellen, dass die Webseite dauerhaft online ist, empfiehlt es sich, die nachfolgenden Schritte auf einem Heim-Server (also einen dauerhaft laufenden PC) auszuführen. Das kann ein so sparsamer Computer wie der Raspberry Pi oder ein vollwertiger Rack-Server mit Intel- oder AMD-CPU sein.

Als Testzweck kann der Webserver auch auf einem normalen Computer eingerichtet werden. Doch steht die Webseite dann nicht mehr zur Verfügung, sobald der Computer vom Internet getrennt wird.

Docker

Die Einrichtung von Docker ist recht selbsterklärend. Wie man Docker Desktop oder (nur) Docker Engine installiert wird unter diesen zwei Verweisen beschrieben.

Unter macOS kann man zudem auch Homebrew verwenden, um Docker Desktop zu installieren:

brew install --cask docker

Sobald Docker gestartet ist, steht alles bereit, um einen Webserver einzurichten.

Webserver Container

Es gibt verschiedene Server die eine Webseite bereitstellen können. Server wie Ngnix oder Apache sind hierbei bekannt und weit verbreitet. Aber um lediglich eine statische Webseite bereitzustellen, sind diese Server - wie man im Fachjargon sagen würde - overkill. Daher verwende ich hier einen Server namens httpd im Container busybox - der wohlmöglich kleinste Server für statische Webseiten.

Für Testzwecke sollte man einen leeren Ordner erstellen, und darin eine Datei names index.html speichern, die folgenden Inhalt hat:

<!DOCTYPE html>
<html lang="de">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Test</title>
</head>

<body>
    <h1>Hallo, Welt!</h1>
</body>

</html>

Nun kann der Container mit dem Befehl docker wie folgt ausgeführt werden:

docker run \
    -d \
    --rm \
    -p 8080:80 \
    -v /path/to/folder:/www \
    busybox \
    /bin/sh -c 'cd /www; busybox httpd -f -v -p 80'

Der Befehl ist in verschiedene Zeilen aufgeteilt, sodass er einfacher erklärt werden kann:

Unter der Adresse http://127.0.0.1:8080/ sollte nun das bekannte Hallo, Welt! im Browser angezeigt werden.

Soweit so gut. Nur ist dieser Webserver noch nicht mit dem Internet verbunden. Hierfür muss eine Domäne und Cloudflare eingerichtet werden.

Cloudflare Container

Um auf den Webserver über das Internet zugreifen zu können braucht man zuerst ein Konto bei Cloudflare. Das anzulegen ist kostenlos. Nun muss man seine Domäne bei Coudflare registrieren. Hierfür gibt es auch eine Kostenlose Option:

cloudflare-free-domain

Nun müssen die Nameserver beim Registrar (wie Ionos, Godaddy etc.) aktualisiert werden, um die Dienste von Cloudflare für die Domain zu aktivieren. Diese Nameserver haben in der Regel die folgende Namensstruktur: *.ns.cloudflare.com. Sobald die Änderung beim Registrar angewendet wurden, kann es losgehen.

Im Zero Trust Menü von Cloudflare, kann man im Reiter Networks einen Cloudflared Tunnel einrichten. Klickt man sich durch das Setup durch, erhält man am Ende eine Auswahl, wie man den Tunnel für verschiedene Umgebungen einrichten kann. Für Docker wird dann ein Befehl wie folgt angezeigt:

docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token xxxxxx....

Der Token wird automatisch generiert und ist für jeden Tunnel einzigartig.

Damit der Container losgelöst ausgeführt wird und nach seiner Beendigung gelöscht wird, empfiehlt es sich, die Zusätze -d und --rm hinzuzufügen. Der Befehl ist nachfolgend auf mehrere Zeilen aufgeteilt, um ihn besser lesbar zu machen, und der Token wird nicht als Teil des Startbefehls übergeben, sondern als Umgebungsvariable (was angeblich sicherer ist):

docker run \
    -d \
    --rm \
    cloudflare/cloudflared:latest \
    -e TUNNEL_TOKEN=xxxxxx.... \
    tunnel --no-autoupdate run

Führt man den Befehl so aus, wird ein Container gestartet, der sich mit Cloudflare verbindet und einen verschlüsselten Tunnel erstellt. Der Tunnel ermöglicht es, dass Cloudflare Webanfragen an die registrierte Domäne über den Tunnel an einen bestimmten Endpunkt im Heimnetz weiterleitet. Somit kann über Cloudflare gezielt mit Servern im Heimnetz kommuniziert werden. Dafür kann man nun öffentliche Hostnamen (Domänen und Sub-Domänen) im Menü des Tunnels registrieren und sie dort mit der Adresse des Servers verbinden.

Hat der Computer, auf dem der zuvor eingerichtete Webserver im Container läuft z.B. die IP Adresse 192.168.1.2, so kann man bei Cloudflare den Dienst über http://192.168.1.2:8080 mit einem Hostnamen verbinden.

Das hilf aber nur begrenzt, wenn sich die IP Adresse des Computers (z.B. aufgrund einer neuen Zuweisung über DHCL) ändert. Da der Cloudflare Container und der Webserver Container nicht unmittelbar miteinander in Verbindung stehen, müsste man dann die IP Adresse im Tunnel aktualisieren. Um dies zu vermeiden, können die zwei Container im selben Netzwerk gestartet werden.

Docker Netzwerk

Um Container in einem bestimmten Netzwerk zu starten, muss man zuerst ein Netzwerk definieren. Ich nenne es hier der Einfachheit halber cloudflare:

docker network create cloudflare

Man kann nun prüfen, dass das Netzwerk erstellt wurde:

docker network inspect cloudflare

Und sollte dann ein Ergebnis wie folgt erhalten:

[
    {
        "Name": "cloudflare",
        "Id": "xxxxx....",
        "Created": "2024-11-23T18:53:29.439745922Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

Nun muss man beide Container anhalten und mit dem Zusatz --network=cloudflare neu starten. Um die Container zu beenden braucht man ihre IDs, die man wie folgt erhalten kann:

docker container ls

Wodurch die folgende Ausgebe zu sehen sein sollte:

CONTAINER ID   IMAGE                           COMMAND                  CREATED         STATUS         PORTS                  NAMES
aaa...         cloudflare/cloudflared:latest   "cloudflared --no-au…"   5 minutes ago   Up 5 minutes                          brave_greider
bbb...         busybox                         "/bin/sh -c 'cd /www…"   5 minutes ago   Up 5 minutes   0.0.0.0:8080->80/tcp   wizardly_dhawan

Mit dem folgenden Befehl kann dann jeder Container angehalten werden (die ID muss entsprechend geändert werden):

docker container stopp <ID>

Beide Container können nun mit den folgenden Befehlen neugestartet werden, allerdings so, dass sie in dem Netzwerk cloudflare gestartet werden:

docker run \
    --network=cloudflare \
    -d \
    --rm \
    -e TUNNEL_TOKEN=xxxxxx.... \
    cloudflare/cloudflared:latest \
    tunnel --no-autoupdate run

Wenn nun der Webserver gestartet wird, sollte ihm auch ein eindeutiger Name zugewiesen werden, sodass er im Netzwerk cloudflare einfacher zu identifizieren ist:

docker run \
    --name=busybox-httpd \
    --network=cloudflare \
    -d \
    --rm \
    -p 8080:80 \
    -v /path/to/folder:/www \
    busybox \
    /bin/sh -c 'cd /www; busybox httpd -f -v -p 80'

Nun kann man anstelle der IP Adresse im Menü des Tunnels bei Cloudflare einfach den Dienst wie folgt angeben:

http://busybox-httpd:80

Denn der da sowohl der Cloudflare Container, als auch der Webserver Container im selben Netzwerk laufen, kann der eine Container den anderen sehen und über dessen Namen mit ihm kommunizieren. Das Bedeutet, dass der Hostname http://busybox-httpd:80 nie das Netzwerk der zwei Container verlässt, während vorher die Anfrage einen kleinen Umweg über den Host machen musste. Demnach kann man auch den Webserver Container starten, ohne den Port mit -p 8080:80 im Heimnetz zu veröffentlichen. Lässt man diesen Zusatz vom Startbefehl aus, so ist der Webserver nur noch über den Cloudflare Container, und somit nur noch über den damit verbundenen öffentlichen Hostnamen erreichbar. Doch wird dadurch die Webentwicklung etwas umständlich, weshalb ich oft meine Container über einen lokalen Port erreichbar mache.

Und somit kann man einen Webserver lokal betreiben, der über das Internet erreichbar ist, womit der Beitrag eigentlich abgeschlossen ist.

Allerdings wirkt das Erstellen eines Netzwerks und der koordinierte Start von zwei Containern etwas umständlich. Um dies besser zu Koordinieren gibt es ein Tool namens Docker Compose

Docker Compose

Mit dem Befehl docker compose werden Informationen aus einer YAML Datei namens compose.yaml ausgelesen und dementsprechend mehrere Container mit bestimmten Konfigurationen gestartet. Da die Datei Angaben zu beiden Containern und dem Netzwerk enthält, ist es einfacher, die Container aufeinander abzustimmen. Der Inhalt der Datei sieht dann wie folgt aus:

services:
  webserver:
    container_name: busybox-httpd
    image: busybox
    restart: unless-stopped
    working_dir: /www
    command: httpd -f -v -p 80
    volumes:
      - ./:/www

  tunnel:
    container_name: cloudflared-tunnel
    image: cloudflare/cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=xxxxxx....

networks:
  default:
    name: cloudflare

Hierbei werden zwei Dienste (webserver und tunnel) definiert und ein Netzwerk (cloudflare), das beide miteinander verbindet. Die Angaben zu den Diensten sind entsprechend der Container von vorhin, nur anstelle von Flags werden die Container mit Werten im YAML Format konfiguriert.

Die Container werden dann wie folgt gestartet:

docker compose up

Man sieht nun eine Ausgabe die folgendes enthalten sollte:

    ...
[+] Running 2/2
 ✔ Container cloudflared-tunnel  Created          0.2s 
 ✔ Container busybox-httpd       Created          0.2s 
    ...

Das bedeutet, dass beide Container gestartet wurden. Mit strg + c werden die Container gestoppt:

    ....
[+] Stopping 2/2
 ✔ Container cloudflared-tunnel  Stopped           0.7s 
 ✔ Container busybox-httpd       Stopped           10.1s 
    ...

Möchte man die Ausgabe nicht sehen, können die Container mit dem Zusatz -d losgelöst gestartet werden:

docker compose up -d

Da die Tastenkombination nun nicht mehr funktioniert, können sie mit dem folgendem Befehl gestoppt werden:

docker compose stop

Und somit sind wir wirklich beim Ende des Beitrags angekommen.

Mögliche Fehlerbehebung

Es kann vorkommen, dass bei einem Neustart der Container mittels Docker Compose einer der Container sich nicht mit dem Netzwerk verbindet. Hierfür kann man versuchen, die gestoppten Container zu löschen bevor man sie wieder startet:

docker compose rm

Alternativ kann man das Netzwerk wie gehabt erstellen und dann in der YAML Datei angeben, dass ein externes Netzwerk benutzt werden soll:

    ...

networks:
  default:
    name: cloudflare
    external: true