Das Problem mit vielen Diensten auf einem Server

Ein Server, viele Dienste. Immich auf Port 2283, Portainer auf 9443, Mailcow auf 8030, Stirling-PDF auf 8088. Jeder Dienst hat seinen eigenen Port – und der Nutzer muss sich alle merken, Zertifikate manuell verwalten und Ports in der Firewall freigeben.

Das skaliert nicht. Die Lösung ist ein Reverse Proxy – ein vorgelagerter Dienst, der eingehende Anfragen anhand der Domain auf den richtigen Container weiterleitet. Nur Port 80 und 443 müssen offen sein. Alle anderen Ports bleiben intern.

Traefik ist genau das – und geht einen Schritt weiter: Es erkennt Docker-Container automatisch, liest ihre Labels und konfiguriert sich selbst. Kein manuelles Bearbeiten von Konfigurationsdateien bei jedem neuen Dienst.

💡 Niveau: Einsteigerfreundlich. Ein laufender Docker-Host und Grundkenntnisse in DNS werden vorausgesetzt.


Was ist Traefik?

Traefik ist ein moderner Reverse Proxy und Load Balancer, der speziell für containerisierte Umgebungen entwickelt wurde. Anders als Nginx oder Apache konfiguriert sich Traefik dynamisch – es beobachtet den Docker-Daemon und registriert neue Container automatisch, sobald sie mit den richtigen Labels versehen werden.

Kernfunktionen:

  • 🔀 Automatisches Routing – Domain → Container via Docker-Labels
  • 🔒 Automatische TLS-Zertifikate via Let’s Encrypt (HTTP- oder DNS-Challenge)
  • 🔄 HTTP → HTTPS Redirect out of the box
  • 📊 Dashboard – Übersicht aller Routen, Services und Middleware
  • 🧩 Middleware – Auth, Rate-Limiting, Header-Manipulation u.v.m.

Kernkonzepte

Bevor die Konfiguration Sinn ergibt, lohnt ein Blick auf die vier zentralen Bausteine von Traefik:

EntryPoints

EntryPoints sind die Eingangstore – sie definieren, auf welchem Port Traefik lauscht:

Port 80  → EntryPoint "web"       (HTTP)
Port 443 → EntryPoint "websecure" (HTTPS)
Port 8899 → EntryPoint "metrics"  (Prometheus)

Jede eingehende Verbindung landet zuerst an einem EntryPoint.

Routers

Routers nehmen eine Anfrage vom EntryPoint entgegen und entscheiden, wohin sie weitergeleitet wird. Die Entscheidung basiert auf Regeln – meistens dem Hostnamen:

Host(`immich.example.com`) → immich-server:2283
Host(`portainer.example.com`) → portainer:9443

Services

Services sind die eigentlichen Ziele – also die Container, an die Traefik weiterleitet. Bei Docker-Integration werden sie automatisch aus den Container-Metadaten erstellt.

Middleware

Middleware sitzt zwischen Router und Service und verändert oder prüft die Anfrage. Typische Anwendungsfälle: Authentifizierung, HTTPS-Redirect, Header hinzufügen, Rate-Limiting.

Das Zusammenspiel

Internet
   │
   ▼
EntryPoint :443 (websecure)
   │
   ▼
Router: Host(`immich.example.com`) + TLS
   │
   ├── Middleware: [authentik-auth] (optional)
   │
   ▼
Service: immich-server:2283

Architektur

                        Internet
                           │
                    Port 80 / 443
                           │
            ┌──────────────▼──────────────┐
            │           Traefik           │
            │                             │
            │  EntryPoint :80  (web)      │
            │  EntryPoint :443 (websecure)│
            │  EntryPoint :8899 (metrics) │
            │                             │
            │  /var/run/docker.sock (ro)  │◄── Docker Daemon
            │  ./config/traefik.yml       │    (liest Labels)
            │  ./config/config.yml        │
            │  ./data/certs/              │
            │  ./data/logs/               │
            └──────────────┬──────────────┘
                           │
              proxy network (bridge)
                           │
          ┌────────────────┼────────────────┐
          │                │                │
    ┌─────▼──────┐  ┌──────▼─────┐  ┌──────▼─────┐
    │   Immich   │  │ Portainer  │  │  Stirling  │
    │  :2283     │  │  :9443     │  │   :8080    │
    └────────────┘  └────────────┘  └────────────┘

    monitoring network
          │
    ┌─────▼──────────┐
    │ otel-collector │
    │ :4317 / :4318  │
    └────────────────┘

