Run a Self-Hosted Jitsi Meet for Video Calls
Deploy a private Jitsi Meet server with docker-jitsi-meet, configure TURN/STUN for NAT traversal, set up systemd auto-start, and scale with multiple Videobridges.
Before you start
- ▸A public-facing server with a static IP and a DNS A record pointing to it
- ▸Docker Engine 24+ and the docker-compose-plugin installed
- ▸Inbound firewall rules open for ports 80, 443, 10000/udp, 3478/udp, and 5349/tcp
- ▸A registered domain name you control for Let's Encrypt certificate issuance
Running your own Jitsi Meet server gives you a private, auditable video conferencing platform with no per-seat licensing costs. The fastest production-ready path is docker-jitsi-meet, the official Docker Compose stack maintained by the Jitsi team. This guide walks through a full deployment on a public-facing server, configuring TURN/STUN for NAT traversal, and tuning for more than a handful of concurrent users. A brief JaaS comparison at the end helps you decide when self-hosting stops making sense.
Prerequisites and Infrastructure
You need a dedicated virtual or bare-metal server reachable from the internet. Minimum specs for up to ~25 concurrent participants: 4 vCPUs, 8 GB RAM, 40 GB disk. For larger rooms, plan ~1 vCPU and ~1 GB RAM per 10 simultaneous video streams processed by the Videobridge.
- A fully-qualified domain name (e.g.
meet.example.com) with an A record pointing to your server's public IP. - Ports open inbound: 80/tcp, 443/tcp, 10000/udp (Videobridge media), 3478/udp (STUN), 5349/tcp (TURN over TLS). If you run a TURN server on the same host, also open 5349/udp.
- Docker Engine 24+ and the Compose plugin (
docker compose, not the legacydocker-composebinary). - A non-root user with sudo access.
Step 1 — Install Docker Engine
Debian / Ubuntu
sudo apt update && sudo apt install -y ca-certificates curl gnupg
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \
| sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker $USER # re-login after this
Fedora / RHEL 9 / Rocky 9
sudo dnf install -y dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
Arch Linux
sudo pacman -S docker docker-compose
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
Step 2 — Pull and Configure docker-jitsi-meet
cd /opt
sudo git clone https://github.com/jitsi/docker-jitsi-meet jitsi
sudo chown -R $USER:$USER /opt/jitsi
cd /opt/jitsi
cp env.example .env
Generate strong secrets for the stack — the helper script writes them directly into .env:
./gen-passwords.sh
Create the persistent data directories the Compose file expects:
mkdir -p ~/.jitsi-meet-cfg/{web,transcripts,prosody/config,prosody/prosody-plugins-custom,jicofo,jvb,jigasi,jibri}
Now edit .env for your deployment. The critical variables:
nano /opt/jitsi/.env
Set at minimum:
# Public hostname — must match your DNS A record
PUBLIC_URL=https://meet.example.com
# Let the web container obtain a cert via Let's Encrypt
ENABLE_LETSENCRYPT=1
LETSENCRYPT_DOMAIN=meet.example.com
[email protected]
# Restrict room creation to authenticated users (recommended)
ENABLE_AUTH=1
ENABLE_GUESTS=1
AUTH_TYPE=internal
# Videobridge public IP (required when server is behind NAT or a cloud provider)
JVB_ADVERTISE_IPS=203.0.113.10
Step 3 — Configure TURN / STUN
WebRTC clients behind symmetric NAT cannot reach the Videobridge on UDP 10000 without a TURN relay. Jitsi ships coturn as an optional service in the Compose stack. Enable it in .env:
ENABLE_TURN=1
JIGASI_XMPP_USER=focus
# coturn will bind to these ports
TURN_TRANSPORT=udp,tcp
TURN_PORT=3478
TURNS_PORT=5349
Then in docker-compose.yml (or a docker-compose.override.yml), verify the coturn service is un-commented and that it mounts your TLS certificate. Point coturn at the cert that Certbot already placed on the host:
cat >> docker-compose.override.yml << 'EOF'
services:
coturn:
volumes:
- /etc/letsencrypt/live/meet.example.com/fullchain.pem:/etc/ssl/certs/turn.crt:ro
- /etc/letsencrypt/live/meet.example.com/privkey.pem:/etc/ssl/private/turn.key:ro
EOF
If you prefer a dedicated external TURN server (e.g. for geo-distribution), install coturn on a separate host and set TURN_CREDENTIALS and TURN_HOST in .env instead of running the bundled service.
Step 4 — Start the Stack
cd /opt/jitsi
docker compose up -d
Watch the logs for certificate issuance (takes 30–90 seconds on first run):
docker compose logs -f web
Once you see Configuration complete; ready for start up, browse to https://meet.example.com. The page should load over HTTPS with a valid certificate.
Step 5 — Add an Admin User
With AUTH_TYPE=internal, create users via the Prosody container:
docker compose exec prosody /bin/bash
prosodyctl --config /config/prosody.cfg.lua register alice meet.example.com 'strongpassword'
exit
Registered users can create rooms. Guests can join rooms that already exist if ENABLE_GUESTS=1.
Step 6 — Systemd Service for Auto-Start
Ensure the stack restarts on reboot without relying on Docker's own restart policy alone:
sudo tee /etc/systemd/system/jitsi.service << 'EOF'
[Unit]
Description=Jitsi Meet (docker-jitsi-meet)
Requires=docker.service
After=docker.service network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/jitsi
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now jitsi
Scaling: Multiple Videobridges (JVB)
A single Videobridge maxes out around 500 simultaneous video streams depending on bitrate. For heavier load, run additional JVB instances — on the same host or separate servers — registered to the same XMPP/Prosody core.
On each additional JVB host, set these extra environment variables before starting only the jvb service:
# On the additional JVB node
XMPP_SERVER=meet.example.com # the Prosody host
JVB_ADVERTISE_IPS=203.0.113.20 # this node's public IP
JVB_AUTH_PASSWORD=<same value from primary .env>
Jicofo performs Octo-based bridge selection automatically once multiple JVBs are registered. Monitor bridge utilisation via the JVB REST API on port 8080:
curl http://localhost:8080/colibri/stats | python3 -m json.tool
Verification
- Open
https://meet.example.comin a browser — certificate should be valid, no mixed-content warnings. - Start a test call from two different networks (especially one behind a home NAT). Audio and video should establish within a few seconds; if they don't, TURN is misconfigured.
- Check the browser's WebRTC internals (
chrome://webrtc-internalsorabout:webrtcin Firefox) — the active candidate pair should show a relay or srflx candidate using your TURN/STUN server, not just a host candidate. - Review container health:
docker compose ps— all services should showUp (healthy)orUp.
Troubleshooting
- Video freezes for remote participants: UDP 10000 is likely blocked. Confirm your firewall allows inbound UDP 10000 and that
JVB_ADVERTISE_IPSmatches the server's real public IP, not an RFC 1918 address. - Let's Encrypt fails: Port 80 must be reachable before cert issuance. Temporarily disable any existing web server on port 80, or use the
DNS-01challenge path and setENABLE_LETSENCRYPT=0while managing certs externally with Certbot, then mount them into the web container. - Users can't create rooms: Verify
ENABLE_AUTH=1and that accounts were registered in Prosody. Check Jicofo logs:docker compose logs jicofo. - High CPU on a single core: The JVB is single-threaded per bridge instance by design in the relay path. Add more JVB instances rather than adding vCPUs to one.
Self-Hosted vs. JaaS (Jitsi as a Service)
Jitsi as a Service (JaaS) is 8x8's managed Jitsi platform. It handles infrastructure, scaling, TURN, and certificate management for you, billed per monthly active user beyond a free tier. Self-hosting wins on data sovereignty, fixed cost at scale, and customisation depth. JaaS wins on zero-ops overhead, global edge PoPs for low-latency TURN, and unpredictable or bursty usage patterns where paying per MAU is cheaper than provisioning for peak. If your team is under ~50 people and ops bandwidth is thin, JaaS is worth the comparison before committing to maintenance of this stack.
Frequently asked questions
- Why does video work on the local network but freeze for external users?
- The most common cause is UDP port 10000 being blocked by your firewall or cloud security group, or JVB_ADVERTISE_IPS being set to a private RFC 1918 address instead of the server's real public IP.
- How many concurrent participants can one server handle?
- A single Videobridge on a 4-vCPU/8 GB server typically handles 25–50 participants in one room comfortably. Capacity drops fast if everyone sends 720p HD video; encourage participants to disable video when not speaking.
- Can I use an existing reverse proxy like Nginx or Caddy in front of Jitsi?
- Yes. Set ENABLE_LETSENCRYPT=0, handle TLS at the proxy, and proxy pass HTTPS to the web container on port 80. Ensure your proxy forwards WebSocket connections and sets the correct X-Forwarded-Proto header.
- What is the difference between STUN and TURN, and do I need both?
- STUN helps clients discover their public IP/port (no relay needed). TURN relays media when direct and STUN connections both fail, typically behind symmetric or port-restricted NAT. You need TURN for reliable connectivity across all real-world networks; STUN alone is insufficient.
- How do I update docker-jitsi-meet to a newer release?
- Pull the latest tags with git pull in /opt/jitsi, then run docker compose pull followed by docker compose up -d. Check the project's GitHub releases page for breaking .env variable changes before upgrading major versions.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.