This guide explains how voice and video calls work in Vampfire, why TURN servers are needed, and how to configure networking for reliable multi-user communication.
Vampfire uses LiveKit (built on WebRTC) for real-time voice, video, and screen sharing.
Here's what happens when you start a call:
- Your browser requests a token from the Vampfire server
- Your browser connects to the LiveKit server via WebSocket
- LiveKit negotiates a peer-to-peer media path between participants
- Audio/video streams flow directly between participants (or through TURN if needed)
LiveKit requires a persistent WebSocket connection between the browser and the LiveKit server.
| Scenario | LiveKit URL Format | Notes |
|---|---|---|
| Local / HTTP only | ws://your-server:7880 |
Development only |
| Behind reverse proxy with SSL | wss://livekit.yourdomain.com |
Required for production |
- Modern browsers block mixed content (HTTP page + WS connection)
- If your site is served over HTTPS, LiveKit must use WSS
- Most reverse proxies (Nginx Proxy Manager, Cloudflare, SWAG) can terminate SSL and proxy WebSocket traffic to LiveKit on port 7880
Your reverse proxy needs to forward WebSocket connections to LiveKit:
Nginx example:
server {
listen 443 ssl;
server_name livekit.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:7880;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400;
}
}Nginx Proxy Manager:
- Add a new proxy host for
livekit.yourdomain.com - Point it to your server IP, port
7880 - Enable SSL (Let's Encrypt)
- Under Advanced, add:
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
This is the most common source of confusion.
Inside Docker, containers talk to each other by service name:
- Vampfire → LiveKit:
ws://livekit:7880✅ (works inside Docker) - Redis:
redis://redis:6379✅ (works inside Docker)
Browsers cannot reach Docker's internal network. The LIVEKIT_URL environment
variable must be a URL that the end-user's browser can reach:
# ❌ WRONG — browsers can't resolve "livekit" (Docker internal hostname)
LIVEKIT_URL=ws://livekit:7880
# ✅ CORRECT — public URL that browsers can reach
LIVEKIT_URL=wss://livekit.yourdomain.com
# ✅ CORRECT — direct IP access (HTTP only, development)
LIVEKIT_URL=ws://192.168.1.100:7880
Open your LIVEKIT_URL in a browser. If you see a connection or get a WebSocket
error, the URL is reachable. If you get "site not found", fix your DNS or proxy config.
Network Address Translation (NAT) is used by routers to share a single public IP address among many devices. It's everywhere — home routers, mobile networks, corporate firewalls.
WebRTC tries to establish direct connections between participants. NAT makes this difficult because:
- Devices behind NAT don't have public IP addresses
- NAT firewalls block unsolicited incoming connections
- Symmetric NAT (common on mobile networks) blocks most connection attempts
A TURN server acts as a relay:
User A ←→ TURN Server ←→ User B
When direct connections fail, media flows through the TURN server instead. This adds a small amount of latency but ensures reliable connectivity.
| Scenario | Direct Connection | TURN Needed? |
|---|---|---|
| Same local network | ✅ Usually works | No |
| Home network → Home network | ✅ Often works | Sometimes |
| Mobile network (4G/5G) | ❌ Usually blocked | Yes |
| Corporate firewall | ❌ Often blocked | Yes |
| VPN | ❌ Often blocked | Yes |
Rule of thumb: If you want calls to work reliably for all users, set up TURN.
If you already run a Coturn server (or want to set one up), configure LiveKit to use it.
- Edit
livekit.yamland uncomment the TURN section:
turn:
enabled: true
domain: turn.yourdomain.com
tls_port: 443
udp_port: 3478
external_tls:
- address: turn.yourdomain.com
port: 443
protocol: tls
username: your-turn-username
credential: your-turn-password- Set the TURN environment variables in your
.env:
TURN_HOST=turn.yourdomain.com
TURN_PORT=3478
TURN_USERNAME=vampfire
TURN_PASSWORD=your-strong-passwordIf you don't have a TURN server, you can run Coturn alongside Vampfire.
Add this to your docker-compose.yml:
coturn:
image: coturn/coturn:latest
ports:
- "3478:3478/tcp"
- "3478:3478/udp"
- "5349:5349/tcp"
- "49152-49200:49152-49200/udp"
volumes:
- ./turnserver.conf:/etc/turnserver.conf:ro
command: -c /etc/turnserver.conf
restart: unless-stoppedCreate a turnserver.conf file:
# Coturn configuration for Vampfire
listening-port=3478
tls-listening-port=5349
realm=yourdomain.com
server-name=yourdomain.com
# Authentication
lt-cred-mech
user=vampfire:your-strong-password
# Logging
log-file=/var/log/turnserver.log
verbose
# Performance
total-quota=100
stale-nonce=600
no-multicast-peers
Then configure LiveKit to use it in livekit.yaml:
turn:
enabled: true
domain: yourdomain.com
udp_port: 3478
tls_port: 5349| Port | Protocol | Service | Purpose |
|---|---|---|---|
| 80 | TCP | Vampfire | HTTP web interface |
| 443 | TCP | Vampfire | HTTPS web interface |
| 7880 | TCP | LiveKit | WebSocket signaling |
| 7881 | TCP | LiveKit | WebRTC TCP fallback |
| 50000-50060 | UDP | LiveKit | WebRTC media traffic |
| 3478 | TCP/UDP | TURN (optional) | TURN signaling |
| 5349 | TCP | TURN (optional) | TURN over TLS |
| 49152-49200 | UDP | TURN (optional) | TURN media relay |
Ensure these ports are open in your firewall/router:
# Minimum required
ufw allow 80/tcp # Vampfire HTTP
ufw allow 443/tcp # Vampfire HTTPS
ufw allow 7880/tcp # LiveKit WebSocket
ufw allow 7881/tcp # LiveKit TCP fallback
ufw allow 50000:50060/udp # LiveKit media
# If using TURN
ufw allow 3478/tcp # TURN signaling
ufw allow 3478/udp # TURN signaling
ufw allow 5349/tcp # TURN TLS
ufw allow 49152:49200/udp # TURN media relay| Browser | Voice | Video | Screen Share | Notes |
|---|---|---|---|---|
| Chrome (Android) | ✅ | ✅ | ✅ | Best experience |
| Firefox (Android) | ✅ | ✅ | ❌ | No screen share |
| Safari (iOS) | ✅ | ✅ | ❌ | Requires iOS 14.5+ |
| Chrome (iOS) | ✅ | ✅ | ❌ | Uses Safari's WebRTC engine |
iOS note: All iOS browsers use Safari's WebRTC engine due to Apple's restrictions. Screen sharing is not supported on iOS.
Cause: TURN server not configured, or TURN server unreachable.
Fix:
- Set up a TURN server (see above)
- Verify TURN is reachable:
nc -zv turn.yourdomain.com 3478 - Check LiveKit logs:
docker compose logs livekit
Cause: Symmetric NAT blocking direct peer connections.
Fix:
- Configure a TURN server — this is the #1 fix
- Check that UDP ports 50000-50060 are open on your firewall
- Try from a different network to isolate the issue
Cause: LiveKit WebSocket URL is wrong or unreachable from the browser.
Fix:
- Check
LIVEKIT_URL— it must be reachable from the browser, not just Docker - If using HTTPS,
LIVEKIT_URLmust usewss://(notws://) - Check your reverse proxy forwards WebSocket connections (see above)
- Test: open
LIVEKIT_URLin your browser — you should get a WebSocket error, not "site not found"
Cause: Mobile networks use symmetric NAT which blocks direct connections.
Fix:
- Set up a TURN server — mobile networks almost always require TURN
- Ensure your TURN server supports UDP relay
Cause: LiveKit is only accessible within Docker or your LAN.
Fix:
- Set
LIVEKIT_URLto a publicly accessible URL (notws://livekit:7880) - Port-forward or reverse-proxy LiveKit port 7880
- Set
use_external_ip: trueinlivekit.yaml(already set in the default config)