Skip to main content

Jitsi Deployment

Jitsi Meet runs on a single EC2 instance using Docker Compose. Seven services provide video conferencing with automatic transcription and TURN relay for global NAT traversal.

Live URL: https://book.aiqlick.com

EC2 Instance

PropertyValue
Instance IDi-0620d2e23695f5bfc
Typet3.xlarge (4 vCPU, 16GB RAM)
Public IP16.16.21.64 (Elastic IP)
Regioneu-north-1 (Stockholm)
OSUbuntu 22.04
EBS20GB gp3
Swap2GB (/swapfile, vm.swappiness=10)
Deploy path/opt/jitsi
IAM ProfileEC2-SSM-Profile
Security Groupjitsi-sg

SSH Access

SSH aliases are configured in ~/.ssh/config (all use aiqlick-backend-key.pem):

ssh jitsi                      # Jitsi EC2 (16.16.21.64) — only EC2 left in the account
# backend / backend-dev / ai / ai-dev SSH aliases were retired with the
# Phase 7 cleanup of 2026-05-14; backend + bg-tasks now run on ECS Fargate.
# Open a shell in a running task with `aws ecs execute-command` instead —
# see https://docs.aiqlick.com/docs/infrastructure/aws/ssm

Common commands:

ssh jitsi "docker compose -f /opt/jitsi/docker-compose.yml ps"
ssh jitsi "docker compose -f /opt/jitsi/docker-compose.yml logs -f jigasi"
ssh jitsi "docker stats --no-stream"

Docker Compose Services

ServiceImageMemoryPurpose
webaiqlick-meeting:cbbd5e1 (ECR)512 MBClassic Jitsi Meet frontend, nginx with Let's Encrypt SSL. Pinned to cbbd5e1 to preserve the Jitsi proxy.
meeting-appaiqlick-meeting:latest (ECR)256 MBThe Expo React Native web app, served on meet.aiqlick.com via the web reverse proxy.
prosodyjitsi/prosody:stable-9823512 MBXMPP server — JWT auth, MUC rooms, TURN credentials
jicofojitsi/jicofo:stable-9823768 MBConference focus — allocates JVB, invokes Jigasi for transcription
jvbjitsi/jvb:stable-98236 GBVideo bridge (SFU) — receives and selectively forwards media
jigasiaiqlick-jigasi (ECR)1.5 GBTranscription gateway — streams audio to Whisper, broadcasts captions
coturncoturn/coturn:4.6256 MBTURN server for NAT traversal (UDP/TCP/TLS)
dozzleamir20/dozzle128 MBLive log viewer (localhost:8888 via SSM tunnel)

The web and jigasi images are custom builds stored in ECR (842697652860.dkr.ecr.eu-north-1.amazonaws.com). The other services use stock images.

Network Ports

PortProtocolServicePurpose
80TCPwebHTTP (redirects to HTTPS)
443TCPwebHTTPS — web app, BOSH/WebSocket proxy
10000UDPjvbMedia traffic (RTP/SRTP)
20000-20050UDPjigasiJigasi media traffic
3478UDP/TCPcoturnSTUN + TURN
5349TCPcoturnTURN over TLS
49152-49200UDPcoturnTURN relay ports

All internal service communication uses the meet.jitsi Docker network.

Configuration Files

The deploy.sh script copies these config files to host-mounted volumes during deployment:

