Matrix ist ein dezentrales, offenes Protokoll für sichere, Echtzeit-Kommunikation. Es ermöglicht dir, deinen eigenen Chat-Server zu betreiben, der vollständig unter deiner Kontrolle steht und mit anderen Matrix-Servern kommunizieren kann (Federation). In diesem ausführlichen Tutorial zeige ich dir, wie du einen vollständigen Matrix-Server mit Docker Compose aufsetzt, inklusive Synapse (der Referenz-Server), Element (Web-Client), Traefik (Reverse-Proxy), TURN-Server für Voice/Video und optional WhatsApp-Bridge.

Was ist Matrix?

Matrix ist ein offenes Protokoll für dezentrale, Echtzeit-Kommunikation. Die wichtigsten Vorteile:

  • Dezentral: Jeder kann seinen eigenen Server betreiben
  • Federiert: Server können miteinander kommunizieren (wie E-Mail)
  • Ende-zu-Ende verschlüsselt: Sichere Kommunikation
  • Open Source: Vollständig transparent und anpassbar
  • Multi-Client: Verschiedene Clients für verschiedene Plattformen
  • Rich Features: Text, Voice, Video, Dateien, etc.

Architektur-Übersicht

Dieses Setup umfasst folgende Komponenten:

  1. Synapse: Der Matrix-Homeserver (Hauptkomponente)
  2. PostgreSQL: Datenbank für Synapse
  3. Element Web: Web-basierter Chat-Client
  4. Traefik: Reverse-Proxy mit automatischem SSL
  5. Coturn: TURN-Server für Voice/Video-Calls
  6. LiveKit: Moderne WebRTC-Infrastruktur für Video-Calls
  7. Synapse Admin: Web-Interface zur Server-Verwaltung
  8. Mautrix WhatsApp (optional): Bridge zu WhatsApp

Voraussetzungen

Bevor wir mit der Installation beginnen, stelle sicher, dass du folgende Voraussetzungen erfüllst:

Systemanforderungen

  • Docker Version 20.10 oder höher - Download
  • Docker Compose Version 2.0 oder höher - Download
  • Mindestens 2GB RAM (4GB+ empfohlen)
  • Mindestens 10GB freier Speicherplatz
  • Eine Domain mit DNS-Zugriff (für SSL-Zertifikate)

Installation prüfen

Überprüfe, ob Docker und Docker Compose installiert sind:

docker --version
docker compose version

DNS-Konfiguration

Du benötigst eine Domain mit folgenden DNS-Einträgen:

  • matrix.deine-domain.de → Deine Server-IP (für Synapse)
  • chat.deine-domain.de → Deine Server-IP (für Element)
  • rtc.deine-domain.de → Deine Server-IP (für LiveKit, optional)
  • jwt.deine-domain.de → Deine Server-IP (für JWT-Service, optional)

Wichtig: Stelle sicher, dass diese DNS-Einträge aktiv sind, bevor du mit dem Setup beginnst, da Let’s Encrypt die Domain-Validierung durchführt.

Schritt 1: Projekt-Verzeichnis erstellen

Erstelle ein neues Verzeichnis für dein Matrix-Setup:

mkdir matrix-server
cd matrix-server

Schritt 2: Umgebungsvariablen konfigurieren

Erstelle eine .env Datei mit allen notwendigen Konfigurationswerten:

nano .env

Füge folgende Variablen ein und passe sie an deine Umgebung an:

# Synapse Konfiguration
SYNAPSE_SERVER_NAME=matrix.deine-domain.de
SYNAPSE_REPORT_STATS=true

# PostgreSQL
POSTGRES_PASSWORD=dein_sicheres_datenbank_passwort

# AWS S3 für Media Storage (optional, aber empfohlen)
AWS_ACCESS_KEY_ID=dein_aws_access_key
AWS_SECRET_ACCESS_KEY=dein_aws_secret_key
AWS_DEFAULT_REGION=eu-central-1
AWS_S3_ADDRESSING_STYLE=path

