# Docker mit Traefik

Hier geht es um Experimente mit Docker Traefik. Geplant war ein kleiner Server, der einen reinen HTML-Blog (Hugo), eine Wissensdatenbank (BookStack) und einen WebDav-Server zur Verfügung stellt. Alle Services sollten als Docker Services installiert werden und nur über https und Subdomains ansprechbar sein. Die Zertifikatsverwaltung sollte automatisiert erfolgen. Also das ideale Einsatzgebiet für Traefik.

# Die Basis

#### Warum tue ich das?

Mit Traefik hatte ich zu Beginn meiner Docker-Experimente schon einmal gearbeitet. Ursache war ein Artikel in der Zeitschrift c't, die die Möglichkeiten von Traefik einmal angerissen haben.  
Auf die Idee, ein neues Traefik-Experiment anzugehen, brachte mich eine Information meines Lieblings-Cloud-Hosters Hetzner. Dort gibt es seit kurzem Server auf Basis der ARM64 Architektur zu mieten. Als Besitzer eines der MacBooks Pro mit Apple M1 Max Chip, der auf eben dieser ARM-Architektur basiert, fand ich das Thema spannend. Zumal ich gelernt habe, dass gerade im Docker Umfeld lange nicht alles als ARM-Ready zu bezeichnen ist. Und das, obwohl ARM-CPUs nicht nur im Serversektor auf enorme Zuwachsraten schauen können.

#### Welche Ziele möchte ich erreichen?

Um dem Experiment einen tieferen Sinn zu geben, habe ich mir Ziele gesteckt, die bei erfolgreicher und zufriedenstellender Umsetzung zu einer Umgestaltung meiner derzeitigen Web-Infrastuktur führen könnten. Konkret lauten diese:

