Eigener Familien-Chat mit Matrix, Synapse und Discord-Bridge
Warum überhaupt?
Discord ist praktisch — aber die Daten liegen bei einem US-Konzern, Werbung kann jederzeit kommen, und man hat keine Kontrolle. Für die Familie wollte ich etwas Eigenes: Audio, Video, Bildschirmübertragung, Emojis, Bilder, Videos. Und weil nicht alle sofort von Discord wegkommen wollen: eine Bridge, die beide Welten verbindet.
Das Ergebnis: ein selbst gehosteter Matrix-Server auf meinem VPS, der mit Discord föderiert. Alle Familienmitglieder können über Element oder FluffyChat chatten — und wer noch auf Discord ist, schreibt trotzdem mit.
Die Architektur
Internet
│
Traefik (SSL, Routing)
│
├── chat.magholder.click → Synapse (Matrix-Homeserver)
├── element.magholder.click → Element Web (Browser-Client)
└── /.well-known/matrix → Nginx (Federation-Infos)
Intern:
Synapse ←→ PostgreSQL (Datenbank)
Synapse ←→ mautrix-discord (Discord-Bridge)
Alle Container laufen in Docker mit Traefik als Reverse Proxy und automatischen Let's Encrypt-Zertifikaten — so wie ich es auch für meine anderen Projekte mache.
Der holprige Weg: Conduit
Ich hatte ursprünglich mit Conduit angefangen — ein leichtgewichtiger Matrix-Homeserver in Rust, SQLite-basiert, keine externen Dependencies. Klingt perfekt.
War es nicht.
Die Probleme:
- Der Admin-Raum hat auf keine Befehle reagiert
appservice_registration_filesin der Config hat nichts bewirkt- User anlegen war nur über einen kurzen API-Umweg möglich
- Appservice-Support ist laut Conduit-Doku selbst als "experimentell" markiert
Nach ein paar Stunden Debugging war klar: Conduit ist für den Heimgebrauch ohne Bridges gut genug, aber für mautrix-discord zu unfertig. Der Wechsel zu Synapse war die richtige Entscheidung.
Lesson learned: Für einfache Familienchats ohne Bridges tut Conduit seinen Job. Wer Bridges braucht, nimmt Synapse.
Synapse + PostgreSQL
Synapse ist der offizielle Matrix-Homeserver von matrix.org. Schwerer als Conduit, aber stabil und mit ausgereiftem Appservice-Support.
Netzwerk-Überlegung
Die Bridge und die Datenbank sollen nicht von außen erreichbar sein. Deshalb zwei Docker-Netzwerke:
matrix-internal— nur für Synapse, PostgreSQL und die Bridge (kein Traefik)traefik-public— für Synapse und Element (Traefik routet rein)
Synapse hängt in beiden Netzwerken und ist damit das einzige Gateway nach innen.
docker-compose.yml
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: synapse
POSTGRES_USER: synapse
POSTGRES_PASSWORD: "SicheresPasswort!"
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- matrix-internal
synapse:
image: matrixdotorg/synapse:latest
restart: unless-stopped
depends_on:
- postgres
volumes:
- ./synapse:/data
- ./discord-bridge/registration.yaml:/data/discord-registration.yaml:ro
networks:
- matrix-internal
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.synapse.rule=Host(`chat.magholder.click`)"
- "traefik.http.routers.synapse.entrypoints=websecure"
- "traefik.http.routers.synapse.tls.certresolver=letsencrypt"
- "traefik.http.services.synapse.loadbalancer.server.port=8008"
element:
image: vectorim/element-web:latest
restart: unless-stopped
volumes:
- ./element-config.json:/app/config.json:ro
networks:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.element.rule=Host(`element.magholder.click`)"
- "traefik.http.routers.element.entrypoints=websecure"
- "traefik.http.routers.element.tls.certresolver=letsencrypt"
- "traefik.http.services.element.loadbalancer.server.port=80"
matrix-wellknown:
image: nginx:alpine
restart: unless-stopped
volumes:
- ./wellknown:/usr/share/nginx/html/.well-known/matrix:ro
networks:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.matrix-wellknown.rule=Host(`chat.magholder.click`) && PathPrefix(`/.well-known/matrix`)"
- "traefik.http.routers.matrix-wellknown.priority=10"
- "traefik.http.routers.matrix-wellknown.entrypoints=websecure"
- "traefik.http.routers.matrix-wellknown.tls.certresolver=letsencrypt"
- "traefik.http.services.matrix-wellknown.loadbalancer.server.port=80"
mautrix-discord:
image: dock.mau.dev/mautrix/discord:latest
restart: unless-stopped
depends_on:
- synapse
volumes:
- ./discord-bridge:/data
networks:
- matrix-internal
- traefik-public
volumes:
postgres_data:
networks:
matrix-internal:
internal: true
traefik-public:
external: true
Synapse-Config generieren
docker run --rm \
-v /opt/matrix/synapse:/data \
-e SYNAPSE_SERVER_NAME=chat.magholder.click \
-e SYNAPSE_REPORT_STATS=no \
matrixdotorg/synapse:latest generate
Wichtige Anpassungen in synapse/homeserver.yaml:
database:
name: psycopg2
args:
user: synapse
password: "SicheresPasswort!"
database: synapse
host: postgres
cp_min: 5
cp_max: 10
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
bind_addresses: ['0.0.0.0'] # WICHTIG: sonst hört Synapse nur auf localhost
resources:
- names: [client, federation]
compress: false
enable_registration: true
registration_requires_token: true
max_upload_size: 50M
app_service_config_files:
- /data/discord-registration.yaml
Well-Known für Federation
Damit andere Matrix-Server wissen wo euer Server zu erreichen ist:
wellknown/server:
{ "m.server": "chat.magholder.click:443" }
wellknown/client:
{
"m.homeserver": {
"base_url": "https://chat.magholder.click"
}
}
Die Discord-Bridge: mautrix-discord
Config generieren und anpassen
mkdir -p /opt/matrix/discord-bridge/logs
docker compose run --rm mautrix-discord # erzeugt config.yaml
Die wichtigsten Stellen in discord-bridge/config.yaml:
homeserver:
address: http://synapse:8008 # intern! nicht die öffentliche URL
domain: chat.magholder.click
appservice:
address: http://mautrix-discord:29334
database:
type: sqlite3-fk-wal
uri: file:/data/mautrix-discord.db?_txlock=immediate
bridge:
permissions:
"*": relay
"chat.magholder.click": user
"@gunnar:chat.magholder.click": admin
double_puppet_server_map:
chat.magholder.click: https://chat.magholder.click
Wichtig: homeserver.address muss auf den internen Container-Namen zeigen (http://synapse:8008), nicht auf die öffentliche Domain. Sonst versucht die Bridge DNS-Lookups die im internen Netzwerk nicht funktionieren.
Registration bei Synapse anmelden
# Alte registration.yaml löschen und neu generieren
rm /opt/matrix/discord-bridge/registration.yaml
docker compose run --rm mautrix-discord
# Datei lesbar machen
chmod 644 /opt/matrix/discord-bridge/registration.yaml
Logs-Verzeichnis nicht vergessen
Die Bridge schreibt Logs in ./logs/ — das Verzeichnis muss existieren, sonst crasht der Container lautlos:
mkdir -p /opt/matrix/discord-bridge/logs
Stolperfallen-Sammlung
Nach einem langen Tag hier die Fehler die mich am meisten Zeit gekostet haben:
1. bind_addresses in der Synapse-Config Ohne bind_addresses: ['0.0.0.0'] hört Synapse nur auf localhost — die Bridge kann sich nicht verbinden.
2. Bridge-Logs Verzeichnis Wenn ./logs/ nicht existiert, crasht der Container ohne Fehlermeldung. docker compose logs zeigt dann einfach nichts.
3. homeserver.address in der Bridge-Config Muss die interne URL sein (http://synapse:8008), nicht die öffentliche. Sonst schlägt DNS-Lookup fehl weil der Container-Name von außen nicht auflösbar ist.
4. chmod 644 für die registration.yaml Synapse braucht Leserechte auf die Datei. Ohne den chmod gibt es einen PermissionError beim Start.
5. Conduit Admin-Raum Hat bei mir nie funktioniert. Falls ihr Conduit nutzen wollt und Bridges braucht: direkt zu Synapse greifen.
Ergebnis
Der Stack läuft stabil. Die Familie chattet über Element und FluffyChat, Discord-Nachrichten kommen und gehen über die Bridge. Video und Audio funktionieren über Element Call direkt im Browser — kein extra Tool nötig.
Empfohlene Clients:
- Windows/Linux Desktop: Element Desktop
- Android/iOS: FluffyChat (bessere UX)
- Browser: Element Web
Für Discord-Umsteiger: Cinny (cinny.in) hat eine Discord-ähnliche UI, unterstützt aber kein Audio/Video.
Der komplette Stack liegt in /opt/matrix/ mit allen Config-Dateien versioniert — so kann ich bei Problemen schnell wiederherstellen.