Traefik auf Server/Raspberry Pi installieren und einrichten: Reverse Proxy für Docker mit Let’s Encrypt HTTPS-Zertifikaten

Als Video ansehen
Bereitgestellt über YouTube

Traefik auf Server/Raspberry Pi installieren und einrichten: Reverse Proxy für Docker mit Let’s Encrypt HTTPS-Zertifikaten

Wer mehrere Webanwendungen auf einem Server betreiben möchte, benötigt einen Reverse Proxy. Wie das Konzept in der Theorie funktioniert, habe ich in einem eigenen Beitrag ausführlicher erklärt. Hier schauen wir uns die praktische Einrichtung von Traefik an. Ob ihr dabei einen Raspberry Pi mit Raspberry Pi OS verwendet, eine lokale VM oder einen Cloudserver, spielt dabei keine Rolle. Wichtig ist, dass ihr Debian oder ein Debian-Derivat wie Ubuntu einsetzt.

Hinweis: Bei jedem Heimserver (egal ob Raspberry Pi oder X86) benötigt ihr zusätzlich eine öffentliche IPv4-Adresse (sofern ihr IPv4 nutzen möchtet, was i.d.R. der Fall ist), NAT/Firewall-Regeln in eurem Router sowie eine dynamische DNS-Adresse. Ansonsten sind eure HTTP-Dienste nicht im Internet erreichbar, was wiederum eine Voraussetzung für Let’s Encrypt ist.

Warum Traefik und nicht Nginx, Apache & co?

Traefik bietet verschiedene Integrationen (Provider genannt) für unter anderem Docker, Kubernetes und andere. Das vereinfacht die Konfiguration in diesen Umgebungen enorm: Man kann mit Labels festlegen, wie ein Container erreichbar sein soll. Das händische Anpassen der Konfiguration in Nginx oder einem anderen Reverse Proxy entfällt eben so wie alles was damit zusammen hängt, etwa der Vergabe von statischen IP-Adressen für die Container. Dennoch ist Traefik effizient, bereits in Version 1.4 wurde 85% des Durchsatzes von Nginx erreicht.

Zwar kann man Traefik auch alleinstehend nutzen, also völlig ohne Docker & co. Allerdings fehlen dann einige der Vorteile. Hier sollte man zudem bedenken, dass Traefik ein reiner Reverse Proxy ist. Man kann also nicht – im Gegensatz zu Apache oder Nginx – etwa statische Dateien ausliefern oder per angebundenem PHP/Python dynamische Inhalte generieren. Dafür ist ein eigener Container notwendig. Wer etwa statische Dateien ausliefern möchte, benötigt also einen Nginx/Apache2 Container hinter Traefik dafür.

Installation von Traefik unter Debian/Ubuntu/Raspberry Pi OS

Voraussetzung ist, dass ihr bereits Docker und Docker-Compose installiert habt. Wir nutzen Version 2. Beides ist auf Debian unter x86 Servern sehr ähnlich, da das Raspberry Pi OS ebenfalls darauf basiert. Ob Docker korrekt eingerichtet wurde, könnt ihr mit folgendem Befehl prüfen:

$ docker compose version
Docker version 20.10.14, build a224086

Für Traefik würde ich einen eigenen Ordner anlegen und in der Yaml-Datei nur Traefik bereitstellen. In diesem Beispiel legen wir die Konfigurationsdateien für die Container in /etc/docker/container, dies ist aber frei wählbar.

mkdir -p /etc/docker/container/traefik
vim docker-compose.yml

Für Traefik würde ich einen eigenen Ordner anlegen und in der Yaml-Datei nur Traefik bereitstellen. Beim Image ist zu empfehlen, zumindest eine Hauptversion (oder wie hier mit Nebenversion) zu wählen. Eine Liste der unterstützten Versionen ist hier zu finden.

services:
  traefik:
    image: traefik:v2.6
    restart: always
    command:
      # Experimentell, um das Dashboard ohne Zugriffsschutz aufzurufen
      #- "--api.insecure=true"
      - "--providers.docker"
      - "--providers.docker.exposedByDefault=false"
      - "--providers.docker.network=traefik_web"
      - "--entrypoints.http.address=:80"
      - "--entrypoints.http.http.redirections.entrypoint.to=https"
      - "--entrypoints.http.http.redirections.entrypoint.scheme=https"
      - "--entrypoints.https.address=:443"
      # Vermeidet, dass wir den resolver in jedem container mit "traefik.http.routers.https.tls.certresolver=le" angeben muessen
      - "--entrypoints.https.http.tls.certResolver=le"
      - "--certificatesresolvers.le.acme.tlschallenge=true"
      - "--certificatesresolvers.le.acme.email=deine@mail.de"
      - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - web

