Setup

Network configuration

Every port Tyto uses, opening them, NAT and port forwarding, common failures and the security picture.

The ports

Port Protocol Used for Needed when
80 TCP HTTP + Let’s Encrypt (ACME) challenge always
443 TCP HTTPS — API, app, realtime, voice signal always
443 UDP HTTP/3 optional
7881 TCP Voice media, TCP fallback voice
50000–50100 UDP Voice media (WebRTC/RTP) voice

A separate client stack needs only 80/443 on its host. In reverse-proxy mode the proxy owns 80/443 and forwards to HTTP_PORT — but the voice media ports still go directly to the core host. No voice ports needed with VOICE_ENABLED=false.

Everything else — database, cache, search, the LiveKit signal port — stays on the internal Docker network and is never published to the host.

Opening the firewall

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw allow 7881/tcp
sudo ufw allow 50000:50100/udp

On cloud hosts the same list goes into the provider’s security group / network firewall — both layers must agree.

NAT & port forwarding

Hosting behind a home/office router: forward the same ports to the server’s LAN address. Voice needs the whole UDP range forwarded — the server discovers its own public IP (STUN) and advertises it to browsers, which then send media straight at public-ip:50000–50100.

Warning

CGNAT breaks self-hosting. If your ISP puts you behind carrier-grade NAT (public IP starts with 100.64–127.x, or your router’s WAN IP differs from what ifconfig.me shows), inbound connections can’t reach you at all — no forwarding will help. Options: ask the ISP for a real IP, or rent a small VPS and tunnel.

Other NAT pitfalls:

  • Double NAT (ISP box + your own router) — forward on both, or bridge the ISP box.
  • Hairpin NAT — clients on the same LAN reaching the public IP only works if the router supports NAT loopback; otherwise use split-horizon DNS pointing the domain at the LAN address internally.

When something doesn’t work

Symptom Usual cause
Certificate issuance fails 80/tcp unreachable from the internet (ACME needs it)
Site loads, voice connects then silence UDP range not open/forwarded — media can’t flow
Voice works on wifi, not on mobile client network blocks UDP; 7881/tcp fallback also blocked
Works remotely, not from home LAN hairpin NAT (above)
Nothing reachable despite forwarding CGNAT (above)

Top privacy: VPN-only

For a team or family server, the strongest posture is exposing nothing to the internet: put the server and every client device on a VPN (WireGuard, Tailscale) and skip all the forwarding above — no public ports, no ACME, no scanning traffic, CGNAT irrelevant.

One catch: browsers require a secure context (HTTPS) for microphone access and Web Push — plain HTTP inside the VPN silently breaks voice and push notifications. Ways to keep TLS without going public:

  • Tailscaletailscale serve gives the host a trusted *.ts.net certificate and proxies to the stack: set BEHIND_PROXY=true + HTTP_PORT, point serve at it. Easiest path.
  • Own CA — issue an internal certificate (e.g. mkcert, step-ca) and distribute trust to member devices; terminate TLS in a local reverse proxy, same BEHIND_PROXY setup.
  • Public name, private address — keep a real domain but resolve it to the VPN address; certificates then need a DNS-01-capable proxy in front (Caddy with a DNS plugin, Traefik), since HTTP-01 can’t reach you.

Voice media flows over the VPN like everything else — the UDP range stays closed to the world.

Security picture

  • The exposed surface is deliberately small: one web endpoint (TLS by Caddy) plus the voice media ports. Media is DTLS-SRTP encrypted and useless without a session negotiated over the authenticated signal channel.
  • Internal services (MariaDB, Valkey, Meilisearch, LiveKit’s signal port) are not reachable from outside — don’t publish them “for debugging” on a public host.
  • Brute-force and flood protection is app-level — rate limits — no fail2ban required.
  • Keep the Docker API itself unexposed; it’s root on the host.