Traefik hängt in drei Netzwerken: proxy für alle öffentlich erreichbaren Dienste, gluetun_vpn für Dienste die über ein VPN geroutet werden, und monitoring für den OTel-Collector.


Verzeichnisstruktur

mkdir -p /opt/traefik/config
mkdir -p /opt/traefik/data/certs
mkdir -p /opt/traefik/data/logs

cd /opt/traefik
/opt/traefik/
├── compose.yml
├── .env                        # HETZNER_API_TOKEN
├── config/
│   ├── traefik.yml             # Statische Konfiguration
│   └── config.yml              # Dynamische Konfiguration (Middleware)
├── data/
│   ├── certs/
│   │   └── hetzner-acme.json   # Let's Encrypt Zertifikate (auto-generiert)
│   └── logs/
│       └── access.log          # Access Logs (JSON)
└── otel-collector-config.yaml

Die .env-Datei enthält den Hetzner API-Token:

HETZNER_API_TOKEN=deinHetznerAPIToken

docker-compose.yml

services:
  traefik:
    image: "traefik:v3.5.4"
    container_name: "traefik"
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - proxy
      - gluetun_vpn
      - monitoring
    environment:
      - HETZNER_API_TOKEN=${HETZNER_API_TOKEN}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.rule=Host(`traefik.docker.example.com`)"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.tls.certresolver=hetzner"
      - "traefik.http.routers.traefik.service=api@internal"
    ports:
      - "80:80"
      - "443:443"
      - "8111:8080"
      - "8899:8899"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - ./config/:/etc/traefik/:ro
      - ./data/certs:/var/traefik/certs/:rw
      - ./data/logs:/var/log/traefik/:rw

security_opt: no-new-privileges

security_opt:
  - no-new-privileges:true

Verhindert, dass Prozesse im Container über setuid-Binaries erhöhte Rechte erlangen. Eine wichtige Härtungsmaßnahme für einen Container mit Socket-Zugriff.

volumes

volumes:
  - "/var/run/docker.sock:/var/run/docker.sock:ro"
  - ./config/:/etc/traefik/:ro
  - ./data/certs:/var/traefik/certs/:rw
  - ./data/logs:/var/log/traefik/:rw

Der Docker-Socket wird mit :ro (read-only) gemountet – Traefik muss Container nur beobachten, nicht steuern. ./config/ enthält die statische und dynamische Konfiguration, ebenfalls read-only. ./data/certs/ und ./data/logs/ brauchen Schreibzugriff – dort landen Zertifikate und Access Logs.

Labels – das Traefik-Dashboard

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.traefik.rule=Host(`traefik.docker.example.com`)"
  - "traefik.http.routers.traefik.entrypoints=websecure"
  - "traefik.http.routers.traefik.tls.certresolver=hetzner"
  - "traefik.http.routers.traefik.service=api@internal"

Traefik routet sich hier selbst – das Dashboard ist über traefik.docker.example.com erreichbar. api@internal ist ein eingebauter Service der das Dashboard bereitstellt.

ports

ports:
  - "80:80"    # HTTP (wird zu HTTPS weitergeleitet)
  - "443:443"  # HTTPS
  - "8111:8080" # Traefik-internes Dashboard (direkt, ohne Routing)
  - "8899:8899" # Prometheus Metrics EntryPoint

traefik.yml – Statische Konfiguration

Die statische Konfiguration wird beim Start eingelesen und ändert sich nicht zur Laufzeit. Änderungen erfordern einen Container-Neustart.

Grundeinstellungen

global:
  checkNewVersion: false
  sendAnonymousUsage: false

log:
  level: ERROR

accessLog:
  format: "json"
  filePath: "/var/log/traefik/access.log"