networks:
  web:
    name: traefik_web

Schauen wir uns die Konfigurationsdirektiven etwas näher an:

  • providers.docker.exposedbydefault=false legt fest, dass neue Container nicht automatisch von Traefik erreichbar gemacht werden. Sicherheitstechnisch halte ich es für sinnvoll, nur die gewünschten Container freizugeben, dazu später im Beispiel mehr.
  • providers.docker.network=traefik_web ist das Standardnetzwerk, welches für die Kommunikation von Traefik mit dem Container des Dienstes verwendet werden. Dies vermeidet Probleme, falls der Container über mehrere Netzwerke verfügt – in diesem Falle wird möglicherweise das falsche automatisiert ausgewählt.
  • entrypoints.http.address=:80 definiert einen Einstiegspunkt namens http auf Port 80. In den nachfolgenden Zeilen leiten wir diesen auf einen zweiten Einstiegspunkt namens https um mit entsprechendem Protokoll. Für viele Anwendungsfälle ist das sinnvoll, wer dies nicht möchte kann die Weiterleitung mit diesen zwei Zeilen deaktivieren.
  • entrypoints.https.address=:443 wie der vorherige, nur HTTPS auf Port 443.
  • certificatesresolvers.le.acme.tlschallenge=true ermöglicht die automatische Ausstellung und Erneuerung von TLS-Zertifikaten über den kostenfreien Dienst Let’s Encrypt. Hierzu benötigt der ACME-Standard eine E-Mail Adresse. In der zuletzt angegebenen Json-Datei werden alle dazu nötigen Daten gespeichert, wie etwa das Zertifikat oder der private Schlüssel.
  • Als Reverse-Proxy geben wir Port 80/443 für HTTP(S) nach außen hin frei. Port 8080 ist für das Dashboard, falls gewünscht.
  • In den Volumes hängen wir zum einen den Docker-Socket ein. Dies ist notwendig, damit Traefik die laufenden Container einsehen und entsprechende Routen anlegen kann. Zusätzlich wird ein Unterordner letsencrypt erstellt und gemountet, worin die Zertifikate und Schlüssel gespeichert werden. Dementsprechend sollte dieser Ordner gut geschützt werden.
  • Das Netzwerk web (erhält systemweit den Ordnername als Präfix und heißt daher traefik_web) soll für alle Container genutzt werden, die wir hinter Traefik als Reverse Proxy schalten. So können wir jene Container die mit Traefik kommunizieren müssen von anderen trennen, die dies nicht sollen. Beispielsweise für eine Datenbank.

Um das Image herunterzulagen und den Container im Hintergrund zu starten:

docker compose up -d

Man kann nun den Hostname oder die IP-Adresse des Systemes im Browser aufrufen und sollte eine 404 page not found Fehlerseite sehen. Dies ist die Standardseite von Traefik, wenn keine passende Route gefunden wurde. Durch unsere Weiterleitung ist auch diese Fehlerseite bereits mit einem automatisch erstellten HTTPS-Zertifikat versehen, ggf. erhaltet ihr noch eine Zertifikatswarnung.

Bereitstellen eines einfachen Webserver-Containers über Traefik

Damit ist Traefik bereit, um einen Container bereitzustellen. Als einfaches Beispiel-Szenario nehmen wir hier einen Nginx Webserver. Auch hier würde ich einen Ordner erstellen, in dem wir alle Konfigurationsdateien platzieren, etwa:

$ mkdir /etc/docker/container/nginx-demo
$ cd /etc/docker/container/nginx-demo

Darin wird eine docker-compose.yml erstellt, mit ein paar Besonderheiten:

services:
  nginx:
    image: nginx:1.20
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nginx.rule=Host(`demo.u-labs.de`)"
    networks:
      - traefik_web