# TURN Server Konfiguration
TURN_DOMAIN=rtc.deine-domain.de
TURN_REALM=rtc.deine-domain.de
TURN_STATIC_AUTH_SECRET=generiere_ein_langes_zufaelliges_secret
EXTERNAL_IP=deine_oeffentliche_ip_adresse
RELAY_IP=deine_oeffentliche_ip_adresse

# LiveKit Konfiguration
LIVEKIT_API_KEY=generiere_einen_api_key
LIVEKIT_API_SECRET=generiere_ein_langes_secret

# Mautrix WhatsApp Bridge (optional)
MW_POSTGRES_PASSWORD=dein_whatsapp_bridge_datenbank_passwort

Wichtige Konfigurationsoptionen im Detail

SYNAPSE_SERVER_NAME:

  • Dies ist der Domain-Name deines Matrix-Servers
  • Muss mit deiner DNS-Konfiguration übereinstimmen
  • Beispiel: matrix.example.com

SYNAPSE_REPORT_STATS:

  • true: Sende anonyme Statistiken an matrix.org
  • false: Keine Statistiken senden

POSTGRES_PASSWORD:

  • Starkes Passwort für die PostgreSQL-Datenbank
  • Verwende einen Passwort-Generator für Production

TURN_STATIC_AUTH_SECRET:

  • Generiere ein zufälliges Secret für TURN-Authentifizierung
  • Mindestens 32 Zeichen lang
  • Generieren mit: openssl rand -hex 32

LIVEKIT_API_KEY und LIVEKIT_API_SECRET:

  • Für Video-Calls benötigt
  • Generiere mit: openssl rand -hex 16 (für Key) und openssl rand -hex 32 (für Secret)

Sicherheitshinweis: Bewahre deine .env Datei sicher auf und teile sie niemals öffentlich!

Schritt 3: Verzeichnisstruktur erstellen

Erstelle die notwendigen Verzeichnisse für Konfigurationen und Daten:

mkdir -p synapse element/config.json traefik well-known/matrix/server whatsapp certs

Schritt 4: Traefik ACME-Datei erstellen

Erstelle die Datei für Let’s Encrypt-Zertifikate:

touch traefik/acme.json
chmod 600 traefik/acme.json

Die Berechtigung 600 ist wichtig, damit nur der Besitzer die Datei lesen/schreiben kann.

Schritt 5: Synapse-Konfiguration generieren

Synapse benötigt eine initiale Konfigurationsdatei. Wir generieren sie mit dem offiziellen Synapse-Image:

docker run -it --rm \
  -v $(pwd)/synapse:/data \
  -e SYNAPSE_SERVER_NAME=matrix.deine-domain.de \
  -e SYNAPSE_REPORT_STATS=true \
  matrixdotorg/synapse:latest generate

Wichtig: Ersetze matrix.deine-domain.de mit deinem tatsächlichen Domain-Namen!

Dies erstellt die Datei synapse/homeserver.yaml. Wir werden diese später anpassen.

Schritt 6: Synapse-Konfiguration anpassen

Öffne die generierte synapse/homeserver.yaml und passe folgende Einstellungen an:

nano synapse/homeserver.yaml

Wichtige Konfigurationen:

Datenbank-Verbindung (bereits konfiguriert durch Umgebungsvariablen):

database:
  name: psycopg2
  args:
    user: synapse
    password: ${POSTGRES_PASSWORD}
    database: synapse
    host: db
    cp_min: 5
    cp_max: 10

Media Storage (für S3, optional):

media_store_path: "/data/media_store"
s3:
  enabled: true
  bucket_name: "dein-s3-bucket-name"
  region_name: "eu-central-1"
  access_key_id: "${AWS_ACCESS_KEY_ID}"
  secret_access_key: "${AWS_SECRET_ACCESS_KEY}"
  addressing_style: "path"

TURN-Server Konfiguration:

turn_uris:
  - "turn:rtc.deine-domain.de:3478?transport=udp"
  - "turn:rtc.deine-domain.de:3478?transport=tcp"
  - "turns:rtc.deine-domain.de:5349?transport=udp"
  - "turns:rtc.deine-domain.de:5349?transport=tcp"

turn_shared_secret: "${TURN_STATIC_AUTH_SECRET}"
turn_user_lifetime: 86400000
turn_allow_guests: true

