mirror of
https://github.com/samjage/matrix.git
synced 2026-06-06 02:40:43 +00:00
first pass
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
# 🧩 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
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ backbone network │
|
||||
│ │
|
||||
Client ──HTTPS──▶ │ Caddy ──▶ tuwunel:6167 │
|
||||
│ ──▶ lk-jwt-service │
|
||||
│ ──▶ host.docker.internal:7880 (livekit)
|
||||
└─────────────────────────────┘
|
||||
|
||||
host network (UDP): livekit (ports 7880, 7881, 50000-60000)
|
||||
coturn (ports 3478, 5349, 49152-65535)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 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.
|
||||
Reference in New Issue
Block a user