Jitsi Services
Seven Docker services run on a single EC2 instance (t3.xlarge, 4 vCPU, 16GB RAM, 16.16.21.64) to provide video conferencing with automatic transcription at https://book.aiqlick.com.
Service Overview
| Service | Image | Memory Limit | Purpose |
|---|---|---|---|
| web | ECR (aiqlick-meeting) | 512 MB | Custom Jitsi Meet frontend with Let's Encrypt |
| prosody | jitsi/prosody:stable-9823 | 512 MB | XMPP server, JWT auth, MUC rooms |
| jicofo | jitsi/jicofo:stable-9823 | 768 MB | Conference focus, allocates JVB and invokes Jigasi |
| jvb | jitsi/jvb:stable-9823 | 6 GB | Video bridge (SFU), handles all media traffic |
| jigasi | ECR (aiqlick-jigasi) | 1.5 GB | Transcription gateway with Whisper WebSocket |
| coturn | coturn/coturn:4.6 | 256 MB | TURN server for NAT traversal (global reach) |
| dozzle | amir20/dozzle | 128 MB | Live log viewer (localhost:8888, SSM tunnel only) |
Total memory budget: ~9.7 GB allocated, ~6.3 GB reserved for OS/Docker/headroom.
Web (aiqlick-meeting)
Custom Jitsi Meet build served by nginx with Let's Encrypt TLS. Base image jitsi/web:stable-9823 with the aiqlick-meeting React build copied to /usr/share/jitsi-meet/. A custom-config.js is deployed by deploy.sh for video quality settings and STUN server configuration.
Nginx Routes
| Path | Upstream | Purpose |
|---|---|---|
/ | Static files | Jitsi Meet React app |
/http-bind | xmpp.meet.jitsi:5280 | BOSH connection to Prosody |
/xmpp-websocket | xmpp.meet.jitsi:5280 | WebSocket signaling to Prosody |
/colibri-ws | jvb:9090 | Colibri WebSocket to JVB |
Video Quality Settings
Configured in custom-config.js:
- Resolution: ideal 720p, max 1080p / 30fps (most webcams cap at 720p; the higher max lets capable cams send 1080p when bandwidth allows)
channelLastN=4— only forward 4 active speaker video streams to each receiver. Drops bandwidth pressure on constrained networks where receiver BWE often sits at 200–650 kbps. Layer suspension still hides unwatched layers automatically.enableLayerSuspension=true— JVB stops forwarding unwatched simulcast layers- Screen share capped at 15fps
- Bitrate caps per simulcast layer (overrides upstream Jitsi defaults via
config.videoQuality.maxBitratesVideoincustom-config.js):
| Codec | LOW | STANDARD | HIGH |
|---|---|---|---|
| VP8 / H264 | 300 kbps | 800 kbps | 2500 kbps |
| VP9 | 200 kbps | 600 kbps | 2000 kbps |
| AV1 | 150 kbps | 450 kbps | 1500 kbps |
Constrained receivers are unaffected — TCC bandwidth estimation still pins them to LD/SD layers automatically. The bumped HIGH cap only unlocks crisper 720p for clients that can actually send the higher bitrate.
Nginx Proxy Timeouts
The Jitsi web container's upstream meet.conf template does NOT set proxy_read_timeout for the /xmpp-websocket location, so nginx defaults to 60s. An idle XMPP WebSocket gets silently terminated, smacks resume often fails, and Prosody evicts the MUC occupant via the kick path — visible as Member role changed from PARTICIPANT to VISITOR - not supported! in Jicofo logs.
nginx/custom-meet.conf (in jitsi-deploy) is appended to meet.conf by the web container's 10-config init and overrides the default at the server scope:
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
The colibri-ws location sets its own per-location timeouts (3600s) and is unaffected. See Diagnosing Jitsi kicks for the full incident analysis.
Prosody (XMPP Server)
Lua-based XMPP server (jitsi/prosody:stable-9823) handling JWT authentication, presence, messaging, and MUC rooms.
Authentication
Prosody uses JWT authentication (AUTH_TYPE=jwt) with token_verification module:
JWT_APP_ID=aiqlick— issuer and audience identifierJWT_APP_SECRET— shared secret for HMAC-SHA256 verificationJWT_ALLOW_EMPTY=1— allows non-JWT users to join as guests
The token_affiliation module maps the moderator flag in JWT tokens to XMPP MUC roles. Only users with "moderator": true in their JWT get the moderator role — all other participants join as regular users.
Virtual Hosts
| Domain | Auth | Purpose |
|---|---|---|
meet.jitsi | JWT (token_verification) | Main domain for end users |
auth.meet.jitsi | internal_hashed | Internal services (Jicofo, JVB, Jigasi) |
hidden.meet.jitsi | internal_hashed | Invisible participants (transcriber) |
The hidden.meet.jitsi VirtualHost is only generated when ENABLE_TRANSCRIPTIONS=1 is in Prosody's environment. Without it, Jigasi cannot join as a hidden participant.
MUC Components
| MUC | Purpose |
|---|---|
muc.meet.jitsi | Conference rooms (user-facing) |
internal-muc.meet.jitsi | Service discovery (brewery rooms) |
Brewery rooms inside internal-muc:
- jvbbrewery — JVB instances register here with load stats
- JigasiBrewery — Jigasi instances register with
supports_transcriptionfeature
Smacks (Stream Management) Tuning
Upstream Jitsi defaults smacks_hibernation_time = 60s, smacks_max_unacked_stanzas = 5, smacks_max_old_sessions = 1. Mobile clients, WiFi handovers, and even brief proxy hiccups routinely exceed 60s, causing Prosody to evict occupants — see kick diagnosis.
custom-prosody.cfg.lua (in jitsi-deploy) is copied into /config/conf.d/ by deploy.sh and loaded after jitsi-meet.cfg.lua (alphabetical), so its globals override the upstream defaults:
smacks_hibernation_time = 600;
smacks_max_unacked_stanzas = 10;
smacks_max_old_sessions = 5;
This is defense-in-depth; the primary kick fix is the nginx WS timeout in Web → Nginx Proxy Timeouts.
TURN Credential Delivery
Prosody uses mod_external_services (XEP-0215) — loaded globally — to advertise TURN servers to XMPP clients. The external_service_secret is written to /config/conf.d/turn-secret.cfg.lua by deploy.sh from the .env TURN_SECRET value.
Without external_service_secret, Prosody sends TURN URLs without credentials, causing browser error: InvalidAccessError: Failed to construct 'RTCPeerConnection'.
Jicofo (Conference Focus)
Java application (jitsi/jicofo:stable-9823) that orchestrates conferences. When the first user joins a room, Jicofo allocates a JVB, negotiates media sessions, and invites Jigasi when transcription is requested.
Key Responsibilities
- Conference creation — allocates resources on first join
- JVB selection — picks a video bridge from
jvbbrewerybased on load - Jigasi invocation — finds a Jigasi with
supports_transcriptioninJigasiBrewery - Session negotiation — coordinates SDP offers/answers between participants and JVB
Configuration
jicofo.bridge.brewery-jid = "jvbbrewery@internal-muc.meet.jitsi"
jicofo.jigasi.brewery-jid = "JigasiBrewery@internal-muc.meet.jitsi"
ENABLE_TRANSCRIPTIONS=1 must be set in Jicofo's environment for it to detect and invoke Jigasi.
ENABLE_AUTO_OWNER is disabled — moderator roles are controlled exclusively by JWT tokens via Prosody's token_affiliation module, not by join order.
JVB (Jitsi Videobridge)
Selective Forwarding Unit (jitsi/jvb:stable-9823) that receives media from all participants and forwards each stream only to those who need it. JVB does not mix or transcode media.
Port Mapping
| Port | Protocol | Purpose |
|---|---|---|
| 10000 | UDP | All video and audio RTP/SRTP traffic |
| 9090 | TCP | Colibri WebSocket (internal, proxied by nginx) |
NAT Traversal
JVB runs inside Docker with a private IP (172.18.0.x). DOCKER_HOST_ADDRESS tells JVB its EC2 public IP (16.16.21.64) so it advertises the correct ICE candidate. On startup, JVB joins jvbbrewery@internal-muc.meet.jitsi and publishes load stats for Jicofo bridge selection.
Custom Configuration (custom-jvb.conf)
A custom JVB config extends first-transfer-timeout from the default 15 seconds to 120 seconds. This gives Jigasi extra time to establish its Colibri WebSocket connection — the URL extraction from Jingle IQs can be delayed when Smack re-serializes XML with inherited xmlns attributes. Without this, JVB expires the Jigasi endpoint and Jicofo tears down the conference.
Deployed by deploy.sh to /root/.jitsi-meet-cfg/jvb/custom-jvb.conf, auto-included by JVB's main jvb.conf via HOCON include.
Jigasi (Transcription Gateway)
Custom fork of Jitsi Jigasi running in transcriber mode (no SIP). Joins conferences as a hidden participant, streams audio to Whisper, and broadcasts captions back to the room.
| Base image | jitsi/jigasi:stable-9823 |
|---|---|
| Custom layer | WhisperTranscriptionService.java (WebSocket client to background-tasks) |
Transcription Flow
- Jigasi joins room on
hidden.meet.jitsi(invisible to users) - Establishes Colibri WebSocket connection with JVB for audio forwarding
- Receives audio from JVB
- Streams audio chunks via WebSocket to
wss://ai.aiqlick.com/transcription/ws - Receives transcript text from Whisper service
- Broadcasts captions to all participants via XMPP
Colibri WebSocket Connection
Jigasi must establish a Colibri WebSocket connection with JVB so JVB can forward audio via its EndpointMessageTransport. The URL is extracted from Jingle session-initiate/transport-info IQs using regex.
The extraction uses multiple patterns:
- Direct xmlns —
xmlnsattribute directly on<web-socket>element - Relaxed fallback — matches any
<web-socket url='wss://...'>without requiring xmlns (handles Smack namespace inheritance)
If the URL is not available when the call starts, a retry mechanism (5 attempts, 2 seconds apart) schedules reconnection attempts.
Configuration
org.jitsi.jigasi.mode=transcriber
org.jitsi.jigasi.transcriber.customService=org.jitsi.jigasi.transcription.WhisperTranscriptionService
org.jitsi.jigasi.transcriber.whisperUrl=wss://ai.aiqlick.com/transcription/ws
org.jitsi.jigasi.xmpp.hiddenDomain=hidden.meet.jitsi
ICE/NAT Notes
- Jigasi does NOT need
ICE4J_PUBLIC_ADDRESS— it only communicates with JVB on the Docker network - Setting
ICE4J_PUBLIC_ADDRESSon Jigasi actually breaks ICE connectivity between JVB and Jigasi JIGASI_ALWAYS_USE_JVB=trueandJIGASI_DISABLE_P2P=trueare required (container environments don't support P2P)
XMPP Race Condition
Jigasi does not auto-retry XMPP connections. If Prosody is restarted independently, Jigasi must also be restarted. The deploy.sh script handles this automatically — it checks for brewery join after deploy and restarts Jigasi if needed.
Coturn (TURN Server)
TURN relay server (coturn/coturn:4.6) for users behind corporate firewalls, strict NATs, or restricted networks.
How It Works
Prosody's mod_external_services sends TURN URLs to XMPP clients with time-limited HMAC-SHA1 credentials generated from TURN_SECRET.
Ports
| Port | Protocol | Purpose |
|---|---|---|
| 3478 | UDP/TCP | STUN + TURN |
| 5349 | TCP | TURN over TLS |
| 49152-49200 | UDP | TURN relay ports |
Dozzle (Live Log Viewer)
Web UI for live container log monitoring. Bound to 127.0.0.1:8888 — not publicly accessible.
Access via SSM port forwarding:
aws ssm start-session --profile Administrator-842697652860 --region eu-north-1 \
--target i-0620d2e23695f5bfc --document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["8888"],"localPortNumber":["8888"]}'
# Then open http://localhost:8888
Inter-Service Communication
| From | To | Protocol | Purpose |
|---|---|---|---|
| Browser | Web | HTTPS (443) | Load app, proxy signaling |
| Web | Prosody / JVB | HTTP (5280 / 9090) | BOSH, WebSocket, Colibri proxy |
| Browser | JVB | UDP (10000) | Media (video/audio) |
| Browser | Coturn | UDP/TCP (3478, 5349) | TURN relay (when direct fails) |
| Prosody | Jicofo | XMPP | Room events, conference orchestration |
| Jicofo | JVB / Jigasi | XMPP | Channel allocation, transcription invocation |
| Jigasi | JVB | Colibri WS + RTP | Audio forwarding and media |
| Jigasi | background-tasks | WSS | Stream audio, receive transcripts |
Required Environment Variables
| Variable | Services | Purpose |
|---|---|---|
DOCKER_HOST_ADDRESS | JVB, Coturn | EC2 public IP for ICE/NAT |
ENABLE_TRANSCRIPTIONS=1 | Prosody, Jicofo, Web | Hidden domain + Jigasi detection |
JIGASI_MODE=transcriber | Jigasi | Transcription-only mode (no SIP) |
JIGASI_TRANSCRIBER_USER / _PASSWORD | Prosody, Jigasi | XMPP credentials for transcriber |
JWT_APP_ID / JWT_APP_SECRET | Prosody | JWT authentication for moderator roles |
TURN_SECRET | Prosody, Coturn | Shared secret for TURN credentials |
JIGASI_ALWAYS_USE_JVB=true | Jigasi | Force JVB mode (required in Docker) |