diff --git a/services/www/content/articles/2026/matrix-server-deployment-guide.mdx b/services/www/content/articles/2026/matrix-server-deployment-guide.mdx new file mode 100644 index 0000000..c78e5f3 --- /dev/null +++ b/services/www/content/articles/2026/matrix-server-deployment-guide.mdx @@ -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 --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) diff --git a/services/www/content/registry.ts b/services/www/content/registry.ts index 955f20f..d520cc6 100644 --- a/services/www/content/registry.ts +++ b/services/www/content/registry.ts @@ -1,5 +1,6 @@ import React from 'react'; import * as TestArticle from './articles/2026/test-article.mdx'; +import * as MatrixServerDeploymentGuide from './articles/2026/matrix-server-deployment-guide.mdx'; export interface Article { slug: string; @@ -40,6 +41,10 @@ function register(module: unknown, slug: string): Article { export const articles: Record = { '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( diff --git a/services/www/rsbuild.config.ts b/services/www/rsbuild.config.ts index 4e492c4..c85ddc6 100644 --- a/services/www/rsbuild.config.ts +++ b/services/www/rsbuild.config.ts @@ -16,6 +16,8 @@ export default defineServiceConfig({ index: './src/app/index.tsx', 'articles/index': './src/app/articles/index.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', }, }, }); diff --git a/services/www/src/app/articles/2026/matrix-server-deployment-guide.tsx b/services/www/src/app/articles/2026/matrix-server-deployment-guide.tsx new file mode 100644 index 0000000..29c4e6d --- /dev/null +++ b/services/www/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 ( + + + + ); + }, + { + title: `${article.title} - Vladimir Eremin`, + description: article.description, + } +); diff --git a/services/www/src/components/layouts/ArticleLayout/ArticleLayout.tsx b/services/www/src/components/layouts/ArticleLayout/ArticleLayout.tsx index a3c42ca..5cb74e8 100644 --- a/services/www/src/components/layouts/ArticleLayout/ArticleLayout.tsx +++ b/services/www/src/components/layouts/ArticleLayout/ArticleLayout.tsx @@ -68,7 +68,7 @@ export const ArticleLayout: React.FC = ({ return ( - + diff --git a/services/www/src/components/layouts/IndexLayout/IndexLayout.tsx b/services/www/src/components/layouts/IndexLayout/IndexLayout.tsx index 2eaa66f..8a1c18e 100644 --- a/services/www/src/components/layouts/IndexLayout/IndexLayout.tsx +++ b/services/www/src/components/layouts/IndexLayout/IndexLayout.tsx @@ -28,8 +28,8 @@ export function IndexLayout({ children }: IndexLayoutProps): React.JSX.Element { const currentYear = new Date().getFullYear(); return ( - - + + {children}