User Tools

Site Tools


computing:element-call

This is an old revision of the document!



  • element-call
  • Jonathan Haack
  • Haack's Networking
  • webmaster@haacksnetworking.org

Element Call


Introduction

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.1
  • ghcr.io/element-hq/lk-jwt-service:latest
  • docker.io/livekit/livekit-server:latest

Do not use podman generate systemd. Units come from Quadlet files in ~/.config/containers/systemd/.

1. DNS

call.gnulinux.club and webrtc.gnulinux.club → 8.28.86.82 and 2604:fa40:0:10::9.

2. Firewall (root)

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'

3. Secret (worker)

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

4. Certificates (root)

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.

5. LiveKit config (worker)

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.

6. Element Call config (worker)

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

7. Quadlets

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.

8. Verify

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.

9. Upgrade script

/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.

10. Apache (root)

/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.

11. Synapse (Matrix VM)

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.

12. Well-known (Matrix VM)

/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

13. Element Web

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.

Facts

  • UD

— oemb1905 2026/10/10 01:06

computing/element-call.1791594542.txt.gz · Last modified: by oemb1905