User Tools

Site Tools


computing:element-call

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
computing:element-call [2026/10/10 00:57] – created oemb1905computing:element-call [2026/10/10 06:03] (current) – oemb1905
Line 1: Line 1:
-# Element Call (LiveKit) — Rootless Podman Quadlet+-------------------------------------------
  
-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.+  * **element-call** 
 +  * **Jonathan Haack** 
 +  * **Haack's Networking** 
 +  * **webmaster@haacksnetworking.org**
  
-| 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.+//Element Call//
  
-Images:+------------------------------------------- 
 +~~NOTOC~~
  
-- `ghcr.io/element-hq/element-call:v0.26.1` +==== Introduction ====
-- `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/`.+This tutorial is for Debian users who already self-host Matrix-Synapse and need to build an Element Call instance to support it with. Now, in my case, I manually self-host [[https://wiki.haacksnetworking.org/doku.php?id=computing:synapse|Matrix-Synapse]]. But/and, when I built this instance, Element Call did not exist - Jitsi was not only an add-on, but the standard. Over time, that changed, but/and I had never set it up. I eventually got to it about a year ago and then again about 8 months ago, but failed both times. Manual installation has proven difficult. Given I had recently setup my new virtual docker/OCI host, I thought this might be a perfect use-case for it. Here's the overall layout of what I came up with:
  
----+^ 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 |
  
-## 1. DNS+Since this was the 4th container on the same instance, I need to avoid using ports 8080, 8082, 8088, 3478, and 443. I just used 3479 instead, since HPB was already using 3478. The main proxy port was also easy to adjust. 
  
-`call.gnulinux.club` and `webrtc.gnulinux.club` → `8.28.86.82` and `2604:fa40:0:10::9`.+Images:
  
----+  * ''ghcr.io/element-hq/element-call:v0.26.1'' 
 +  * ''ghcr.io/element-hq/lk-jwt-service:latest'' 
 +  * ''docker.io/livekit/livekit-server:latest''
  
-## 2. Firewall (root)+==== 1. DNS ====
  
-```bash +Set your A/AAAA records to point at the virtual machine / OCI container host. The reverse proxy we set up later will handle the rest. 
-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' +
-```+
  
----+==== 2. Firewall (root) ====
  
-## 3. Secret (worker)+Here's the ufw rules I came up with:
  
-```bash+<code> 
 +ufw allow 3479/udp 
 +ufw allow 35000:35100/udp 
 +ufw allow 7881/tcp 
 +ufw allow 50100:50200/udp 
 +</code> 
 + 
 +==== 3. Secret (worker) ==== 
 + 
 +Let's get the secrets cut: 
 + 
 +<code>
 mkdir -p ~/element-call mkdir -p ~/element-call
 umask 077 umask 077
 openssl rand -hex 32 | tee ~/element-call/livekit.secret openssl rand -hex 32 | tee ~/element-call/livekit.secret
 chmod 600 ~/element-call/livekit.secret chmod 600 ~/element-call/livekit.secret
-```+</code>
  
-Key name: `matrixrtc`. Placeholder used below:+==== 4. Certificates (root) ====
  
-``` +After the key and project directory are ready, we can cut the cert:
-LIVEKIT_SECRET=replace-with-your-livekit-secret +
-```+
  
---- +<code>
- +
-## 4. Certificates (root) +
- +
-```bash+
 certbot certonly --apache -d call.gnulinux.club -d webrtc.gnulinux.club certbot certonly --apache -d call.gnulinux.club -d webrtc.gnulinux.club
 a2enmod proxy proxy_http proxy_wstunnel headers rewrite ssl a2enmod proxy proxy_http proxy_wstunnel headers rewrite ssl
-```+</code>
  
-Apache is the only process that reads these.+==== 5. LiveKit config (worker) ====
  
----+Let's pull the images and create our configuration files:
  
-## 5. LiveKit config (worker)+<code> 
 +podman pull ghcr.io/element-hq/element-call:v0.26.1 
 +podman pull ghcr.io/element-hq/lk-jwt-service:latest 
 +podman pull docker.io/livekit/livekit-server:latest
  
-```bash 
 cat > ~/element-call/livekit.yaml << 'EOF' cat > ~/element-call/livekit.yaml << 'EOF'
 port: 7880 port: 7880
Line 101: Line 106:
 EOF EOF
 chmod 600 ~/element-call/livekit.yaml chmod 600 ~/element-call/livekit.yaml
-```+</code>
  
-`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.+Make sure ''auto_create: false'' is declared - The JWT service creates rooms.
  
----+==== 6. Element Call config (worker) ====
  
-## 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''. On the VM, establish the endpoint:
  
-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`. +<code>
- +
-```bash+
 cat > ~/element-call/config.json << 'EOF' cat > ~/element-call/config.json << 'EOF'
 { {
Line 128: Line 131:
 } }
 EOF EOF
-```+</code>
  
----+==== 7. Quadlets ====
  
-## 7. Quadlets+Also on the VM, make sure the quadlet is setup. You should note that ''lk-jwt'' must resolve ''webrtc.gnulinux.club'' which is the host. Without ''AddHost'', room creation hairpins to the public IP and gets connection refused so we define the hostname inside the container with an extra line to address this failure. The other thing is ''LIVEKIT_FULL_ACCESS_HOMESERVERS'' is ''gnulinux.club'', not ''matrix.gnulinux.club''.
  
-`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`. +<code>
- +
-```bash+
 systemctl --user disable --now container-element-call.service container-lk-jwt.service container-livekit.service 2>/dev/null || true 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 \ rm -f ~/.config/systemd/user/container-element-call.service \
Line 202: Line 203:
 systemctl --user start container-lk-jwt.service systemctl --user start container-lk-jwt.service
 systemctl --user start container-element-call.service systemctl --user start container-element-call.service
-```+</code>
  
-`WantedBy=default.target` starts them. Do not `systemctl enable` a Quadlet unit. Do not `podman generate systemd`.+==== 8. Verify ====
  
----+Let's make sure those quadlets all function:
  
-## 8. Verify +<code>
- +
-```bash+
 systemctl --user is-active container-livekit.service container-lk-jwt.service container-element-call.service 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}}' podman inspect element-call lk-jwt livekit --format '{{.Name}} cpus={{.HostConfig.NanoCpus}} memory={{.HostConfig.Memory}}'
Line 216: Line 215:
 curl -sS http://127.0.0.1:8083/config.json; echo curl -sS http://127.0.0.1:8083/config.json; echo
 podman logs --tail 15 livekit podman logs --tail 15 livekit
-```+</code>
  
-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`.+Here, we are looking for ''active'' on all three and ''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'' and/or the instance's IP.
  
----+==== 9. Upgrade script ====
  
-## 9. Upgrade script+Here's a simple upgrade script for the OCI container. Create ''nano /usr/local/bin/upgrade-element-call.sh''. In that file, place:
  
-`/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. +<code>
- +
-```bash+
 #!/bin/bash #!/bin/bash
 set -euo pipefail set -euo pipefail
 +export XDG_RUNTIME_DIR=/run/user/$(id -u)
 +export DBUS_SESSION_BUS_ADDRESS=unix:path=${XDG_RUNTIME_DIR}/bus
  
 podman pull docker.io/livekit/livekit-server:latest podman pull docker.io/livekit/livekit-server:latest
Line 248: Line 247:
 curl -sS http://127.0.0.1:8083/config.json curl -sS http://127.0.0.1:8083/config.json
 echo echo
-```+</code>
  
-Run it as `worker`:+==== 10. Apache (root) ====
  
-```bash +Now that the service is created and running, make sure dns for a/aaaa is ready and then let's cut the cert.
-su - worker -c '/bin/bash /usr/local/bin/upgrade-element-call.sh' +
-```+
  
-Do not `sudo -u worker`. That drops the session bus.+  sudo certbot certonly --apache -d call.gnulinux.club 
 +  sudo certbot certonly --apache -d webrtc.gnulinux.club
  
----+We also need to create the reverse proxy so we can forward external requests upstream to the local listening services we just created. For the cert(s) ''nano /etc/apache2/sites-available/call.gnulinux.club.conf'' and drop in:
  
-## 10. Apache (root) +<code>
- +
-`/etc/apache2/sites-available/call.gnulinux.club.conf`: +
- +
-```apache+
 <VirtualHost *:80> <VirtualHost *:80>
     ServerName call.gnulinux.club     ServerName call.gnulinux.club
Line 270: Line 264:
     RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]     RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
 </VirtualHost> </VirtualHost>
 +</code>
 +
 +Inside ''nano /etc/apache2/sites-available/call.gnulinux.club-ssl.conf'' something like:
  
 +<code>
 <VirtualHost *:443> <VirtualHost *:443>
     ServerName call.gnulinux.club     ServerName call.gnulinux.club
Line 282: Line 280:
     ProxyPassReverse / http://127.0.0.1:8083/     ProxyPassReverse / http://127.0.0.1:8083/
 </VirtualHost> </VirtualHost>
-```+</code>
  
-`/etc/apache2/sites-available/webrtc.gnulinux.club.conf`:+Inside ''nano /etc/apache2/sites-available/webrtc.gnulinux.club.conf'':
  
-```apache+<code>
 <VirtualHost *:80> <VirtualHost *:80>
     ServerName webrtc.gnulinux.club     ServerName webrtc.gnulinux.club
Line 292: Line 290:
     RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]     RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
 </VirtualHost> </VirtualHost>
 +</code>
  
 +Inside ''nano /etc/apache2/sites-available/webrtc.gnulinux.club-ssl.conf'':
 +
 +<code>
 <VirtualHost *:443> <VirtualHost *:443>
     ServerName webrtc.gnulinux.club     ServerName webrtc.gnulinux.club
Line 311: Line 313:
     ProxyPassReverse /livekit/sfu/ http://127.0.0.1:7880/     ProxyPassReverse /livekit/sfu/ http://127.0.0.1:7880/
 </VirtualHost> </VirtualHost>
-```+</code>
  
-```bash+Once that's done, let's enable them and check the endpoints: 
 + 
 +<code>
 a2ensite call.gnulinux.club.conf webrtc.gnulinux.club.conf a2ensite call.gnulinux.club.conf webrtc.gnulinux.club.conf
 apache2ctl configtest && systemctl reload apache2 apache2ctl configtest && systemctl reload apache2
Line 319: Line 323:
 curl -fsS -o /dev/null -w 'call %{http_code}\n' https://call.gnulinux.club/ curl -fsS -o /dev/null -w 'call %{http_code}\n' https://call.gnulinux.club/
 curl -s https://call.gnulinux.club/config.json curl -s https://call.gnulinux.club/config.json
-```+</code>
  
-Both status lines must be `200`. The config curl must be JSON.+Both status lines must be ''200''. The config curl must be JSON.
  
----+==== 11. Synapse (Matrix VM) ====
  
-## 11. Synapse (Matrix VM)+On the Matrix VM, we need to edit ''nano /etc/matrix-synapse/homeserver.yaml'':
  
-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`. +<code>
- +
-```yaml+
 experimental_features: experimental_features:
   msc3266_enabled: true   msc3266_enabled: true
Line 349: Line 351:
     - type: livekit     - type: livekit
       livekit_service_url: "https://webrtc.gnulinux.club/livekit/jwt"       livekit_service_url: "https://webrtc.gnulinux.club/livekit/jwt"
-```+</code>
  
-```bash+Then, restart the service: 
 + 
 +<code>
 sudo systemctl restart matrix-synapse sudo systemctl restart matrix-synapse
-``` +</code>
- +
-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:+You can test with:
  
-```bash+<code>
 curl -s -H "Authorization: Bearer TOKEN" \ curl -s -H "Authorization: Bearer TOKEN" \
   https://matrix.gnulinux.club/_matrix/client/unstable/org.matrix.msc4143/rtc/transports   https://matrix.gnulinux.club/_matrix/client/unstable/org.matrix.msc4143/rtc/transports
-```+</code>
  
-The body must contain `https://webrtc.gnulinux.club/livekit/jwt`.+The body of the output should contain ''https://webrtc.gnulinux.club/livekit/jwt''.
  
----+==== 12. Well-known (Matrix VM) ====
  
-## 12. Well-known (Matrix VM)+In an initial build, I left old comments/notes inside ''nano /var/www/gnulinux.club/.well-known/matrix/client'' commented out at the end, but the API parser can't handle comments. Everything - literally - must be valid JSON. The comments made Element fail and show ''MISSING_MATRIX_RTC_TRANSPORT''. The new ''nano /var/www/gnulinux.club/.well-known/matrix/client'' should look like:
  
-`/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`. +<code>
- +
-```json+
 { {
   "m.homeserver": {   "m.homeserver": {
Line 386: Line 384:
   ]   ]
 } }
-```+</code>
  
-The nginx vhost for `gnulinux.club` must allow the Element Call origin to read it:+The nginx vhost for ''gnulinux.club'' must allow the Element Call origin to read it:
  
-```nginx+<code>
 location /.well-known/matrix/client { location /.well-known/matrix/client {
     default_type application/json;     default_type application/json;
Line 400: Line 398:
     }     }
 } }
-```+</code>
  
-```bash+Reload service and check API endpoint: 
 + 
 +<code>
 nginx -t && systemctl reload nginx nginx -t && systemctl reload nginx
 curl -s https://gnulinux.club/.well-known/matrix/client curl -s https://gnulinux.club/.well-known/matrix/client
-```+</code>
  
----+==== 13. Element Web ====
  
-## 13. Element Web+In ''element.gnulinux.club'' ''config.json'', replace the hosted call URL block with the following:
  
-In `element.gnulinux.club` `config.json`, replace the hosted call URL: +<code>
- +
-```json+
 "element_call": { "element_call": {
   "url": "https://call.gnulinux.club",   "url": "https://call.gnulinux.club",
Line 420: Line 418:
   "brand": "Element Call"   "brand": "Element Call"
 } }
-``` +</code>
- +
-`https://call.element.io` shows `MISSING_MATRIX_RTC_TRANSPORT` even when Synapse is correct. Hard-refresh after the change. +
- +
----+
  
-## Facts+That should be it. Debug and review line by line if stuff is failing. Reach out on Matrix if needed.
  
--+ --- //[[alerts@haacksnetworking.org|oemb1905]] 2026/10/10 05:54//
computing/element-call.1791593845.txt.gz · Last modified: by oemb1905