← Все заметки
Приложения и сервисы

Self-hosted Matrix на AlmaLinux 9: Synapse, Element X и MatrixRTC/LiveKit

Практический разбор приватного Matrix-сервера с PostgreSQL, nginx, Element X и видеозвонками через LiveKit за NAT и общим TLS-ingress.

Статус: актуальноОпубликовано: 2026-09-23Обновлено: 2026-09-23

Проверено на: AlmaLinux 9.8 · Synapse 1.161.0 · LiveKit 1.13.7 · lk-jwt-service 0.7.0 · Element X 26.09.2

Нужно было заменить старый Jabber и получить приватный мессенджер без зависимости от Telegram: с нормальным Android-клиентом, локальными пользователями, E2EE, файлами, push-уведомлениями и рабочими аудио/видеозвонками.

В итоге получилась полностью self-hosted схема на Matrix Synapse с PostgreSQL, Element X и MatrixRTC/LiveKit. Самая интересная часть проекта оказалась не в установке Synapse, а в том, как правильно состыковать Matrix identity, .well-known, OpenID, LiveKit, reverse proxy, NAT и split DNS.

Ниже — архитектура, ключевые решения и проблемы, которые пришлось разбирать по ходу внедрения.

01 / ЗАДАЧА

Исходные требования были простыми:

Главный принцип — не открывать наружу лишние сервисы и не строить отдельный публичный frontend для каждого компонента.

02 / АРХИТЕКТУРА

Matrix VM работает на AlmaLinux 9.

Основные компоненты:

Matrix VM
├── Synapse
├── PostgreSQL
├── nginx
├── LiveKit
└── lk-jwt-service

Снаружи используется общий TLS-ingress:

Internet
   |
   | TCP/443
   v
public IP
   |
   v
srv-prom / nginx
   |
   +--> matrix.shandrov.ru
   |       |
   |       v
   |   srv-matrix-01 nginx
   |       |
   |       v
   |   Synapse :8008
   |
   +--> rtc.shandrov.ru
           |
           v
       srv-matrix-01 nginx
           |
           +--> /livekit/sfu --> LiveKit :7880
           |
           +--> /livekit/jwt --> lk-jwt-service :8080

WebRTC media идёт напрямую на LiveKit:

Internet
   |
   +--> TCP/7881
   |
   +--> UDP/7882
           |
           v
      LiveKit SFU

Такой вариант позволил не ломать уже существующий HTTPS ingress, на котором одновременно работают Grafana и другие сервисы.

03 / MATRIX IDENTITY И SYNAPSE

Ключевой параметр Synapse:

server_name: "shandrov.ru"
public_baseurl: "https://matrix.shandrov.ru/"

Это принципиальное разделение.

server_name задаёт Matrix identity:

@admin:shandrov.ru
@user1:shandrov.ru
@user2:shandrov.ru

А public_baseurl указывает, где реально находится homeserver.

Synapse слушает только loopback:

listeners:
  - port: 8008
    type: http
    tls: false
    bind_addresses:
      - "127.0.0.1"

Наружу он напрямую не публикуется.

В качестве БД используется PostgreSQL 16. Для media store выделен отдельный диск, смонтированный в /srv/matrix-media.

Саморегистрация отключена:

enable_registration: false
allow_guest_access: false

Пользователи создаются через консольный register_new_matrix_user.

04 / USER DIRECTORY ДЛЯ ПРИВАТНОГО HOMESERVER

На маленьком закрытом сервере нет смысла заставлять пользователей вручную вводить полный MXID каждого собеседника.

Поэтому был включён локальный каталог:

user_directory:
  enabled: true
  search_all_users: true
  prefer_local_users: true
  exclude_remote_users: true

После этого Element X начал нормально находить локальных пользователей.

Это мелочь, но для домашнего или корпоративного private homeserver сильно улучшает usability.

05 / TLS INGRESS И SPLIT DNS

Публичный 443/tcp уже использовался Grafana, поэтому напрямую пробрасывать его на Matrix VM было нельзя.

Решение — общий nginx ingress с SNI:

grafana.shandrov.ru -> Grafana
matrix.shandrov.ru  -> Synapse frontend
rtc.shandrov.ru     -> MatrixRTC / LiveKit

