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)

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