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
- User anlegen:
docker exec matrix-synapse register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008 - Admin-Rechte vergeben: In der Datenbank:
UPDATE users SET admin = 1 WHERE name = '@username:example.com'; - Federation testen:
curl https://matrix.example.com/_matrix/federation/v1/version - Element öffnen: https://element.example.com – einloggen und testen
- 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
