2026-02-11 19:49:29 +00:00
# Matrix RTC/Element Call Setup
:::info
This guide assumes that you are using docker compose for deployment. LiveKit only provides Docker images.
:::
2026-03-03 19:48:40 +00:00
:::tip
2026-03-29 19:34:56 +01:00
You can find help setting up MatrixRTC in our dedicated room - [#matrixrtc:continuwuity.org](https://matrix.to/#/%23matrixrtc%3Acontinuwuity.org)
2026-03-03 19:48:40 +00:00
:::
2026-02-11 19:49:29 +00:00
## Instructions
### 1. Domain
LiveKit should live on its own domain or subdomain. In this guide we use `livekit.example.com` - this should be replaced with a domain you control.
2026-02-11 20:49:51 +00:00
Make sure the DNS record for the (sub)domain you plan to use is pointed to your server.
2026-02-11 19:49:29 +00:00
### 2. Services
2026-02-21 16:53:19 +00:00
Using LiveKit with Matrix requires two services - LiveKit itself, and a service (`lk-jwt-service`) that grants Matrix users permission to connect to it.
2026-02-11 19:49:29 +00:00
You must generate a key and secret to allow the Matrix service to authenticate with LiveKit. `LK_MATRIX_KEY` should be around 20 random characters, and `LK_MATRIX_SECRET` should be around 64. Remember to replace these with the actual values!
2026-02-11 23:09:07 +00:00
:::tip Generating the secrets
LiveKit provides a utility to generate secure random keys
```bash
2026-05-06 09:58:32 +00:00
docker run --rm livekit/livekit-server:latest generate-keys
# API Key: APIUxUnMnSkuFWV
# API Secret: t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A
2026-02-11 23:09:07 +00:00
```
:::
2026-03-09 16:46:02 +00:00
Create a `docker-compose.yml` file as following:
2026-03-05 03:46:21 +00:00
2026-02-11 19:49:29 +00:00
```yaml
services:
2026-02-16 02:55:26 +00:00
lk-jwt-service:
2026-02-11 19:49:29 +00:00
image: ghcr.io/element-hq/lk-jwt-service:latest
2026-02-16 02:55:26 +00:00
container_name: lk-jwt-service
2026-02-11 19:49:29 +00:00
environment:
- LIVEKIT_JWT_BIND=:8081
2026-03-09 16:46:02 +00:00
- LIVEKIT_URL=wss://livekit.example.com # your LiveKit domain
- LIVEKIT_FULL_ACCESS_HOMESERVERS=example.com # your server_name
# Replace these with the generated values as above
- LIVEKIT_KEY=LK_MATRIX_KEY # APIUxUnMnSkuFWV
- LIVEKIT_SECRET=LK_MATRIX_SECRET # t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A
2026-02-11 19:49:29 +00:00
restart: unless-stopped
ports:
- "8081:8081"
2026-02-16 02:55:26 +00:00
livekit:
2026-02-11 19:49:29 +00:00
image: livekit/livekit-server:latest
2026-02-16 02:55:26 +00:00
container_name: livekit
2026-02-11 19:49:29 +00:00
command: --config /etc/livekit.yaml
restart: unless-stopped
volumes:
- ./livekit.yaml:/etc/livekit.yaml:ro
network_mode: "host" # /!\ LiveKit binds to all addresses by default.
# Make sure port 7880 is blocked by your firewall to prevent access bypassing your reverse proxy
# Alternatively, uncomment the lines below and comment `network_mode: "host"` above to specify port mappings.
# ports:
# - "127.0.0.1:7880:7880/tcp"
# - "7881:7881/tcp"
# - "50100-50200:50100-50200/udp"
```
2026-02-11 20:11:52 +00:00
Next, we need to configure LiveKit. In the same directory, create `livekit.yaml` with the following content - remembering to replace `LK_MATRIX_KEY` and `LK_MATRIX_SECRET` with the values you generated:
2026-02-11 19:49:29 +00:00
```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:
LK_MATRIX_KEY: LK_MATRIX_SECRET
2026-03-03 19:48:40 +00:00
# replace these with your key-secret pair. Example:
# APIUxUnMnSkuFWV: t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A
2026-02-11 19:49:29 +00:00
```
#### Firewall hints
You will need to allow ports `7881/tcp` and `50100:50200/udp` through your firewall. If you use UFW, the commands are: `ufw allow 7881/tcp` and `ufw allow 50100:50200/udp`.
### 3. Telling clients where to find LiveKit
2026-02-23 02:47:11 -05:00
To tell clients where to find LiveKit, you need to add the address of your `lk-jwt-service` to the `[global.matrix_rtc]` config section using the `foci` option.
2026-02-11 19:49:29 +00:00
2026-04-20 18:54:23 +00:00
The variable should be a list of servers serving as MatrixRTC endpoints. Replace the URL with the address you are deploying your instance of lk-jwt-service to:
2026-02-16 01:37:31 +00:00
```toml
2026-02-23 02:47:11 -05:00
[global.matrix_rtc]
foci = [
2026-02-16 01:37:31 +00:00
{ type = "livekit", livekit_service_url = "https://livekit.example.com" },
]
```
2026-04-20 18:54:23 +00:00
This will expose LiveKit information on the following endpoints for clients to discover:
- `/_matrix/client/unstable/org.matrix.msc4143/rtc/transports` (MSC4143 unstable, behind auth)
- `/.well-known/matrix/client` (fallback, not behind auth. Only enabled if `[global.well_known].client` is set)
2026-02-16 01:37:31 +00:00
2026-02-11 19:49:29 +00:00
### 4. Configure your Reverse Proxy
Reverse proxies can be configured in many different ways - so we can't provide a step by step for this.
2026-03-07 17:50:25 +00:00
All paths should be forwarded to LiveKit by default, with the exception of the following path prefixes, which should be forwarded to the JWT/Authentication service:
2026-02-11 19:49:29 +00:00
- `/sfu/get`
- `/healthz`
- `/get_token`
2026-02-16 02:55:26 +00:00
<details>
<summary>Example caddy config</summary>
2026-04-20 18:54:23 +00:00
2026-02-16 02:55:26 +00:00
```
2026-02-21 02:30:39 +00:00
livekit.example.com {
2026-02-16 02:55:26 +00:00
# for lk-jwt-service
@lk-jwt-service path /sfu/get* /healthz* /get_token*
route @lk-jwt-service {
2026-02-16 04:32:35 +01:00
reverse_proxy 127.0.0.1:8081
2026-02-16 02:55:26 +00:00
}
# for livekit
reverse_proxy 127.0.0.1:7880
}
```
2026-04-20 18:54:23 +00:00
2026-02-16 02:55:26 +00:00
</details>
2026-02-16 04:32:03 +01:00
<details>
<summary>Example nginx config</summary>
2026-04-20 18:54:23 +00:00
2026-02-16 04:32:03 +01:00
```
server {
2026-02-21 02:30:39 +00:00
server_name livekit.example.com;
2026-02-16 04:32:03 +01:00
# for lk-jwt-service
location ~ ^/(sfu/get|healthz|get_token) {
proxy_pass http://127.0.0.1:8081$request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
2026-02-16 03:47:16 +00:00
proxy_buffering off;
2026-02-16 04:32:03 +01:00
}
2026-02-21 16:53:19 +00:00
# for LiveKit
2026-02-16 04:32:03 +01:00
location / {
proxy_pass http://127.0.0.1:7880$request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_buffering off;
# websocket
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
```
2026-02-16 04:39:18 +01:00
Note that for websockets to work, you need to have this somewhere outside your server block:
```
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
```
2026-04-20 18:54:23 +00:00
2026-02-16 04:32:03 +01:00
</details>
2026-02-16 02:55:26 +00:00
<details>
<summary>Example traefik router</summary>
2026-04-20 18:54:23 +00:00
2026-02-16 02:55:26 +00:00
```
# on LiveKit itself
traefik.http.routers.livekit.rule=Host(`livekit.example.com`)
# on the JWT service
traefik.http.routers.livekit-jwt.rule=Host(`livekit.example.com`) && (PathPrefix(`/sfu/get`) || PathPrefix(`/healthz`) || PathPrefix(`/get_token`))
```
2026-04-20 18:54:23 +00:00
2026-02-16 02:55:26 +00:00
</details>
2026-02-11 19:49:29 +00:00
### 6. Start Everything
Start up the services using your usual method - for example `docker compose up -d`.
2026-03-06 17:29:43 +00:00
## Additional TURN configuration
2026-02-11 19:49:29 +00:00
2026-03-07 17:50:25 +00:00
### Using LiveKit's built-in TURN server
2026-02-21 02:58:12 +00:00
2026-03-07 17:50:25 +00:00
LiveKit includes a built-in TURN server which can be used in place of an external option. This TURN server will only work with LiveKit, so you can't use it for legacy Matrix calling or anything else.
2026-02-21 02:58:12 +00:00
If you don't want to set up a separate TURN server, you can enable this with the following changes:
```yaml
### add this to livekit.yaml ###
turn:
enabled: true
udp_port: 3478
relay_range_start: 50300
relay_range_end: 50400
domain: livekit.example.com
```
```yaml
2026-03-06 17:29:43 +00:00
### add these to livekit's docker-compose ###
2026-02-21 02:58:12 +00:00
ports:
- "3478:3478/udp"
- "50300-50400:50300-50400/udp"
2026-03-06 17:29:43 +00:00
### if you're using `network_mode: host`, you can skip this part
2026-02-21 02:58:12 +00:00
```
2026-04-23 19:39:16 +00:00
Recreate the LiveKit container (with `docker-compose up -d livekit`) to apply these changes. Remember to allow the new `3478/udp` and `50300:50400/udp` ports through your firewall.
2026-03-06 17:29:43 +00:00
2026-02-21 02:58:12 +00:00
### Integration with an external TURN server
2026-02-11 19:49:29 +00:00
2026-03-17 14:03:00 +00:00
If you've already [set up coturn](./turn), you can configure Livekit to use it.
:::tip Avoid port clashes between the two services
Before continuing, make sure coturn's `min-port` and `max-port` do not overlap with LiveKit's port range:
2026-02-11 19:49:29 +00:00
2026-02-11 20:48:30 +00:00
```ini
2026-03-17 14:03:00 +00:00
# in your coturn.conf
2026-02-11 19:49:29 +00:00
min-port=50201
max-port=65535
```
2026-03-17 14:03:00 +00:00
:::
2026-02-11 19:49:29 +00:00
2026-03-07 17:50:25 +00:00
Generate a long random secret for LiveKit, and add it to your coturn config under the `static-auth-secret` option. You can add as many secrets as you want, so set a different one for LiveKit to use.
2026-02-11 19:49:29 +00:00
2026-03-07 17:50:25 +00:00
Then configure LiveKit, making sure to replace `COTURN_SECRET` with the one you generated:
2026-02-11 19:49:29 +00:00
```yaml
# livekit.yaml
rtc:
turn_servers:
2026-02-21 03:03:30 +00:00
- host: coturn.example.com
2026-02-11 19:49:29 +00:00
port: 3478
2026-03-07 17:50:25 +00:00
protocol: udp
2026-02-11 19:49:29 +00:00
secret: "COTURN_SECRET"
2026-02-21 03:03:30 +00:00
- host: coturn.example.com
2026-03-07 17:50:25 +00:00
port: 3478
protocol: tcp
2026-02-11 19:49:29 +00:00
secret: "COTURN_SECRET"
2026-02-21 03:03:30 +00:00
- host: coturn.example.com
2026-03-07 17:50:25 +00:00
port: 5349
protocol: tls # Only if you have already set up TLS in your coturn
2026-02-11 19:49:29 +00:00
secret: "COTURN_SECRET"
```
2026-03-17 14:03:00 +00:00
Restart LiveKit and coturn to apply these changes.
2026-02-21 04:02:57 +00:00
## Testing
2026-02-11 20:48:30 +00:00
2026-05-06 09:58:32 +00:00
To test that LiveKit is successfully integrated with Continuwuity, you will need to replicate its [Token Exchange Flow](https://github.com/element-hq/lk-jwt-service#%EF%B8%8F-how-it-works--token-exchange-flow). Follow the steps below while checking Docker logs (`docker-compose logs --follow`), in order to help [troubleshooting](#troubleshooting) any issues.
2026-02-21 04:02:57 +00:00
2026-03-04 14:31:48 +00:00
First, you will need an access token for your current login session. These can be found in your client's settings or obtained via [this website](https://timedout.uk/mxtoken.html).
2026-03-04 08:48:41 +00:00
2026-05-06 09:58:32 +00:00
Then, using that token, fetch the discovery endpoints for MatrixRTC services:
2026-04-20 18:54:23 +00:00
```bash
2026-05-06 09:58:32 +00:00
curl -H "Authorization: Bearer <session-access-token>" \
2026-04-20 18:54:23 +00:00
https://matrix.example.com/_matrix/client/unstable/org.matrix.msc4143/rtc/transports
```
In the output, you should see the LiveKit URL matching the one [configured above](#3-telling-clients-where-to-find-livekit).
With the same token, request another OpenID token for use with the lk-jwt-service:
2026-03-04 08:48:41 +00:00
2026-03-06 17:29:43 +00:00
```bash
2026-04-20 18:54:23 +00:00
curl -X POST -H "Authorization: Bearer <session-access-token>" \
2026-02-21 04:02:57 +00:00
https://matrix.example.com/_matrix/client/v3/user/@user:example.com/openid/request_token
2026-04-20 18:54:23 +00:00
```
You will see a response as below:
```json
2026-03-05 03:46:21 +00:00
{"access_token":"<openid_access_token>","token_type":"Bearer","matrix_server_name":"example.com","expires_in":3600}
2026-02-21 04:02:57 +00:00
```
Next, create a `payload.json` file with the following content:
<details>
<summary>`payload.json`</summary>
```json
{
"room_id": "abc",
"slot_id": "xyz",
"openid_token": {
"matrix_server_name": "example.com",
2026-03-05 03:46:21 +00:00
"access_token": "<openid_access_token>",
2026-02-21 04:02:57 +00:00
"token_type": "Bearer"
},
"member": {
"id": "xyz",
"claimed_device_id": "DEVICEID",
"claimed_user_id": "@user:example.com"
}
}
```
2026-03-05 03:46:21 +00:00
Replace `matrix_server_name` and `claimed_user_id` with your information, and `<openid_access_token>` with the one you got from the previous step. Other values can be left as-is.
2026-02-21 04:02:57 +00:00
</details>
2026-03-04 08:48:41 +00:00
You can then send this payload to the lk-jwt-service:
2026-02-21 04:02:57 +00:00
2026-03-06 17:29:43 +00:00
```bash
2026-05-06 09:58:32 +00:00
curl -X POST -d @payload.json https://livekit.example.com/get_token
2026-04-20 18:54:23 +00:00
```
The lk-jwt-service will, after checking against Continuwuity, answer with a `jwt` token to create a LiveKit media room:
```json
2026-03-04 08:48:41 +00:00
{"url":"wss://livekit.example.com","jwt":"a_really_really_long_string"}
2026-02-21 04:02:57 +00:00
```
2026-04-20 18:54:23 +00:00
Use this token to test at the [LiveKit Connection Tester](https://livekit.io/connection-test). If everything works there, then you have set up LiveKit successfully!
2026-02-21 04:02:57 +00:00
2026-03-03 19:48:40 +00:00
## Troubleshooting
2026-05-06 09:58:32 +00:00
To debug any issues, you can place a call or redo the Testing instructions, and check the container logs for any specific errors. Use `docker-compose logs --follow` to follow these logs in real-time.
2026-03-03 19:48:40 +00:00
2026-03-06 17:29:43 +00:00
### Common errors in Element Call UI
2026-03-03 19:48:40 +00:00
2026-03-04 08:54:25 +00:00
- `MISSING_MATRIX_RTC_FOCUS`: LiveKit is missing from Continuwuity's config file
- "Waiting for media" popup always showing: a LiveKit URL has been configured in Continuwuity, but your client cannot connect to it for some reason
2026-03-03 19:48:40 +00:00
2026-05-06 09:58:32 +00:00
For browser-based clients, you can also inspect connections using DevTools' Networking tab, to see which requests are erroring out.
2026-03-06 17:29:43 +00:00
### Docker loopback networking issues
Some distros do not allow Docker containers to connect to its host's public IP by default. This would cause `lk-jwt-service` to fail connecting to `livekit` or `continuwuity` on the same host. As a result, you would see connection refused/connection timeouts log entries in the JWT service, even when `LIVEKIT_URL` has been configured correctly.
2026-03-03 19:48:40 +00:00
2026-03-06 17:29:43 +00:00
To alleviate this, you can try one of the following workarounds:
- Use `network_mode: host` for the `lk-jwt-service` container (instead of the default bridge networking).
2026-03-03 19:48:40 +00:00
- Add an `extra_hosts` file mapping livekit's (and continuwuity's) domain name to a localhost address:
2026-03-06 17:29:43 +00:00
```diff
2026-03-03 19:48:40 +00:00
# in docker-compose.yaml
services:
lk-jwt-service:
...
2026-03-06 17:29:43 +00:00
+ extra_hosts:
2026-03-07 08:23:57 +00:00
+ - "livekit.example.com:127.0.0.1"
2026-03-06 17:29:43 +00:00
+ - "matrix.example.com:127.0.0.1"
2026-03-03 19:48:40 +00:00
```
2026-03-06 17:29:43 +00:00
- (**untested, use at your own risk**) Implement an iptables workaround as shown [here](https://forums.docker.com/t/unable-to-connect-to-host-service-from-inside-docker-container/145749/6).
2026-03-03 19:48:40 +00:00
After implementing the changes and restarting your compose, you can test whether the connection works by cURLing from a sidecar container:
2026-03-06 17:29:43 +00:00
```bash
2026-05-06 09:58:32 +00:00
docker run --rm --net container:lk-jwt-service docker.io/curlimages/curl https://livekit.example.com
# OK
2026-03-03 19:48:40 +00:00
```
### Workaround for non-federating servers
2026-02-22 22:18:39 +00:00
2026-03-08 12:42:04 +00:00
When deploying on servers with federation disabled (`allow_federation = false`), LiveKit will fail as it can't fetch the required [OpenID endpoint](https://spec.matrix.org/v1.17/server-server-api/#get_matrixfederationv1openiduserinfo) via federation paths.
As a workaround, you can enable federation, but forbid all remote servers via the following config parameters:
```toml
### in your continuwuity.toml file ###
allow_federation = true
forbidden_remote_server_names = [".*"]
```
Subscribe to issue [!1440](https://forgejo.ellis.link/continuwuation/continuwuity/issues/1440) for future updates on this matter.
2026-02-22 22:18:39 +00:00
2026-02-21 04:02:57 +00:00
## Related Documentation
2026-02-21 02:43:05 +00:00
Guides:
- [Element Call self-hosting documentation](https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md)
2026-02-21 16:53:19 +00:00
- [Community guide with overview of LiveKit's mechanisms](https://tomfos.tr/matrix/livekit/)
2026-02-21 04:02:57 +00:00
- [Community guide using systemd](https://blog.kimiblock.top/2024/12/24/hosting-element-call/)
2026-02-21 02:43:05 +00:00
Specifications:
2026-04-20 18:54:23 +00:00
- [MSC4143 - MatrixRTC proposal](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)
- [MSC4195 - LiveKit proposal](https://github.com/matrix-org/matrix-spec-proposals/pull/4195)
2026-02-21 02:43:05 +00:00
2026-03-05 03:46:21 +00:00
Source code:
2026-02-21 02:43:05 +00:00
- [Element Call](https://github.com/element-hq/element-call)
- [lk-jwt-service](https://github.com/element-hq/lk-jwt-service)
- [LiveKit server](https://github.com/livekit/livekit)