Files

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:

    docker network create proxy
    

One-Time Secret Generation

Coturn secret

# 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

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)

pwgen -s 32 1

Paste into docker-compose.yml → CONTINUWUITY_REGISTRATION_TOKEN.


Starting Up

# 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

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)

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)

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
5010050200 UDP LiveKit media No — direct
5020165535 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_tokenhttp://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:

    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:
      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:
      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

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