Skip to main content
Fluxer uses LiveKit as its WebRTC Selective Forwarding Unit (SFU) for voice and video calls. This guide covers deployment, configuration, and troubleshooting.

Overview

LiveKit provides:

Voice Channels

Multi-participant voice channels with spatial audio, noise suppression, and echo cancellation.

Video Calls

HD video with simulcast, screen sharing, and adaptive bitrate.

TURN/STUN

NAT traversal with built-in TURN server for users behind restrictive firewalls.

Recording

Optional call recording and egress to S3-compatible storage.

Deployment Options

You can deploy LiveKit in several ways:

LiveKit Configuration

Create config/livekit.yaml for self-hosted deployments:
config/livekit.yaml
port: 7880

# API Keys (generate with `livekit-server generate-keys`)
keys:
  APIxxxxxxxxx: secretxxxxxxxxxxxxxxxxx

rtc:
  # Port range for WebRTC connections
  port_range_start: 50000
  port_range_end: 50100
  
  # TCP fallback port
  tcp_port: 7881
  
  # Use external IP for clients (auto-detected if not set)
  # use_external_ip: true
  # node_ip: 203.0.113.42

turn:
  enabled: true
  
  # TURN server for NAT traversal
  udp_port: 3478
  
  # External TURN server (optional)
  # external_tls: true

room:
  # Automatically create rooms when clients connect
  auto_create: true
  
  # Maximum participants per room
  max_participants: 100
  
  # Timeout before empty rooms are deleted (seconds)
  empty_timeout: 300
  
  # Enable room departure timeout
  departure_timeout: 20

# Webhook for events (participant joined, track published, etc.)
webhook:
  api_key: webhookxxxxxxxxxxxxx
  urls:
    - https://chat.example.com/api/webhooks/livekit

# Logging
logging:
  level: info
  # sample: true  # Enable to reduce log volume

# Redis for distributed deployments (optional)
# redis:
#   address: redis:6379
#   db: 0

# Recording (optional)
# egress:
#   s3:
#     access_key: your-s3-key
#     secret: your-s3-secret
#     endpoint: s3.amazonaws.com
#     bucket: livekit-recordings

Generate API Keys

If not using livekitctl, generate keys manually:
docker run --rm livekit/livekit-server:v1.9.11 generate-keys
Output:
API Key: APIxxxxxxxxx
API Secret: secretxxxxxxxxxxxxxxxxx

Fluxer Configuration

Update your config/config.json to enable voice:
config/config.json
{
  "integrations": {
    "voice": {
      "enabled": true,
      
      // API credentials from livekit.yaml
      "api_key": "APIxxxxxxxxx",
      "api_secret": "secretxxxxxxxxxxxxxxxxx",
      
      // WebSocket URL for clients (wss:// in production)
      "url": "wss://chat.example.com/livekit",
      
      // Webhook endpoint for LiveKit events
      "webhook_url": "https://chat.example.com/api/webhooks/livekit",
      
      // Default voice region (optional)
      "default_region": {
        "id": "default",
        "name": "Default Region",
        "emoji": "🌐",
        "latitude": 40.7128,
        "longitude": -74.0060
      }
    }
  }
}
url
string
required
LiveKit WebSocket URL that clients connect to. Must be publicly accessible.
  • Self-hosted: wss://livekit.example.com or wss://chat.example.com/livekit
  • LiveKit Cloud: wss://your-project.livekit.cloud
  • Development: ws://localhost:7880 (HTTP OK for localhost)
webhook_url
string
Endpoint where LiveKit sends event webhooks. Used to sync room state and participant changes.
default_region
object
If specified, Fluxer automatically creates this voice region on startup if none exist. Useful for single-region deployments.

Network Configuration

Required Ports

LiveKit requires these ports to be accessible from the internet:
PortProtocolPurposeFirewall Rule
7880TCPHTTP/WebSocket (internal)Not exposed (behind reverse proxy)
7881TCPRTC over TCP (fallback)ufw allow 7881/tcp
3478UDPTURN/STUNufw allow 3478/udp
50000-50100UDPRTP/RTCP media streamsufw allow 50000:50100/udp
Critical: UDP ports cannot be proxied through Cloudflare Tunnels, ngrok, or similar HTTP-only tunnels. They must be directly accessible via the server’s public IP.

Firewall Configuration

# Allow HTTPS (for WebSocket signaling via reverse proxy)
sudo ufw allow 443/tcp

# Allow LiveKit UDP ports
sudo ufw allow 7881/tcp
sudo ufw allow 3478/udp
sudo ufw allow 50000:50100/udp

sudo ufw enable

Reverse Proxy Configuration

Proxy the LiveKit WebSocket endpoint through your existing reverse proxy:
/etc/nginx/sites-available/fluxer
server {
    listen 443 ssl http2;
    server_name chat.example.com;
    
    # ... existing Fluxer config ...
    
    # LiveKit WebSocket proxy
    location /livekit {
        proxy_pass http://127.0.0.1:7880;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # Disable buffering for WebSocket
        proxy_buffering off;
        
        # Increase timeouts
        proxy_read_timeout 3600;
        proxy_send_timeout 3600;
    }
}

DNS Configuration

If using a dedicated subdomain for LiveKit:
A    livekit.chat.example.com  →  203.0.113.42
AAAA livekit.chat.example.com  →  2001:db8::1
Then configure LiveKit to advertise this domain:
livekit.yaml
rtc:
  use_external_ip: false  # Use domain instead of IP
  # Clients will connect to livekit.chat.example.com

Multi-Region Voice

