I'm trying to get a voice channel working for t/suki's Tuwunel matrix server. Originally, I tried setting up just Element Call. This allowed me and other users to join calls, but no one could talk to each other. From what I've gathered in my previous experiments trying to get this to work, I think a TURN server is required for Element Call to work at all. So, I've decided to set up Coturn as recommended by the Tuwunel guide:
/opt/matrix-rtc/docker-compose.yml:
services:
coturn:
image: docker.io/coturn/coturn:4.8.0-alpine
container_name: coturn
restart: unless-stopped
network_mode: host
volumes:
- ./coturn.conf:/etc/coturn/turnserver.conf
matrix-rtc-jwt:
image: ghcr.io/element-hq/lk-jwt-service:latest
container_name: matrix-rtc-jwt
environment:
- LIVEKIT_JWT_BIND=:8081
- LIVEKIT_URL=wss://matrix-rtc.tsuki.games
- LIVEKIT_KEY=<livekit key>
- LIVEKIT_SECRET=<livekit secret>
- LIVEKIT_FULL_ACCESS_HOMESERVERS=tsuki.games
restart: unless-stopped
network_mode: host
matrix-rtc-livekit:
image: livekit/livekit-server:latest
container_name: matrix-rtc-livekit
command: --config /etc/livekit.yaml
restart: unless-stopped
volumes:
- ./livekit.yaml:/etc/livekit.yaml:ro
network_mode: hostI'm using host networking as
suggested in the official Tuwunel guide, although
I'm using it for all of the services instead of just
coturn and
matrix-rtc-livekit since it appears
that matrix-rtc-jwt also needs to be on
the same network as matrix-rtc-livekit
in order to work.
/opt/matrix-rtc/coturn.conf:
use-auth-secret
static-auth-secret=<coturn secret>
realm=matrix-rtc.tsuki.games
/opt/matrix-rtc/livekit.yaml:
port: 7880
bind_addresses:
- ""
rtc:
tcp_port: 7881
port_range_start: 50100
port_range_end: 50200
use_external_ip: true
enable_loopback_candidate: false
keys:
<livekit key>:<livekit secret>/opt/matrix/tuwunel.toml
[global]
server_name = "tsuki.games"
address = "0.0.0.0"
query_over_tcp_only = true
allow_registration = false
require_auth_for_profile_requests = true
allow_public_room_directory_over_federation = true
show_all_local_users_in_user_directory = true
trusted_servers = [
"matrix.expiredpopsicle.com",
]
turn_uris = [
"turns:matrix-rtc.tsuki.games?transport=udp",
"turns:matrix-rtc.tsuki.games?transport=tcp",
]
turn_secret = "<coturn secret>"
auto_join_rooms = [ "#tsuki:tsuki.games", "#general:tsuki.games" ]
single_sso = true
[[global.identity_provider]]
brand = "Discourse"
client_id = "matrix"
client_secret = "<oidc secret>"
issuer_url = "https://forum.tsuki.games/oauth2"
callback_url = "https://matrix.tsuki.games/_matrix/client/unstable/login/sso/callback/matrix"
base_path = "oauth2"
[global.well_known]
client = "https://matrix.tsuki.games"
server = "matrix.tsuki.games:443"
[[global.well_known.rtc_transports]]
type = "livekit"
livekit_service_url = "https://matrix-rtc.tsuki.games"However, I use a reverse proxy called NGINX, which is running in a docker container provided by SWAG. This means I have to somehow tell the reverse proxy how to route requests to the host container.
I found docker/for-linux#264, in which a user points out that this can only be done on linux by adding the following to the container's configuration...
services:
myservice:
extra_hosts:
- host.docker.internal:host-gateway...where host.docker.internal could
be anything. I copy this exact configuration and
note that the /etc/hosts file has been
updated:
exodrifter@moonlight:/opt/swag$ sudo docker compose exec swag cat /etc/hosts
[sudo: authenticate] Password:
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00:: ip6-localnet
ff00:: ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.1 host.docker.internal
172.18.0.3 619aca4f5894
However, for some reason, nslookup
still fails to resolve
host.docker.internal. I decide to
ignore this issue and update the nginx configuration
to go to the specified IP address:
/opt/swag/config/nginx/proxy-confs/matrix-rtc.subdomain.conf:
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name matrix-rtc.*;
include /config/nginx/ssl.conf;
# lk-jwt-service
location ~ ^(/sfu/get|/healthz) {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app 172.17.0.1;
set $upstream_port 8081;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
# livekit
location / {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app 172.17.0.1;
set $upstream_port 7880;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade;
}
}
However, requests still fail and trying to load
https://matrix-rtc.tsuki.games/healthz
in my web browser results in a timeout. Another
comment from the earlier issue suggests UFW may
be the problem. Checking the UFW log at
/var/log/ufw.log I can see that
requests from my SWAG container running NGINX is
being blocked from connecting to my docker
network.
2026-02-17T22:28:25.907194+00:00 moonlight kernel: [UFW BLOCK] IN=br-f0ffa4389059 OUT= MAC=8a:42:46:12:b8:7b:3e:54:03:87:ee:e5:08:00 SRC=172.18.0.3 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=65380 DF PROTO=TCP SPT=38856 DPT=8081 WINDOW=64240 RES=0x00 SYN URGP=0
2026-02-17T22:28:47.936020+00:00 moonlight kernel: [UFW BLOCK] IN=br-f0ffa4389059 OUT= MAC=8a:42:46:12:b8:7b:3e:54:03:87:ee:e5:08:00 SRC=172.18.0.3 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=49979 DF PROTO=TCP SPT=54202 DPT=8081 WINDOW=64240 RES=0x00 SYN URGP=0
So, I ran ufw allow from 172.18.0.3
and tried to load
https://matrix-rtc.tsuki.games/healthz
in my web browser. This time the request
succeeded!
However, if I try to join a voice call, the websocket connection disconnects after a few seconds and I still cannot talk to anyone. I realize at this point that I never set up a reverse proxy for Coturn, and I have no idea how that would work since it's a websocket connection.
I find coturn/coturn#702, in which someone posted their nginx configuration for Coturn and mentioned that they were using NGINX through SWAG, which is exactly what I'm doing.