Jitsi self-hosted vs Zoom: what actually changes
Consumer video conferencing services (Zoom, Google Meet, Teams) impose time limits on free plans, participant quotas, and keep your recordings on their servers. Self-hosted Jitsi Meet removes these constraints: without limits rooms, without limits duration, no account required to join a meeting, and video streams that stay on your infrastructure. This is particularly relevant for an organization concerned about the confidentiality of its meetings, a firm handling sensitive data, or anyone who wants to embed video calling in a third-party application (Jitsi provides a stable iframe API). Running costs come down to the VPS bill — no per-seat subscription, no software surcharge.
Concrete benefits of self-hosted Jitsi
- Meetings with no time limit and no cap on the number of rooms — no timer cutting the call.
- No account required for participants: a link is enough to join.
- iframe API to embed video calling directly in your application or website.
- Transport encryption (DTLS-SRTP) and full control over any recordings.
- Complete customization: logo, colors, room domain name.
- No telemetry sent to a third party — video streams never leave your VPS.
Concrete prerequisites before you start
Jitsi is sized by bandwidth, not CPU. For meetings up to 10 participants: 2 vCPU, 4 GB RAM minimum, generous outbound traffic (100 Mbps recommended). Above 20 simultaneous participants, plan for 8 GB RAM and monitor JVB network load. Three ports must be reachable from the internet: TCP 443 (HTTPS and TURN/TLS), TCP 80 (Let's Encrypt renewal), UDP 10000 (JVB media). Add TCP 3478 if you expose coturn on its native port. You need Docker 24+ and Compose V2, a domain name meet.yourdomain.com with an A record pointing to the VPS public IP, and a dedicated, stable public IP — a shared IP makes coturn configuration significantly harder.
Step-by-step deployment: Docker, .env, coturn and first test
Fetch docker-jitsi-meet
Clone the official repository: git clone https://github.com/jitsi/docker-jitsi-meet. Copy env.example to .env, then run ./gen-passwords.sh — this script generates all internal secrets (Prosody, Jicofo, JVB). Do not skip this step: default values are not secrets.
Set PUBLIC_URL and DOCKER_HOST_ADDRESS
Two variables in .env are critical: PUBLIC_URL=https://meet.yourdomain.com (used by the web client to build room URLs) and DOCKER_HOST_ADDRESS=<VPS_PUBLIC_IP> (the address JVB announces to clients to establish the direct media connection). Without DOCKER_HOST_ADDRESS, JVB announces an internal Docker address and no external client can reach it.
Enable Let's Encrypt in .env
Add to .env: ENABLE_LETSENCRYPT=1, LETSENCRYPT_DOMAIN=meet.yourdomain.com, [email protected]. Verify that port 80 is open before starting — Let's Encrypt uses an HTTP-01 challenge that requires a response on that port.
Configure coturn (TURN server)
This is the step most guides skim over and that causes installations to fail. In .env, enable: ENABLE_TURN=1, TURN_HOST=meet.yourdomain.com, TURN_PORT=443, TURN_TRANSPORT=tls. Port 443 over TLS crosses the most restrictive firewalls, which block non-standard ports but let through anything resembling HTTPS. Then add: TURN_CREDENTIALS=a_strong_secret, TURN_TLS_CERT=/etc/letsencrypt/live/meet.yourdomain.com/fullchain.pem, TURN_TLS_KEY=/etc/letsencrypt/live/meet.yourdomain.com/privkey.pem. If coturn runs in a separate container, mount these files as volumes. One often-missed detail: coturn blocks RFC1918 ranges (192.168.x.x, 10.x.x.x, 172.16.x.x) by default. If your VPS has an internal network interface in those ranges, add no-multicast-peers and verify that denied-peer-ip does not include the JVB address.
Start the stack and check the JVB
Run docker compose up -d. Wait 30 seconds, then: docker compose logs jvb 2>&1 | grep -E 'register|connected|error'. You should see a line showing that JVB registered with Jicofo. If you see Failed to register or Connection refused, Prosody is not ready yet — retry after a minute. Then verify coturn responds: nc -zv meet.yourdomain.com 443 (TCP) and nc -zvu meet.yourdomain.com 10000 (UDP).
First video test from two different networks
Open https://meet.yourdomain.com from your usual machine and a second device on a different network (a phone on mobile data, for example). If both participants see and hear each other, TURN is working. A test between two machines on the same local network does not validate coturn — it succeeds peer-to-peer without ever using the relay.
Advanced configuration: recording, authentication and limits
Meeting recording. Jitsi includes Jibri for local recording or RTMP streaming. Jibri requires a second VPS or at minimum 4 additional vCPU, as it captures the browser render in real time. Enable ENABLE_RECORDING=1 in .env and deploy a separate Jibri container pointing to your Jicofo. Organizer authentication. By default, anyone can create a room. Enable AUTH_TYPE=internal to require an account on the organizer's side while letting guests join freely. Create host accounts with docker exec <prosody-container> prosodyctl --config /config/prosody.cfg.lua register admin meet.yourdomain.com password. Limiting anonymous participants. Add ENABLE_LOBBY=1 so a moderator approves each entry before accessing the room. Combined with AUTH_TYPE=internal, this gives full control over who accesses what.
Hardening: fail2ban on the TURN service
coturn exposes a service reachable from the internet. Install fail2ban and create a filter on failed TURN authentication attempts: coturn logs write ERROR: 401, realm on each failure. A fail2ban jail with maxretry=10 and bantime=600 is enough to deter automated scans without affecting your legitimate users. Verify that fail2ban watches /var/log/coturn.log (default path if coturn runs natively) or mount that file from the container.
Troubleshooting: the four most common errors
Missing audio or video stuck on 'connecting' — coturn + UDP 10000. This is the most frequent failure. Check in this order: (1) ufw status — are TCP 443 and UDP 10000 open? (2) docker compose logs coturn | grep -i error — does coturn start without certificate errors? (3) From an external machine: curl -v telnet://meet.yourdomain.com:3478 — does coturn respond? (4) In browser logs (F12 → Console): do you see ICE failed or failed to gather candidates? If so, coturn is not reachable from outside. Compare TURN_HOST in .env with the actual VPS IP. Connection refused on TCP 443. If Let's Encrypt could not issue a certificate (port 80 was closed on first start), the web container attempts to serve HTTPS with a self-signed certificate that the browser rejects. Fix: open port 80, delete the volume ~/.jitsi-meet-cfg/web and re-run docker compose up -d. Screen sharing refused by the browser. Screen sharing requires a secure context (valid HTTPS). If you see getUserMedia is not supported, the certificate is invalid or the domain is not on HTTPS. Check PUBLIC_URL — it must start with https:// with a domain whose certificate is valid, not a raw IP. Persistent 'ICE failed' error despite a working coturn. Check the DOCKER_HOST_ADDRESS variable: it must contain the VPS public IP, not the internal Docker container IP (172.17.x.x). If your VPS sits behind cloud NAT (rare but possible), you need the external NAT IP, not the network interface IP.
Your Jitsi is running — manage bandwidth
Jitsi runs in SFU mode: the video bridge (JVB) relays each stream to each participant, so outbound bandwidth scales with the number of participants multiplied by the number of streams. For 10 participants at 720p, expect 20 to 40 Mbps outbound continuously. Enable lastN=5 (last N active videos) to cap client-side streams in large meetings, and watch docker stats jvb to anticipate saturation. Above about twenty regular simultaneous participants, deploy several JVB instances on separate VPSes behind a single Jicofo: that is Jitsi's native horizontal scaling architecture.