Version: 25.05.20
In the following tutorial, we want to add Element Call as a Docker container to complement our Matrix Synapse server (Docker).
Since the beginning of 2025, calls via ElementX no longer automatically work through Jitsi or Coturn as they did with Element Classic. Instead, the Element Call backend (LiveKit and JWT) for ElementX must be self-hosted.
As Element Classic is no longer being developed, all users will soon need to switch to ElementX. However, there are many advantages to using Element Call (via LiveKit and JWT):
✅ Decentralized & Federated – No central authority; works across different Matrix homeservers.
✅ End-to-End Encrypted – Secure and private conversations.
✅ Standalone & Widget Mode – Usable as a standalone app or embedded within Matrix clients.
✅ WebRTC-Based – No additional software required.
✅ Scalable with LiveKit – Supports large meetings via SFU (MSC4195: MatrixRTC with LiveKit backend).
✅ Raise Hand Feature – Participants can signal when they want to speak, helping to keep conversations organized.
✅ Emoji Reactions – Users can react with emojis 👍️ 🎉 👏 🤘 to boost engagement and interactivity.
In our tutorial, we will install LiveKit and the JWT service to make Element Call work.

LiveKit and the JWT Service play central roles in extending Element Call for large and scalable meetings:
🔊 LiveKit – For Scalable Voice/Video Calls
LiveKit is a media server based on SFU (Selective Forwarding Unit) architecture that handles the processing and distribution of audio and video streams. It is needed when many participants join a meeting simultaneously—beyond what peer-to-peer connections (used in small calls) can handle.
Why is it necessary?
- Enables large meetings with high quality.
- Reduces the load on clients by centrally forwarding streams.
🔐 JWT Service – For Secure Authentication
The JWT (JSON Web Token) service issues signed tokens that authenticate users to LiveKit. Only clients with a valid token are allowed to join a call via LiveKit.
Why is it necessary?
- Prevents unauthorized access to LiveKit.
- Ensures secure and controlled participation in calls.
In short:
🔧 LiveKit enables large, stable meetings.
🔒 The JWT service ensures that only authorized users can participate.
Both components are essential for the scalable and secure operation of Element Call.ement Call in einer produktiven Umgebung.
🛠 Prerequisites for this guide:
- Matrix Synapse has already been fully set up according to this tutorial: Tutorial (English)
- Very good knowledge of Linux and the terminal is required
Some preparations for Element Call were already made during the Matrix Synapse setup.
Here is a quick summary of the most important ones:
⚠️ In the firewall of your VPS provider—or in UFW on Linux, if used—the following ports have been opened:
UDP: 50100–50200
TCP: 7881–7882

⚠️ The Docker network “matrixnetwork” has been set up so that the Docker containers (Synapse, LiveKit, and JWT) are all on the same network:
docker network create --subnet=172.37.51.0/24 matrixnetwork
The “homeserver.yaml” file of Synapse must also already include the necessary preparations for Element Call:
experimental_features:
msc3266_enabled: true
msc4222_enabled: true
msc4140_enabled: true
max_event_delay_duration: 24h
rc_message:
per_second: 0.5
burst_count: 30
rc_delayed_event_mgmt:
per_second: 1
burst_count: 20
# Aktiviert die Bereitstellung der .well-known URL zur Weiterleitung föderativer Kommunikation an Port 443 - wird später für Element Call benötigt !!
serve_server_wellknown: true
And the Docker container file for Synapse must have been added to the Docker network.
networks:
matrixnetwork:
ipv4_address: 172.37.51.13
If the tutorial has been followed, then everything has already been set up correctly.
🔧 Installation of Element Call (LiveKit and JWT)
We will now begin setting up Element Call and log in to the server:
ssh root@SERVER-IP
First, we make sure the server is up to date using the following command:
apt-get update && apt-get upgrade && apt-get autoremove
After everything has been updated, we create a directory in our Docker project folder (for example, in the /home/ directory):
cd /home/
mkdir elementcall
We switch to the project directory using:
cd elementcall
To allow LiveKit and the JWT service to communicate securely with each other, we generate two random key pairs and take note of them so we can enter them later. We use the following command for this:
tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 64
Now we set up the docker-compose.yml file in the /elementcall/ directory:
nano docker-compose.yml
The Docker Compose file contains the following content, with some adjustments required:
services:
matrix-element-call-jwt:
image: ghcr.io/element-hq/lk-jwt-service:latest
networks:
matrixnetwork:
ipv4_address: 172.37.51.14
container_name: matrix-element-call-jwt
hostname: matrix-element-call-jwt
environment:
- LK_JWT_PORT=8080
- LIVEKIT_URL=https://rtc.matrix.domain.com/livekit/sfu
- LIVEKIT_KEY=zsmqzHmunjc3CgfoiWzb8fGXpNZVJ2nQmzttzNkcA7qNWTGDJGCPPQiYp8sAudOB
- LIVEKIT_SECRET=uck5Phkw96pckJIzvsOBkrHfwgZv4d7NDT3uhjmzkl2YQvGUh4O50gYu54i83I2j
- LIVEKIT_LOCAL_HOMESERVERS=domain.com
restart: always
ports:
- :8080
matrix-element-call-livekit:
image: livekit/livekit-server:latest
networks:
matrixnetwork:
ipv4_address: 172.37.51.15
container_name: matrix-element-call-livekit
hostname: matrix-element-call-livekit
command: --dev --config /etc/livekit.yaml
ports:
- :7880/tcp
- 7881:7881/tcp
- 7882:7882/tcp
- 50100-50200:50100-50200/udp
restart: always
volumes:
- ./data/matrix-element-call-livekit/config.yaml:/etc/livekit.yaml:ro
networks:
matrixnetwork:
external: true
The following adjustments need to be made in the file:
- matrixnetwork IPv4 addresses: These should be adjusted to match the “matrixnetwork” (see above, Docker network). The IPs for each container should be assigned manually here (in sequential order for Synapse, LiveKit, and JWT – e.g.,
.13
,.14
,.15
,.16
, etc.). - LIVEKIT_URL: Enter the subdomain under which the LiveKit server will be accessible. This subdomain must still be created later (see below).
- LIVEKIT_KEY and LIVEKIT_SECRET: Enter the two randomly generated codes from earlier here.
- LIVEKIT_LOCAL_HOMESERVERS: Enter the main domain of your VPS server here—not a subdomain, but the full domain like
domain.com
.
A quick note about the environment variable:
LIVEKIT_LOCAL_HOMESERVERS=domain.com
This variable is part of a LiveKit feature still under development. It defines which homeservers are allowed to initiate new calls. In the future, only users from your own homeserver will be able to start calls; federated users will still be able to join, but not initiate new calls.
This is especially useful for public services with access control. While the setting currently has no effect, you can already define it now to be prepared.
If you want to allow multiple homeservers to initiate new calls, simply separate them with commas:
LIVEKIT_LOCAL_HOMESERVERS=domain.com,example.com
Next, we’ll create the LiveKit configuration:
mkdir -p ./data/matrix-element-call-livekit && nano ./data/matrix-element-call-livekit/config.yaml
The following content goes into the file (use nano as the editor):
port: 7880
bind_addresses:
- "0.0.0.0"
rtc:
tcp_port: 7881
port_range_start: 50100
port_range_end: 50200
use_external_ip: false
ips:
includes:
- 111.150.60.108/32
logging:
level: debug
turn:
enabled: false
domain: localhost
cert_file: ""
key_file: ""
tls_port: 5349
udp_port: 443
external_tls: true
keys:
zsmqzHmunjc3CgfoiWzb8fGXpNZVJ2nQmzttzNkcA7qNWTGDJGCPPQiYp8sAudOB: "uck5Phkw96pckJIzvsOBkrHfwgZv4d7NDT3uhjmzkl2YQvGUh4O50gYu54i83I2j"
About the variables:
- includes: This is the IP address of your VPS (be sure to include the
/32
at the end!). - keys: These are the keys from the
docker-compose.yml
file above (first the LIVEKIT_KEY, then the LIVEKIT_SECRET).
Next, create the DNS entry for Element Call with your VPS provider:
An A record is fully sufficient — an AAAA record is not needed.
rtc.matrix.domain.com 7200 IN A 111.150.60.108
Next step: Set up the forwarding rules using the NGINX Proxy Manager (NPM).