For global deployments, deploy multiple LiveKit servers in different regions.

1. Deploy Regional LiveKit Servers

Deploy LiveKit in each region (e.g., us-east, eu-west, ap-south):
# Region: US East (Virginia)
sudo livekitctl bootstrap \
  --livekit-domain livekit-us.chat.example.com \
  --turn-domain turn-us.chat.example.com \
  --email [email protected]

# Region: EU West (Ireland)  
sudo livekitctl bootstrap \
  --livekit-domain livekit-eu.chat.example.com \
  --turn-domain turn-eu.chat.example.com \
  --email [email protected]

2. Add Regions in Fluxer Admin Panel

Navigate to Admin Panel → Voice → Regions and add:
[
  {
    "id": "us-east",
    "name": "US East",
    "emoji": "🇺🇸",
    "latitude": 37.7749,
    "longitude": -122.4194,
    "servers": [
      {
        "url": "wss://livekit-us.chat.example.com",
        "api_key": "APIxxxxxxxxx",
        "api_secret": "secretxxxxxxxxxxxxxxxxx"
      }
    ]
  },
  {
    "id": "eu-west",
    "name": "EU West",
    "emoji": "🇪🇺",
    "latitude": 53.3498,
    "longitude": -6.2603,
    "servers": [
      {
        "url": "wss://livekit-eu.chat.example.com",
        "api_key": "APIxxxxxxxxx",
        "api_secret": "secretxxxxxxxxxxxxxxxxx"
      }
    ]
  }
]
Fluxer automatically selects the closest region based on user latency.

Monitoring

LiveKit Logs

sudo livekitctl logs --service livekit.service --lines 200

# Follow logs in real-time
sudo journalctl -u livekit.service -f

Health Checks

# LiveKit health endpoint
curl http://localhost:7880/

# Check if ports are listening
ss -tuln | grep -E '7880|7881|3478|50000'

# Test UDP connectivity (from client)
nc -u -v -w 1 chat.example.com 3478

Metrics

LiveKit exposes Prometheus metrics at http://localhost:7880/metrics:
prometheus.yml
scrape_configs:
  - job_name: 'livekit'
    static_configs:
      - targets: ['localhost:7880']
Key metrics:
  • livekit_room_total - Active rooms
  • livekit_participant_total - Connected participants
  • livekit_track_published_total - Published audio/video tracks
  • livekit_packet_total - RTP packets sent/received

Troubleshooting

  1. Verify voice is enabled in config:
    grep -A 5 '"voice"' config/config.json
    
  2. Check Fluxer logs for LiveKit connection errors:
    docker compose logs fluxer_server | grep -i livekit
    
  3. Ensure api_key and api_secret match between livekit.yaml and Fluxer config.
Symptoms: “Connecting…” spinner never completes
  1. Check if LiveKit is accessible:
    curl -I https://chat.example.com/livekit
    # Should return 404 or 426 (Upgrade Required)
    
  2. Verify WebSocket URL in config matches your reverse proxy path.
  3. Check browser console for WebSocket errors (F12 → Console).
  4. Test with LiveKit’s CLI:
    livekit-cli join-room \
      --url wss://chat.example.com/livekit \
      --api-key APIxxxxxxxxx \
      --api-secret secretxxxxxxxxxxxxxxxxx \
      --room test-room \
      --identity test-user
    
Cause: UDP ports blocked or TURN not working
  1. Verify UDP ports are open:
    sudo ufw status | grep -E '3478|7881|50000'
    
  2. Test TURN server:
    # Install trickle-ice
    npm install -g trickle-ice
    
    # Test TURN connectivity
    trickle-ice chat.example.com 3478
    
  3. Check LiveKit logs for ICE connection failures:
    sudo journalctl -u livekit.service | grep -i 'ice\|turn'
    
  4. Ensure TURN is enabled in livekit.yaml:
    turn:
      enabled: true
      udp_port: 3478
    
  1. Enable echo cancellation in LiveKit:
    audio:
      echo_cancellation: true
      noise_suppression: true
      auto_gain_control: true
    
  2. Ask users to use headphones or reduce speaker volume.
  3. Check if multiple clients are running on the same device.
  1. Check number of concurrent rooms/participants:
    curl -s http://localhost:7880/metrics | grep livekit_room_total
    
  2. Consider scaling horizontally (add more LiveKit nodes).
  3. Reduce video quality in client settings:
    • Lower resolution (720p → 480p)
    • Reduce framerate (30fps → 15fps)
    • Disable simulcast for low-end clients

Advanced Configuration

Custom Codecs

livekit.yaml
audio:
  # Prefer Opus codec
  codecs:
    - opus

video:
  # Use VP8 (better compatibility) or VP9 (better compression)
  codecs:
    - vp8
    - h264

Recording to S3

livekit.yaml
egress:
  s3:
    access_key: your-s3-access-key
    secret: your-s3-secret-key
    endpoint: s3.amazonaws.com
    bucket: livekit-recordings
    region: us-east-1
Trigger recording via API:
curl -X POST https://chat.example.com/api/v1/channels/:id/record \
  -H "Authorization: Bearer your-token"

Redis for Distributed LiveKit

For multi-node LiveKit deployments, use Redis for state synchronization:
livekit.yaml
redis:
  address: redis:6379
  db: 0
  username: default
  password: your-redis-password
All LiveKit nodes must share the same Redis instance.

Next Steps

Scaling Guide

Scale voice infrastructure globally with multi-region deployments

Architecture

Understand how LiveKit integrates with Fluxer

Build docs developers (and LLMs) love