LiveKit Integration (für moderne Video-Calls):

livekit:
  url: "wss://rtc.deine-domain.de/livekit/sfu"
  api_key: "${LIVEKIT_API_KEY}"
  api_secret: "${LIVEKIT_API_SECRET}"

Federation (wichtig für Kommunikation mit anderen Servern):

federation_domain_whitelist: []
# Leer lassen = alle Domains erlauben
# Oder spezifische Domains auflisten

Registrierung (für neue Benutzer):

enable_registration: true
enable_registration_without_verification: false
registration_shared_secret: "generiere_ein_langes_secret_hier"

Schritt 7: Element-Konfiguration erstellen

Erstelle die Konfigurationsdatei für Element Web:

nano element/config.json

Füge folgende Konfiguration ein:

{
  "default_server_config": {
    "m.homeserver": {
      "base_url": "https://matrix.deine-domain.de",
      "server_name": "matrix.deine-domain.de"
    },
    "m.identity_server": {
      "base_url": "https://vector.im"
    }
  },
  "default_server_name": "matrix.deine-domain.de",
  "brand": "Dein Chat",
  "integrations_ui_url": "https://scalar.vector.im/",
  "integrations_rest_url": "https://scalar.vector.im/api",
  "integrations_widgets_urls": [
    "https://scalar.vector.im/_matrix/integrations/v1",
    "https://scalar.vector.im/_matrix/integrations/v2",
    "https://scalar-staging.vector.im/_matrix/integrations/v1",
    "https://scalar-staging.vector.im/_matrix/integrations/v2",
    "https://scalar-staging.riot.im/scalar/api"
  ],
  "bug_report_endpoint_url": "https://element.io/bugreports/submit",
  "defaultCountryCode": "DE",
  "showLabsSettings": true,
  "features": {
    "feature_new_spinner": true,
    "feature_pinning": true,
    "feature_custom_status": true,
    "feature_custom_tags": true,
    "feature_state_resolver": true,
    "feature_mjolnir": true,
    "feature_dnd": true,
    "feature_bridge_state": true,
    "feature_presence_in_room_list": true,
    "feature_cross_signing": true,
    "feature_new_device_manager": true,
    "feature_video_rooms": true,
    "feature_element_call": true,
    "feature_livekit": true
  },
  "default_federate": true,
  "default_theme": "light",
  "roomDirectory": {
    "servers": [
      "matrix.deine-domain.de",
      "matrix.org"
    ]
  },
  "settingDefaults": {
    "breadcrumbs": true
  },
  "jitsi": {
    "preferredDomain": "meet.jit.si"
  },
  "livekit": {
    "url": "wss://rtc.deine-domain.de/livekit/sfu",
    "jwt_service_url": "https://jwt.deine-domain.de"
  }
}

Wichtig: Ersetze alle deine-domain.de Einträge mit deiner tatsächlichen Domain!

Schritt 8: Well-Known Konfiguration

Erstelle die Well-Known-Dateien für Matrix-Discovery:

mkdir -p well-known/matrix

Erstelle well-known/matrix/server:

nano well-known/matrix/server

Füge folgendes ein (ersetze mit deiner Domain):

{
  "m.server": "matrix.deine-domain.de:443"
}

Erstelle well-known/matrix/client:

nano well-known/matrix/client
{
  "m.homeserver": {
    "base_url": "https://matrix.deine-domain.de"
  },
  "m.identity_server": {
    "base_url": "https://vector.im"
  }
}

Schritt 9: Docker Compose Datei erstellen

Erstelle die docker-compose.yml Datei:

nano docker-compose.yml

Füge die vollständige docker-compose.yml ein (siehe unten im Dokument).

Schritt 10: Container starten

Starte alle Container:

docker compose up -d

Der -d Flag startet die Container im Hintergrund. Du kannst den Status überprüfen mit:

docker compose ps

Die Container sollten jetzt starten. Der erste Start kann einige Minuten dauern, da:

  • PostgreSQL die Datenbank initialisiert
  • Synapse die Datenbank-Schema erstellt
  • Traefik SSL-Zertifikate von Let’s Encrypt anfordert

