← Blog

Self-host Jitsi Meet for video calls

Railyard

June 14, 2026 · 6 min read

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.

See the honest version of the shortcut

Pick Jitsi Meet in Railyard's templates catalog, and read the one real limitation before you do.

Free plan, no card, and 14 days of Pro for every new team. One flat price per team.