Try a call

Run

docker compose up locally. Same images in production.

Rill is one process that copies audio and video between two browsers, plus optional recording in a second container. Local and production use the same images. Cargo and bun are for compiling this repo — not the product path.

Local

sh
docker compose up

Open http://127.0.0.1:3000. Create call, share the link, enable microphone and camera in two browsers.

That command rebuilds rill and rill-web from this tree (Docker cache if nothing changed), publishes 3000/tcp (site), 7880/tcp (SFU API + signaling) and 50000/udp (media), and runs rill --dev --bind 0.0.0.0:7880. ICE is advertised as 127.0.0.1 so host browsers on Docker Desktop Mac can send UDP. API key devkey, secret secret — refused outside --dev. The site at :3000 proxies /v1/signal to the SFU so the browser uses the same origin. REST (/v1/calls, tokens) stays on :7880.

The same file starts rill-record and Garage so you can test recording. Omit those services and calls still work. The public demo does not start recording.

Chrome and desktop Safari. iOS is not proven. CI e2e is Chromium only.

Production

Same images. Set domain, keys, public IP, UDP 50000, and HTTPS. Calls are in memory: a restart ends every call; your app creates calls again.

sh
docker compose run --rm --no-deps \
  -v "$PWD:/out" rill \
  init --domain rill.example.com --ip 203.0.113.10 --out /out

Writes compose.yaml, Caddyfile, .env, coturn config. Do not commit .env. Generated .env sets RILL_BIND=127.0.0.1:7880 so the SFU HTTP API listens on loopback; Caddy reverse-proxies only /v1, /health, and /ready — not /metrics. Scrape Prometheus from the VPS: http://127.0.0.1:7880/metrics. Point DNS at the VPS. Then:

sh
docker compose up -d

Images: ghcr.io/treyorr/rill and ghcr.io/treyorr/rill/rill-record (set RILL_IMAGE / RILL_RECORD_IMAGE after a v* tag). Host network on a Linux VPS is the honest UDP path. Publishing 7880/tcp and 50000/udp works if RILL_PUBLIC_IP is the machine’s public address, not a Docker bridge.

If signaling works and video does not, the advertised ICE address is wrong or UDP 50000 is blocked.

First-party site

Linux VPS, host network. This is https://rill.tlo3.com — site + SFU, no recording. rill init stays operator SFU (no try-it UI). Ampere (Oracle ARM) builds from this tree — first docker compose build is slow.

sh
cp env.site.example .env
# set RILL_API_KEY, RILL_API_SECRET, RILL_PUBLIC_IP (openssl rand -hex 16 / 32)
docker compose -f compose.site.yaml up -d --build

DNS A rill.tlo3.com → the VPS public IPv4, DNS-only (not Cloudflare-proxied: UDP 50000 cannot go through a proxy). Open 80/tcp, 443/tcp, 50000/udp on both the cloud security list and the host firewall. Optional push deploy: .github/workflows/site.yml when vars.SITE_DEPLOY=true (no SFU log dump).

Check: curl -sf https://rill.tlo3.com/ready and open https://rill.tlo3.com/. Logs: advertised_ice_ip= must be that public IP.

ICE, STUN, TURN

Rill is ICE-lite. The SFU advertises one host candidate: RILL_PUBLIC_IP on UDP 50000. Browsers send media there. --dev never STUNs and advertises 127.0.0.1.

In production, set RILL_PUBLIC_IP to the VPS public IPv4. If it is unset, Rill STUN-discovers once at boot (RILL_STUN_URLS, default Google). Discovery failure refuses start — set the IP instead of debugging STUN.

TURN is optional, for clients that cannot send UDP to you (strict firewalls, some mobile networks). It is a small coturn sidecar, not a Rill feature. Default rill init includes coturn: 3478/udp and 3478/tcp, plus relay 49152–49251/udp. Delete the coturn service if every client can reach UDP 50000. /ready stays green if coturn is down.

Do not put TURN/TLS on 443 (Caddy owns 443). UDP-blocked clients use turn:…:3478?transport=tcp.

Health

PathMeaning
GET /healthProcess is up
GET /readySockets are bound. During drain this becomes 503
sh
curl -sf https://rill.example.com/health
curl -sf https://rill.example.com/ready

/ on the SFU is 404. Humans use your app (or apps/web on :3000). /metrics is env-gated (RILL_METRICS=0 disables it).

Drain: /ready becomes 503, no new calls, live calls continue, then callers are told the server is shutting down after RILL_SHUTDOWN_TIMEOUT (default 30s). Compose stop_grace_period is 40s.

Env

VariableRole
RILL_API_KEYREST bearer
RILL_API_SECRETJoin tokens and join/leave webhooks
RILL_PUBLIC_URLPublic HTTPS origin
RILL_PUBLIC_IPICE address. Unset → STUN once at boot; failure refuses start
RILL_STUN_URLSBoot-time IP discovery only. Never in --dev
RILL_BINDHTTP listen (init writes 127.0.0.1:7880)
RILL_UDP_PORTMedia (default 50000)
RILL_CORS_ORIGINSYour widget origins
RILL_WEBHOOK_URLJoin/leave/call-ended POSTs
RILL_JOIN_TIMEOUTCreated, nobody ever joined. Default 5m (30m in --dev)
RILL_EMPTY_TIMEOUTHad people, now 0. Default 120s (30m in --dev)
RILL_CALL_LIFETIMEWall clock. Default 24h
RILL_MAX_CALLSIn-memory cap
RILL_SIGNAL_RECONNECT_GRACEHold media after a signaling drop (default 15s, max 120s)
RILL_TURN_URLS / RILL_TURN_SECRETOptional coturn
RILL_RECORD_SOCKETEnable recording fork (127.0.0.1:7420 or a unix path)
RILL_METRICS0 disables /metrics

Occupancy 1 does not time out. devkey / secret are refused here. Bucket keys stay on rill-record, not this process. Process-level video/screen/audio bitrate caps are operator env knobs (RILL_VIDEO_MAX_*, RILL_SCREEN_MAX_*, RILL_AUDIO_BITRATE) — not a per-call API.

Ports

PortWhy
443/tcpHTTPS + signaling (Caddy)
80/tcpLet’s Encrypt
50000/udpAudio and video

Type to search…

↑↓ navigate open esc close