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
- 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