checkNewVersion: false und sendAnonymousUsage: false deaktivieren alle Verbindungen nach außen die nicht dem Traffic-Routing dienen. Das Access Log im JSON-Format ist maschinenlesbar und kann direkt von Log-Aggregatoren (Loki, Elasticsearch) verarbeitet werden.

EntryPoints

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: ":443"
  metrics:
    address: ":8899"

Der web-EntryPoint auf Port 80 leitet alle eingehenden HTTP-Anfragen automatisch und dauerhaft (HTTP 301) auf HTTPS weiter. websecure auf Port 443 ist der eigentliche Haupt-EntryPoint. metrics stellt einen separaten Port für Prometheus bereit – dieser sollte nicht öffentlich erreichbar sein.

Docker-Provider

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    filename: /etc/traefik/config.yml
    watch: true

exposedByDefault: false ist eine wichtige Sicherheitseinstellung: Ohne explizites traefik.enable=true-Label wird kein Container automatisch nach außen exponiert. Der File-Provider liest config.yml für statisch definierte Middleware – watch: true ermöglicht Hot-Reload ohne Neustart.

serversTransport

serversTransport:
  insecureSkipVerify: true

Erlaubt Traefik, selbstsignierte Zertifikate auf Backend-Diensten zu akzeptieren. Relevant wenn Dienste wie Portainer intern bereits TLS verwenden. In einer kontrollierten internen Umgebung akzeptabel – sollte aber bewusst gesetzt sein.


Hetzner DNS-Challenge & Let’s Encrypt

certificatesResolvers:
  hetzner:
    acme:
      email: "admin@example.com"
      storage: /var/traefik/certs/hetzner-acme.json
      dnsChallenge:
        provider: hetzner
        delayBeforeCheck: 0
        resolvers:
          - "213.133.100.102:53"
          - "213.239.204.242:53"

Warum DNS-Challenge statt HTTP-Challenge?

Let’s Encrypt bietet zwei Wege zur Zertifikatsausstellung: HTTP-Challenge (Traefik muss über Port 80 erreichbar sein) und DNS-Challenge (ein TXT-Record wird im DNS gesetzt).

Die DNS-Challenge hat entscheidende Vorteile:

  • Wildcard-Zertifikate möglich (*.example.com) – ein Zertifikat für alle Subdomains
  • Kein Port 80 nötig – funktioniert auch hinter einer strikten Firewall
  • Intern erreichbare Dienste können zertifiziert werden, ohne nach außen exponiert zu sein

Für die Hetzner DNS-Challenge braucht Traefik einen API-Token mit DNS-Schreibrechten. Dieser wird als Umgebungsvariable HETZNER_API_TOKEN übergeben – nie direkt in der Konfiguration.

Die resolvers zeigen auf Hetzners eigene DNS-Server – das stellt sicher, dass Traefik den frisch gesetzten TXT-Record auch wirklich von der autoritativen Quelle liest, nicht von einem gecachten Resolver.

Die Datei hetzner-acme.json muss beim ersten Start existieren und die richtigen Berechtigungen haben:

touch data/certs/hetzner-acme.json
chmod 600 data/certs/hetzner-acme.json

Docker-Labels in der Praxis

Jeder Dienst konfiguriert seine eigene Traefik-Route über Labels. Das Muster ist immer gleich:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.<name>.rule=Host(`subdomain.example.com`)"
  - "traefik.http.routers.<name>.entrypoints=websecure"
  - "traefik.http.routers.<name>.tls.certresolver=hetzner"
  - "traefik.docker.network=proxy"

traefik.docker.network=proxy ist wichtig wenn ein Container in mehreren Netzwerken hängt – Traefik muss wissen, über welches Netzwerk es den Container erreichen soll.

Beispiel: Portainer mit HTTPS-Backend

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.portainer.rule=Host(`portainer.docker.example.com`)"
  - "traefik.http.routers.portainer.entrypoints=websecure"
  - "traefik.http.routers.portainer.tls.certresolver=hetzner"
  - "traefik.http.services.portainer.loadbalancer.server.scheme=https"
  - "traefik.http.services.portainer.loadbalancer.server.port=9443"
  - "traefik.docker.network=proxy"

