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
| Property | Value |
|---|---|
| Instance ID | i-0620d2e23695f5bfc |
| Type | t3.xlarge (4 vCPU, 16GB RAM) |
| Public IP | 16.16.21.64 (Elastic IP) |
| Region | eu-north-1 (Stockholm) |
| OS | Ubuntu 22.04 |
| EBS | 20GB gp3 |
| Swap | 2GB (/swapfile, vm.swappiness=10) |
| Deploy path | /opt/jitsi |
| IAM Profile | EC2-SSM-Profile |
| Security Group | jitsi-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
| Service | Image | Memory | Purpose |
|---|---|---|---|
| web | aiqlick-meeting:cbbd5e1 (ECR) | 512 MB | Classic Jitsi Meet frontend, nginx with Let's Encrypt SSL. Pinned to cbbd5e1 to preserve the Jitsi proxy. |
| meeting-app | aiqlick-meeting:latest (ECR) | 256 MB | The Expo React Native web app, served on meet.aiqlick.com via the web reverse proxy. |
| prosody | jitsi/prosody:stable-9823 | 512 MB | XMPP server — JWT auth, MUC rooms, TURN credentials |
| jicofo | jitsi/jicofo:stable-9823 | 768 MB | Conference focus — allocates JVB, invokes Jigasi for transcription |
| jvb | jitsi/jvb:stable-9823 | 6 GB | Video bridge (SFU) — receives and selectively forwards media |
| jigasi | aiqlick-jigasi (ECR) | 1.5 GB | Transcription gateway — streams audio to Whisper, broadcasts captions |
| coturn | coturn/coturn:4.6 | 256 MB | TURN server for NAT traversal (UDP/TCP/TLS) |
| dozzle | amir20/dozzle | 128 MB | Live 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
| Port | Protocol | Service | Purpose |
|---|---|---|---|
| 80 | TCP | web | HTTP (redirects to HTTPS) |
| 443 | TCP | web | HTTPS — web app, BOSH/WebSocket proxy |
| 10000 | UDP | jvb | Media traffic (RTP/SRTP) |
| 20000-20050 | UDP | jigasi | Jigasi media traffic |
| 3478 | UDP/TCP | coturn | STUN + TURN |
| 5349 | TCP | coturn | TURN over TLS |
| 49152-49200 | UDP | coturn | TURN 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 file | Host Path | Purpose |
|---|---|---|
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.
WEB_RECREATE semanticsThe 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)
- Config sync — copies
custom-config.js,custom-jvb.confto host volumes - TURN secret — writes
external_service_secretto Prosodyconf.d/from.envTURN_SECRET - Prosody plugins — syncs
token_affiliationand custom plugins to host volume - Secrets sync — fetches
jitsi/productionfrom AWS Secrets Manager, merges into.env - ECR login + pull — authenticates and pulls latest images
- Deploy —
docker compose up -d(no--force-recreate, only recreates changed services) - Health checks — waits for each service healthy in dependency order (up to 120s per service)
- Brewery verification — checks Jigasi joined
JigasiBreweryusing--sincetimestamp to avoid stale log matches; auto-restarts Jigasi if not joined - Web endpoint — curls
https://book.aiqlick.com/to verify public access - Rollback — on any health check failure, reverts to
PREV_SHAand 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
| Service | Method | Interval | Start Period |
|---|---|---|---|
| web | curl -sf http://localhost/ | 30s | 40s |
| prosody | bash -c '</dev/tcp/localhost/5222' | 30s | 30s |
| jicofo | ls /proc/1/status | 30s | 30s |
| jvb | wget -qO- http://localhost:8080/about/health | 30s | 30s |
| jigasi | ls /proc/1/status | 30s | 60s |
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
| Service | Heap | Container Limit |
|---|---|---|
| JVB | 4096m | 6 GB |
| Jicofo | 512m | 768 MB |
| Jigasi | 1024m | 1.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.
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.