Self-host Jitsi Meet for video calls
Follow this on your own server. Start free, no card needed
Zoom and Google Meet both cap free calls by time or participant count, and the paid tiers bill per host, per month, whether or not you actually use them that month. Jitsi Meet is open-source video conferencing that does the same core job, browser-based calls with screen sharing and no install, as self-hosted software: four containers, one domain, and no per-minute or per-seat bill once it’s running. This guide sets it up from an empty server using the official images, with Caddy handling HTTPS in front, and explains the one piece (a single UDP port) that determines whether calls actually connect once more than one person is behind a different network.
By the end you’ll have a working Jitsi Meet instance at your own domain, understand why that UDP port matters, and know the honest state of Railyard’s one-click path for this specific app.
What you’ll need
- A server with real headroom. A single test call between two people on a local network fits on a Hetzner CX23 (2 vCPU, 4 GB RAM, €5.99/month excluding VAT), but sustained team meetings want more: a CX33 (4 vCPU, 8 GB RAM, €8.99/month) is a safer baseline once several people are actually relying on this daily.
- A domain with a DNS A record pointed at the server (this guide uses
meet.example.com). - The server’s public IPv4 address, known ahead of time, not just assigned (you’ll need it for NAT traversal in Step 3).
- SSH access and about 20 minutes.
Step 1: Provision the server and install Docker
Create the server on Ubuntu 24.04, point DNS at it, and install Docker:
ssh root@203.0.113.30
apt update && apt upgrade -y
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
docker --version
docker compose version
Step 2: Get the official configuration directories
Jitsi’s own docs direct you to download a tagged release rather than clone the repository directly, since the compose file on the default branch can be ahead of what the published images actually support. This guide uses the current release, stable-11248-1:
mkdir -p ~/.jitsi-meet-cfg/{web,transcripts,prosody/config,prosody/prosody-plugins-custom,jicofo,jvb}
chmod 777 ~/.jitsi-meet-cfg/transcripts
These directories hold Jitsi’s own generated configuration and persist across upgrades; losing them means regenerating passwords and any customizations.
Step 3: Write the environment file
CONFIG=~/.jitsi-meet-cfg
HTTP_PORT=8000
HTTPS_PORT=8443
JVB_PORT=10000
PUBLIC_URL=https://meet.example.com
DISABLE_HTTPS=1
ENABLE_LETSENCRYPT=0
ENABLE_HTTP_REDIRECT=0
XMPP_DOMAIN=meet.jitsi
XMPP_AUTH_DOMAIN=auth.meet.jitsi
XMPP_MUC_DOMAIN=muc.meet.jitsi
XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi
XMPP_GUEST_DOMAIN=guest.meet.jitsi
XMPP_SERVER=xmpp.meet.jitsi
JICOFO_COMPONENT_SECRET=
JICOFO_AUTH_PASSWORD=
JVB_AUTH_PASSWORD=
JVB_ADVERTISE_IPS=203.0.113.30
ENABLE_GUESTS=1
TZ=Etc/UTC
Generate the three blank secrets with:
for v in JICOFO_COMPONENT_SECRET JICOFO_AUTH_PASSWORD JVB_AUTH_PASSWORD; do
echo "$v=$(openssl rand -hex 16)"
done
Paste each value into .env in place of the blank. DISABLE_HTTPS=1 and ENABLE_LETSENCRYPT=0 matter specifically because Caddy is handling HTTPS here instead of the web container’s own built-in option, which would otherwise fight Caddy for port 443. JVB_ADVERTISE_IPS is the setting most self-hosted Jitsi guides bury: it’s the server’s public IP, told explicitly to the videobridge so it can advertise a reachable address to participants behind NAT instead of only its private Docker network address.
Step 4: Write the Docker Compose file
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy-data:/data
- caddy-config:/config
depends_on:
- web
web:
image: ghcr.io/jitsi/web:stable-11248-1
restart: unless-stopped
volumes:
- ${CONFIG}/web:/config
- ${CONFIG}/transcripts:/usr/share/jitsi-meet/transcripts
env_file:
- .env
depends_on:
- jvb
prosody:
image: ghcr.io/jitsi/prosody:stable-11248-1
restart: unless-stopped
expose:
- "5222"
- "5269"
- "5347"
- "5280"
volumes:
- ${CONFIG}/prosody/config:/config
- ${CONFIG}/prosody/prosody-plugins-custom:/prosody-plugins-custom
env_file:
- .env
jicofo:
image: ghcr.io/jitsi/jicofo:stable-11248-1
restart: unless-stopped
volumes:
- ${CONFIG}/jicofo:/config
env_file:
- .env
depends_on:
- prosody
jvb:
image: ghcr.io/jitsi/jvb:stable-11248-1
restart: unless-stopped
ports:
- "${JVB_PORT:-10000}:${JVB_PORT:-10000}/udp"
volumes:
- ${CONFIG}/jvb:/config
env_file:
- .env
depends_on:
- prosody
volumes:
caddy-data:
caddy-config:
The jvb service is the one with a real host port published directly, 10000/udp, rather than hidden behind Caddy. That’s not a simplification; HTTPS can be proxied, but the actual audio and video media stream cannot, since WebRTC needs a direct UDP path. Validate before starting:
docker compose config
Step 5: Write the Caddyfile
meet.example.com {
reverse_proxy web:8000
}
Step 6: Bring it up and open the firewall
docker compose up -d
docker compose ps
You should see all five services (caddy, web, prosody, jicofo, jvb) as running. Open the UDP port explicitly if the server has a firewall active:
ufw allow 10000/udp
ufw allow 80/tcp
ufw allow 443/tcp
Visit https://meet.example.com; you should see Jitsi Meet’s own landing page with a valid certificate. Start a meeting and join from two different networks, not two tabs on the same machine, since same-network testing won’t reveal a NAT or firewall problem with the UDP port.
The shortcut: do it with Railyard, honestly
Railyard has a Jitsi Meet stack template that provisions all four application containers (web, prosody, jicofo, jvb) together: pick Jitsi Meet in the templates catalog. It’s worth being direct about where it stands today: Railyard’s deployment manifest doesn’t yet have a way to publish a UDP port or advertise a server’s public IP to a container the way JVB_ADVERTISE_IPS and the host UDP mapping in Step 4 do. The containers come up and the web interface loads, but calls between participants on different networks won’t get a working media path until that’s solved. If you need Jitsi Meet actually handling calls today, Steps 1 through 6 above are the real path; the Railyard template is tracked as a known gap rather than something papered over.
Troubleshooting
The page loads but calls never connect between two different networks. This is almost always JVB_ADVERTISE_IPS not set, or set to the server’s private address instead of its public one, combined with the UDP port not actually open on the firewall. Confirm both; a call between two browser tabs on the same machine will work even with this broken, which is why that’s not a valid test.
HTTPS won’t load, or Caddy logs a port conflict. Check that DISABLE_HTTPS=1 and ENABLE_LETSENCRYPT=0 are actually set in .env; without them the web container’s own embedded nginx tries to bind 443 as well.
One participant has audio but no video, or vice versa. Usually a restrictive network on that one participant’s side (corporate firewall, hotel wifi) rather than the server; have them try from a different network before assuming the server is misconfigured.
Out of memory with more than a handful of participants. Jitsi’s videobridge routes media for every participant, and that’s real CPU and memory work that scales with the number of people, not a fixed cost. docker stats jvb while a call is running shows whether it’s the constraint; a CX33 or larger has more room than a CX23.
Changes to .env don’t take effect. Most Jitsi environment variables are only read once, when a container starts; docker compose up -d after an .env change doesn’t always recreate a container that’s already running unchanged. Force it with docker compose up -d --force-recreate.
FAQ
Can I record meetings? Yes, through Jibri, a separate recording component this guide doesn’t set up; it needs its own container and more server resources, and is worth adding only once the base setup above is confirmed working.
How many people can one server handle? It depends more on video quality and server size than a fixed number; a CX33 comfortably handles a small team’s regular meetings, and CPU on the jvb container is the thing to watch as participant count grows.
Do I need TURN for this to work? Most of the time, no, since JVB_ADVERTISE_IPS plus an open UDP port covers the common case. A TURN server helps specifically when a participant is behind a firewall that blocks direct UDP entirely, which is a smaller slice of real-world networks than it used to be.
Is Jitsi Meet actually free to run? The software is; the server and its bandwidth are the real cost, and video calls use considerably more bandwidth than a typical web app, worth factoring in if a provider meters outbound traffic closely.
What’s the mobile experience like? Jitsi has native iOS and Android apps that work with any self-hosted server; point them at https://meet.example.com the same as the browser version.
Can I run this behind a NAT router at home instead of a cloud server? Yes, with one extra step: port-forward both the UDP media port and TCP 443 from the router to the machine running Docker, and set JVB_ADVERTISE_IPS to the router’s own public IP rather than the machine’s local address, since that’s what the videobridge needs to hand out to remote participants.
Jitsi Meet is one of the more honest self-hosting stories here specifically because the one thing that actually matters, whether a call connects across two networks, comes down to one UDP port and one IP address being set correctly, not a mystery. Get those two things right and the rest is four containers and a reverse proxy, running calls with nobody counting minutes.
Why people pick Railyard for this
- Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
- One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
- One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
- Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
- Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.