Matrix-Server komplett selbst hosten – Synapse, Element, LiveKit, VoIP & mehr mit Docker Compose

Warum einen eigenen Matrix-Server?

Matrix ist das offene, dezentrale Protokoll für sichere Echtzeit-Kommunikation. Mit deinem eigenen Matrix-Server hast du die volle Kontrolle über Chats, Sprach-/Videoanrufe und Daten – komplett DSGVO-konform, ohne Abhängigkeit von WhatsApp, Telegram oder Discord.

In dieser Anleitung richte ich einen kompletten Matrix-Stack mit 11 Docker-Containern auf – von der Homeserver-Installation über WebRTC-VoIP mit LiveKit bis hin zu automatischen Borg-Backups.

Architektur

                    ┌────────────────────┐
                    │    Nginx (SSL)      │
                    │    Port 443         │
                    └───────┬────────────┘
                            │
        ┌───────────────────┼───────────────────────┐
        │                   │                       │
   ┌────▼─────┐      ┌─────▼──────┐         ┌──────▼──────┐
   │ Element  │      │ Synapse    │         │ Element Call│
   │ Web UI   │      │ Homeserver │         │ VoIP Client │
   │ :8765    │      │ :8008      │         │ :8768       │
   └──────────┘      └──┬────┬────┘         └──────┬──────┘
                        │    │                      │
          ┌─────────────┘    └──────────┐           │
          │                             │           │
   ┌──────▼──────┐            ┌────────▼────────┐  │
   │ PostgreSQL  │            │ TURN/STUN       │  │
   │ (Datenbank) │            │ (Coturn)        │◄─┘
   └──────┬──────┘            └─────────────────┘
          │
   ┌──────▼──────────────────────────────────────┐
   │ Weitere Services:                            │
   │  • ntfy (Push-Notifications)                 │
   │  • Synapse-Admin (Web-GUI)                   │
   │  • Maubot (Bot-Plattform)                    │
   │  • LiveKit + JWT-Service (native VoIP)       │
   │  • State Compressor (Performance)            │
   └──────────────────────────────────────────────┘

Alle 11 Container im Überblick

Service Image Port Funktion
synapse matrixdotorg/synapse 8008 Matrix Homeserver (Federation + Client)
matrix-db postgres:16-alpine 5432 PostgreSQL Datenbank
element vectorim/element-web 8765 Element Web Client
element-call element-hq/element-call 8768 Element Call (native VoIP)
coturn coturn/coturn 3478/5349 TURN/STUN für WebRTC
livekit livekit/livekit-server 7880 SFU für Gruppen-VoIP
livekit-jwt Custom (Python) 8769 JWT-Token-Generator für LiveKit
ntfy binwiederhier/ntfy 8767 Push-Benachrichtigungen
synapse-admin awesometechnologies/synapse-admin 8766 Admin-Weboberfläche
maubot maubot-local 29316 Bot-Plattform
synapse-compressor rust-synapse-compress-state State-Compression (Performance)

Warum 11 Container? Ist das nicht übertrieben?

Matrix ist kein „einfacher“ Dienst. Es ist ein Kommunikations-Ökosystem. Ein Homeserver allein bringt dir nichts – du brauchst:

  • Datenbank (PostgreSQL) – alle Chats, Räume, Kontakte, Dateien
  • Element Web – der Client, in dem deine Nutzer chatten
  • Element Call – Sprach-/Videoanrufe im Browser
  • Coturn – TURN/STUN-Server, damit Verbindungen auch hinter NAT/Firewalls funktionieren
  • LiveKit – SFU (Selective Forwarding Unit) für Gruppen-VoIP
  • ntfy – Push-Benachrichtigungen aufs Handy (ohne Google/Apple)
  • Synapse-Admin – GUI für User-Management statt Kommandozeile
  • Maubot – Bot-Plattform für Automatisierungen
  • Synapse Compressor – hält die Datenbank klein und schnell

Jeder Container erfüllt genau eine Aufgabe – das ist der Docker-Gedanke. Fällt einer aus (z.B. ntfy), funktionieren die Chats trotzdem, nur die Push-Benachrichtigungen fehlen. Das macht das System robust und wartbar.