Schritt 11: Logs überwachen

Überwache die Logs, um sicherzustellen, dass alles korrekt startet:

# Alle Container
docker compose logs -f

# Nur Synapse
docker compose logs -f synapse

# Nur Traefik (für SSL-Status)
docker compose logs -f traefik

Wichtige Log-Meldungen

Synapse:

  • Server started, listening on port 8008 → Server läuft
  • Database is ready → Datenbankverbindung erfolgreich
  • Federation is ready → Federation aktiviert

Traefik:

  • Certificate obtained from ACME → SSL-Zertifikat erfolgreich
  • Server configuration reloaded → Konfiguration geladen

Schritt 12: Ersten Benutzer erstellen

Nachdem Synapse gestartet ist, erstelle den ersten Admin-Benutzer:

docker compose exec synapse register_new_matrix_user -c /data/homeserver.yaml -a -u admin -p dein_admin_passwort

Wichtig:

  • -a macht den Benutzer zum Administrator
  • Verwende ein starkes Passwort!
  • Speichere die Credentials sicher!

Schritt 13: Auf den Server zugreifen

Nach erfolgreichem Start kannst du auf folgende URLs zugreifen:

  • Element Web Client: https://chat.deine-domain.de
  • Synapse API: https://matrix.deine-domain.de
  • Synapse Admin: http://deine-server-ip:8081
  • Traefik Dashboard: http://deine-server-ip:8080 (falls aktiviert)

Schritt 14: Erste Anmeldung

  1. Öffne https://chat.deine-domain.de in deinem Browser
  2. Klicke auf “Sign In”
  3. Wähle “Edit” neben dem Server-Namen
  4. Gib https://matrix.deine-domain.de ein
  5. Melde dich mit deinem Admin-Account an

Erweiterte Konfiguration

Media Storage mit S3

Für Production-Umgebungen wird empfohlen, Media-Dateien in S3 zu speichern:

  1. Erstelle einen S3-Bucket bei AWS (oder kompatiblem Service)
  2. Konfiguriere die AWS-Credentials in der .env Datei
  3. Aktiviere S3 in der homeserver.yaml (siehe Schritt 6)

WhatsApp-Bridge einrichten

Die Mautrix WhatsApp Bridge ermöglicht es, WhatsApp-Nachrichten über Matrix zu empfangen/senden:

  1. Starte die Bridge: docker compose up -d mautrix-whatsapp
  2. Öffne http://deine-server-ip:29318 im Browser
  3. Folge der Anleitung zum QR-Code-Scan
  4. Die Bridge wird automatisch als AppService in Synapse registriert

Backup-Strategie

Datenbank-Backup:

docker compose exec db pg_dump -U synapse synapse > backup_$(date +%Y%m%d).sql

Synapse-Daten-Backup:

tar -czf synapse_backup_$(date +%Y%m%d).tar.gz synapse/

Automatische Backups (mit Cron):

# Füge zu crontab hinzu (crontab -e)
0 2 * * * cd /pfad/zum/matrix-server && docker compose exec -T db pg_dump -U synapse synapse > backups/db_$(date +\%Y\%m\%d).sql

Häufige Probleme und Lösungen

Problem: SSL-Zertifikat wird nicht erstellt

Symptome: Traefik-Logs zeigen ACME-Fehler

Lösungen:

  • Überprüfe, ob die DNS-Einträge korrekt sind: dig matrix.deine-domain.de
  • Stelle sicher, dass Port 80 und 443 von außen erreichbar sind
  • Überprüfe die Firewall-Einstellungen
  • Warte einige Minuten, Let’s Encrypt hat Rate-Limits

Problem: Synapse startet nicht

Symptome: Container stoppt sofort oder Logs zeigen Fehler

Lösungen:

  • Überprüfe die Logs: docker compose logs synapse
  • Stelle sicher, dass PostgreSQL läuft: docker compose ps db
  • Überprüfe die homeserver.yaml auf Syntax-Fehler
  • Stelle sicher, dass die Datenbank-Credentials korrekt sind

Problem: Federation funktioniert nicht

