🤖 PR generated for branch `fix/cyrillic-fonts-dev-certs-and-page-head`.
### Changes Summary
- **Fonts**: Add Cyrillic glyph subsets for Lato, EB Garamond, and Fira
Code.
- **Dev**: Support remote Root CA and encrypted dev CA key in
`.env.dev`.
- **UI & Kits**: Add size variant to `Breadcrumbs` component and enlarge
`PageHead` breadcrumbs with tuned collapse scroll threshold.
- **ESLint**: Add `no-hanging-particles` i18n & markdown typography rule
with auto-fixer.
- **Content & i18n**: Apply `ru-text` editorial and typography polish
(em dashes, number ranges, «ё» restoration, non-breaking spaces) across
`services/www` and `services/docs`.
436 lines
16 KiB
Plaintext
436 lines
16 KiB
Plaintext
---
|
||
title: 'Matrix Server Deployment Guide'
|
||
description: 'Understanding and deploying Synapse, Coturn, LiveKit SFU, and Native Matrix RTC'
|
||
createdDate: 2026-02-15
|
||
tags: ['self-hosting', 'guides', 'matrix']
|
||
---
|
||
|
||
Deploying a Matrix communications stack involves more than running a single web server. Matrix is an open, federated protocol for real-time, decentralized, end-to-end encrypted communication. While basic messaging requires only a homeserver and a database, modern voice and video capabilities—ranging from peer-to-peer 1:1 calls to multi-party group video conferences—require dedicated media relays and authentication bridges.
|
||
|
||
This article walks through the inner workings and deployment of a production-ready Matrix stack utilizing **Synapse**, **PostgreSQL**, **Coturn**, **LiveKit SFU**, and **lk-jwt-service**.
|
||
|
||
---
|
||
|
||
## Technical Stack Architecture
|
||
|
||
To understand how a Matrix communications deployment functions, we must look at how each of its five core components handles data:
|
||
|
||
1. **Synapse**: The primary homeserver engine. It manages user authentication, room state, message synchronization, end-to-end encryption (E2EE) key distribution, and federation with other homeservers across the global Matrix network.
|
||
2. **PostgreSQL**: The relational database powering Synapse. While Synapse supports SQLite for quick local experimentation, PostgreSQL is mandatory for production to handle concurrent database transactions, index room state, and maintain message history efficiently.
|
||
3. **Coturn (STUN/TURN)**: The NAT traversal relay. When two users initiate a 1:1 call, their devices attempt a direct peer-to-peer WebRTC connection. If firewalls or symmetric NATs block direct traffic, Coturn acts as a relay server, forwarding encrypted audio and video packets between peers over UDP or TCP.
|
||
4. **LiveKit SFU**: A high-performance Selective Forwarding Unit. Peer-to-peer meshes become bandwidth-prohibitive in group calls with three or more participants. LiveKit receives media streams from each speaker and forwards them to all other room members, reducing client upstream bandwidth.
|
||
5. **lk-jwt-service**: The authentication bridge for Element Call and Element X Native Matrix RTC (governed by MSC4143 and MSC4186). When a user joins a group call in Element X, `lk-jwt-service` verifies their Matrix session with Synapse and issues a short-lived JSON Web Token (JWT), allowing them to authenticate with LiveKit automatically.
|
||
6. **Reverse Proxy (Traefik / Nginx)**: The edge gateway. It terminates TLS certificates, handles WebSocket protocol upgrades for LiveKit signaling (`/livekit`), injects CORS headers for web clients, and serves Matrix client and federation auto-discovery JSON payloads (`.well-known`).
|
||
|
||
---
|
||
|
||
## 1. The Docker Infrastructure (docker-compose.yml)
|
||
|
||
Organizing these services into containerized units ensures isolated environments, consistent startup ordering, and clean network mapping.
|
||
|
||
In our setup, PostgreSQL includes a healthcheck (`pg_isready`) so that Synapse waits until database initialization is complete before attempting schema migrations. Services like Coturn, LiveKit, and `lk-jwt-service` run in network namespace mode (`network_mode: "service:synapse"`) or dedicated container networks, simplifying loopback communication while binding necessary host WebRTC and STUN/TURN ports directly.
|
||
|
||
Notice that specific port ranges are opened for media traffic:
|
||
- **Port 3478 & 5349 (TCP/UDP)**: Coturn STUN and TURN signaling.
|
||
- **Port Range 49152-49172 (UDP)**: Coturn media relay channels.
|
||
- **Port 7880 (TCP)**: LiveKit HTTP signaling and WebSockets.
|
||
- **Port 7881 (TCP)**: LiveKit WebRTC TCP fallback.
|
||
- **Port Range 50000-50020 (UDP)**: LiveKit SFU media channels.
|
||
|
||
The configuration below finalizes the infrastructure definition:
|
||
|
||
```yaml
|
||
networks:
|
||
matrix:
|
||
name: matrix-network
|
||
|
||
volumes:
|
||
synapse_data:
|
||
postgres_data:
|
||
|
||
services:
|
||
postgres:
|
||
image: postgres:16-alpine
|
||
container_name: matrix-db
|
||
environment:
|
||
POSTGRES_DB: synapse
|
||
POSTGRES_USER: synapse
|
||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||
POSTGRES_INITDB_ARGS: '--encoding=UTF-8 --lc-collate=C --lc-ctype=C'
|
||
volumes:
|
||
- postgres_data:/var/lib/postgresql/data
|
||
networks:
|
||
- matrix
|
||
healthcheck:
|
||
test: ['CMD-SHELL', 'pg_isready -U synapse -d synapse']
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
start_period: 30s
|
||
restart: unless-stopped
|
||
|
||
synapse:
|
||
image: ghcr.io/element-hq/synapse:v1.146.0
|
||
container_name: matrix-synapse
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
environment:
|
||
SYNAPSE_SERVER_NAME: '${DOMAIN}'
|
||
SYNAPSE_REPORT_STATS: 'no'
|
||
SYNAPSE_CONFIG_PATH: '/data/homeserver.yaml'
|
||
volumes:
|
||
- ./homeserver.yaml:/data/homeserver.yaml:ro
|
||
- synapse_data:/data
|
||
networks:
|
||
- matrix
|
||
ports:
|
||
# Matrix Client & Federation HTTP API
|
||
- '8008:8008'
|
||
# Coturn STUN/TURN Signaling
|
||
- '3478:3478/tcp'
|
||
- '3478:3478/udp'
|
||
- '5349:5349/tcp'
|
||
- '5349:5349/udp'
|
||
# Coturn Media Relay UDP Range
|
||
- '49152-49172:49152-49172/udp'
|
||
# LiveKit Signaling & WebRTC Ports
|
||
- '7880:7880/tcp'
|
||
- '7881:7881/tcp'
|
||
- '50000-50020:50000-50020/udp'
|
||
restart: unless-stopped
|
||
|
||
coturn:
|
||
image: coturn/coturn:4.8-alpine
|
||
container_name: matrix-coturn
|
||
depends_on:
|
||
- synapse
|
||
volumes:
|
||
- ./turnserver.conf:/etc/coturn/turnserver.conf:ro
|
||
network_mode: 'service:synapse'
|
||
command: ['-c', '/etc/coturn/turnserver.conf']
|
||
restart: unless-stopped
|
||
|
||
livekit:
|
||
image: livekit/livekit-server:v1.9
|
||
container_name: matrix-livekit
|
||
depends_on:
|
||
- synapse
|
||
volumes:
|
||
- ./livekit.yaml:/etc/livekit.yaml:ro
|
||
environment:
|
||
LIVEKIT_KEYS: '${LK_KEY}: ${LK_SECRET}'
|
||
network_mode: 'service:synapse'
|
||
command: --config /etc/livekit.yaml --node-ip ${SERVER_IP}
|
||
restart: unless-stopped
|
||
|
||
lk-jwt:
|
||
image: ghcr.io/element-hq/lk-jwt-service:0.4.1
|
||
container_name: matrix-lk-jwt
|
||
depends_on:
|
||
- synapse
|
||
environment:
|
||
LK_JWT_PORT: 8080
|
||
LIVEKIT_URL: wss://${DOMAIN}/livekit
|
||
LIVEKIT_KEY: '${LK_KEY}'
|
||
LIVEKIT_SECRET: '${LK_SECRET}'
|
||
network_mode: 'service:synapse'
|
||
restart: unless-stopped
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Homeserver Configuration (homeserver.yaml)
|
||
|
||
Synapse reads its operational rules from `homeserver.yaml`. Understanding key sections of this file prevents common administrative issues:
|
||
|
||
- **Server Identity (`server_name`)**: This string defines your server's authority and user ID domain suffix (e.g., `@user:matrix.example.com`). Changing this later breaks existing user IDs and room history.
|
||
- **Database Connection (`database`)**: Connects Synapse to PostgreSQL using the `psycopg2` driver. Setting `cp_min` and `cp_max` manages connection pooling efficiently under load.
|
||
- **Media Retention (`media_retention`)**: Matrix room members frequently share images and files. Setting `remote_media_lifetime` to 14 days automatically purges cached media from federated servers, protecting your disk space while preserving local media (`local_media_lifetime: 365d`).
|
||
- **VoIP Integration (`turn_uris` & `turn_shared_secret`)**: Informs Synapse of your Coturn installation. When two users place a call, Synapse uses `turn_shared_secret` to generate ephemeral, time-limited credentials, allowing clients to authenticate with Coturn securely.
|
||
- **Native Matrix RTC (MSC4143 / MSC4186)**: Modern clients like Element X and Element Call use Matrix Specification Proposals MSC4143 (RTC Foci) and MSC4186 (LiveKit Call Authentication). Adding `extra_well_known_client_content` tells Matrix clients that group call authentication is handled by your `lk-jwt-service` endpoint (`https://matrix.example.com/livekit/jwt`).
|
||
|
||
The complete `homeserver.yaml` configuration is structured as follows:
|
||
|
||
```yaml
|
||
# Server Identity
|
||
server_name: "matrix.example.com"
|
||
pid_file: /data/homeserver.pid
|
||
public_baseurl: "https://matrix.example.com"
|
||
|
||
# Listeners
|
||
listeners:
|
||
- port: 8008
|
||
tls: false
|
||
type: http
|
||
x_forwarded: true
|
||
resources:
|
||
- names: [client, federation]
|
||
compress: false
|
||
|
||
# Database
|
||
database:
|
||
name: psycopg2
|
||
args:
|
||
user: "synapse"
|
||
password: "YOUR_POSTGRES_PASSWORD"
|
||
database: "synapse"
|
||
host: "postgres"
|
||
cp_min: 5
|
||
cp_max: 10
|
||
|
||
# Security & Registration
|
||
enable_registration: false
|
||
enable_registration_captcha: false
|
||
registration_shared_secret: "YOUR_REGISTRATION_SHARED_SECRET"
|
||
|
||
# Media Storage & Retention
|
||
media_store_path: "/data/media_store"
|
||
max_upload_size: "50M"
|
||
max_image_pixels: "32M"
|
||
dynamic_thumbnails: true
|
||
|
||
media_retention:
|
||
remote_media_lifetime: 14d
|
||
local_media_lifetime: 365d
|
||
|
||
caches:
|
||
global_factor: 0.5
|
||
|
||
# VoIP / Coturn (1:1 Calls)
|
||
turn_uris:
|
||
- "turn:matrix.example.com:3478?transport=udp"
|
||
- "turn:matrix.example.com:3478?transport=tcp"
|
||
turn_shared_secret: "YOUR_TURN_SHARED_SECRET"
|
||
turn_user_lifetime: 1h
|
||
turn_allow_guests: true
|
||
|
||
# Native Matrix RTC (MSC4143 / MSC4186 for Element X Group Calls)
|
||
experimental_features:
|
||
msc3266_enabled: true
|
||
msc4222_enabled: true
|
||
|
||
serve_server_wellknown: true
|
||
extra_well_known_client_content:
|
||
"org.matrix.msc4186.stable": {"enabled": true}
|
||
"org.matrix.msc4143.rtc_foci": [
|
||
{
|
||
"type": "livekit",
|
||
"livekit_service_url": "https://matrix.example.com/livekit/jwt"
|
||
}
|
||
]
|
||
|
||
report_stats: false
|
||
```
|
||
|
||
---
|
||
|
||
## 3. LiveKit SFU Configuration (livekit.yaml)
|
||
|
||
LiveKit powers group calls by receiving publisher streams and distributing them to subscribers. Key directives in `livekit.yaml` control media routing:
|
||
|
||
- **`bind_addresses` & `port`**: Sets LiveKit to listen for HTTP signaling and WebSocket connections on port `7880`.
|
||
- **`rtc.use_external_ip: true`**: Crucial for cloud server environments behind NAT. Instructs LiveKit to discover and advertise the host node's public IP address (`--node-ip`) to connecting WebRTC clients.
|
||
- **`rtc.port_range_start` & `rtc.port_range_end`**: Restricts dynamic UDP WebRTC media allocation to ports `50000-50020`, keeping firewall policy simple.
|
||
- **`room.auto_create: true`**: Allows rooms to be dynamically allocated whenever `lk-jwt-service` issues a valid access token.
|
||
|
||
Here is the resulting `livekit.yaml`:
|
||
|
||
```yaml
|
||
port: 7880
|
||
bind_addresses:
|
||
- 0.0.0.0
|
||
|
||
rtc:
|
||
tcp_port: 7881
|
||
port_range_start: 50000
|
||
port_range_end: 50020
|
||
use_external_ip: true
|
||
|
||
room:
|
||
auto_create: true
|
||
|
||
logging:
|
||
level: info
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Coturn Relay Configuration (turnserver.conf)
|
||
|
||
Coturn handles fallback media relaying for 1:1 calls. When configuring `turnserver.conf`, several settings ensure reliability and security:
|
||
|
||
- **`listening-port=3478`**: Standard port for STUN/TURN requests over UDP and TCP.
|
||
- **`external-ip`**: Tells Coturn its public IP address so it can communicate valid relay candidates to remote peers.
|
||
- **`min-port` & `max-port`**: Limits dynamic UDP relay allocation to `49152-49172`.
|
||
- **`use-auth-secret` & `static-auth-secret`**: Enables secret-based authentication matching `turn_shared_secret` in `homeserver.yaml`.
|
||
- **`realm`**: The domain name associated with your TURN server, required for HMAC token verification.
|
||
- **`no-multicast-peers`**: Prevents malicious users from abusing the TURN relay to probe internal private network addresses.
|
||
|
||
Below is the complete `turnserver.conf` file:
|
||
|
||
```conf
|
||
listening-port=3478
|
||
listening-ip=0.0.0.0
|
||
|
||
# Public IP address of your server
|
||
external-ip=203.0.113.10
|
||
|
||
min-port=49152
|
||
max-port=49172
|
||
|
||
fingerprint
|
||
use-auth-secret
|
||
static-auth-secret=YOUR_TURN_SHARED_SECRET
|
||
realm=matrix.example.com
|
||
|
||
total-quota=100
|
||
stale-nonce=600
|
||
log-file=stdout
|
||
simple-log
|
||
no-multicast-peers
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Environment Setup (.env)
|
||
|
||
Storing credentials in environment variables keeps secret values out of configuration files and version control.
|
||
|
||
Secrets such as `DB_PASSWORD`, `LK_SECRET`, and `TURN_SECRET` should be cryptographically random strings. We use `openssl` to generate secure 64-character hex strings.
|
||
|
||
Here is the `.env` template file:
|
||
|
||
```bash
|
||
# Infrastructure
|
||
DOMAIN=matrix.example.com
|
||
SERVER_IP=203.0.113.10
|
||
ADMIN_EMAIL=admin@example.com
|
||
|
||
# Database
|
||
DB_PASSWORD=YOUR_POSTGRES_PASSWORD
|
||
|
||
# LiveKit (Group Calls)
|
||
LK_KEY=matrix_lk_key
|
||
LK_SECRET=YOUR_LIVEKIT_SECRET
|
||
|
||
# Coturn (1:1 Calls)
|
||
TURN_SECRET=YOUR_TURN_SHARED_SECRET
|
||
```
|
||
|
||
Generating secrets with OpenSSL:
|
||
|
||
```bash
|
||
# Generate DB_PASSWORD, LK_SECRET, and TURN_SECRET
|
||
openssl rand -hex 32
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Reverse Proxy and Client Discovery (.well-known)
|
||
|
||
A reverse proxy routes external HTTPS traffic to internal services and satisfies protocol requirements for web clients:
|
||
|
||
1. **CORS Headers**: Web clients like Element Web or Element Call make cross-origin API requests. The proxy must return appropriate `Access-Control-Allow-*` headers.
|
||
2. **WebSocket Proxying (`/livekit/`)**: LiveKit signaling requires persistent WebSocket connections (`Upgrade: $http_upgrade` and `Connection: "Upgrade"`).
|
||
3. **Auto-Discovery (`/.well-known/matrix/client` & `/.well-known/matrix/server`)**: Matrix clients inspect these URIs upon user login to locate the homeserver API base URL and LiveKit JWT service without asking users to type custom port numbers or subdomains.
|
||
|
||
The following **Nginx** configuration puts all routing rules into practice:
|
||
|
||
```nginx
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name matrix.example.com;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
|
||
|
||
# CORS Headers for Web Clients (Element Web / Element Call)
|
||
add_header Access-Control-Allow-Origin * always;
|
||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
||
add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Matrix-User-ID" always;
|
||
|
||
if ($request_method = 'OPTIONS') {
|
||
return 204;
|
||
}
|
||
|
||
# Matrix Client & Federation API
|
||
location /_matrix {
|
||
proxy_pass http://127.0.0.1:8008;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Forwarded-For $remote_addr;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
client_max_body_size 50M;
|
||
}
|
||
|
||
# LiveKit SFU Signal (WebSockets)
|
||
location /livekit/ {
|
||
rewrite ^/livekit/(.*) /$1 break;
|
||
proxy_pass http://127.0.0.1:7880;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "Upgrade";
|
||
proxy_set_header Host $host;
|
||
}
|
||
|
||
# LiveKit JWT Token Service for Element X
|
||
location /livekit/jwt/ {
|
||
rewrite ^/livekit/jwt/(.*) /$1 break;
|
||
proxy_pass http://127.0.0.1:8080;
|
||
proxy_set_header Host $host;
|
||
}
|
||
|
||
# Matrix Client Well-Known Auto-Discovery
|
||
location /.well-known/matrix/client {
|
||
default_type application/json;
|
||
return 200 '{"m.homeserver":{"base_url":"https://matrix.example.com"},"org.matrix.msc4186.stable":{"enabled":true},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://matrix.example.com/livekit/jwt"}]}';
|
||
}
|
||
|
||
# Matrix Server Well-Known Federation Discovery
|
||
location /.well-known/matrix/server {
|
||
default_type application/json;
|
||
return 200 '{"m.server":"matrix.example.com:443"}';
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Account Provisioning and Maintenance
|
||
|
||
With public registration disabled (`enable_registration: false`), accounts are provisioned via Synapse's CLI tool.
|
||
|
||
### Admin User Registration
|
||
|
||
Run `register_new_matrix_user` inside the Synapse container to create your initial administrator account:
|
||
|
||
```bash
|
||
docker exec -it matrix-synapse register_new_matrix_user \
|
||
-c /data/homeserver.yaml http://localhost:8008 \
|
||
--user admin --password "SECURE_USER_PASSWORD" --admin
|
||
```
|
||
|
||
### Transactional Email Configuration (SMTP)
|
||
|
||
To enable email verification and password reset functionality, append the `email` configuration block to `homeserver.yaml`:
|
||
|
||
```yaml
|
||
email:
|
||
smtp_host: "smtp.example.com"
|
||
smtp_port: 587
|
||
smtp_user: "notifications@example.com"
|
||
smtp_pass: "YOUR_SMTP_PASSWORD"
|
||
require_transport_security: true
|
||
notif_from: "Matrix Server <notifications@example.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Resources
|
||
|
||
- [Synapse Documentation](https://element-hq.github.io/synapse/)
|
||
- [LiveKit Server Documentation](https://docs.livekit.io/)
|
||
- [Element lk-jwt-service GitHub](https://github.com/element-hq/lk-jwt-service)
|
||
- [MSC4143: Native Matrix RTC Foci](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)
|
||
- [MSC4186: LiveKit Call Authentication](https://github.com/matrix-org/matrix-spec-proposals/pull/4186)
|