# Matrix Self-Hosted Stack — Setup Guide ## File Overview ``` matrix-stack/ ├── docker-compose.yml # All services: Continuwuity, Traefik, Coturn, LiveKit, lk-jwt-service ├── continuwuity.toml # Homeserver config: TURN URIs, LiveKit foci ├── livekit.yaml # LiveKit media server config ├── coturn.conf # TURN/STUN server config └── README.md # This file ``` --- ## Prerequisites 1. Two DNS A records pointing to your server's public IP: - `matrix.example.com` - `livekit.example.com` (If you want your Matrix IDs as `@user:example.com` rather than `@user:matrix.example.com`, also point `example.com` to your server.) 2. Docker + Docker Compose installed. 3. Create the external proxy network Traefik uses: ```bash docker network create proxy ``` --- ## One-Time Secret Generation ### Coturn secret ```bash # Install pwgen if needed: apt install pwgen pwgen -s 64 1 ``` Paste the output into both `coturn.conf` (static-auth-secret) and `continuwuity.toml` (turn_secret). They MUST match. ### LiveKit keys ```bash docker run --rm livekit/livekit-server:latest generate-keys ``` This outputs a key (~20 chars) and secret (~64 chars). Paste them into: - `livekit.yaml` → keys section - `docker-compose.yml` → lk-jwt-service LIVEKIT_KEY / LIVEKIT_SECRET ### Registration token (for invite-only signup) ```bash pwgen -s 32 1 ``` Paste into docker-compose.yml → CONTINUWUITY_REGISTRATION_TOKEN. --- ## Starting Up ```bash # First boot — creates admin user automatically docker compose up -d # Get your admin password docker compose logs homeserver | grep "Created user" # IMPORTANT: Edit docker-compose.yml and remove the --execute flag # from the homeserver command, then restart: docker compose up -d homeserver ``` Log in with any Matrix client (Element, Cinny, etc.) using `@admin:example.com` and the generated password. --- ## Firewall Rules Open these ports on your server's firewall (ufw examples shown). ### HTTP/HTTPS — handled by Traefik ```bash ufw allow 80/tcp # HTTP (redirected to HTTPS by Traefik) ufw allow 443/tcp # HTTPS for matrix.example.com and livekit.example.com ``` ### Coturn — TURN/STUN (raw UDP/TCP, NOT proxied by Traefik) ```bash ufw allow 3478/tcp # STUN + TURN ufw allow 3478/udp ufw allow 5349/tcp # TURN over TLS (if you configured TLS in coturn.conf) ufw allow 5349/udp ufw allow 50201:65535/udp # Coturn media relay port range ``` ### LiveKit — RTC media (raw UDP/TCP, NOT proxied by Traefik) ```bash ufw allow 7881/tcp # LiveKit direct TCP (for clients that can't UDP) ufw allow 50100:50200/udp # LiveKit media relay port range ``` ### Summary table | Port(s) | Protocol | Service | Via Traefik? | |----------------|-----------|--------------------|--------------| | 80 | TCP | HTTP (→ HTTPS) | Yes | | 443 | TCP | HTTPS | Yes | | 3478 | TCP + UDP | Coturn TURN/STUN | No — direct | | 5349 | TCP + UDP | Coturn TURN TLS | No — direct | | 7881 | TCP | LiveKit direct TCP | No — direct | | 50100–50200 | UDP | LiveKit media | No — direct | | 50201–65535 | UDP | Coturn media relay | No — direct | **Key point**: Traefik only handles ports 80 and 443. The Matrix homeserver (6167) and LiveKit HTTP (7880) are never exposed directly — Traefik proxies them internally. Coturn and LiveKit's RTC ports bypass Traefik entirely and are opened directly to the internet. --- ## Can I use an External NGINX Proxy Manager instead of Traefik? **Short answer: yes, but with important caveats.** ### What NPM can handle - `matrix.example.com` → proxy to `http://YOUR_SERVER_IP:6167` - Enable WebSockets - Set `X-Forwarded-For`, `Host`, `X-Real-IP` headers - `livekit.example.com` with path splitting: - `/sfu/get`, `/healthz`, `/get_token` → `http://YOUR_SERVER_IP:8081` - Everything else → `http://YOUR_SERVER_IP:7880` - Enable WebSockets on both - NPM handles Let's Encrypt TLS automatically ### What NPM cannot handle - **Coturn** — raw UDP/TCP, not HTTP. NPM (like Traefik) can't proxy it. You just open ports 3478/5349 directly and point DNS at your server IP. - **LiveKit RTC ports** (7881/tcp, 50100-50200/udp) — same story. These bypass any reverse proxy entirely. ### Changes needed to docker-compose.yml for NPM 1. **Remove the entire `traefik:` service block.** 2. **Remove all `labels:` blocks** from homeserver and lk-jwt-service. 3. **Expose the ports NPM needs to reach:** ```yaml homeserver: ports: - "127.0.0.1:6167:6167" # NPM proxies this lk-jwt-service: ports: - "127.0.0.1:8081:8081" # NPM proxies this # livekit already uses network_mode: host, so 7880 is available # on the host at 127.0.0.1:7880 automatically ``` Using `127.0.0.1:` prefix means only NPM (on the same host or your network) can reach them — not the open internet. 4. **Remove the `acme:` volume** (NPM handles TLS). 5. In NPM, configure two proxy hosts: **matrix.example.com** - Forward to: `http://YOUR_SERVER_IP:6167` - Websockets: ON - Custom nginx config: ```nginx proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; ``` **livekit.example.com** - Forward to: `http://YOUR_SERVER_IP:7880` (default) - Websockets: ON - Advanced tab — add this custom config for path splitting: ```nginx location ~ ^/(sfu/get|healthz|get_token) { proxy_pass http://YOUR_SERVER_IP:8081$request_uri; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_buffering off; } ``` ### NPM on a separate machine? If NPM runs on a different server, replace `127.0.0.1` with your Matrix server's internal IP, and make sure port 6167 is firewalled to only allow connections from NPM's IP. --- ## Verification ### Test TURN credentials ```bash curl "https://matrix.example.com/_matrix/client/r0/voip/turnServer" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" | jq ``` Should return a JSON object with `uris`, `username`, `password`. ### Test TURN connectivity 1. Go to https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/ 2. Paste the credentials from above 3. Click "Gather candidates" — look for `relay` type candidates ### Test LiveKit 1. GET `https://livekit.example.com/healthz` — should return 200 2. Use https://livekit.io/connection-test with a token from `/get_token` ### Test federation https://federationtester.matrix.org — enter your domain --- ## Troubleshooting **Federation not working** - Check `.well-known/matrix/server` is reachable: `curl https://example.com/.well-known/matrix/server` - Should return: `{"m.server":"matrix.example.com:443"}` **TURN not working** - Verify firewall allows 3478/udp from the internet - Check coturn logs: `docker compose logs coturn` - Confirm `turn_secret` in continuwuity.toml matches `static-auth-secret` in coturn.conf **Element Call / group calls failing** - Check lk-jwt-service is reachable: `curl https://livekit.example.com/healthz` - Confirm LiveKit UDP ports (50100-50200) are open - Check `foci` URL in continuwuity.toml matches where lk-jwt-service is deployed