Warum Element und nicht ein anderer Client?

Element ist der Referenz-Client für Matrix – entwickelt von den gleichen Leuten, die Matrix gebaut haben (Element-HQ, früher New Vector). Du bekommst alle neuen Features zuerst, die beste UX und die aktivste Entwicklung. Andere Clients wie FluffyChat oder SchildiChat sind spezialisierter, aber für einen allgemeinen Server ist Element die richtige Wahl.

Warum LiveKit + Coturn? Reicht nicht eines?

Gute Frage! Beide erfüllen unterschiedliche Aufgaben:

  • Coturn ist ein TURN/STUN-Server – er hilft Verbindungen durch NAT/Firewalls zu bekommen (holepunching). Wenn zwei Nutzer in verschiedenen Netzwerken telefonieren, kann Coturn die Daten weiterleiten, wenn eine direkte Verbindung nicht möglich ist.
  • LiveKit ist ein SFU-Server (Selective Forwarding Unit) für Gruppen-VoIP. Stell es dir wie eine Telefonkonferenz-Brücke vor: Ein Teilnehmer sendet seinen Audio/Video-Stream an LiveKit, und LiveKit verteilt ihn an alle anderen Teilnehmer. Das entlastet die Endgeräte massiv – bei 5 Teilnehmern muss jeder nur EINEN Stream senden und 4 empfangen, statt selbst 4 Verbindungen aufzubauen.

Beide zusammen ermöglichen native Gruppen-VoIP-Räume in Matrix – ohne dass Browser-Plugins oder zusätzliche Apps nötig sind.

1. Voraussetzungen

  • Server mit Ubuntu 24.04 LTS (AMD64/ARM64)
  • Docker & Docker Compose installiert
  • Eigene Domain (z.B. matrix.example.com, livekit.example.com)
  • SSL-Zertifikate (Let’s Encrypt via Certbot)
  • Mindestens 4 GB RAM, 20 GB Disk

2. docker-compose.yml – Der komplette Stack

Synapse + PostgreSQL

services:
  synapse:
    image: matrixdotorg/synapse:latest
    container_name: matrix-synapse
    restart: unless-stopped
    volumes:
      - ./synapse-data:/data
    environment:
      SYNAPSE_SERVER_NAME: "example.com"
      SYNAPSE_REPORT_STATS: "no"
    ports:
      - "127.0.0.1:8008:8008"
    depends_on:
      matrix-db:
        condition: service_healthy

  matrix-db:
    image: postgres:16-alpine
    container_name: matrix-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: synapse
      POSTGRES_USER: synapse
      POSTGRES_PASSWORD: DEIN_SICHERES_DB_PASSWORT
      POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U synapse -d synapse"]
      interval: 10s
      retries: 5

Element Web + Element Call

  element:
    image: vectorim/element-web:latest
    container_name: matrix-element
    restart: unless-stopped
    volumes:
      - ./element-config.json:/app/config.json
    ports:
      - "127.0.0.1:8765:80"

  element-call:
    image: ghcr.io/element-hq/element-call:latest
    container_name: matrix-element-call
    restart: unless-stopped
    volumes:
      - ./element-call-config.json:/app/config.json
    ports:
      - "127.0.0.1:8768:80"

Element Config (element-config.json)

{
    "default_server_config": {
        "m.homeserver": {
            "base_url": "https://matrix.example.com"
        }
    },
    "brand": "Mein Matrix",
    "defaultCountryCode": "DE"
}

Element Call Config

{
  "homeserver_url": "https://matrix.example.com",
  "default_device_name": "Element Call",
  "features": {
    "feature_video_rooms": true
  }
}

Coturn (TURN/STUN)

  coturn:
    image: coturn/coturn:latest
    container_name: matrix-coturn
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./coturn/turnserver.conf:/etc/turnserver.conf:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
    command: ["-c", "/etc/turnserver.conf"]

turnserver.conf

realm=matrix.example.com
server-name=matrix.example.com
listening-port=3478
tls-listening-port=5349
external-ip=DEINE_SERVER_IP
min-port=49152
max-port=65535

