Tutorial

Self-hosting

Host Jitsi on your own VPS

Jitsi Meet is an open source video conferencing solution that works directly in the browser, with no installation on the participant's side. Self-hosting it on a VPS gives you as many rooms as you need, with no time limit and no data collection. The deployment itself is quick — configuring the TURN server (coturn) is what causes most installations to fail: without it, video works on a local network but stops as soon as a participant is behind a corporate NAT or a carrier-grade NAT provider. This guide covers the full installation, advanced coturn configuration, video bridge monitoring, and troubleshooting of the most common errors.

11 min read6 steps
Contents· Jitsi self-hosted vs Zoom: what actually changes1/9
  1. 01Jitsi self-hosted vs Zoom: what actually changes
  2. 02Concrete benefits of self-hosted Jitsi
  3. 03Concrete prerequisites before you start
  4. 04Step-by-step deployment: Docker, .env, coturn and first test
  5. 05Advanced configuration: recording, authentication and limits
  6. 06Hardening: fail2ban on the TURN service
  7. 07Troubleshooting: the four most common errors
  8. 08Monitoring: tracking bandwidth and participants
  9. 09Your Jitsi is running — manage bandwidth

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: rooms with no cap on participants, no meeting time limit, 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. The Jitsi documentation recommends 8 GB of RAM (4 GB is enough for small meetings), around 4 dedicated cores for a basic server, and above all a reliable network link: a 720p stream uses about 2.5 Mbit/s, and a 100 Mbit/s link will not carry twenty participants in high definition. Monitor JVB network load. Three ports must be reachable from the internet: TCP 443 (HTTPS), TCP 80 (Let's Encrypt challenge and redirect to HTTPS), UDP 10000 (JVB media). TCP port 4443, mentioned in older guides, is no longer on the official list. If coturn runs on the same VPS, add its ports: 3478 over UDP and TCP, 5349 for TLS (443 is already taken by the web container). You need Docker with Compose V2 (the docker compose command), 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

  1. Fetch docker-jitsi-meet

    Download the latest release from the releases page of github.com/jitsi/docker-jitsi-meet and unzip it: the official guide advises against cloning the repository, whose main branch pulls the unstable images. Create the configuration folders listed in the guide (~/.jitsi-meet-cfg/…, including storage and tmp, writable by uid 1000): the containers run rootless and refuse to start otherwise. 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. The stable-11248-3 release (November 2026) is the latest stable; avoid master and the unstable images in production — internal components change without notice.

  2. Set PUBLIC_URL, the ports and JVB_ADVERTISE_IPS

    In .env, first set HTTP_PORT=80 and HTTPS_PORT=443 (8000 and 8443 by default). Two variables are then critical: PUBLIC_URL=https://meet.yourdomain.com (used by the web client to build room URLs) and JVB_ADVERTISE_IPS=<VPS_PUBLIC_IP> (the address JVB announces to clients to establish the direct media connection; it replaces the former DOCKER_HOST_ADDRESS). Without JVB_ADVERTISE_IPS, JVB tries to discover its public IP via STUN; if the announced address is wrong, calls fail as soon as more than two participants join the meeting.

  3. 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. The web container obtains and renews the certificate with acme.sh (an internal renewal service) and keeps it in ~/.jitsi-meet-cfg/storage/web: keep that folder across restarts. Also add LETSENCRYPT_ACME_SERVER=letsencrypt: without it, acme.sh uses ZeroSSL by default.

  4. Configure coturn (TURN server)

    This is the step most guides skim over and that causes installations to fail. docker-jitsi-meet does not ship coturn: install it separately (package or container), then declare it in .env so that Prosody hands clients temporary credentials: TURNS_HOST=turn.yourdomain.com and TURNS_PORT for TLS, plus TURN_HOST=turn.yourdomain.com, TURN_PORT=3478 and TURN_TRANSPORT=udp,tcp. Port 443 over TLS crosses the most restrictive firewalls, which block non-standard ports but let through anything resembling HTTPS; it needs an IP the web container does not already use, otherwise keep 5349.

    Then generate a shared secret (openssl rand -hex 32) and put it in TURN_CREDENTIALS in .env and in coturn (use-auth-secret and static-auth-secret=<the same secret>); never no-auth, which would turn your server into a relay open to anyone. The TLS certificate is declared in the coturn configuration (cert= and pkey=), not in .env. If coturn runs in a separate container, mount these files as volumes.

    One often-missed detail: coturn does not block RFC1918 ranges (192.168.x.x, 10.x.x.x, 172.16.x.x) by default: only loopback is refused. An authenticated client can therefore relay traffic into your internal network. Add no-multicast-peers and denied-peer-ip lines for those ranges, then, if JVB has an internal address, allow only that one with allowed-peer-ip.

  5. 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 the ports respond: nc -zv meet.yourdomain.com 443 (web, TCP), nc -zv turn.yourdomain.com 3478 (coturn, TCP) and nc -zvu meet.yourdomain.com 10000 (JVB, UDP; a UDP success is only indicative).

  6. 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 captures a browser render in real time: one instance records only one meeting at a time, and the Jitsi documentation asks for at least 8 GB of RAM per instance at 720p, preferably on a separate server. Enable ENABLE_RECORDING=1 in .env and start the bundled Jibri container: docker compose -f docker-compose.yml -f jibri.yml up -d. Organizer authentication. By default, anyone can create a room: a public instance left that way becomes free video calling for strangers, on your bandwidth. Enable ENABLE_AUTH=1, AUTH_TYPE=internal and ENABLE_GUESTS=1 to require an account on the organizer's side while letting guests join freely. Create host accounts with docker compose exec prosody prosodyctl --config /run/prosody/config/prosody.cfg.lua register admin meet.jitsi <password>: meet.jitsi is the internal XMPP domain, not your public name, and the password should be generated (openssl rand -base64 18) rather than reused. Limiting anonymous participants. ENABLE_LOBBY=1 makes the lobby available: once the moderator turns it on, each entry must be approved before accessing the room. Combined with ENABLE_AUTH=1, 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: failures show up in the coturn log with a 401 code; check the exact format of your version before writing the filter regex. A fail2ban jail with maxretry=10 and bantime=600 is enough to deter automated scans without affecting your legitimate users. Pin the log with log-file=/var/log/turnserver.log in the coturn configuration (without this option, coturn picks a file itself in /var/log, /var/tmp or /tmp), have fail2ban watch that path, 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) the coturn logs (journalctl -u coturn when native, docker logs <coturn-container> otherwise) — 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. Check that TURN_HOST and TURNS_HOST in .env resolve to the actual IP of the coturn server. Connection refused on TCP 443. If Let's Encrypt could not issue a certificate (port 80 was closed on first start), the web container stops after the message Failed to obtain a certificate from the Let's Encrypt CA and nothing answers on 443 any more. Fix: open port 80, check HTTP_PORT=80 and re-run docker compose up -d; there is no need to delete the configuration, and every new attempt counts against Let's Encrypt's limits. 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 JVB_ADVERTISE_IPS variable (formerly DOCKER_HOST_ADDRESS): 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.

Monitoring: tracking bandwidth and participants

The JVB video bridge exposes a real-time statistics API at http://localhost:8080/colibri/stats. This JSON endpoint reports the number of active conferences, participants, inbound and outbound bandwidth in kbit/s, and the average RTT (rtt_aggregate). Port 8080 is only published on 127.0.0.1: keep it that way. For continuous monitoring, JVB already exposes its metrics in Prometheus format at /metrics (same private port); on the Prosody side, enable PROSODY_ENABLE_METRICS=1 in .env. Then connect a Prometheus + Grafana stack: the repository ships prometheus.yml and grafana.yml, which publish ports 9090 and 3000 on all interfaces. Restrict them to 127.0.0.1 or a VPN, because ports published by Docker bypass ufw rules.

Without Prometheus, minimal monitoring is achieved with a cron that queries /colibri/stats and alerts when throughput exceeds a threshold: curl -s http://127.0.0.1:8080/colibri/stats | python3 -c "import json,sys; s=json.load(sys.stdin); print(s.get('bit_rate_download',0), s.get('bit_rate_upload',0))".

To estimate capacity before saturation: a JVB's capacity depends first on the network link and the resolutions being sent; measure it on your own load rather than relying on a generic figure. As the limit approaches, rtt_aggregate climbs and perceived quality drops before the CPU saturates — that is the signal to add another JVB instance.

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. A 720p stream weighs about 2.5 Mbit/s according to the Jitsi documentation: multiply by the number of streams actually sent to each participant to estimate outbound throughput. Set channelLastN: 5 in custom-config.js (the web folder of the configuration) to receive only the 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. For multi-organization or multi-domain deployments, keep in mind that a docker-jitsi-meet installation has a single PUBLIC_URL: if each organization needs its own accounts and an isolated room namespace, plan one instance per organization, on its own subdomain.

Launch your Jitsi server on a ServOrbit Cloud VPS

A Cloud VPS with a dedicated public IP, generous bandwidth and root access lets you deploy Jitsi Meet, coturn and Jibri with no meeting limit. Your video calls stay on your infrastructure.