Files
ermnvldmr.com/services/www/content/articles/2026/matrix-server-deployment-guide.mdx
T

281 lines
7.9 KiB
Plaintext

---
title: 'Matrix Server Deployment Guide'
description: 'Synapse & LiveKit servers deployment from scratch'
createdDate: 2026-02-15
tags: ['matrix', 'deployment', 'guide']
---
This guide is for **educational purposes** only. It describes how to deploy a private communications infrastructure to understand the decentralized Matrix protocol and the technical implementation of media relays.
## Why Self-Host?
Centralized platforms often compromise data for analytics or are subject to external policy shifts. By hosting your own Matrix instance, you achieve:
- **Data Sovereignty**: You own the hardware and the database.
- **End-to-End Encryption (E2EE)**: Messages are encrypted locally on devices; the server only relays ciphertext.
- **Infrastructure Resilience**: Your communication is independent of large provider outages.
- **Metadata Privacy**: You control logs regarding who communicates with whom.
---
## Technical Stack Overview
1. **Synapse**: The "Homeserver." It handles the Matrix protocol, account logic, and message synchronization.
2. **Postgres**: The database. Essential for production performance (SQLite is for testing only).
3. **Coturn (STUN/TURN)**: When two people call each other, firewalls often block direct connections. Coturn acts as a relay to bypass these NAT restrictions.
4. **LiveKit**: A modern media engine (SFU). While Coturn is for 1:1 calls, LiveKit handles group calls by multiplexing video/audio streams efficiently.
5. **lk-jwt-service**: An authentication bridge. This service checks if a user is logged into your Synapse server and issues a temporary token (JWT) to allow them into the call.
---
## 1. The Deployment Stack
This configuration is generic, uses **Docker Volumes**, and is set for **UID 1000:1000** for standard hardening.
```yaml
networks:
proxy:
name: matrix-proxy
internal:
internal: true
volumes:
synapse_data:
postgres_data:
traefik_certs:
services:
traefik:
image: traefik:v3.0
container_name: matrix-proxy
command:
- '--providers.docker=true'
- '--providers.docker.exposedbydefault=false'
- '--entrypoints.web.address=:80'
- '--entrypoints.websecure.address=:443'
- '--entrypoints.web.http.redirections.entrypoint.to=websecure'
- '--certificatesresolvers.myresolver.acme.dnschallenge.provider=cloudflare'
- '--certificatesresolvers.myresolver.acme.email=${ADMIN_EMAIL}'
- '--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json'
environment:
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
ports:
- '80:80'
- '443:443'
volumes:
- traefik_certs:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy
restart: unless-stopped
synapse:
image: ghcr.io/element-hq/synapse:v1.146.0
container_name: matrix-synapse
user: '1000:1000'
depends_on:
postgres: { condition: service_healthy }
volumes:
- synapse_data:/data
environment:
SYNAPSE_SERVER_NAME: '${DOMAIN}'
SYNAPSE_REPORT_STATS: 'no'
SYNAPSE_CONFIG_PATH: '/data/homeserver.yaml'
networks:
- proxy
- internal
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.synapse.rule=Host(`${DOMAIN}`)'
- 'traefik.http.services.synapse.loadbalancer.server.port=8008'
restart: unless-stopped
postgres:
image: postgres:16-alpine
container_name: matrix-db
user: '1000:1000'
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: synapse
POSTGRES_USER: synapse
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_INITDB_ARGS: '--encoding=UTF-8 --lc-collate=C --lc-ctype=C'
networks:
- internal
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U synapse']
interval: 10s
restart: unless-stopped
livekit:
image: livekit/livekit-server:v1.9
container_name: matrix-livekit
command: --config /etc/livekit.yaml --node-ip ${SERVER_IP}
environment:
LIVEKIT_KEYS: '${LK_KEY}: ${LK_SECRET}'
volumes:
- ./livekit.yaml:/etc/livekit.yaml:ro
ports:
- '7880:7880'
- '7881:7881'
- '50000-50020:50000-50020/udp'
networks:
- proxy
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.lk.rule=Host(`${DOMAIN}`) && PathPrefix(`/livekit`)'
- 'traefik.http.middlewares.lk-strip.stripprefix.prefixes=/livekit'
- 'traefik.http.routers.lk.middlewares=lk-strip'
restart: unless-stopped
lk-jwt:
image: ghcr.io/element-hq/lk-jwt-service:0.4.1
container_name: matrix-lk-jwt
environment:
LIVEKIT_URL: wss://${DOMAIN}/livekit
LIVEKIT_KEY: '${LK_KEY}'
LIVEKIT_SECRET: '${LK_SECRET}'
networks:
- proxy
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.lk-jwt.rule=Host(`${DOMAIN}`) && PathPrefix(`/livekit/jwt`)'
- 'traefik.http.middlewares.lk-jwt-strip.stripprefix.prefixes=/livekit/jwt'
- 'traefik.http.routers.lk-jwt.middlewares=lk-jwt-strip'
restart: unless-stopped
coturn:
image: coturn/coturn:4.8-alpine
container_name: matrix-coturn
user: '1000:1000'
ports:
- '3478:3478'
- '3478:3478/udp'
- '5349:5349'
- '5349:5349/udp'
- '49152-49172:49152-49172/udp'
volumes:
- ./turnserver.conf:/etc/coturn/turnserver.conf:ro
restart: unless-stopped
```
---
## 2. Configuration
Create a `.env` file in the same directory. **Do not skip the secret generation step below.**
```bash
# --- Infrastructure ---
DOMAIN=matrix.example.com
SERVER_IP=1.2.3.4
ADMIN_EMAIL=admin@example.com
# --- Cloudflare (for Traefik SSL) ---
CF_DNS_API_TOKEN=your_cloudflare_api_token_here
# --- Database ---
DB_PASSWORD=generate_this_with_openssl
# --- LiveKit Secrets ---
# API Key can be any alphanumeric string
LK_KEY=matrix_lk_key
# Secret MUST be generated
LK_SECRET=generate_this_with_openssl
# --- Coturn / Synapse VoIP ---
# Used for STUN/TURN authentication
TURN_SECRET=generate_this_with_openssl
```
### Generating Secrets
Use `openssl` to generate secure, random strings for your `.env` entries:
```bash
# Generate DB_PASSWORD, LK_SECRET, and TURN_SECRET
openssl rand -hex 32
```
---
## 3. Nginx Counterpart & Certificates
If you don't use Traefik, obtain certificates via **Certbot**:
```bash
sudo certbot certonly --standalone -d matrix.example.com
```
### Nginx Config
```nginx
server {
listen 443 ssl;
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;
location /_matrix {
proxy_pass http://synapse:8008;
proxy_set_header Host $host;
}
location /livekit/ {
rewrite ^/livekit/(.*) /$1 break;
proxy_pass http://livekit:7880;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /livekit/jwt/ {
rewrite ^/livekit/jwt/(.*) /$1 break;
proxy_pass http://lk-jwt:8080;
}
}
```
---
## 4. User Registration
### Manual Registration (Admin)
For private servers, disable public registration in `homeserver.yaml` and add users via CLI:
```bash
docker exec -it matrix-synapse register_new_matrix_user \
-c /data/homeserver.yaml http://localhost:8008 \
--user <username> --password <password> --admin
```
### SMTP (External Sign-up)
Add to `homeserver.yaml` to allow email verification:
```yaml
email:
smtp_host: 'smtp.gmail.com'
smtp_port: 587
smtp_user: 'yourname@gmail.com'
smtp_pass: 'xxxx xxxx xxxx xxxx' # Google App Password
force_tls: true
```
---
## Resources
- [Synapse GitHub](https://github.com/element-hq/synapse)
- [LiveKit GitHub](https://github.com/livekit/livekit)
- [lk-jwt-service GitHub](https://github.com/element-hq/lk-jwt-service)
- [Element X Apps](https://www.google.com/search?q=https://element.io/element-x)