feat: Add matrix deployment article
This commit is contained in:
6 files changed
+325
-3
No files matched your search
@@ -0,0 +1,282 @@
|
|||||||
|
---
|
||||||
|
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)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as TestArticle from './articles/2026/test-article.mdx';
|
import * as TestArticle from './articles/2026/test-article.mdx';
|
||||||
|
import * as MatrixServerDeploymentGuide from './articles/2026/matrix-server-deployment-guide.mdx';
|
||||||
|
|
||||||
export interface Article {
|
export interface Article {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -40,6 +41,10 @@ function register(module: unknown, slug: string): Article {
|
|||||||
|
|
||||||
export const articles: Record<string, Article> = {
|
export const articles: Record<string, Article> = {
|
||||||
'test-article': register(TestArticle, '2026/test-article'),
|
'test-article': register(TestArticle, '2026/test-article'),
|
||||||
|
'matrix-server-deployment-guide': register(
|
||||||
|
MatrixServerDeploymentGuide,
|
||||||
|
'2026/matrix-server-deployment-guide'
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sortedArticles = Object.values(articles).sort(
|
export const sortedArticles = Object.values(articles).sort(
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export default defineServiceConfig({
|
|||||||
index: './src/app/index.tsx',
|
index: './src/app/index.tsx',
|
||||||
'articles/index': './src/app/articles/index.tsx',
|
'articles/index': './src/app/articles/index.tsx',
|
||||||
'articles/2026/test-article': './src/app/articles/2026/test-article.tsx',
|
'articles/2026/test-article': './src/app/articles/2026/test-article.tsx',
|
||||||
|
'articles/2026/matrix-server-deployment-guide':
|
||||||
|
'./src/app/articles/2026/matrix-server-deployment-guide.tsx',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { MARKDOWN_COMPONENTS } from '@ermnvldmr/ui';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { articles } from '../../../../content/registry';
|
||||||
|
import { ArticleLayout } from '../../../components/layouts/ArticleLayout/ArticleLayout';
|
||||||
|
import { createPage } from '../../../lib/core/createPage';
|
||||||
|
|
||||||
|
const article = articles['matrix-server-deployment-guide'];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (!article) {
|
||||||
|
throw new Error('Article not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
createPage(
|
||||||
|
function MatrixServerDeploymentGuidePage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<ArticleLayout
|
||||||
|
createdDate={article.createdDate}
|
||||||
|
description={article.description}
|
||||||
|
tags={article.tags}
|
||||||
|
title={article.title}
|
||||||
|
updatedDate={article.updatedDate}
|
||||||
|
>
|
||||||
|
<article.Component components={MARKDOWN_COMPONENTS} />
|
||||||
|
</ArticleLayout>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `${article.title} - Vladimir Eremin`,
|
||||||
|
description: article.description,
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -68,7 +68,7 @@ export const ArticleLayout: React.FC<ArticleLayoutProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IndexLayout>
|
<IndexLayout>
|
||||||
<Container as="article" className={cn('min-w-[720px]', className)}>
|
<Container as="article" className={cn('w-8/12', className)}>
|
||||||
<VStack className="w-full" gap={8}>
|
<VStack className="w-full" gap={8}>
|
||||||
<VStack as="header" gap={4}>
|
<VStack as="header" gap={4}>
|
||||||
<HStack align="center" gap={2} wrap="wrap">
|
<HStack align="center" gap={2} wrap="wrap">
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ export function IndexLayout({ children }: IndexLayoutProps): React.JSX.Element {
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack align="center" className="min-h-screen bg-[var(--rb-background)] transition-colors duration-200" justify="between">
|
<VStack align="center" className="min-h-screen w-screen bg-[var(--rb-background)] transition-colors duration-200" justify="between">
|
||||||
<VStack align="stretch" as="main" className="min-h-[105vh] py-8 mx-auto" justify="start">
|
<VStack align="center" as="main" className="min-h-[105vh] py-8 mx-auto" justify="start">
|
||||||
{children}
|
{children}
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack as="footer" className="w-full" gap={0}>
|
<VStack as="footer" className="w-full" gap={0}>
|
||||||
|
|||||||
Reference in new issue
Block a user