Внутри сети эти имена должны разрешаться не в публичный адрес, а прямо в LAN-адрес ingress.

На MikroTik используются split-DNS записи:

grafana.shandrov.ru -> 10.20.30.10
matrix.shandrov.ru  -> 10.20.30.10
rtc.shandrov.ru     -> 10.20.30.10

Отсутствие split DNS для одного из имён сразу проявилось характерно: curl по локальному адресу работал, а браузер пытался идти через публичный IP и получал timeout.

После добавления локальной DNS-записи и очистки DNS-кэша проблема исчезла.

06 / MATRIX DISCOVERY

Так как Matrix ID используют домен shandrov.ru, а homeserver находится на matrix.shandrov.ru, на основном сайте нужен:

https://shandrov.ru/.well-known/matrix/client

Содержимое:

{
  "m.homeserver": {
    "base_url": "https://matrix.shandrov.ru"
  },
  "org.matrix.msc4143.rtc_foci": [
    {
      "type": "livekit",
      "livekit_service_url": "https://rtc.shandrov.ru/livekit/jwt"
    }
  ]
}

Основной сайт находится на отдельном VPS и обслуживается Caddy.

Важно: этот endpoint только сообщает клиенту адрес homeserver и RTC authorization service. Matrix-трафик через web-сервер сайта не проксируется.

07 / MATRIXRTC И LIVEKIT

Современный Element X использует MatrixRTC. Одного coturn для этого недостаточно.

Для звонков понадобились:

Synapse
   |
   +--> MatrixRTC transport discovery
   |
lk-jwt-service
   |
   +--> проверка Matrix/OpenID
   +--> выдача LiveKit JWT
   |
LiveKit SFU

Используемые на момент внедрения версии:

Synapse          1.161.0
LiveKit          1.13.7
lk-jwt-service   0.7.0

LiveKit настроен на:

port: 7880

rtc:
  tcp_port: 7881
  udp_port: 7882
  use_external_ip: true
  advertise_internal_ip: true
  skip_external_ip_validation: true

7880/tcp используется только за reverse proxy.

Наружу напрямую опубликованы только WebRTC media:

7881/tcp
7882/udp

Для небольшой установки удобно использовать один UDP mux port 7882, а не большой диапазон UDP-портов.

Дополнительно пришлось увеличить receive buffer:

net.core.rmem_max = 5000000

После этого предупреждение LiveKit о слишком маленьком UDP receive buffer исчезло.

08 / MATRIXRTC В SYNAPSE

В Synapse включены необходимые experimental features:

experimental_features:
  msc3266_enabled: true
  msc4143_enabled: true
  msc4222_enabled: true
  msc4502_enabled: true
  msc4512_enabled: true

MatrixRTC transport:

matrix_rtc:
  transports:
    - type: livekit
      url: "wss://rtc.shandrov.ru/livekit/sfu"
      livekit_service_url: "https://rtc.shandrov.ru/livekit/jwt"

Для OpenID в listener добавлен resource:

resources:
  - names:
      - client
      - openid

При этом полноценная federation не включалась.

09 / APPLICATION SERVICE ДЛЯ LK-JWT-SERVICE

lk-jwt-service подключён к Synapse как Application Service.

Критический момент — правильные имена параметров MSC4502/MSC4512:

io.element.msc4502.scopes:
  - "urn:matrix:client:io.element.msc4502:rooms:is_joined"

io.element.msc4512.proxy_prefix: "rtc/livekit"
io.element.msc4512.proxy_url: "http://127.0.0.1:8080"

До исправления этих полей Synapse загружал Application Service, но в журнале показывал:

proxy_url: None
proxy_prefix: None
scopes: set()

После корректной конфигурации:

proxy_url: http://127.0.0.1:8080
proxy_prefix: rtc/livekit
scopes: urn:matrix:client:io.element.msc4502:rooms:is_joined

Это хороший пример того, почему одного systemctl status недостаточно: сервис был active, но функциональная интеграция ещё не работала.

10 / ДВЕ ОШИБКИ, КОТОРЫЕ СТОИЛИ БОЛЬШЕ ВСЕГО ВРЕМЕНИ

MISSING_MATRIX_RTC_TRANSPORT

Первоначально Element X показывал:

MISSING_MATRIX_RTC_TRANSPORT

Причина была ожидаемая: homeserver ещё не публиковал MatrixRTC transport.