Repo fileHost PathPurpose
custom-config.js/root/.jitsi-meet-cfg/web/Video bitrate caps, channelLastN, STUN servers, screen-share fps
nginx/custom-meet.conf/root/.jitsi-meet-cfg/web/nginx/Server-scope proxy_read_timeout 3600s for the XMPP WebSocket — appended to meet.conf by the web container's 10-config init. Without this, nginx kills idle WSs at 60s and triggers the VISITOR kick cascade.
custom-jvb.conf/root/.jitsi-meet-cfg/jvb/Extended first-transfer-timeout (120s) for Jigasi
custom-prosody.cfg.lua/root/.jitsi-meet-cfg/prosody/config/conf.d/Smacks hibernation tuning (600s); loaded after upstream jitsi-meet.cfg.lua so its globals override the defaults
turn-secret.cfg.lua/root/.jitsi-meet-cfg/prosody/config/conf.d/Auto-generated from .env TURN_SECRET
prosody-plugins-custom/*.lua/root/.jitsi-meet-cfg/prosody/prosody-plugins-custom/Custom Prosody modules (token_affiliation)

All files survive container restarts via host-mounted Docker volumes.

caution
WEB_RECREATE semantics

The web container generates the served config.js (and meet.conf) once at startup by appending custom-config.js (and custom-meet.conf) to the upstream templates. After that the files are fixed for the container's lifetime — docker compose up -d alone won't re-pick-up changes. deploy.sh md5-compares the files before/after copy and force-recreates the web container only when they actually changed (WEB_RECREATE=1). All other services use plain up -d to preserve active meetings.

CI/CD Pipeline

Workflow file: .github/workflows/deploy.yml

Trigger: Push to main with changes to docker-compose.yml, .env.example, custom-config.js, custom-jvb.conf, custom-prosody.cfg.lua, nginx/**, or scripts/**. Also supports workflow_dispatch for manual triggers.

Deployment Steps

Deploy Script Details (scripts/deploy.sh)

  1. Config sync — copies custom-config.js, custom-jvb.conf to host volumes
  2. TURN secret — writes external_service_secret to Prosody conf.d/ from .env TURN_SECRET
  3. Prosody plugins — syncs token_affiliation and custom plugins to host volume
  4. Secrets sync — fetches jitsi/production from AWS Secrets Manager, merges into .env
  5. ECR login + pull — authenticates and pulls latest images
  6. Deploydocker compose up -d (no --force-recreate, only recreates changed services)
  7. Health checks — waits for each service healthy in dependency order (up to 120s per service)
  8. Brewery verification — checks Jigasi joined JigasiBrewery using --since timestamp to avoid stale log matches; auto-restarts Jigasi if not joined
  9. Web endpoint — curls https://book.aiqlick.com/ to verify public access
  10. Rollback — on any health check failure, reverts to PREV_SHA and force-recreates

SSM Timeout

The GitHub Actions workflow uses a polling loop (10-minute max, 10s intervals) instead of aws ssm wait command-executed (which has a 100s default max). This prevents CI failures when the deploy script takes 3-5 minutes for health checks and brewery verification.

Health Checks

ServiceMethodIntervalStart Period
webcurl -sf http://localhost/30s40s
prosodybash -c '</dev/tcp/localhost/5222'30s30s
jicofols /proc/1/status30s30s
jvbwget -qO- http://localhost:8080/about/health30s30s
jigasils /proc/1/status30s60s

Jigasi uses depends_on: prosody: condition: service_healthy to prevent the XMPP race condition on normal startup.

Log Rotation

All containers use json-file driver with max-size: 10m and max-file: 3 to prevent disk exhaustion.

JVM Memory

ServiceHeapContainer Limit
JVB4096m6 GB
Jicofo512m768 MB
Jigasi1024m1.5 GB

Troubleshooting

No Transcription

# Check Jicofo detected Jigasi
ssh jitsi "docker compose -f /opt/jitsi/docker-compose.yml logs jicofo | grep -i 'Added brewery instance'"

# Check Jigasi registered with transcription support
ssh jitsi "docker logs jitsi-jigasi-1 | grep -i 'TranscriptionGateway'"

# Verify hidden domain exists in Prosody
ssh jitsi "sudo docker exec jitsi-prosody-1 grep -A3 'hidden.meet.jitsi' /config/conf.d/jitsi-meet.cfg.lua"

Jigasi XMPP Race Condition

Jigasi does not auto-retry XMPP connections. If Prosody is restarted independently, Jigasi must be restarted too:

ssh jitsi "sudo docker compose -f /opt/jitsi/docker-compose.yml restart jigasi"

# Verify brewery join
ssh jitsi "docker logs --since 1m jitsi-jigasi-1 | grep 'Joined call control'"

TURN Credentials Missing

# Verify Prosody has external_service_secret
ssh jitsi "sudo docker exec jitsi-prosody-1 grep external_service_secret /config/conf.d/turn-secret.cfg.lua"

# If missing, re-run deploy or manually restore from .env
ssh jitsi "sudo bash -c 'TURN_SECRET=\$(grep TURN_SECRET /opt/jitsi/.env | cut -d= -f2-) && echo \"external_service_secret = \\\"\$TURN_SECRET\\\";\" > /root/.jitsi-meet-cfg/prosody/config/conf.d/turn-secret.cfg.lua && cd /opt/jitsi && docker compose restart prosody'"

Container Health Issues

ssh jitsi "docker ps --format 'table {{.Names}}\t{{.Status}}'"
ssh jitsi "docker stats --no-stream"
ssh jitsi "df -h / && free -h"

Rollback

# SSH to the instance
ssh jitsi

# Rollback to a previous commit
cd /opt/jitsi
git log --oneline -5
sudo git checkout <commit-hash>
sudo bash scripts/deploy.sh

For ECR image rollback, update the image tag in docker-compose.yml to a specific SHA and re-deploy.

info

The meeting-app and jigasi containers are built in separate repositories (aiqlick-meeting and jigasi). The classic web container is permanently pinned to an older tag (cbbd5e1) because aiqlick-meeting was converted into a standalone Expo application. The new Expo app is served alongside the Jitsi backend on meet.aiqlick.com, reverse-proxied by the web container.