Symptome: Kann nicht mit anderen Matrix-Servern kommunizieren

Lösungen:

  • Überprüfe, ob Port 8448 von außen erreichbar ist
  • Teste mit: https://federationtester.matrix.org/#matrix.deine-domain.de
  • Stelle sicher, dass die Well-Known-Dateien korrekt sind
  • Überprüfe die Traefik-Labels für Federation

Problem: TURN-Server funktioniert nicht

Symptome: Voice/Video-Calls funktionieren nicht

Lösungen:

  • Überprüfe die TURN-Konfiguration in homeserver.yaml
  • Stelle sicher, dass die Ports 3478 und 5349 geöffnet sind
  • Überprüfe die TURN_STATIC_AUTH_SECRET in der .env
  • Teste mit: https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/

Problem: Element lädt nicht

Symptome: Weißer Bildschirm oder Fehler beim Laden

Lösungen:

  • Überprüfe die config.json auf Syntax-Fehler
  • Stelle sicher, dass die Domain in der Konfiguration korrekt ist
  • Überprüfe die Browser-Konsole auf Fehler
  • Stelle sicher, dass Traefik die Route korrekt konfiguriert hat

Performance-Optimierung

Datenbank-Optimierung

Füge in homeserver.yaml hinzu:

database:
  args:
    cp_min: 5
    cp_max: 10
    cp_reconnect: true

Synapse-Optimierung

Für größere Installationen:

media_store_path: "/data/media_store"
max_upload_size: "50M"
max_image_pixels: "32M"

Ressourcen-Limits

Füge in docker-compose.yml Ressourcen-Limits hinzu:

synapse:
  deploy:
    resources:
      limits:
        cpus: '2'
        memory: 2G
      reservations:
        cpus: '1'
        memory: 1G

Sicherheits-Best-Practices

  1. Firewall konfigurieren:
    • Öffne nur Ports 80, 443, 8448
    • Blockiere alle anderen Ports
  2. Regelmäßige Updates:
    docker compose pull
    docker compose up -d
    
  3. Starke Passwörter:
    • Verwende Passwort-Manager
    • Aktiviere 2FA für Admin-Accounts
  4. Backups:
    • Tägliche Datenbank-Backups
    • Wöchentliche Voll-Backups
    • Teste die Wiederherstellung regelmäßig
  5. Monitoring:
    • Überwache Container-Logs
    • Setze Alerts für kritische Fehler
    • Überwache Ressourcen-Nutzung

Nächste Schritte

Nach erfolgreicher Installation kannst du:

  1. Weitere Benutzer einladen: Erstelle Accounts für deine Community
  2. Räume erstellen: Organisiere deine Kommunikation in Räumen
  3. Bridges einrichten: Verbinde mit anderen Chat-Services
  4. Bots hinzufügen: Automatisiere Aufgaben mit Matrix-Bots
  5. Custom Branding: Passe Element an dein Branding an

Fazit

Mit diesem Setup hast du einen vollständig funktionsfähigen Matrix-Server mit:

  • ✅ Sichere, verschlüsselte Kommunikation
  • ✅ Voice- und Video-Calls
  • ✅ Federation mit anderen Servern
  • ✅ Automatisches SSL
  • ✅ Moderne Web-Client
  • ✅ Admin-Interface

Für weitere Informationen und fortgeschrittene Konfigurationen schaue in die offizielle Synapse-Dokumentation.

Viel Erfolg mit deinem Matrix-Server! 💬🔐


Docker Compose Konfiguration

Hier ist die vollständige docker-compose.yml Datei:

