Self-hosting Fluxer: findings, bugs, and patches from a real deployment

(#1037) Feature Shipped self-hosting

Thread

Comment by @mgabor3141
RexSystem 1 vote originally by @mgabor3141 on GitHub OP

7: LiveKit deployment — port mapping crash, host networking, and dynamic IP

Docker port mapping crash with UDP ranges

The upstream compose.yaml maps LiveKit ports like this:
ports:
  - '7881:7881'
  - '3478:3478/udp'
  - '50000-50100:50000-50100/udp'
That 50000-50100 range creates 101 individual iptables/nftables rules in the Docker proxy. On my system this caused the entire Docker networking stack to hang — all containers lost connectivity and the host became unresponsive. Had to hard reboot. This will hit anyone whose Docker setup uses iptables-based port mapping (the default).

Solution: host networking

LiveKit works much better with network_mode: host. It binds directly to the host's interfaces, avoids the port mapping overhead entirely, and also solves NAT hairpinning issues. With Docker's bridge networking, WebRTC clients on the same LAN as the server couldn't connect — the ICE candidates advertised the external IP, but hairpin NAT back through the router failed. Host networking eliminates this since LiveKit sees the real network interfaces and can advertise both internal and external IPs correctly.
fluxer-livekit:
  image: livekit/livekit-server:v1.9.11
  container_name: fluxer-livekit
  restart: unless-stopped
  network_mode: host
  command: ["--config", "/etc/livekit/livekit.yaml"]
  volumes:
    - ./config/livekit.yaml:/etc/livekit/livekit.yaml:ro
In the LiveKit config, restrict which interfaces/subnets it listens on so it picks up the right IP:
rtc:
  tcp_port: 7881
  udp_port: 7882
  use_external_ip: true
  node_ip: <your-public-ip>
  interfaces:
    includes:
      - br0          # your LAN-facing interface
  ips:
    includes:
      - 192.168.0.0/24   # your LAN subnet

turn:
  enabled: true
  udp_port: 3479   # remapped from 3478 to avoid conflicts (e.g. Nextcloud Talk)
Since it's on the host network, the LiveKit signaling endpoint needs to go through Traefik using the host IP rather than the Docker service name. The wss:// signaling URL in Fluxer config points to a Traefik route that proxies to 127.0.0.1:7880.

Dynamic IP and the node_ip problem

node_ip in the LiveKit config tells clients which IP to send media to. If you're on a residential connection with a dynamic IP, this value goes stale when your IP changes. LiveKit reads the config once at startup and has no mechanism to detect IP changes. My workaround uses three scripts: Entrypoint — resolves a DDNS hostname to an IP at startup:
#!/bin/sh
RESOLVED_IP=$(getent hosts "$LIVEKIT_NODE_HOSTNAME" | awk '{print $1; exit}')
echo "$RESOLVED_IP" > /tmp/node_ip
sed "s|node_ip:.*|node_ip: $RESOLVED_IP|" /etc/livekit/livekit.yaml > /tmp/livekit.yaml
exec /livekit-server --config /tmp/livekit.yaml
Healthcheck — detects when the IP has changed since startup:
#!/bin/sh
wget -qO- http://127.0.0.1:7880 > /dev/null 2>&1 || exit 1

if [ -n "$LIVEKIT_NODE_HOSTNAME" ] && [ -f /tmp/node_ip ]; then
  CURRENT_IP=$(getent hosts "$LIVEKIT_NODE_HOSTNAME" | awk '{print $1; exit}')
  STARTUP_IP=$(cat /tmp/node_ip)
  if [ -n "$CURRENT_IP" ] && [ "$CURRENT_IP" != "$STARTUP_IP" ]; then
    echo "IP changed: $STARTUP_IP -> $CURRENT_IP"
    exit 1
  fi
fi
Autoheal — a container like willfarrell/autoheal watches for unhealthy containers and restarts them. When the healthcheck fails due to IP change, autoheal restarts LiveKit, which re-runs the entrypoint and picks up the new IP.
labels:
  - autoheal=true
healthcheck:
  test: ["CMD-SHELL", "/bin/sh /etc/livekit/healthcheck.sh"]
  interval: 30s
  timeout: 5s
  retries: 3
This is a hack — there's a window between IP change and restart where voice is broken. A proper fix would be LiveKit supporting periodic IP re-resolution or a reload signal, but this works well enough for a home setup where IP changes are infrequent.

Port forwarding summary

With host networking, forward these on your router to the Fluxer host:
PortProtocolPurpose
3479UDPTURN/STUN
7881TCPICE TCP fallback
7882UDPPrimary media (RTP/RTCP)
Note: the upstream default uses 50000-50100/udp as the RTP range. With a single udp_port instead, LiveKit multiplexes all media over one port. This is fine for a small instance and avoids the port range mapping issue entirely.