use-auth-secret
static-auth-secret=DEIN_TURN_SECRET

cert=/etc/letsencrypt/live/example.com/fullchain.pem
pkey=/etc/letsencrypt/live/example.com/privkey.pem

Wichtig: Coturn läuft im network_mode: host, weil es die UDP-Port-Range 49152-65535 direkt benötigt. Diese Ports müssen in deiner Firewall offen sein!

LiveKit + JWT-Service (native Gruppen-VoIP)

  livekit:
    image: livekit/livekit-server:latest
    container_name: matrix-livekit
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./livekit/livekit.yaml:/etc/livekit.yaml:ro
    command: --config /etc/livekit.yaml

  livekit-jwt:
    build: ./livekit-jwt-custom
    container_name: matrix-livekit-jwt
    restart: unless-stopped
    ports:
      - "127.0.0.1:8769:8080"

livekit.yaml

port: 7880
rtc:
  port_range_start: 50000
  port_range_end: 50200
  use_external_ip: false
  node_ip: "DEINE_SERVER_IP"
keys:
  DEIN_LIVEKIT_API_KEY: DEIN_LIVEKIT_API_SECRET
turn:
  enabled: true
  domain: matrix.example.com
  cert_file: /etc/letsencrypt/live/example.com/fullchain.pem
  key_file: /etc/letsencrypt/live/example.com/privkey.pem
  tls_port: 5348
  udp_port: 3479

ntfy (Push-Benachrichtigungen)

  ntfy:
    image: binwiederhier/ntfy:latest
    container_name: matrix-ntfy
    restart: unless-stopped
    volumes:
      - ./ntfy-data:/var/lib/ntfy
      - ./ntfy-server.yml:/etc/ntfy/server.yml:ro
    command: ["serve"]
    ports:
      - "127.0.0.1:8767:80"

ntfy-server.yml

base-url: "https://ntfy.example.com"
listen-http: ":80"
behind-proxy: true
cache-file: "/var/lib/ntfy/cache.db"
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
enable-signup: false
enable-login: true

Synapse-Admin & Maubot

  synapse-admin:
    image: awesometechnologies/synapse-admin:latest
    ports:
      - "127.0.0.1:8766:80"

  maubot:
    image: maubot-local
    container_name: matrix-maubot
    restart: unless-stopped
    volumes:
      - ./maubot:/data
    ports:
      - "127.0.0.1:29316:29316"

State Compressor (läuft täglich)

  synapse-auto-compressor:
    image: ghcr.io/matrix-org/rust-synapse-compress-state:latest
    container_name: matrix-synapse-auto-compressor
    restart: unless-stopped
    environment:
      PGPASSWORD: DEIN_DB_PASSWORT
    entrypoint: ["/bin/sh", "/scripts/compressor-entrypoint.sh"]
    volumes:
      - ./compressor-entrypoint.sh:/scripts/compressor-entrypoint.sh:ro

compressor-entrypoint.sh

#!/bin/bash
CONN_STR="host=matrix-db port=5432 user=synapse password=DEIN_PASSWORT dbname=synapse"
while true; do
    synapse_auto_compressor -p "$CONN_STR" -c 500 -n 100 -l 3
    sleep 86400
done

3. Synapse Homeserver Konfiguration

Die homeserver.yaml – die zentrale Konfigurationsdatei:

Datenbank

database:
  name: psycopg2
  args:
    user: synapse
    password: DEIN_PASSWORT
    database: synapse
    host: matrix-db
    cp_min: 5
    cp_max: 10

Listener

listeners:
  - port: 8008
    resources:
      - names: [client, federation]
    type: http
    x_forwarded: true

E-Mail (für Passwort-Reset & Validierung)

email:
  smtp_host: mail.example.com
  smtp_port: 587
  smtp_user: matrix@example.com
  smtp_pass: DEIN_PASSWORT
  enable_tls: true
  notif_from: "Mein Matrix <matrix@example.com>"
  app_name: Mein Matrix
  enable_notifs: true

LiveKit Integration (native Gruppen-VoIP)

experimental_features:
  msc3401_native_group_voip: true

