Eigener Familien-Chat mit Matrix, Synapse und Discord-Bridge

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_files in 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.

Read more

KI auf dem eigenen Server: Wie wir ein lokales LLM für die automatische Auswertung von Pflegeinformationen eingerichtet haben

Wir arbeiten seit einiger Zeit am Angehörigen-Kompass, einem Projekt, das pflegende Angehörige mit aktuellen Informationen zu Leistungen, Formularen und Prozessen unterstützen soll. Dabei stellt sich immer wieder die gleiche Frage: Woher kommen aktuelle, verlässliche Daten – und wie halten wir sie aktuell? Gesetze ändern sich. Pflegegeld-Beträge werden angepasst. Formulare werden überarbeitet.

By gunnar