This is an old revision of the document!
Element Call
Rootless Podman as user worker on host support (8.28.86.82, 2604:fa40:0:10::9). Apache terminates TLS for the two HTTP names. The containers speak plain HTTP on localhost. TURN is UDP on 3479. Talk keeps 3478. Apache keeps TCP 443. Synapse stays on the Matrix VM.
| Name | Container | Bind | Public |
|---|---|---|---|
| Element Call UI | element-call | 127.0.0.1:8083 → 8080 | https://call.gnulinux.club |
| JWT | lk-jwt | 127.0.0.1:8085 → 8080 | https://webrtc.gnulinux.club/livekit/jwt/ |
| LiveKit signalling | livekit | 127.0.0.1:7880 → 7880 | https://webrtc.gnulinux.club/livekit/sfu/ |
| ICE TCP | same | 7881/tcp | 8.28.86.82:7881 |
| ICE UDP | same | 50100-50200/udp | that range |
| TURN listen | same | 3479/udp | webrtc.gnulinux.club:3479 |
| TURN relay | same | 35000-35100/udp | that range |
Do not use 8080, 8082, 8088, 3478, or 443.
Images:
ghcr.io/element-hq/element-call:v0.26.1ghcr.io/element-hq/lk-jwt-service:latestdocker.io/livekit/livekit-server:latest
Do not use podman generate systemd. Units come from Quadlet files in ~/.config/containers/systemd/.
call.gnulinux.club and webrtc.gnulinux.club → 8.28.86.82 and 2604:fa40:0:10::9.
ufw allow 3479/udp comment 'Element Call TURN UDP' ufw allow 35000:35100/udp comment 'Element Call TURN relay' ufw allow 7881/tcp comment 'Element Call ICE TCP' ufw allow 50100:50200/udp comment 'Element Call ICE UDP'
mkdir -p ~/element-call umask 077 openssl rand -hex 32 | tee ~/element-call/livekit.secret chmod 600 ~/element-call/livekit.secret
Key name: matrixrtc. Placeholder used below:
LIVEKIT_SECRET=replace-with-your-livekit-secret
certbot certonly --apache -d call.gnulinux.club -d webrtc.gnulinux.club a2enmod proxy proxy_http proxy_wstunnel headers rewrite ssl
Apache is the only process that reads these.
cat > ~/element-call/livekit.yaml << 'EOF'
port: 7880
bind_addresses:
- "0.0.0.0"
rtc:
tcp_port: 7881
port_range_start: 50100
port_range_end: 50200
node_ip: 8.28.86.82
use_external_ip: true
room:
auto_create: false
logging:
level: info
keys:
matrixrtc: "replace-with-your-livekit-secret"
webhook:
api_key: matrixrtc
urls:
- https://webrtc.gnulinux.club/livekit/jwt/sfu_webhook
turn:
enabled: true
domain: webrtc.gnulinux.club
udp_port: 3479
relay_range_start: 35000
relay_range_end: 35100
EOF
chmod 600 ~/element-call/livekit.yaml
node_ip is the public IPv4 clients use for media. It does not take Apache's TCP 443. auto_create: false is required. The JWT service creates rooms.
The image serves files from /app. A mount on /usr/share/nginx/html is ignored. The file must exist before the container starts, or Podman creates a directory and nginx falls through to index.html.
cat > ~/element-call/config.json << 'EOF'
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.gnulinux.club",
"server_name": "gnulinux.club"
},
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://webrtc.gnulinux.club/livekit/jwt"
}
]
}
}
EOF
lk-jwt must resolve webrtc.gnulinux.club to the host. Without AddHost, room creation hairpins to the public IP and gets connection refused. LIVEKIT_FULL_ACCESS_HOMESERVERS is gnulinux.club, not matrix.gnulinux.club.
systemctl --user disable --now container-element-call.service container-lk-jwt.service container-livekit.service 2>/dev/null || true
rm -f ~/.config/systemd/user/container-element-call.service \
~/.config/systemd/user/container-lk-jwt.service \
~/.config/systemd/user/container-livekit.service
podman rm -f element-call lk-jwt livekit
mkdir -p ~/.config/containers/systemd
cat > ~/.config/containers/systemd/container-element-call.container << 'EOF'
[Container]
ContainerName=element-call
Image=ghcr.io/element-hq/element-call:v0.26.1
PublishPort=127.0.0.1:8083:8080
Volume=/home/worker/element-call/config.json:/app/config.json:ro,Z
PodmanArgs=--cpus=1 --memory=512m
[Service]
Restart=always
TimeoutStopSec=120
[Install]
WantedBy=default.target
EOF
cat > ~/.config/containers/systemd/container-lk-jwt.container << 'EOF'
[Container]
ContainerName=lk-jwt
Image=ghcr.io/element-hq/lk-jwt-service:latest
PublishPort=127.0.0.1:8085:8080
AddHost=webrtc.gnulinux.club:host-gateway
Environment=LIVEKIT_JWT_BIND=:8080
Environment=LIVEKIT_URL=wss://webrtc.gnulinux.club/livekit/sfu
Environment=LIVEKIT_KEY=matrixrtc
Environment=LIVEKIT_SECRET=replace-with-your-livekit-secret
Environment=LIVEKIT_FULL_ACCESS_HOMESERVERS=gnulinux.club
PodmanArgs=--cpus=1 --memory=512m
[Service]
Restart=always
TimeoutStopSec=120
[Install]
WantedBy=default.target
EOF
cat > ~/.config/containers/systemd/container-livekit.container << 'EOF'
[Container]
ContainerName=livekit
Image=docker.io/livekit/livekit-server:latest
PublishPort=127.0.0.1:7880:7880/tcp
PublishPort=7881:7881/tcp
PublishPort=3479:3479/udp
PublishPort=35000-35100:35000-35100/udp
PublishPort=50100-50200:50100-50200/udp
Volume=/home/worker/element-call/livekit.yaml:/etc/livekit.yaml:Z
Exec=--config /etc/livekit.yaml
PodmanArgs=--cpus=2 --memory=4g
[Service]
Restart=always
TimeoutStopSec=120
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user reset-failed container-element-call.service container-lk-jwt.service container-livekit.service
systemctl --user start container-livekit.service
systemctl --user start container-lk-jwt.service
systemctl --user start container-element-call.service
WantedBy=default.target starts them. Do not systemctl enable a Quadlet unit. Do not podman generate systemd.
systemctl --user is-active container-livekit.service container-lk-jwt.service container-element-call.service
podman inspect element-call lk-jwt livekit --format '{{.Name}} cpus={{.HostConfig.NanoCpus}} memory={{.HostConfig.Memory}}'
curl -fsS http://127.0.0.1:8085/healthz; echo
curl -sS http://127.0.0.1:8083/config.json; echo
podman logs --tail 15 livekit
Expect active on all three. config.json must be JSON, not the HTML page. LiveKit should log turn.portUDP 3479, relay_range_start 35000, and nodeIP 8.28.86.82.
/usr/local/bin/upgrade-element-call.sh. A tag change is an edit to Image= in the matching .container file before daemon-reload. The script does not recreate the units.
#!/bin/bash set -euo pipefail podman pull docker.io/livekit/livekit-server:latest podman pull ghcr.io/element-hq/lk-jwt-service:latest podman pull ghcr.io/element-hq/element-call:v0.26.1 systemctl --user stop container-livekit.service systemctl --user stop container-lk-jwt.service systemctl --user stop container-element-call.service systemctl --user daemon-reload systemctl --user reset-failed container-livekit.service container-lk-jwt.service container-element-call.service systemctl --user start container-livekit.service systemctl --user start container-lk-jwt.service systemctl --user start container-element-call.service curl -fsS http://127.0.0.1:8085/healthz echo curl -sS http://127.0.0.1:8083/config.json echo
Run it as worker:
su - worker -c '/bin/bash /usr/local/bin/upgrade-element-call.sh'
Do not sudo -u worker. That drops the session bus.
/etc/apache2/sites-available/call.gnulinux.club.conf:
<VirtualHost *:80>
ServerName call.gnulinux.club
RewriteEngine On
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
<VirtualHost *:443>
ServerName call.gnulinux.club
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/call.gnulinux.club/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/call.gnulinux.club/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
ProxyPass / http://127.0.0.1:8083/
ProxyPassReverse / http://127.0.0.1:8083/
</VirtualHost>
/etc/apache2/sites-available/webrtc.gnulinux.club.conf:
<VirtualHost *:80>
ServerName webrtc.gnulinux.club
RewriteEngine On
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
<VirtualHost *:443>
ServerName webrtc.gnulinux.club
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/webrtc.gnulinux.club/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/webrtc.gnulinux.club/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
ProxyTimeout 3600
ProxyPass /livekit/jwt/ http://127.0.0.1:8085/
ProxyPassReverse /livekit/jwt/ http://127.0.0.1:8085/
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/livekit/sfu/(.*) ws://127.0.0.1:7880/$1 [P,L]
ProxyPass /livekit/sfu/ http://127.0.0.1:7880/
ProxyPassReverse /livekit/sfu/ http://127.0.0.1:7880/
</VirtualHost>
a2ensite call.gnulinux.club.conf webrtc.gnulinux.club.conf
apache2ctl configtest && systemctl reload apache2
curl -fsS -o /dev/null -w 'jwt %{http_code}\n' https://webrtc.gnulinux.club/livekit/jwt/healthz
curl -fsS -o /dev/null -w 'call %{http_code}\n' https://call.gnulinux.club/
curl -s https://call.gnulinux.club/config.json
Both status lines must be 200. The config curl must be JSON.
No new ports. Existing HTTPS is enough. Append this at the bottom of /etc/matrix-synapse/homeserver.yaml, same indent as pid_file. Do not nest it under email or database.
experimental_features:
msc3266_enabled: true
msc4143_enabled: true
msc4222_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
matrix_rtc:
transports:
- type: livekit
livekit_service_url: "https://webrtc.gnulinux.club/livekit/jwt"
sudo systemctl restart matrix-synapse
Wait until it is active. A curl during those few seconds returns nginx 502. The LiveKit secret is not added here. Synapse only advertises the JWT URL. lk-jwt checks the OpenID token.
This Synapse serves the unstable route only. /_matrix/client/v1/rtc/transports returns M_UNRECOGNIZED. Element Call uses the unstable route. That is expected.
With a real Element access token:
curl -s -H "Authorization: Bearer TOKEN" \ https://matrix.gnulinux.club/_matrix/client/unstable/org.matrix.msc4143/rtc/transports
The body must contain https://webrtc.gnulinux.club/livekit/jwt.
/var/www/gnulinux.club/.well-known/matrix/client must be valid JSON. No # comments. A comment makes Element fail parse and show MISSING_MATRIX_RTC_TRANSPORT.
{
"m.homeserver": {
"base_url": "https://matrix.gnulinux.club"
},
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://webrtc.gnulinux.club/livekit/jwt"
}
]
}
The nginx vhost for gnulinux.club must allow the Element Call origin to read it:
location /.well-known/matrix/client {
default_type application/json;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization" always;
if ($request_method = OPTIONS) {
return 204;
}
}
nginx -t && systemctl reload nginx curl -s https://gnulinux.club/.well-known/matrix/client
In element.gnulinux.club config.json, replace the hosted call URL:
"element_call": {
"url": "https://call.gnulinux.club",
"use_exclusively": true,
"participant_limit": 8,
"brand": "Element Call"
}
https://call.element.io shows MISSING_MATRIX_RTC_TRANSPORT even when Synapse is correct. Hard-refresh after the change.
— oemb1905 2026/10/10 01:06