livekit:
  server_url: "https://livekit.example.com"
  jwt_key: "DEIN_API_KEY"
  jwt_secret: "DEIN_API_SECRET"

TURN

turn_uris:
  - "turn:matrix.example.com?transport=udp"
  - "turn:matrix.example.com?transport=tcp"
turn_shared_secret: "DEIN_TURN_SECRET"
turn_user_lifetime: "1h"
turn_allow_guests: false

Media & Speicher

enable_registration: false
max_upload_size: "500M"
max_avatar_size: 2M
max_image_pixels: 32M

media_retention:
  remote_media_lifetime: 365d

dynamic_thumbnails: false
thumbnail_sizes:
  - width: 32   height: 32   method: crop
  - width: 96   height: 96   method: crop
  - width: 320  height: 240  method: scale
  - width: 640  height: 480  method: scale
  - width: 800  height: 600  method: scale

4. Nginx Reverse-Proxy

Alle Services laufen hinter einem Nginx-Reverse-Proxy auf Port 443:

# matrix.example.com → Synapse Client API (+ Federation)
server {
    listen 443 ssl http2;
    server_name matrix.example.com;

    location /_matrix {
        proxy_pass http://127.0.0.1:8008;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        client_max_body_size 500M;
    }

    location /_synapse {
        proxy_pass http://127.0.0.1:8008;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

# element.example.com → Element Web Client
server {
    listen 443 ssl http2;
    server_name element.example.com;
    location / {
        proxy_pass http://127.0.0.1:8765;
    }
}

# call.example.com → Element Call
server {
    listen 443 ssl http2;
    server_name call.example.com;
    location / {
        proxy_pass http://127.0.0.1:8768;
    }
}

# livekit.example.com → LiveKit JWT-Service
server {
    listen 443 ssl http2;
    server_name livekit.example.com;
    location / {
        proxy_pass http://127.0.0.1:8769;
        client_max_body_size 10M;
    }
}

# admin.example.com → Synapse Admin
server {
    listen 443 ssl http2;
    server_name admin.example.com;
    location / {
        proxy_pass http://127.0.0.1:8766;
    }
}

# ntfy.example.com → Push-Notifications
server {
    listen 443 ssl http2;
    server_name ntfy.example.com;
    location / {
        proxy_pass http://127.0.0.1:8767;
    }
}

5. LiveKit JWT-Service (Python)

Ein selbst geschriebener Python-Dienst, der Matrix-Access-Tokens validiert und LiveKit-JWT-Tokens für native VoIP-Räume ausstellt:

from http.server import HTTPServer, BaseHTTPRequestHandler
import json, time, hmac, hashlib, base64, urllib.request

LIVEKIT_URL = "wss://livekit.example.com"
LIVEKIT_API_KEY = "DEIN_API_KEY"
LIVEKIT_API_SECRET = "DEIN_API_SECRET"
HOMESERVER = "https://matrix.example.com"

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        # 1. Matrix Access Token validieren
        token = self.headers.get("Authorization", "").replace("Bearer ", "")
        req = urllib.request.Request(
            f"{HOMESERVER}/_matrix/client/v3/account/whoami",
            headers={"Authorization": f"Bearer {token}"}
        )
        try:
            with urllib.request.urlopen(req, timeout=5) as resp:
                user_id = json.loads(resp.read()).get("user_id")
        except:
            self.send_response(403); return

        # 2. LiveKit JWT generieren
        room_id = self.path.strip("/").split("/")[-1]
        now = int(time.time())
        header = b64url(json.dumps({"alg": "HS256"}))
        payload = b64url(json.dumps({
            "iss": LIVEKIT_API_KEY, "sub": user_id,
            "iat": now, "exp": now + 3600, "room": room_id,
            "join": True, "canPublish": True, "canSubscribe": True,
        }))
        sig = b64url(hmac.new(LIVEKIT_API_SECRET.encode(), header + b"." + payload, hashlib.sha256).digest())
        lk_token = f"{header}.{payload}.{sig}"

        self.send_response(200)
        self.wfile.write(json.dumps({"url": LIVEKIT_URL, "token": lk_token}).encode())

HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()

6. Borg Backup & Restore

Backup-Skript (täglich via Cron)

#!/bin/bash
BORG_REPO="/opt/backup/Matrix"
export BORG_PASSPHRASE="DEINE_BORG_PASSPHRASE"

# 1. PostgreSQL Dump
docker exec matrix-postgres pg_dump -U synapse synapse > /opt/matrix/postgres_dump.sql

# 2. Container stoppen
cd /opt/matrix && docker compose down --timeout 30

# 3. Borg Backup
borg create --compression=zstd,3 --exclude-caches 
    ::"matrix-$(date +%Y%m%d-%H%M%S)" 
    /opt/matrix

# 4. Alte Backups bereinigen (letztes behalten)
borg prune --keep-last=1 --force

# 5. Container starten
docker compose up -d
rm -f /opt/matrix/postgres_dump.sql

Restore-Skript

#!/bin/bash
ARCHIVE="$1"  # z.B. matrix-20260609-120000

cd /opt/matrix && docker compose down

# Daten sichern als Fallback
mv /opt/matrix /opt/matrix.pre-restore-$(date +%s)

# Borg wiederherstellen
mkdir -p /opt/matrix
borg extract ::"$ARCHIVE" --destination /

# PostgreSQL Dump einspielen
docker compose up -d matrix-db
sleep 10
docker exec -i matrix-postgres psql -U synapse -d synapse < /opt/matrix/postgres_dump.sql

# Alle Container starten
docker compose up -d

7. Erste Schritte nach der Installation

  1. User anlegen: docker exec matrix-synapse register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008
  2. Admin-Rechte vergeben: In der Datenbank: UPDATE users SET admin = 1 WHERE name = '@username:example.com';
  3. Federation testen: curl https://matrix.example.com/_matrix/federation/v1/version
  4. Element öffnen: https://element.example.com – einloggen und testen
  5. ntfy einrichten: Element → Einstellungen → Benachrichtigungen → Push-Server: https://ntfy.example.com

8. Firewall & Ports

Port Protokoll Service
443 TCP Nginx (HTTPS)
80 TCP Nginx (HTTP → HTTPS Redirect)
3478 TCP+UDP Coturn (TURN/STUN)
5349 TCP+UDP Coturn (TLS)
49152-65535 UDP Coturn Port-Range
7880 TCP LiveKit
50000-50200 UDP LiveKit RTC Port-Range

9. Wartung & Monitoring

# Alle Container Status
docker ps --format "table {{.Names}}t{{.Status}}"

# Synapse Logs
docker logs matrix-synapse --tail 50

# Datenbank-Größe
docker exec matrix-postgres psql -U synapse -c "
    SELECT pg_database_size('synapse')/1024/1024 AS mb;"

# Federation Tester
https://federationtester.matrix.org/?server_name=example.com

# Tägliches Backup läuft via Cron
# State Compression läuft automatisch

Fazit

Du hast jetzt einen kompletten, produktionsreifen Matrix-Stack mit:

  • ✅ Synapse Homeserver (Federation + Client)
  • ✅ PostgreSQL 16 mit automatischer State Compression
  • ✅ Element Web Client + Element Call (native Gruppen-VoIP)
  • ✅ Coturn TURN/STUN für zuverlässige WebRTC-Verbindungen
  • ✅ LiveKit SFU + Custom JWT-Service für skalierbare VoIP-Räume
  • ✅ ntfy Push-Benachrichtigungen (auch für iOS!)
  • ✅ Synapse-Admin Web UI für User-Management
  • ✅ Maubot für eigene Bots
  • ✅ Borg Backup/Restore mit 1-Klick Recovery
  • ✅ Nginx Reverse-Proxy mit SSL

🔗 matrix.org | Weitere Projekte

X

Basti

Entwickler und Smart-Home-Enthusiast aus Deutschland. Ich entwickle Open-Source-Integrationen für Home Assistant (IDM Wärmepumpe, Violet Pool Controller), Modbus-Tools (ModBridge) und betreibe verschiedene Server-Dienste (Matrix, Seafile, SOGo).

GitHub →

Schreibe einen Kommentar