We create the domain that we already specified in the docker-compose.yml
file and for which an A record already exists with our VPS provider (LIVEKIT_URL):

The IP address we enter here is the internal Docker IP (from matrixnetwork) for the JWT service as defined in the docker-compose.yml
.
However, under Advanced, we also need to enter a Custom NGINX Proxy Configuration:

location ^~ /livekit/jwt/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://172.37.51.14:8080/;
}
location ^~ /livekit/sfu/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
proxy_set_header Accept-Encoding gzip;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://172.37.51.15:7880/;
}
Again, for clarification:
The IP address ending in .14
points to the internal Docker container for JWT, and the IP address ending in .15
refers to the LiveKit IP address—both as defined in the docker-compose.yml
.
We now save the entry in the NPM (NGINX Proxy Manager).
👉 Well-Known Configuration
A crucial part of the Matrix setup is the .well-known entry. It tells clients like ElementX where your homeserver is located and how to reach the LiveKit backend for MatrixRTC (Element Call).
🔧 What matters most is the domain on which this entry is available—it must match the server_name
in your homeserver.yaml
.
Additionally important is /.well-known/matrix/server
, which is needed for federation. This tells other servers where to find your Synapse endpoint—especially important when using delegation or when Synapse is not running directly under the server_name
.
📌 Normally, /.well-known/matrix/client
is automatically created during Synapse installation. For Element Call, we’ll just extend this. However, we’ll go through the full configuration here to ensure all existing entries are properly updated.
🔍 Domain mapping for .well-known:
server_name: matrix.domain.com
→.well-known
should be available athttps://matrix.domain.com
.server_name: domain.com
→.well-known
must be reachable athttps://domain.com
.
We will now set up the .well-known
entry again under “Advanced” in the NGINX Proxy Manager.

proxy_buffering off;
proxy_redirect off;
client_max_body_size 0;
location /.well-known/matrix/client {
default_type application/json;
add_header Content-Type application/json;
add_header "Access-Control-Allow-Origin" *;
return 200 '{"m.homeserver": {"base_url": "https://matrix.domain.com"}, "m.identity_server": {"base_url": "https://vector.im"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "https://rtc.matrix.domain.com/livekit/jwt"}, {"type": "nextgen_new_foci_type", "props_for_nextgen_foci": "val"}]}';
}
We have successfully completed all the setup steps.
Now let’s return to the project directory (/home/elementcall
) and start the container:
cd /home/elementcall
docker compose up -d
We can check the result using “docker ps
” in the command line. We should see that the containers are running.

✅ What have we accomplished in this tutorial?
- Installed Element Call: LiveKit and JWT
- Connected Matrix Synapse with Element Call via additional features
- Set up subdomains (e.g.,
rtc.matrix.domain.com
) - Properly configured the NGINX Proxy Manager
- Correctly created the .well-known entry for the domain
Enjoy secure (video) calls using Element Call (LiveKit + JWT) and ElementX!
You can find the official project and further documentation on GitHub:
👉 https://github.com/element-hq/element-call

bc1q8dxp9mlt3mkvaklu2vn8j6jteqyv53kschc90v

Lightning: itsc@strike.me
