Files
admin d33fede751 Enhance README with network diagram and details
Added network diagram and updated network topology information.
2026-04-06 08:11:30 -04:00

313 lines
8.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🧩 Matrix Stack
> Self-hosted Matrix homeserver with native voice & video calling, TURN relay, and LiveKit WebRTC integration.
Built on **[Tuwunel](https://github.com/jevolk/tuwunel)** — a fast, modern Matrix homeserver — with full [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143) support for in-room calls via LiveKit.
---
## 📦 Services
| Container | Image | Role |
|---|---|---|
| `tuwunel` | `jevolk/tuwunel` | Matrix homeserver |
| `livekit` | `livekit/livekit-server` | WebRTC SFU — voice & video |
| `lk-jwt-service` | `ghcr.io/element-hq/lk-jwt-service` | Issues LiveKit tokens to Matrix clients |
| `coturn` | `coturn/coturn` | TURN/STUN relay for NAT traversal |
---
## ⚙️ Configuration
### 🔑 Environment variables
Copy `.env.example` to `.env` and fill in all values before starting the stack.
```bash
cp .env.example .env
```
| Variable | Description |
|---|---|
| `DOMAIN_NAME` | Your base domain — e.g. `example.com` |
| `EXTERNAL_IP` | Your server's public IPv4 address |
| `STATIC_AUTH_SECRET` | Shared TURN secret between coturn and tuwunel |
| `API_KEY` | LiveKit API key |
| `API_SECRET` | LiveKit API secret |
| `NETWORK_NAME` | External Docker network shared across stacks — default `backbone` |
| `GIT_SHA` | Image tag to deploy — use `latest` for development |
**Generating secrets:**
| Variable | Command |
|---|---|
| `STATIC_AUTH_SECRET` | `openssl rand -hex 32` |
| `API_SECRET` | `openssl rand -hex 32` |
| `API_KEY` | `openssl rand -hex 16` |
| `EXTERNAL_IP` | `curl -4 ifconfig.me` |
> ⚠️ `.env` is gitignored and must **never** be committed.
---
### 🔁 Secret rotation
`scripts/rotate-secrets.sh` regenerates secrets in `.env` and restarts only the affected containers.
```bash
bash scripts/rotate-secrets.sh # rotate everything
bash scripts/rotate-secrets.sh --turn # TURN secret only (restarts coturn + tuwunel)
bash scripts/rotate-secrets.sh --livekit # LiveKit keys only (restarts livekit + lk-jwt-service)
```
> ⚠️ Active calls and sessions will be dropped on rotation. Run during a maintenance window.
Rotation events are logged to `scripts/rotation.log` (gitignored).
**To run on a schedule** — add a cron entry on the host:
```bash
crontab -e
```
```cron
# Rotate all Matrix secrets every 90 days at 3am
0 3 1 */3 * cd /opt/stacks/matrix && bash scripts/rotate-secrets.sh --all >> /var/log/matrix-rotation.log 2>&1
```
---
### 📝 Variable substitution — what works where
Docker Compose substitutes `${VAR}` **only inside `docker-compose.yml`**. Config files mounted as volumes are read as plain text — variables are not expanded inside them.
| File | Substitution | How secrets get in |
|---|---|---|
| `docker-compose.yml` | ✅ Yes | Directly from `.env` |
| `tuwunel/tuwunel.toml` | ❌ No | `server_name` is a required field — update it to your domain. `TUWUNEL_SERVER_NAME` in compose overrides it at runtime but the field must be present |
| `coturn/turnserver.conf` | ❌ No | Secret and realm passed as CLI args in compose |
| `livekit/livekit.yaml` | ❌ No | API keys passed via `LIVEKIT_KEYS` env var in compose |
---
## 🚀 Deployment
### Prerequisites
Both of these must exist before the stack will start — Docker will refuse to create containers otherwise.
**1. Create your Docker network** (set `NETWORK_NAME` in `.env` to match):
```bash
docker network create backbone
```
**2. Create the tuwunel data volume:**
```bash
docker volume create matrix_tuwunel-data
```
### Start the stack
```bash
docker compose up -d
```
---
## 🔀 Reverse Proxy
This stack does not run its own Caddy instance. Routing is handled by a shared Caddy container on the `backbone` Docker network. Add the following blocks to your Caddyfile.
> 💡 **Always use `handle` blocks.** Do not mix bare `respond` directives with a `handle {}` catch-all in the same site block — Caddy's `handle {}` will intercept all requests before a bare `respond` can fire.
---
### 🌐 Base domain — client & server discovery
Serves the `.well-known/matrix` endpoints that tell clients where the homeserver and LiveKit service are. Required for login and calling to work.
```caddy
your.domain {
handle /.well-known/matrix/server {
header Content-Type application/json
header Access-Control-Allow-Origin *
respond `{"m.server":"matrix.your.domain:443"}`
}
handle /.well-known/matrix/client {
header Content-Type application/json
header Access-Control-Allow-Origin *
respond `{
"m.homeserver": {
"base_url": "https://matrix.your.domain"
},
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://livekit.your.domain"
}
]
}`
}
handle {
respond 404
}
}
```
---
### 🏠 Matrix homeserver
Routes Matrix API traffic to tuwunel. WebSocket headers are required for client sync connections.
```caddy
matrix.your.domain {
handle /_version {
respond `{"sha":"{env.GIT_SHA}"}`
}
handle /_matrix/* {
reverse_proxy tuwunel:6167 {
header_up Upgrade {http.request.header.Upgrade}
header_up Connection {http.request.header.Connection}
}
}
handle /_synapse/* {
reverse_proxy tuwunel:6167 {
header_up Upgrade {http.request.header.Upgrade}
header_up Connection {http.request.header.Connection}
}
}
handle {
respond 404
}
}
```
---
### 🔗 Federation — port 8448
Handles server-to-server federation traffic. Requires port `8448` exposed on your Caddy container.
```caddy
matrix.your.domain:8448 {
reverse_proxy tuwunel:6167
}
```
```yaml
# caddy docker-compose.yml
ports:
- "8448:8448"
```
---
### 📹 LiveKit
Routes JWT token requests to `lk-jwt-service` and all WebRTC traffic to the LiveKit server.
LiveKit runs with `network_mode: host` and is **not** on the Docker network — it is reached via `host.docker.internal`. This requires the following in your Caddy compose:
```yaml
extra_hosts:
- "host.docker.internal:host-gateway"
```
```caddy
livekit.your.domain {
handle /sfu/* {
reverse_proxy lk-jwt-service:8080
}
handle /livekit/jwt* {
reverse_proxy lk-jwt-service:8080
}
handle {
reverse_proxy host.docker.internal:7880 {
header_up Upgrade {http.request.header.Upgrade}
header_up Connection {http.request.header.Connection}
}
}
}
```
> ⚠️ WebSocket headers on the LiveKit catch-all are **required**. Omitting them causes `MISSING_MATRIX_RTC_TRANSPORT` errors on clients.
---
## 📋 Network topology
![NetworkDiagram](NetworkDiagram.svg)
```
Backbone network (Docker)
| Ingress | Routes to |
|---|---|
| `Caddy` (HTTPS) | `tuwunel:6167` |
| | `lk-jwt-service` |
| | `host.docker.internal:7880` (livekit) |
Host network (UDP)
| Service | Ports |
|---|---|
| livekit | 7880, 7881, 5000060000 |
| coturn | 3478, 5349, 4915265535 |
```
---
## 📁 Structure
```
.
├── tuwunel/
│ └── tuwunel.toml # Homeserver config — hardcode server_name here
├── livekit/
│ └── livekit.yaml # LiveKit ports and RTC config
├── coturn/
│ ├── turnserver.conf # TURN config — no secrets, those go in compose
│ └── certs/ # Drop fullchain.pem + privkey.pem here to enable TURNS (gitignored)
├── docker-compose.yml
├── .env # Secrets — gitignored, never commit
└── .env.example # Template — safe to commit
```
---
## 🔒 Security notes
- `coturn` blocks RFC1918 ranges in `turnserver.conf` to prevent TURN relay abuse
- Matrix registration is disabled by default in `tuwunel/tuwunel.toml`
---
## 🔐 Enabling TURNS (TURN over TLS)
TURNS encrypts relay traffic on port `5349`. The stack is pre-wired for it — `coturn/certs/` is already mounted into the container and `TUWUNEL_TURN_URIS` already advertises the `turns:` URI to clients. It just needs certs and the config uncommented.
**To enable:**
1. Drop your certificates into `coturn/certs/`:
```
coturn/certs/fullchain.pem
coturn/certs/privkey.pem
```
Certs must be valid for `turn.DOMAIN_NAME`. If you have a wildcard cert (`*.yourdomain.com`) it will work as-is.
2. Uncomment the TLS lines in `coturn/turnserver.conf`:
```
tls-listening-port=5349
cert=/etc/coturn/certs/fullchain.pem
pkey=/etc/coturn/certs/privkey.pem
```
3. Restart coturn:
```bash
docker compose up -d coturn
```
> `coturn/certs/` is gitignored — your private key will never be committed.