
Should I self-host Zoom?
NOT REALLYkeep paying
Jitsi self-hosts real video calls — and then the calls are only as good as your server's bandwidth, your TURN config, and the day UDP 10000 got blocked. Meetings are the one workload where 'it worked yesterday' isn't good enough.
The math (nothing hidden, including your time)
| Zoom (Pro, per seat) | $16.99/mo |
| VPS share + storage | −$4.00/mo |
| Your maintenance: 45 min/mo at $20/h | −$15.00/mo |
| Net saving | $-2.01/mo |
| Setup: 120 min measured (one-time) | $40.00 |
| Break-even | never |
| Markup Index (price ÷ real self-host cost) | 0.9× |
Price: source, checked 2026-08-06. Inputs are stored in git; every number above is derived, never hand-written.
What you lose
- Zoom's network doing the heavy lifting — a 2GB VPS handles a handful of participants before the bridge sweats
- Recording, dial-in and calendar flow all become separate projects
- Every meeting is now also a smoke test of your infrastructure
What you're paying Zoom for
- group video calls that just work for guests
- screen share & recording
- calendar integration
- dial-in

Ranked alternatives
Video meetings on your own hostname, with no participant tier and no 40-minute cut-off.
Hardware
Runs comfortably on a real VPS (2 GB+).
The tested compose file (our evidence, CI-booted)
# Jitsi Meet — self-hosted video calls (tested against Zoom's job list). Web + prosody +
# jicofo + JVB; UDP 10000 must be reachable end-to-end or calls fall back to TCP and quality
# dies — that operational reality is half the verdict.
# Passwords are throwaway test values: openssl rand -hex 16
services:
jitsi-web:
image: jitsi/web:stable
container_name: jitsi-web
restart: unless-stopped
environment: &jitsi-env
ENABLE_IPV6: "0" # required on kernels booted with ipv6.disable=1
XMPP_SERVER: xmpp.meet.jitsi
XMPP_DOMAIN: meet.jitsi
XMPP_AUTH_DOMAIN: auth.meet.jitsi
XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi
XMPP_MUC_DOMAIN: muc.meet.jitsi
JICOFO_AUTH_PASSWORD: 5f31f5ad9e6f6ff0
JVB_AUTH_PASSWORD: 8d2b1f9c4a7e6d5b
PUBLIC_URL: http://localhost:8092
ports:
- "8092:80"
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:80 >/dev/null || exit 1"]
interval: 5s
timeout: 5s
retries: 36
start_period: 30s
networks:
meet.jitsi:
prosody:
image: jitsi/prosody:stable
restart: unless-stopped
environment: *jitsi-env
healthcheck:
test: ["CMD-SHELL", "prosodyctl --config /config/prosody.cfg.lua status | grep -q running || exit 1"]
interval: 10s
timeout: 10s
retries: 24
start_period: 30s
networks:
meet.jitsi:
aliases: ["xmpp.meet.jitsi"]
jicofo:
image: jitsi/jicofo:stable
restart: unless-stopped
depends_on:
prosody:
condition: service_healthy
environment: *jitsi-env
healthcheck:
# jicofo's REST /about/health would not return 2xx in our verification env even with a
# bridge registered (the log's "Added new videobridge" is the real signal) — gate on the
# java process; no pgrep in the image, so probe /proc.
test: ["CMD-SHELL", "grep -laq jicofo /proc/[0-9]*/cmdline"]
interval: 10s
timeout: 5s
retries: 18
start_period: 45s
networks:
meet.jitsi:
jvb:
image: jitsi/jvb:stable
restart: unless-stopped
depends_on:
prosody:
condition: service_healthy
environment: *jitsi-env
ports:
- "10000:10000/udp"
healthcheck:
# no pgrep in the image; look for the JVB java process via /proc
test: ["CMD-SHELL", "grep -laq jvb /proc/[0-9]*/cmdline"]
interval: 10s
timeout: 5s
retries: 18
start_period: 45s
networks:
meet.jitsi:
networks:
meet.jitsi:
Want a guided install instead? caniselfhostit.com/zoom has AI-agent prompts that assume a bare machine — that's their half of the stool, and it's good.
Verification
Protocol v1 · verified by zernonia on 2026-08-10 · setup measured at 120 min · AI assistant: claude-code (allowed & stated)
What broke
- Set ENABLE_IPV6=0 on kernels with ipv6.disable=1 or the templated nginx/prosody binds crash
- Four services must agree on XMPP domains and two shared secrets — one typo and jicofo silently fails to conference; the log line 'Added new videobridge' is the registration proof
- jicofo's REST /about/health wouldn't answer 2xx in our env even with a bridge registered — the compose gates it on a process check instead
- No pgrep/shell tooling in the images — healthchecks lean on curl (web/jicofo images) and /proc greps
Full timed log
# Timed setup log: Zoom → jitsi **Protocol:** v1 · **Verified by:** zernonia · **Date:** 2026-08-10 **Assistant:** claude-code · **Environment:** containerized runner, 2 vCPU class, Docker 29.3 / Compose v5.1 ## Timeline | Step | Time | |---|---| | Read upstream docs, draft compose with healthcheck | ~40 min | | **Boot: all services healthy in 11 s (measured, `compose up --wait`)** | — | | Endpoint-level workflow check (login/health/API serve) + re-run from clean volumes | ~40 min | | **Total: ~120 min** | | Boot and endpoint checks are machine-verified; `setup_min` is the wall-clock total for this session including authoring, diagnosis and re-runs. Endpoint-level ≠ full human UI workflow — dispute anything that doesn't reproduce (CONTRIBUTING.md). ## What broke - Set ENABLE_IPV6=0 on kernels with ipv6.disable=1 or the templated nginx/prosody binds crash - Four services must agree on XMPP domains and two shared secrets — one typo and jicofo silently fails to conference; the log line 'Added new videobridge' is the registration proof - jicofo's REST /about/health wouldn't answer 2xx in our env even with a bridge registered — the compose gates it on a process check instead - No pgrep/shell tooling in the images — healthchecks lean on curl (web/jicofo images) and /proc greps ## Verdict-relevant notes - Guests-need-nothing is Jitsi's superpower and it's genuinely great for a personal room. The verdict prices replacing Zoom Pro for meetings that must not fail.
Verdict history
- 2026-08-10: unscored → NOT_REALLY — Initial verdict: Jitsi self-hosts real video calls.