
Jitsi Meet Docker: Self-Hosted Video Conferencing Guide
Every team eventually hits the same wall: the free video call cuts out at the 40 minute mark just as the important part of the meeting starts, or a client asks where exactly the call data and chat messages are stored, and nobody has a good answer. Running your own video conferencing server fixes both problems at once. No time limits, no per-seat licenses, and the traffic goes through a machine you control. Jitsi Meet makes this realistic even for a small team, and with Docker the whole installation fits in an afternoon, coffee breaks included.
Jitsi Meet is an open source project (Apache 2.0 license) with its home at jitsi.org , and the official Docker setup lives at github.com/jitsi/docker-jitsi-meet . In this guide we deploy it on a fresh VPS, open exactly the ports it needs, get a free Let’s Encrypt certificate and, most importantly, lock room creation behind a login, so strangers cannot use your server as a free conferencing service.
What Jitsi Meet actually is
Jitsi Meet is a video conferencing app that runs entirely in the browser using WebRTC, the standard built into Chrome, Firefox, Edge and Safari that lets a web page capture your camera and microphone and send audio and video in real time. Participants do not need an account or an installed app: they open a link, allow camera access and they are in. Native apps for Android and iOS exist too, and they can connect to your own server.
Under the hood, a Jitsi Meet deployment is not one program but four cooperating services:
- web: an nginx web server that serves the Jitsi Meet interface, terminates HTTPS and, in our setup, obtains the Let’s Encrypt certificate.
- prosody: an XMPP server that handles signaling, meaning all the small control messages: who joined, who is muted, who raised a hand, the chat.
- jicofo (Jitsi Conference Focus): the coordinator that manages each conference, decides which video bridge handles it and sets up the media sessions.
- jvb (Jitsi Videobridge): the component that actually moves audio and video. It is an SFU (Selective Forwarding Unit): it receives each participant’s stream once and forwards it to the others without decoding and re-mixing it, which keeps CPU usage surprisingly low.
The Docker project runs each of these as a container. A container is an isolated package that holds an application plus everything it needs to run, sharing the host’s Linux kernel but with its own files and processes. Docker is the engine that runs containers, and Docker Compose lets us describe several containers in one YAML file and start them all with a single command. That is exactly what docker-jitsi-meet gives us.
Ports, TCP and UDP in one minute
A port is a numbered door on your server’s IP address; each network service listens behind its own door. There are two main kinds of traffic. TCP is reliable and ordered, perfect for web pages. UDP is fire-and-forget: no retransmissions, no waiting. For live audio and video that is a feature, because a packet that arrives late is useless anyway. Jitsi needs three doors open:
- TCP 80: only used for the Let’s Encrypt validation and to redirect visitors to HTTPS.
- TCP 443: the web interface and the signaling (over secure websockets).
- UDP 10000: all media that goes through the video bridge. This is the one people forget.
The two-person trap
When only two people are in a room, Jitsi tries a direct peer-to-peer connection between their browsers and does not use the video bridge at all. That is great for quality, but it hides configuration mistakes: a server with UDP 10000 blocked will happily run perfect two-person calls, then fall apart the moment a third participant joins. Remember this when we test in Step 8.
What you need
- A VPS with a public IPv4 address, at least 2 vCPU and 4 GB RAM, running Debian 13 or Ubuntu 24.04. A KVM VPS is recommended (see the note on LXC below).
- A domain or subdomain where you can create DNS records, for example
meet.example.ro. - A user with
sudorights and SSH access to the server. - An email address for Let’s Encrypt expiry notices.
- About 30 minutes.
A note on LXC containers: Docker inside an LXC VPS needs the “nesting” feature enabled on the container, so ask our support if you are on LXC. Even then, some kernel tunables we use for the video bridge cannot be changed inside an unprivileged LXC container, and recording (Jibri) needs a kernel sound module that LXC cannot load. For Jitsi, a KVM VPS is the comfortable choice.
How big should the VPS be?
Jitsi itself is light when idle. The two Java components (jicofo and jvb) take most of the RAM, and the bridge grows with the number of active streams. The chart below shows approximate numbers we see on a small instance; your values will vary with the release and settings.
In practice: 2 vCPU / 4 GB is comfortable for meetings of up to roughly 10 to 15 people, and 4 vCPU / 8 GB gives headroom for 20 to 40 participants spread over a few rooms. Bandwidth usually becomes the limit before CPU does: each participant sends roughly 1 to 2.5 Mbps of video, and the bridge forwards a copy of the visible streams to everyone else, so a busy 10-person meeting can push something like 20 to 40 Mbps out of the server. Jitsi sends lower resolution layers for small thumbnails (simulcast), which keeps this in check.
Installing Jitsi Meet with Docker: the 8 steps
Here is the full procedure. Each box matches one numbered step below.
Step 1: Point a domain at your VPS
Let’s Encrypt only issues certificates for domain names, and browsers only allow camera access on HTTPS pages, so a domain is not optional. First, find the public IP of your VPS:
ip -brief address show
You will see one line per network interface, something like eth0 UP 203.0.113.10/24 .... The first address on the main interface (ignore lo, which is 127.0.0.1) is your public IPv4.
At your DNS provider, create an A record for the subdomain you want, for example meet.example.ro, pointing to that IP. If you also add an AAAA (IPv6) record, make sure IPv6 really works on the VPS, otherwise Let’s Encrypt may try IPv6 and fail. Check that the name resolves:
getent hosts meet.example.ro
The output should show your VPS IP next to the name. If it prints nothing, wait a few minutes for DNS to propagate before continuing.
Step 2: Install Docker and the Compose plugin
Start with an up-to-date system:
sudo apt update && sudo apt full-upgrade -y
On Ubuntu, check whether a reboot is needed with [ -f /var/run/reboot-required ] && echo "reboot needed". Debian does not always create that file, so on Debian simply reboot if the upgrade installed a new linux-image package. Then install Docker from Docker’s official repository, using the current deb822 format. The commands below detect automatically whether you are on Debian or Ubuntu:
sudo apt install -y ca-certificates curl
. /etc/os-release
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL "https://download.docker.com/linux/${ID}/gpg" -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/${ID}
Suites: ${VERSION_CODENAME}
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
The first block downloads Docker’s signing key, the second writes the repository definition to /etc/apt/sources.list.d/docker.sources, and the last one installs the engine plus the docker compose plugin. Now allow your user to run Docker without sudo:
sudo usermod -aG docker "$USER"
Log out and back in (or reconnect over SSH) so the new group applies, then verify:
docker run --rm hello-world
docker compose version
You should see the “Hello from Docker!” message and a Compose version starting with v2 or newer. This matters for Jitsi: its config path uses ~, and if you run Compose with sudo, ~ becomes /root and your configuration ends up somewhere you did not expect.
Step 3: Download docker-jitsi-meet and generate secrets
The Jitsi team publishes tested releases of the Docker setup, each pinned to matching image versions. We always use the latest release rather than the development branch. Check the releases page to see what is current; the command below fetches it automatically:
sudo apt install -y unzip wget
cd ~
wget -O jitsi-release.zip "$(wget -q -O - https://api.github.com/repos/jitsi/docker-jitsi-meet/releases/latest | grep zipball_url | cut -d'"' -f4)"
unzip -q jitsi-release.zip
mv jitsi-docker-jitsi-meet-* docker-jitsi-meet
cd docker-jitsi-meet
ls
You should see files such as docker-compose.yml, env.example, gen-passwords.sh and some optional extras (jibri.yml, jigasi.yml and others we do not need today). Now create your configuration file, fill it with random internal passwords and create the folders where the containers keep their data:
cp env.example .env
./gen-passwords.sh
chmod 600 .env
mkdir -p ~/.jitsi-meet-cfg/{web,transcripts,prosody/config,prosody/prosody-plugins-custom,jicofo,jvb,jigasi,jibri}
gen-passwords.sh generates the secrets the components use to authenticate to each other. Run it once, before the first start. If you regenerate them later, the existing Prosody data no longer matches and components fail to connect (see troubleshooting). chmod 600 makes .env readable only by you, since it now contains those secrets.
Step 4: Configure the public URL, ports and Let’s Encrypt in .env
Many settings in .env are commented out with # by default. This command shows the lines we care about and their line numbers:
grep -n -E '^#?(HTTP_PORT|HTTPS_PORT|TZ|PUBLIC_URL|JVB_ADVERTISE_IPS|ENABLE_LETSENCRYPT|LETSENCRYPT_DOMAIN|LETSENCRYPT_EMAIL|ENABLE_HTTP_REDIRECT)=' .env
Open the file with nano .env and edit those lines so they look like this, removing the leading # where present and using your own domain, IP and email:
HTTP_PORT=80
HTTPS_PORT=443
TZ=Europe/Bucharest
PUBLIC_URL=https://meet.example.ro
JVB_ADVERTISE_IPS=203.0.113.10
ENABLE_LETSENCRYPT=1
LETSENCRYPT_DOMAIN=meet.example.ro
LETSENCRYPT_EMAIL=admin@example.ro
ENABLE_HTTP_REDIRECT=1
What each line does:
HTTP_PORTandHTTPS_PORT: the defaults are 8000 and 8443, meant for testing. Let’s Encrypt validation requires the web container to answer on the standard ports 80 and 443.PUBLIC_URL: the address people type in their browser. It must match the domain from Step 1, with no port.JVB_ADVERTISE_IPS: the IP address the video bridge tells browsers to send media to. Inside Docker, the bridge only sees a private172.xaddress, so without this it falls back to asking an external STUN server what its public IP is. Setting it explicitly is more reliable. If your VPS has working IPv6, you can list both, separated by a comma.ENABLE_LETSENCRYPT,LETSENCRYPT_DOMAIN,LETSENCRYPT_EMAIL: let the web container obtain and automatically renew a free certificate. While experimenting, you can also setLETSENCRYPT_USE_STAGING=1to avoid Let’s Encrypt rate limits, then remove it for the real certificate.ENABLE_HTTP_REDIRECT: sends anyone who typeshttp://to the HTTPS version.
Save with Ctrl+O, Enter, and exit with Ctrl+X. Nothing else needs changing for a working first start.
Step 5: Open TCP 80, 443 and UDP 10000
Before touching the firewall, one important fact: Docker writes its own iptables rules for published ports, and those rules are evaluated before ufw’s. That means ports published by Jitsi’s containers (80, 443 and UDP 10000) are reachable from the internet even if ufw says “deny”. ufw still matters for services running directly on the host, above all SSH. Adding ufw allow rules for the Jitsi ports does no harm and documents your intent, but never rely on ufw to hide a Docker port.
On Ubuntu, ufw is preinstalled but inactive; on Debian, install it first with sudo apt install -y ufw. Then:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 10000/udp
sudo ufw enable
sudo ufw status verbose
If your SSH daemon listens on a non-standard port, allow that port instead of 22 before running ufw enable, or you will lock yourself out. The status output should list the four rules as ALLOW IN. If there is any other firewall in front of your server (a hardware firewall or an upstream filter), open the same three Jitsi ports there as well.
Optional, KVM only: larger UDP buffers. The video bridge handles many small UDP packets, and the Jitsi project recommends larger kernel buffers for it:
sudo tee /etc/sysctl.d/20-jvb-udp-buffers.conf > /dev/null <<'EOF'
net.core.rmem_max = 10485760
net.core.netdev_max_backlog = 100000
EOF
sudo sysctl --system
The output lists the applied settings. In an unprivileged LXC container these keys are read-only and you will see “permission denied” errors: that is expected, just skip this part.
Step 6: Start Jitsi Meet with Docker and check the certificate
From the ~/docker-jitsi-meet folder, start everything:
cd ~/docker-jitsi-meet
docker compose up -d
docker compose ps
The first run pulls four images, which takes a minute or two, then prints lines like Container docker-jitsi-meet-web-1 Started. docker compose ps should list four services, web, prosody, jicofo and jvb, all with a status of Up. Now watch the web container obtain the certificate:
docker compose logs -f web
Look for the Let’s Encrypt client reporting Cert success near the top. Press Ctrl+C to stop following the log (the container keeps running). Confirm from the server itself:
curl -sI https://meet.example.ro | head -n 1
A status line ending in 200 means nginx is serving the site with a valid certificate. Open https://meet.example.ro in your browser: you should see the Jitsi Meet welcome page with a padlock in the address bar. Renewal is automatic, handled inside the web container, and certificates are kept in ~/.jitsi-meet-cfg/web, so they survive container recreation.
Right now, though, anyone who finds the URL can create rooms. Let’s fix that.
Step 7: Require authentication to create rooms
The mode we want is called “secure domain” with guests: only registered users can create a meeting, and anyone with the link can join after the host has arrived. Edit .env again and set (uncomment if needed):
ENABLE_AUTH=1
ENABLE_GUESTS=1
AUTH_TYPE=internal
AUTH_TYPE=internal stores users in Prosody’s own database, which is ideal for small teams. LDAP and JWT tokens are also supported if you outgrow it. Apply the change:
docker compose up -d
Compose notices the changed environment and prints Recreated for the affected containers. Note that docker compose restart would not pick up .env changes, it only restarts containers with their old settings. Now create your first user:
docker compose exec prosody prosodyctl --config /config/prosody.cfg.lua adduser alice@meet.jitsi
You will be asked for the password twice; it is not echoed and it does not end up in your shell history. meet.jitsi is the internal XMPP domain used by the containers, not your public domain, so keep it exactly like that. Other useful commands:
# change a password
docker compose exec prosody prosodyctl --config /config/prosody.cfg.lua passwd alice@meet.jitsi
# delete a user
docker compose exec prosody prosodyctl --config /config/prosody.cfg.lua deluser alice@meet.jitsi
# list existing users
sudo ls ~/.jitsi-meet-cfg/prosody/config/data/meet%2ejitsi/accounts/
Test it in a private browser window: open https://meet.example.ro/SomeRoomName. Instead of starting the meeting, Jitsi shows that it is waiting for the host, with a button to log in. Log in as alice with the password you chose, and the meeting starts with you as moderator. Guests who open the same link now get in without credentials.
Step 8: Test a real meeting with three people
Because of the two-person peer-to-peer trap, a real test needs at least three participants, ideally on different networks: your laptop, your phone on mobile data (not your office Wi-Fi) and a colleague at home. Start the meeting as an authenticated user, share the link and let everyone turn on their camera.
If all three see and hear each other for several minutes, media is flowing through the video bridge on UDP 10000 and your setup is correct. While the meeting runs, check resource usage on the server:
docker stats --no-stream
You will see CPU and memory per container; jvb should be the busiest, with numbers in the range of the chart above. Hovering over the signal bars in a participant’s tile in the browser also shows connection details such as bitrate and packet loss.
Keeping it updated and backed up
Jitsi publishes new stable releases regularly, with browser compatibility fixes and security updates. Always back up first. Everything that matters, your configuration, certificates and user accounts, lives in ~/.jitsi-meet-cfg and the .env file; there is no separate database:
cd ~
sudo tar czf jitsi-backup-$(date +%F).tar.gz .jitsi-meet-cfg docker-jitsi-meet/.env
sudo chown "$USER": jitsi-backup-*.tar.gz
Files in the config folder are written by the containers as root, so tar needs sudo, and chown gives you ownership of the archive so you can copy it off with scp from your own computer. To restore, stop the stack, move the current ~/.jitsi-meet-cfg aside, extract the archive in your home folder and start again.
To upgrade, download the new release next to the current one, reuse your .env, pull the images, then swap:
cd ~
wget -O jitsi-release.zip "$(wget -q -O - https://api.github.com/repos/jitsi/docker-jitsi-meet/releases/latest | grep zipball_url | cut -d'"' -f4)"
unzip -q jitsi-release.zip
mv jitsi-docker-jitsi-meet-* docker-jitsi-meet.new
cp docker-jitsi-meet/.env docker-jitsi-meet.new/.env
diff docker-jitsi-meet/env.example docker-jitsi-meet.new/env.example
(cd docker-jitsi-meet.new && docker compose pull)
(cd docker-jitsi-meet && docker compose down)
mv docker-jitsi-meet docker-jitsi-meet.old
mv docker-jitsi-meet.new docker-jitsi-meet
cd docker-jitsi-meet && docker compose up -d
The diff shows new or renamed settings in the template; read the release notes and copy anything relevant into your .env. Pulling before stopping keeps downtime to a few seconds. Do it outside meeting hours anyway, and delete docker-jitsi-meet.old once you are happy.
Troubleshooting and common mistakes
Calls work with two people but freeze or drop when a third joins. The classic symptom of media not reaching the bridge. Check that UDP 10000 is open on any upstream firewall, that JVB_ADVERTISE_IPS contains the correct public IP, and that sudo ss -ulpn | grep 10000 shows a process listening. Then look at docker compose logs jvb for errors.
The certificate is not issued. Usually DNS does not point to the server yet, or something else already uses port 80. Run sudo ss -tlpn | grep -E ':80 |:443 ' to see if Apache or another nginx on the host holds those ports. Also confirm HTTP_PORT=80 and HTTPS_PORT=443 in .env. After several failed attempts you may hit Let’s Encrypt rate limits; switch to LETSENCRYPT_USE_STAGING=1 until it works.
I changed .env and nothing happened. Use docker compose up -d, which recreates containers with the new values. docker compose restart keeps the old environment.
The configuration is missing or in /root. You ran Compose with sudo. Add your user to the docker group as in Step 2 and run Compose as yourself.
Components cannot connect after running gen-passwords.sh again. The internal passwords no longer match what Prosody stored at first start. Restore the previous .env from your backup, or stop the stack and start over with an empty ~/.jitsi-meet-cfg (this also deletes your users and certificate).
Guests see “waiting for the host” forever. Somebody has to log in and start the meeting first. If guests cannot join even then, check that ENABLE_GUESTS=1 is set and the containers were recreated.
The browser does not ask for camera access. Browsers only allow camera and microphone on HTTPS. Make sure you are on the https:// address with a valid certificate.
Security notes
- Authentication is the main protection. An open Jitsi server will be found and used by strangers, eating your bandwidth. Keep
ENABLE_AUTH=1on and use strong, unique passwords for every account. - Publish only what is needed. The default compose file publishes only the web ports and UDP 10000; Prosody and Jicofo stay on the internal Docker network, and the bridge’s management API is bound to
127.0.0.1. Do not add extraports:entries, remembering that Docker-published ports bypass ufw. - Protect
.envand the config folder. They contain internal secrets and the user database. Keepchmod 600 .envand store backups somewhere private. - Membership of the
dockergroup is equivalent to root. Only add users you would trust with full root access. - Use the meeting controls. Unguessable room names, the lobby (participants wait until a moderator lets them in) and room passwords are all available from the meeting UI. End-to-end encryption can also be enabled per meeting in supported browsers.
- Harden SSH. Use key-based login, disable password login and consider fail2ban. The video server is only as safe as the machine it runs on.
- Update regularly. Follow the release process above at least every few months, and run
sudo apt update && sudo apt full-upgradefor the host itself.
So, is self-hosted Jitsi worth it?
If your team holds regular calls, works with clients who care about where data goes, or is simply tired of meetings being cut off at 40 minutes, the answer is yes. With Docker, Jitsi Meet is a handful of commands, one config file and three open ports, and with authentication enabled it stays your private conferencing service with no time limits and no per-user fees. The things to remember are few: UDP 10000 must be reachable, JVB_ADVERTISE_IPS must be right, and you test with three people, not two.
You can deploy everything above on a VPS with us in a few minutes, on hardware in our own Romanian data center with plenty of bandwidth for video. If you would rather not manage it yourself, our server administration team can install, secure and keep your Jitsi Meet server updated for you.
FAQ
How many participants can a single VPS handle?
A 2 vCPU / 4 GB VPS handles meetings of roughly 10 to 15 people with video comfortably, and 4 vCPU / 8 GB stretches to several dozen participants spread across rooms. Bandwidth is usually the real limit, so watch your traffic graphs during busy meetings. Jitsi can also scale out with multiple video bridges, but that is a topic for another article.
Do I need a TURN server?
For most users, no: UDP 10000 works from home and mobile networks. Some strict corporate networks block all UDP and only allow HTTPS; for those participants a TURN server listening on TCP 443 helps relay media. docker-jitsi-meet can be configured to use one if this becomes a real need for your users.
Can I record meetings?
Yes, with Jibri, the Jitsi recording component, which docker-jitsi-meet includes as an optional jibri.yml. It runs a full Chrome instance per recording, so plan for extra CPU and RAM, and it needs the snd-aloop kernel module on the host, which is not possible in an LXC container.
Can I run it on an LXC VPS?
It can work if Docker nesting is enabled for your container, but you cannot apply the UDP buffer tuning and recording will not work. For a video server, we recommend a KVM VPS.
Are there any time limits or participant caps?
No. Your server has no artificial limits on meeting length or number of rooms; the only limits are the CPU, RAM and bandwidth of your VPS.
I already run nginx or another reverse proxy on this VPS. What changes?
Leave ENABLE_LETSENCRYPT off, keep Jitsi’s default HTTP_PORT=8000 and HTTPS_PORT=8443, and let your existing proxy handle the certificate and forward traffic to Jitsi, including websocket upgrades. UDP 10000 still goes directly to the video bridge, because media cannot pass through an HTTP proxy.
Do the mobile apps work with my server?
Yes. Install the Jitsi Meet app on Android or iOS, open its settings, enter your server URL (for example https://meet.example.ro) and join rooms as usual. The apps require a valid certificate, which Let’s Encrypt gives you.