- Der Server soll über die Internet-Protokolle IPv4 und IPv6 angesprochen werden können.
- Der Server wird standardmäßig abgesichert.
- Es werden ausschließlich sichere und verschlüsselte Protokolle (HTTPS, DAVS, ...) verwendet.
- Notwendige Zertifikate sollen über Let's Encrypt automatisch bezogen und verwaltet werden.
- Auf dem Server läuft neben den für das Betriebssystem erforderlichen Anwendungen ausschließlich Docker.
- Traefik übernimmt die Zertifikatsverwaltung und Funktion des Proxy.
- Das Traefik-Dashbord ist über die Subdomain traefik.domain.tld erreichbar und mit BasicAuth gesichert.
- Die offizielle Webseite, ein mit hugo erstellter reiner HTML-Blog, wird über Nginx bereitgestellt.
- Die offizielle Webseite ist über [https://domain.tld](https://meinedomain.de) oder [https://www.domain.tld](https://www.meinedomain.de) gleichermaßen erreichbar.
- Eine WebDav-Anbindung ist über die Subdomain davs.domain.tld erreichbar.
- Eine BookStack-Instanz ist über die Subdomain book.domain.tld erreichbar.
- Die für BookStack notwendige Datenbank liegt in einem eigenen Netzwerk und ist von außen nicht zu erreichen.

#### Der erste Schritt - Server anlegen

Also gespannt ans

# Traefik Reverse Proxy

#### Verzeichnisse und Dateien anlegen

```bash
mkdir -p /opt/containers/traefik/data
touch /opt/containers/traefik/data/acme.json
chmod 600 /opt/containers/traefik/data/acme.json
touch /opt/containers/traefik/data/traefik.yml
```

<p class="callout danger">Wichtig: Nicht vergessen, die Datei acme.json (oder acme\_letsencrypt.json) anzulegen</p>

#### ./docker-compose.yml erstellen  


```bash
#
# /opt/containers/trefik/docker-compose.yml
# Reverse Proxy Traefik installieren
#
version: '3.9'
services:
  traefik:
    container_name: traefik
    image: traefik:latest
    volumes:
      # Traefik mit der Zeit vom Server syncronisieren
      - /etc/localtime:/etc/localtime:ro
      # Lesezugriff auf den UNIX Docker socket
      - /var/run/docker.sock:/var/run/docker.sock:ro
      # Traefik-Konfiguration bereitstellen
      - ./data/traefik.yml:/traefik.yml:ro
      # Datei mit Zertifikaten bereitstellen
      - ./data/acme_letsencrypt.json:/acme_letsencrypt.json
      # Datei mit dynamischer Konfiguration bereitstellen
      - ./data/dynamic_conf.yml:/dynamic_conf.yml                                       
    labels:
      # Traefik durch Watchtower aktualisieren lassen
      # https://goneuland.de/docker-images-automatisiert-aktualisieren-mit-watchtower/
      - "com.centurylinklabs.watchtower.enable=true"
      # Traefik für Traefik aktivieren
      - "traefik.enable=true"
      # Als Einstiegspunkt wählen wir direkt HTTPS
      - "traefik.http.routers.traefik.entrypoints=https"
      # Domain anpassen unter der Traefik erreichbar sein soll.
      - "traefik.http.routers.traefik.rule=Host(`traefik.die-steffens.eu`)"
      # Middlewares definieren, welche verwendet werden sollen.
      # Hier die Authentifizierung via htpasswd + alle die unter default definiert sind. 
      - "traefik.http.routers.traefik.middlewares=traefikAuth@file,default@file"
      # SSL Zertifikat abrufen
      - "traefik.http.routers.traefik.tls=true"
      # Definierten Zertifikat Resolver aus traefik.yml wählen
      - "traefik.http.routers.traefik.tls.certresolver=http"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.services.traefik.loadbalancer.server.port=80"
      - "traefik.http.services.traefik.loadbalancer.sticky.cookie.httpOnly=true"
      - "traefik.http.services.traefik.loadbalancer.sticky.cookie.secure=true"
     # Traefik dem Proxy-Netzwerk hinzufügen.
      - "traefik.docker.network=proxy"
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      proxy:
    hostname: traefik
    ports:
      # Ports definieren, welche durch Traefik gemanaget werden.
      - "80:80"
      - "443:443"

networks:
  proxy:
    name: proxy
    driver: bridge
    attachable: true
```

#### ./data/dynamic\_conf.yml füllen

```bash
#
# /opt/containers/trafik/data/dynamic_conf.yml
#
# TLS
# Hier werden alle notwendigen Einstellungen für das Zertifikat getroffen.
# In Kombination mit den Einstellungen unter http.middlewares.default-security-headers bekommen  wir ein A+ Zertifikat.
tls:
  options:
    default:
      minVersion: VersionTLS12
      cipherSuites:
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
        - TLS_AES_128_GCM_SHA256
        - TLS_AES_256_GCM_SHA384
        - TLS_CHACHA20_POLY1305_SHA256
      curvePreferences:
        - CurveP521
        - CurveP384
      sniStrict: true

# Middlewares
# Optionale Optimierungen, die bei jeder Anfrage vorgenommen werden sollen bevor diese an den Zielcontainer geleitet wird.
http:
  middlewares:
    # Eine grundlegende Authentifizierungs-Middleware, um das Traefik-Dashboard via htpasswd zu schützen
    # Um die Authentifizierung für einen Container zu nutzen können wir "traefik.http.routers.definierteRoute.middlewares=traefikAuth@file" nutzen
    traefikAuth:
      basicAuth:
        users:
          - "Kamikaze-Jim:$apr1$P/tG13aC$BdXjI6AtKysGWvRAr9aoI0"

    # Empfohlene Standard-Middleware für die meisten Dienste
    # Hinzufügbar via "traefik.http.routers.definierteRoute.middlewares=default@file"
    # Equivalent mit "traefik.http.routers.definierteRoute.middlewares=default-security-headers@file,gzip@file"
    # Die Liste kann hier auch beliebig erweitert werden
    default:
      chain:
        middlewares:
          - default-security-headers
          - gzip

    # Kompatibilität zu alten Anleitungen. Damit kann auch wieder "traefik.http.routers.definierteRoute.middlewares=secHeader@file" 
    secHeaders:
      chain:
        middlewares:
          - default-security-headers
          - gzip

    # Standard Header
    default-security-headers:
      headers:
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        frameDeny: true
#       Deprecated
#       sslRedirect: true
        #HSTS Configuration
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000
        customFrameOptionsValue: "SAMEORIGIN"
    # Gzip Kompression
    gzip:
      compress: {}

```

##### User und Passwort festlegen

```bash
#
# Erzeugt das für obige Datei erforderliche USR/PWD-Gespann
#
echo $(htpasswd -nb <user> '<password>')
#
# Zum Beispiel:
# echo $(htpasswd -nb jonathan 'supersicher')
# Ausgabe: jonathan:$$apr1$$J8vBlNIm$$lSwLv9iGa8KcCct3EyLD41
# Sollte es an dieser Stelle zu Problemen kommen kann man sich auch das Passwort auf Webseiten
# wie zum Beispiel: https://www.redim.de/blog/passwortschutz-mit-htaccess-einrichten generieren lassen.
```

#### ./data/traefik.yml füllen

```bash
#
# /opt/containers/traefik/data/traefik.yml
#
# Statische Traefik-Konfigurationsdatei
# https://doc.traefik.io/traefik/getting-started/configuration-overview/#the-static-configuration
# https://doc.traefik.io/traefik/reference/static-configuration/cli/

api:
  dashboard: true                             # Aktivieren des Dashboard

# Certificate Resolver
# Diese sind für den Abruf von Zertifikaten von einem ACME-Server zuständig
# https://doc.traefik.io/traefik/https/acme/#certificate-resolvers
certificatesResolvers:
  http:
    acme:
      email: "ralf@rastef.de"                 # E-Mail-Adresse für die Registrierung 
      storage: "acme_letsencrypt.json"        # Datei für die Speicherung von Zertifikate (Ich weiche hier bewusst von dem "Standard": acme.json ab) 
      httpChallenge:
        entryPoint: http

# EntryPoints
# EntryPoints sind die Netzwerk-Eingangspunkte in Traefik. Sie definieren den Port, der die Pakete empfängt.
# https://doc.traefik.io/traefik/routing/entrypoints/
entryPoints:
  http:
    address: ":80"                            # Erstellen des Einstiegspunkt für HTTP (Port 80)
    http:
      redirections:                           # Weiterleitung von HTTP auf HTTPS (Port 80 zu Port 443). 
        entryPoint:
          to: "https"                         # Das Ziel
          scheme: "https"                     # Umleitungszielschema
  https:
    address: ":443"                           # Erstellen des Einstiegspunkt für HTTPS (Port 443)


global:
  checknewversion: true                       # In regelmäßigen Abständen prüfen, ob eine neue Version veröffentlicht wurde.
  sendanonymoususage: false                   # Regelmäßige Übermittlung anonymer Nutzungsstatistiken.

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"   # Den UNIX Docker socket beobachten
    exposedByDefault: false                   # Nur Container ausstellen, die explizit aktiviert sind (mit dem Label traefik.enabled)
    network: "proxy"                          # Standardnetzwerk, das für Verbindungen zu allen Containern verwendet wird.
  file:
    filename: "./dynamic_conf.yml"            # Link zur dynamischen Konfiguration
    watch: true                               # Achten auf Änderungen
  providersThrottleDuration: 10               # Frequenz in welchen Abständen die Konfiguration nachgeladen wird
```

Die eigentliche Anleitung stammt von hier: [https://goneuland.de/traefik-v2-reverse-proxy-mit-crowdsec-einrichten/](https://goneuland.de/traefik-v2-reverse-proxy-mit-crowdsec-einrichten/)

# Webserver mit nginx

Um einen nginx Webserver Container einzubinden, sind folgende Schritte durchzuführen:

- Anlegen des Verzeichnis ***/opt/containers/nginx***
- Anlegen des Verzeichnis ***/opt/containers/nginx/html***

```
sudo mkdir -p /opt/containers/nginx/html
```

Für den Containerstart ist die Datei ***/opt/containers/nginx/docker-compose.yml*** mit folgendem Inhalt anzulegen:

```bash
#
# /opt/containers/nginx/docker-compose.yml
# Startet einen nginx Webserver
#
version: '3'
services:
  nginx:
    image: nginx:latest
    container_name: nginx
    restart: unless-stopped
    networks:
      - proxy
    volumes:
      - ./html:/usr/share/nginx/html
    labels:
      - "traefik.enable=true"
      - "traefik.http.middlewares.redirect2www.redirectregex.regex=^https://die-steffens.eu/(.*)"
      - "traefik.http.middlewares.redirect2www.redirectregex.replacement=https://www.die-steffens.eu/$${1}"
      - "traefik.http.routers.nginx-www.entrypoints=https"
      - "traefik.http.routers.nginx-www.rule=Host(`www.die-steffens.eu`,`die-steffens.eu`)"
      - "traefik.http.routers.nginx-www.tls=true"
      - "traefik.http.routers.nginx-www.tls.certresolver=http"
      - "traefik.http.routers.nginx-www.service=nginx"
      - "traefik.http.routers.nginx-www.middlewares=redirect2www,secHeaders@file"
      - "traefik.http.services.nginx.loadbalancer.server.port=80"
      - "traefik.docker.network=proxy"
networks:
  proxy:
    external: true
```

Der Webserver kann über das unsichere Port 80 Protokoll http oder das sichere Port 443 Protokoll https angesprochen werden. In ersterem Fall wird auf das sichere https Protokoll umgelenkt.

Gestartet und gestoppt wird wie üblich mit:  
`/opt/containers/bookstack/docker compose -f /opt/containers/nginx/docker-compose.yml up -d`  
`/opt/containers/bookstack/docker compose -f /opt/containers/nginx/docker-compose.yml down`

# Bookstack

Um einen Bookstack Container einzubinden, sind folgende Schritte durchzuführen:

- Anlegen des Verzeichnis ***/opt/containers/bookstack***
- Anlegen des Verzeichnis ***/opt/containers/bookstack/data/db***
- Anlegen des Verzeichnis ***/opt/containers/bookstack/data/bs***

```
sudo mkdir -p /opt/containers/bookstack/data/db
sudo mkdir /opt/containers/bookstack/data/bs
```

Für den Containerstart ist die Datei ***/opt/containers/bookstack/docker-compose.yml*** mit folgendem Inhalt anzulegen:

```bash
#
# /opt/containers/bookstack/docker-compose.yml
# Ein Bookstack Container mit MariaDB
#
version: '3'
services:
  db:
    image: lscr.io/linuxserver/mariadb:latest
    container_name: bookstack-db
    networks:
      - db-network
    environment:
      - TZ=Europe/Berlin
      - MYSQL_ROOT_PASSWORD=RaRuRickZaubertrick
      - MYSQL_DATABASE=bookstack
      - MYSQL_USER=bookstack
      - MYSQL_PASSWORD=DaKommstDuNieDrauf
    volumes:
      - ./data/db:/config
    restart: unless-stopped
    labels:
      - traefik.enable=false

  app:
    image: lscr.io/linuxserver/bookstack:latest
    container_name: bookstack-app
    networks:
      - db-network
      - proxy
    depends_on:
      - db
    environment:
      - APP_URL=https://book.die-steffens.eu
      - DB_HOST=db
      - DB_PORT=3306
      - DB_DATABASE=bookstack
      - DB_USERNAME=bookstack
      - DB_PASSWORD=DaKommstDuNieDrauf
    restart: unless-stopped
    volumes:
      - ./data/bs:/config
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.bookstack.entrypoints=https"
      - "traefik.http.routers.bookstack.rule=Host(`book.die-steffens.eu`)"
      - "traefik.http.routers.bookstack.tls=true"
      - "traefik.http.routers.bookstack.tls.certresolver=http"
      - "traefik.http.routers.bookstack.service=bookstack"
      - "traefik.http.routers.bookstack.middlewares=secHeaders@file"
      - "traefik.http.services.bookstack.loadbalancer.server.port=80"
      - "traefik.docker.network=proxy"

networks:
  db-network:
    external: false
  proxy:
    external: true

```

Es werden zwei Container gestartet: Die Datenbank MariaDB und der Bookstack Container. Für die Kommunikation mit der Datenbank wird ein internes Netzwerk eingerichtet, das von Außen nicht zu erreichen ist.

Gestartet und gestoppt wird wie üblich mit:  
`/opt/containers/bookstack/docker compose -f /opt/containers/bookstack/docker-compose.yml up -d`  
`/opt/containers/bookstack/docker compose -f /opt/containers/bookstack/docker-compose.yml down`

# WebDAV Container

Um einen WebDAV Zugang über ein gesichertes Protokoll bereitzustellen, sind nur wenige Schritte erforderlich:

- Das Verzeichnis ***/opt/containers/webdav*** muss angelegt werden.
- Das Verzeichnis ***/opt/containers/webdav/dat***a muss angelegt werden.
- Das Verzeichnis ***/opt/containers/webdav/config*** muss angelegt werden.
- Das Verzeichnis ***/opt/containers/webdav/data*** soll für alle zu lesen und zu beschreiben sein (***chmod 777***).

```bash
sudo mkdir -p /opt/containers/webdav/config
sudo mkdir /opt/containers/webdav/data
sudo chown 777 /opt/containers/webdav/data
```

Im Verzeichnis ***opt/containers/webdav*** ist die nachfolgend aufgezeigte Datei ***docker-compose.yml*** zu erstellen:

```bash
#
# /opt/containers/webdav/docker-compose.yml
# Starts and initializes a WebDAV Container
#
version: '3'
services:
  webdav:
    image: chonjay21/webdav:latest
    container_name: webdav
    restart: unless-stopped
    environment:
      APP_USER_NAME: meganeKAR8879
      APP_USER_PASSWD: qAJsPWCc7YcMro2vfRWHv2tfad2hh2kfWcieMJ2VCmXKT3ggNMRKVLQTSrsEcUNT
      SERVER_NAME: davs.die-steffens.eu
      APP_UID: 1000
      APP_GID: 1000
      APP_UMASK: 007
      TZ: Europe/Berlin
      FORCE_REINIT_CONFIG: true
    networks:
      - proxy
    security_opt:
      - no-new-privileges:true
    volumes:
      - ./data:/var/webdav/data
      - ./config:/webdav/config
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.webdav-secure.entrypoints=https"
      - "traefik.http.routers.webdav-secure.rule=Host(`davs.die-steffens.eu`)"
      - "traefik.http.routers.webdav-secure.tls=true"
      - "traefik.http.routers.webdav-secure.tls.certresolver=http"
      - "traefik.http.routers.webdav-secure.service=webdav"
      - "traefik.http.services.webdav.loadbalancer.server.port=80"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.webdav-secure.middlewares=secHeaders@file"
networks:
  proxy:
    external: true
```

Mit `docker compose -f /opt/containers/webdav/docker-compose.yml up -d` kann der Container gestartet werden.  
Mit `docker compose -f /opt/containers/webdav/docker-compose.yml down` kann der Container gestoppt werden.

Das für alle User offene Datenverzeichnis ist natürlich für komplexere Szenarien mit Benutzenden in unterschiedlichen Rollen nicht zu empfehlen. Als Familiensammelverzeichnis ist es allerdings ausreichend. Weitere Informationen finden sich in der Beschreibung des Containers.