services:
  traefik:
    image: traefik:v3.0
    restart: unless-stopped
    command:
      # Providers
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false

      # Entrypoints
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --entrypoints.federation.address=:8448
      #- --entrypoints.turn-udp.address=:3478/udp
      #- --entrypoints.turn-tcp.address=:3478/tcp
      #- --entrypoints.turns-udp.address=:5349/udp
      #- --entrypoints.turns-tcp.address=:5349/tcp

      # HTTP -> HTTPS redirect
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.web.http.redirections.entrypoint.scheme=https

      # ACME/Let's Encrypt
      - --certificatesresolvers.le.acme.tlschallenge=true
      - --certificatesresolvers.le.acme.email=deine-email@deine-domain.de
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json

      # Optional: set to DEBUG if you need ACME diagnostics
      # - --log.level=DEBUG

      # Optional: dashboard on internal socket only (no port published)
      - --api.dashboard=true
    ports:
      - "80:80"
      - "443:443"
      - "8448:8448"
      #- "3478:3478/udp"
      #- "3478:3478/tcp"
      #- "5349:5349/udp"
      #- "5349:5349/tcp"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik/acme.json:/letsencrypt/acme.json

  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: synapse
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: synapse
      # Ensure C collation for Synapse
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U synapse"]
      interval: 10s
      timeout: 5s
      retries: 10
    volumes:
      - dbdata:/var/lib/postgresql/data

  synapse:
    image: matrixdotorg/synapse:latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      # Keep this aligned with homeserver.yaml server_name
      SYNAPSE_SERVER_NAME: ${SYNAPSE_SERVER_NAME}
      SYNAPSE_REPORT_STATS: ${SYNAPSE_REPORT_STATS}
      AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
      AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
      AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION}
      AWS_S3_ADDRESSING_STYLE: ${AWS_S3_ADDRESSING_STYLE}
    volumes:
      - ./synapse:/data
      - ./whatsapp/:/whatsapp/
      #- mw_data:/data
    ports:
      - "8008:8008"
    labels:
      - traefik.enable=true

      # Client API via 443
      - traefik.http.routers.synapse-web.rule=Host(`matrix.deine-domain.de`) && (PathPrefix(`/_matrix`) || PathPrefix(`/_synapse`))
      - traefik.http.routers.synapse-web.entrypoints=websecure
      - traefik.http.routers.synapse-web.tls.certresolver=le
      - traefik.http.routers.synapse-web.service=synapse-web
      - traefik.http.services.synapse-web.loadbalancer.server.port=8008
      - traefik.http.middlewares.synapse-buf.buffering.maxRequestBodyBytes=0
      - traefik.http.routers.synapse-web.middlewares=synapse-buf

      # Federation via 8448
      - traefik.http.routers.synapse-fed.rule=Host(`matrix.deine-domain.de`) && PathPrefix(`/_matrix`)
      - traefik.http.routers.synapse-fed.entrypoints=federation
      - traefik.http.routers.synapse-fed.tls.certresolver=le
      - traefik.http.routers.synapse-fed.service=synapse-fed
      - traefik.http.services.synapse-fed.loadbalancer.server.port=8008

  element:
    image: vectorim/element-web:latest
    restart: unless-stopped
    depends_on:
      - synapse
    volumes:
      # Provide your Element config
      - ./element/config.json:/app/config.json:ro
    labels:
      - traefik.enable=true
      - traefik.http.routers.element.rule=Host(`chat.deine-domain.de`)
      - traefik.http.routers.element.entrypoints=websecure
      - traefik.http.routers.element.tls.certresolver=le
      - traefik.http.routers.element.service=element
      - traefik.http.services.element.loadbalancer.server.port=80

  admin:
    image: etkecc/synapse-admin
    container_name: admin
    environment:
      REACT_APP_SERVER: https://matrix.deine-domain.de
      TZ: Europe/Berlin
    ports:
      - "8081:80"
    restart: unless-stopped

  certs-dumper:
    image: ldez/traefik-certs-dumper:latest
    restart: unless-stopped
    depends_on:
      - traefik
    volumes:
      - ./traefik/acme.json:/acme.json:ro
      - ./certs:/out
    env_file:
      - .env
    environment:
      # Export-Modus: directory mit fullchain.pem und privkey.pem pro Domain
      - DOMAIN=${TURN_DOMAIN}
    entrypoint:
      - sh
      - -c
      - |
        set -e
        while true; do
          traefik-certs-dumper file --version v2 --watch=false --source /acme.json --domain-subdir --dest /out
          if [ -f "${CERTS_PATH}/certificate.crt" ]; then
            chown 0:65534 "${CERTS_PATH}/certificate.crt" || true
            chmod 0640 "${CERTS_PATH}/certificate.crt" || true
          fi
          if [ -f "${CERTS_PATH}/privatekey.key" ]; then
            chown 0:65534 "${CERTS_PATH}/privatekey.key" || true
            chmod 0640 "${CERTS_PATH}/privatekey.key" || true
          fi
          sleep 3600
        done

  coturn:
    image: coturn/coturn:latest
    container_name: coturn
    restart: unless-stopped
    depends_on:
      - certs-dumper
    user: "root:root"
    entrypoint:
      - sh
      - -c
      - |
        set -e
        #echo "Waiting for $${CERTS_PATH}/certificate.crt and $${CERTS_PATH}/privatekey.key ..."
        #for i in $$(seq 1 180); do
        #  [ -f "$${CERTS_PATH}/certificate.crt" ] && [ -f "$${CERTS_PATH}/privatekey.key" ] && break
        #  sleep 1
        #done
        #[ -f "$${CERTS_PATH}/certificate.crt" ] && [ -f "$${CERTS_PATH}/privatekey.key" ] || { echo "Certs missing after 180s"; exit 1; }

        exec turnserver \
          --log-file=stdout \
          --no-cli \
          --fingerprint \
          --listening-port=3478 \
          --tls-listening-port=5349 \
          --use-auth-secret \
          --static-auth-secret="$${TURN_STATIC_AUTH_SECRET}" \
          --realm="$${TURN_REALM}" \
          --min-port=49160 \
          --max-port=49200 \
          --cert="$${CERTS_PATH}/certificate.crt" \
          --pkey="$${CERTS_PATH}/privatekey.key" \
          --no-tlsv1 \
          --stale-nonce=600 \
          --total-quota=100 \
          --no-multicast-peers \
          --no-tcp-relay \
          $${EXTERNAL_IP:+--external-ip=$${EXTERNAL_IP}} \
          $${RELAY_IP:+--relay-ip=$${RELAY_IP}}
    environment:
      - CERTS_PATH="/certs/${TURN_DOMAIN}"
      - TURN_REALM=${TURN_REALM}
      - TURN_STATIC_AUTH_SECRET=${TURN_STATIC_AUTH_SECRET}
      - TURN_DOMAIN=${TURN_DOMAIN}
      - EXTERNAL_IP=${EXTERNAL_IP}
      - RELAY_IP=${RELAY_IP}
    network_mode: host
    #ports:
    #  - "3478:3478/tcp"
    #  - "3478:3478/udp"
    #  - "5349:5349/tcp"
    #  - "5349:5349/udp"
    #  - "49160-49200:49160-49200/udp"
    volumes:
      - ./certs:/certs:ro
    healthcheck:
      test: ["CMD-SHELL", "timeout 2 bash -c '</dev/tcp/127.0.0.1/3478' || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 5
    labels:
      - traefik.enable=true
      - traefik.tcp.routers.turn-udp.rule=HostSNI(`*`)
      - traefik.tcp.routers.turn-udp.entrypoints=turn-udp
      - traefik.tcp.routers.turn-udp.service=turn-udp
      - traefik.tcp.services.turn-udp.loadbalancer.server.port=3478
      - traefik.tcp.routers.turn-tcp.rule=HostSNI(`*`)
      - traefik.tcp.routers.turn-tcp.entrypoints=turn-tcp
      - traefik.tcp.routers.turn-tcp.service=turn-tcp
      - traefik.tcp.services.turn-tcp.loadbalancer.server.port=3478
      - traefik.tcp.routers.turns-udp.rule=HostSNI(`*`)
      - traefik.tcp.routers.turns-udp.entrypoints=turns-udp
      - traefik.tcp.routers.turns-udp.service=turns-udp
      - traefik.tcp.services.turns-udp.loadbalancer.server.port=5349
      - traefik.tcp.routers.turns-tcp.rule=HostSNI(`*`)
      - traefik.tcp.routers.turns-tcp.entrypoints=turns-tcp
      - traefik.tcp.routers.turns-tcp.service=turns-tcp
      - traefik.tcp.services.turns-tcp.loadbalancer.server.port=5349

  livekit:
    image: livekit/livekit-server:latest
    restart: unless-stopped
    environment:
      LIVEKIT_PORT: "7880"
      LIVEKIT_RTC_TCP_PORT: "7881"
      LIVEKIT_RTC_UDP_PORT: "7882"        # alternativ Port-Range 50000-60000/udp
      LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
      LIVEKIT_WEBRTC_USE_EXTERNAL_IP: "true"
    command: >
      --bind 0.0.0.0
      --node-ip 0.0.0.0
      --port 7880
      #--rtc.tcp_port 7881
      #--rtc.udp_port 7882
    labels:
      - traefik.enable=true
      - traefik.http.routers.livekit.rule=Host(`rtc.deine-domain.de`)
      - traefik.http.routers.livekit.entrypoints=websecure
      - traefik.http.routers.livekit.tls.certresolver=le
      - traefik.http.middlewares.livekit-strip.stripprefix.prefixes=/livekit/sfu
      - traefik.http.routers.livekit.middlewares=livekit-strip
      - traefik.http.services.livekit.loadbalancer.server.port=7880
    ports:
      - "7881:7881/tcp"     # WebRTC TCP fallback
      - "7882:7882/udp"     # WebRTC UDP (UDP-Mux)
      # Falls du statt UDP-Mux lieber Range nutzt: 50000-60000/udp publishen

  lk-jwt:
    image: ghcr.io/element-hq/lk-jwt-service:latest
    restart: unless-stopped
    environment:
      LIVEKIT_URL: "wss://rtc.deine-domain.de/livekit/sfu"   # via Traefik auf 7880
      LIVEKIT_KEY: "${LIVEKIT_API_KEY}"
      LIVEKIT_SECRET: "${LIVEKIT_API_SECRET}"
      # optional: RATE_LIMIT, LOG_LEVEL usw.
    labels:
      - traefik.enable=true
      # Route: Host + Pfadpräfix
      - traefik.http.routers.lkjwt.rule=Host(`jwt.deine-domain.de`)
      - traefik.http.routers.lkjwt.entrypoints=websecure
      - traefik.http.routers.lkjwt.tls.certresolver=le
      - traefik.http.services.lkjwt.loadbalancer.server.port=8080

      # CORS freischalten (sonst blockt der Browser die JWT-Anfrage)
      - traefik.http.middlewares.lkjwt-cors.headers.accesscontrolallowmethods=GET,OPTIONS
      - traefik.http.middlewares.lkjwt-cors.headers.accesscontrolallowheaders=*
      - traefik.http.middlewares.lkjwt-cors.headers.accesscontrolalloworiginlist=*
      - traefik.http.middlewares.lkjwt-cors.headers.addvaryheader=true

      # Middlewares anwenden (Reihenfolge egal)
      - traefik.http.routers.lkjwt.middlewares=lkjwt-cors

  wellknown:
    image: nginx:alpine
    volumes:
      - ./well-known:/usr/share/nginx/html/.well-known:ro
    labels:
      - traefik.enable=true
      - traefik.http.routers.wellknown.rule=Host(`matrix.deine-domain.de`) && PathPrefix(`/.well-known/matrix`)
      - traefik.http.routers.wellknown.entrypoints=websecure
      - traefik.http.routers.wellknown.tls.certresolver=le
      - traefik.http.services.wellknown.loadbalancer.server.port=80

  mw-db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_DB: mautrix_whatsapp
      POSTGRES_USER: mautrix
      POSTGRES_PASSWORD: ${MW_POSTGRES_PASSWORD:-change_me}
    volumes:
      - mw_db:/var/lib/postgresql/data

  mautrix-whatsapp:
    image: dock.mau.dev/mautrix/whatsapp:latest
    restart: unless-stopped
    depends_on:
      - mw-db
    volumes:
      - ./whatsapp/:/data/
    ports:
      - "29318:29318"    # AppService-Callback Port für Synapse
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:29318/health"]
      interval: 30s
      timeout: 5s
      retries: 5

volumes:
  dbdata:
  mw_db:
  mw_data:

Wichtig: Ersetze alle deine-domain.de Einträge in der docker-compose.yml mit deiner tatsächlichen Domain!