После настройки matrix_rtc Synapse начал возвращать:

{
  "rtc_transports": [
    {
      "type": "livekit",
      "url": "wss://rtc.shandrov.ru/livekit/sfu",
      "livekit_service_url": "https://rtc.shandrov.ru/livekit/jwt"
    }
  ]
}

OPEN_ID_ERROR

После этого ошибка изменилась на:

OPEN_ID_ERROR

Element X успешно получал OpenID token от Synapse, но lk-jwt-service должен был проверить его через Matrix OpenID userinfo endpoint.

Так как Matrix identity — shandrov.ru, сервис сначала искал:

https://shandrov.ru/.well-known/matrix/server

А такого endpoint ещё не было.

При отсутствии discovery сервис пытался использовать стандартный federation fallback, что в данной архитектуре не соответствовало реальному адресу Synapse.

Решение:

https://shandrov.ru/.well-known/matrix/server
{
  "m.server": "matrix.shandrov.ru:443"
}

После этого OpenID userinfo начал корректно возвращать:

{
  "sub": "@admin:shandrov.ru"
}

Важно: наличие matrix/server не означает, что на homeserver обязательно нужно включать полную federation. В этой схеме Synapse публикует только необходимый OpenID endpoint.

11 / ПРОВЕРКА RTC БЕЗ КЛИЕНТА

Перед тестом на телефоне была отдельно проверена серверная цепочка.

Запрос токена через MatrixRTC вернул:

HTTP 200
jwt_received = true

lk-jwt-service после этого создал LiveKit room и выдал SFU access token.

LiveKit зарегистрировал создание комнаты и успешный webhook.

При реальном звонке в журнале появилась уже полноценная RTC-сессия:

starting RTC session

То есть удалось проверить отдельно:

Synapse
  -> Application Service
  -> lk-jwt-service
  -> LiveKit Room API
  -> JWT
  -> WebSocket signaling
  -> WebRTC session

И только после этого звонок проверялся через Element X.

12 / REDIS: НУЖЕН ЛИ ОН ЗДЕСЬ

В этой установке lk-jwt-service сейчас работает без Redis.

Это не просто вопрос кэширования.

Если задать:

LIVEKIT_REDIS_URL

lk-jwt-service будет сохранять своё рабочее состояние в Redis и сможет восстановить его после рестарта. Без Redis используется in-memory store.

Для одиночной домашней установки это допустимо: сообщения, комнаты Matrix, media-файлы и история звонков от этого не теряются.

Redis имеет смысл добавить, если нужна:

Для текущего single-node варианта это полезное улучшение, но не обязательная зависимость.

13 / БЕЗОПАСНОСТЬ

Основные решения:

14 / РЕЗЕРВНОЕ КОПИРОВАНИЕ

Для полного восстановления недостаточно только VM backup.

Нужно сохранять как минимум:

PostgreSQL database
/srv/matrix-media
/etc/synapse/homeserver.yaml
Synapse signing key
Synapse secrets
/etc/matrix-rtc
/etc/livekit
nginx configuration
systemd units
TLS/ACME configuration
.well-known configuration

Особенно важен signing key Synapse. Потеря этого файла при восстановлении создаёт гораздо более неприятную проблему, чем потеря обычного конфигурационного файла.

Для текущей установки уже сделан полный backup после того, как сообщения и MatrixRTC были проверены в рабочем состоянии.

15 / РЕЗУЛЬТАТ

В итоге получился закрытый self-hosted Matrix:

Matrix ID:        @user:shandrov.ru
Homeserver:       matrix.shandrov.ru
RTC:              rtc.shandrov.ru
Client:           Element X
Database:         PostgreSQL
SFU:              LiveKit
Authorization:    lk-jwt-service
TLS ingress:      nginx
Root discovery:   Caddy

Работают:

Самая полезная часть этого проекта — не сама установка Synapse, а отладка полного service chain.

Когда Matrix, OpenID, LiveKit, DNS и reverse proxy разнесены по разным хостам, ошибка в одном маленьком discovery endpoint может выглядеть как проблема WebRTC, клиента или firewall.

Поэтому рабочий подход здесь тот же, что и в любой инфраструктуре: проверять каждый слой отдельно и только после этого переходить к end-to-end тесту.