networks:
  traefik_web:
    external: true
  • In den Labels muss Traefik für diesen Container aktiviert werden. Das ist durch providers.docker.exposedbydefault=false in dessen Konfiguration notwendig.
  • Das zweite Label definiert einen Router namens nginx. Diese Bezeichnung ist frei wählbar, muss aber eindeutig sein. Router leiten Anfragen auf einen bestimmten Dienst und verwenden dafür Regeln. In diesem Beispiel gibt es nur eine Regel: Anfragen mit dem Hostname demo.u-labs.de (d.H. man ruft diese Domain im Browser auf), landen bei unserem Nginx-Container.
  • Der Container befindet sich im Netzwerk traefik_web. Dieses Netzwerk haben wir zuvor in der docker-compose.yml von Traefik erstellt. Durch den expliziten Name wird verhindert, dass es aus dem Name des Ordners (traefik) und dem Netzwerk (web) zusammensetzt – dies wäre der Standard. So kann Traefik mit unserem Nginx-Container kommunizieren. Es ist wichtig, dieses Netzwerk als Extern zu deklarieren. Ansonsten fühlt sich Docker Compose dafür verantwortlich und legt ein neues, lokales Netzwerk an.

Nachdem der Container mit docker compose up -d gestartet wurde, kann man die konfigurierte Domain (hier demo.u-labs.de) im Browser aufrufen. Statt der Standard 404 page not found Seite sollte Welcome to nginx erscheinen – die Standard index.html Seite von Nginx:

Wie am Schloss oben zu erkennen, wird HTTPS mit einem gültigen Zertifikat eingesetzt.

Mit einem einzelnen Container bietet profitieren wir noch nicht von den meisten Vorteilen, die ein Reverse Proxy bietet. Denn im vorherigen Beispiel könnten wir schlichtweg Nginx direkt auf Port 80/443 freigeben, mit dem Certbot von Let’s Encrypt automatisch Zertifikate bereitstellen und könnten das gleiche Ergebnis ohne vorgeschalteten Traefik erreichen. Wirklich Sinn macht das erst bei mehreren Diensten. Daher wird die Installation um einen zweiten Container ergänzt, diesmal ein Apache mit PHP.

Im Grunde kann man für weitere Dienste die gleiche Vorlage benutzen und passt nur entsprechend Name des Dienstes, Image, die Regel für Traefik an sowie ggf. für den Dienst spezifische Konfigurationseinstellungen. Letzteres ist in diesem Beispiel ein Volume, in dem ich einen lokalen Ordner einhänge. Der wird von Apache ausgeliefert.

services:
  apache:
    image: php:8.1-apache
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.apache.rule=Host(`php.demo.u-labs.de`)"
    networks:
      - traefik_web
    volumes:
      - ./html:/var/www/html

networks:
  traefik_web:
    external: true

Auch das Starten ist identisch wie beim Nginx-Container. Unter der eingerichteten Domain (hier php.demo.u-labs.de) bekommen wir eine Forbidden Seite zu sehen:

Unten wird aber klar, dass dies von Apache ausgeliefert wird – somit sind wir nicht beim zuvor eingerichteten Nginx-Container gelandet. Der Fehler erscheint, weil keine Indexseite eingerichtet ist und der Apache standardmäßig so eingerichtet ist, in diesem Falle nicht den Inhalt des Ordners aufzulisten. Da der Unterordner html in das Wurzelverzeichnis, welches der Webserver ausliefert, gemountet wird, kann man hier eine Index-Seite anlegen und etwa die PHP-Info ausgeben:

echo '<?php phpinfo();' > html/index.php

Statt des Fehlers führt Apache das Skript mit dem PHP-Modul aus, sodass php.demo.u-labs.de nun Informationen über die eingesetzte PHP-Umgebung anzeigt:

Erweiterte Regeln für den Router und Middlewares

Beide Containern haben nur eine einzige, recht simple Regel auf den Domainname (Hostname). Traefik beherrscht aber auch komplexere Regeln, die aus verschiedenen Bedingungen bestehen können. Eine UND-Verknüpfung mit && legt fest, dass beide Bedingungen erfüllt sein müssen. Bei ODER (||) muss mindestens eine erfüllt sein, aber nicht zwingend eine.

Mit folgendem Label ergänzen wir die Regel etwa um ein Prefix für den Pfad. Das heißt: Der Container ist nicht mehr unter php.demo.u-labs.de erreichbar (also im Wurzelverzeichnis, Pfad = /) sondern unter php.demo.u-labs.de/test bzw. alle Pfade, die mit /test beginnen (auch etwa /test123)

traefik.http.routers.apache.rule=Host(`php.demo.u-labs.de`) && PathPrefix(`/test`)

Dadurch liefert php.demo.u-labs.de nun einen 404 Fehler von Traefik, weil die Route neben dem Host auch fordert, dass der Pfad mit /test beginnt. Im Wurzelverzeichnis lautet der Pfad / somit ist die zweite Bedingung nicht erfüllt. Unter php.demo.u-labs.de/test erscheint ein Fehlermeldung des Apache:

Unser Routing im Traefik funktioniert also grundsätzlich, da der Fehler nicht mehr von Traeffik kommt, sondern vom Apache dahinter Das zeigt die Besonderheiten von Pfaden: Der Reverse Proxy Traefik reicht diese standardmäßig durch, d.H. der Webserver dahinter sucht nun nach einem Ordner namens test. Pfade sind echten Anwendungen etwas schwierig umzusetzen, da diese den Pfad unterstützen müssen. Heißt: Die Anwendung sollte im Optimalfall konfiguriert werden können, dass ihre Basis-Adresse etwa php.demo.u-labs.de lautet.

Alternativ (falls das z.B. nicht möglich ist), kann man dafür eine Middleware einsetzen. Es gibt verschiedene Middlewares, welche die Anfrage oder auch Antwort auf bestimmte Arten verändern. Beispielsweise um die Antwort des Containers zu verändern. Hier benötigen wir die StripPrefix-Middleware: Sie manipuliert die Anfrage, bevor Traefik sie an den Container leitet. Konkret entfernt sie einen angegebenen Pfad: Bei Traefik kommt php.demo.u-labs.de/test an, Traefik entfernt /test und aus Sicht des Anwendungsservers (hier Nginx) lautet die Anfrage php.demo.u-labs.de.

traefik.http.middlewares.strip-testpath.stripprefix.prefixes=/test
traefik.http.routers.apache.middlewares=strip-testpath@docker

Diese zwei Label definieren eine Middleware, die das Prefix /test im Pfad entfernt und nennt diese strip-testpath. Auch dies ist frei wählbar, muss aber im nächsten Schritt mit dem festgelegten Name angesprochen werden: Dort aktivieren wir die definierte Middleware für den Docker-Provider. Nun wird wieder die index.php mit der PHP-Info im Wurzelverzeichnis angezeigt:

Bei echten Anwendungen muss man hier aber Bedenken, dass die Applikation hinter Traefik das in der Regel nicht berücksichtigt. Wenn diese z.B. Links generiert, lauten diese php.demo.u-labs.de und nicht php.demo.u-labs.de/test. Diese würden ins Leere laufen, da Traefik wiederum ja nicht auf php.demo.u-labs.de ohne den /test Pfad reagiert. Viele Systeme eine Einstellung für die Basis-URL, womit sich das korrigieren lässt.

Auslagern der Traefik-Konfiguration in eine eigene Datei

Bisher haben wir sämtliche Konfigurationseinstellungen über Kommandozeilenargumente im Startbefehl übergeben. Das mag anfangs bequem sein, allerdings führt dies schnell zu langen Befehlen und wird unübersichtlich.

Alternativ kann man eine eigene Yaml-Datei festlegen und die Konfiguration dort hierarchisch vornehmen:

--providers.file.filename=/traefik.yml

Die Datei muss entsprechend noch als Volume gemountet werden:

volumes:
  - ./traefik.yml:/traefik.yml

In Yaml übersetzt, sehen die Startparameter wie folgt aus:

providers:
  docker:
    exposedByDefault: false
    network: traefik_web

entryPoints:
  http:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: "https"
          scheme: "https"
  https:
    address: ":443"
    http:
      tls:
        certResolver: le

certificatesResolvers:
  le:
    acme:
      tlschallenge: true
      email: "deine@mail.de"
      storage: "/letsencrypt/acme.json"

Vor allem bei längeren Pfaden in denen mehrere Änderungen vorgenommen werden, spart man sich durch die Hierarchische Struktur das Wiederholen. Etwa bei entrypoints.http.http.redirections.entrypoint. Zudem durch das Einrücken besser ersichtlich, was wohin gehört. Daher ist das meiner Meinung nach die bessere Alternative. Schlussendlich aber Geschmackssache, beide Wege funktionieren.

Detailliertere Logs zur Fehleranalyse

Falls es zu Problemen kommt, kann es hilfreich sein, das Log-Level zu erhöhen:

--log.level=DEBUG

Über die Logs des Containers (z.B. docker logs -f traefik-traefik-1) sieht man dann detailliertere Informationen.

Fazit

Traefik ist ein Reverse Proxy, der sich tief in die Docker-Umgebung installiert. Mit wenigen Zeilen Konfigurationsaufwand kann man mehrere Container übers Web erreichbar machen. Durch die Let’s Encrypt Integration auch mit HTTPS und automatischer Verlängerung der Zertifikate.

Weiterführende Informationen

Leave a Reply