Wenn der Zieldienst selbst HTTPS nutzt (wie Portainer auf Port 9443), muss scheme=https explizit gesetzt werden – sonst versucht Traefik eine HTTP-Verbindung.

Beispiel: Einfacher HTTP-Dienst

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.stirling.rule=Host(`pdf.docker.example.com`)"
  - "traefik.http.routers.stirling.entrypoints=websecure"
  - "traefik.http.routers.stirling.tls.certresolver=hetzner"
  - "traefik.docker.network=proxy"

Kein scheme nötig – Traefik erkennt den internen Port automatisch aus der ports-Definition des Containers.


config.yml – Dynamische Konfiguration

http:
  middlewares:
    middleware-authentik:
      forwardAuth:
        address: "http://authentik_server:9000/outpost.goauthentik.io/auth/traefik"
        trustForwardHeader: true
        authResponseHeaders:
          - X-authentik-username
          - X-authentik-groups
          - X-authentik-email
          - X-authentik-name
          - X-authentik-uid
          - X-authentik-jwt
          - X-authentik-meta-jwks
          - X-authentik-meta-outpost
          - X-authentik-meta-provider
          - X-authentik-meta-app
          - X-authentik-meta-version1

Die dynamische Konfiguration wird zur Laufzeit geladen und bei Änderungen automatisch neu eingelesen (watch: true). Hier werden Middleware-Definitionen abgelegt die von mehreren Diensten wiederverwendet werden.

forwardAuth delegiert die Authentifizierungsentscheidung an einen externen Dienst – in diesem Fall Authentik. Traefik leitet jede Anfrage zuerst an Authentik weiter. Authentik antwortet mit 200 OK (Zugriff erlaubt) oder 401/302 (Weiterleitung zum Login). Die authResponseHeaders werden nach erfolgreicher Authentifizierung an den Backend-Dienst weitergeleitet – so weiß der Dienst, wer eingeloggt ist.

Eingebunden wird die Middleware per Label am jeweiligen Dienst:

labels:
  - "traefik.http.routers.<name>.middlewares=middleware-authentik@file"

Das @file gibt an, dass die Middleware aus dem File-Provider stammt – nicht aus Docker-Labels.


whoami – Test-Dienst

whoami:
  image: "traefik/whoami"
  restart: unless-stopped
  networks:
    - proxy
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.whoami.rule=Host(`whoami.docker.example.com`)"
    - "traefik.http.routers.whoami.entrypoints=web,websecure"

traefik/whoami ist ein minimaler HTTP-Dienst der alle eingehenden Request-Details zurückgibt: Headers, IP-Adresse, verwendetes Protokoll. Ideal um zu prüfen ob Routing, TLS und Header-Weitergabe korrekt funktionieren.


Start

# Zertifikatsdatei vorbereiten
touch data/certs/hetzner-acme.json
chmod 600 data/certs/hetzner-acme.json

# Container starten
docker compose up -d

# Logs beobachten
docker compose logs -f traefik

Beim ersten Start beantragt Traefik automatisch Zertifikate für alle konfigurierten Domains. Die DNS-Challenge dauert je nach DNS-TTL einige Sekunden bis Minuten. Der Fortschritt ist in den Logs sichtbar.

Das Dashboard ist unter https://traefik.docker.example.com erreichbar – dort sind alle aktiven Routers, Services und Middleware in der Übersicht.


Updates & Wartung

# Image updaten
docker compose pull
docker compose up -d

# Logs in Echtzeit
docker compose logs -f traefik

# Zertifikatsstatus prüfen
cat data/certs/hetzner-acme.json | jq '.certificates[].domain'

Fazit

Traefik löst ein konkretes Problem elegant: Viele Dienste, eine Domain-Struktur, automatische Zertifikate – ohne manuelle Konfigurationsdateien für jeden neuen Dienst. Die Label-basierte Konfiguration hält alles dort, wo es hingehört: direkt beim Dienst. Wer einmal einen neuen Container mit vier Labels deployt und ihn Sekunden später mit gültigem HTTPS-Zertifikat erreicht, möchte nicht mehr zurück.