Path A: Cloudflare Tunnel

Routes auth/chat/files/connect services over Cloudflare Tunnel so your host is never directly exposed on ports 80/443. LiveKit media still requires direct port forwarding for UDP voice traffic.

Zero Trust Ingress Lower Host Exposure Cloudflare-managed domain required

Requirements (Read First)

  • Ubuntu host with Docker Engine + Compose plugin.
  • Cloudflare-managed domain plus Zero Trust access (to create a tunnel token).
  • Router access for LiveKit media port forwarding (44381/tcp+udp, 44382/udp).
  • Public IPv4 (or ISP config) for reliable media. CGNAT can break voice.
docker --version
docker compose version
curl -4 ifconfig.me

How This Path Works

Cloudflare Tunnel proxies HTTP/WebSocket services (auth/chat/files/connect) from Cloudflare to your Docker network.

LiveKit media is different: raw UDP/TCP media must still be reachable directly on your host via router forwarding.

Step 0: Get Shared Files

Download the shared base compose file and LiveKit config:

mkdir letschat && cd letschat

wget https://raw.githubusercontent.com/da-stoaz/letschat/main/docker-compose.prod.base.yml

mkdir livekit
wget -O livekit/config.prod.yaml \
  https://raw.githubusercontent.com/da-stoaz/letschat/main/livekit/config.prod.yaml

All images are pre-built — no Rust or Node toolchains needed on the server.

Step 1: Download Path A Files

Inside the letschat directory:

wget https://raw.githubusercontent.com/da-stoaz/letschat/main/docker-compose.prod.tunnel.yml

wget -O .env \
  https://raw.githubusercontent.com/da-stoaz/letschat/main/.env.production.tunnel.example

Step 2: DNS + Tunnel Ingress

Create these hostnames in Cloudflare Zero Trust and configure tunnel ingress rules:

  • auth.example.comhttp://auth-service:44301
  • chat.example.comhttp://spacetimedb:3000 (enable WebSocket support)
  • files.example.comhttp://minio:44390
  • connect.example.comhttp://discovery:80
  • lk.example.com → A record pointing to your host public IP (media is not tunnelled)

Copy the tunnel token from the Zero Trust dashboard — you will need it in the next step.

Step 3: Configure Secrets and Endpoints

Open .env and replace every placeholder. Generate secrets with:

openssl rand -hex 32      # for AUTH_JWT_SECRET, MINIO_SECRET_KEY
openssl rand -base64 32   # for LIVEKIT_API_SECRET

Key fields to fill in:

AUTH_JWT_SECRET=          # generate with openssl
LIVEKIT_API_KEY=letschat-prod
LIVEKIT_API_SECRET=       # generate with openssl
MINIO_ACCESS_KEY=         # any username you choose
MINIO_SECRET_KEY=         # generate with openssl
MINIO_PUBLIC_ENDPOINT=https://files.example.com
DISCOVERY_SPACETIMEDB_URI=wss://chat.example.com
DISCOVERY_AUTH_URL=https://auth.example.com
DISCOVERY_LIVEKIT_URL=ws://lk.example.com:44380
CLOUDFLARE_TUNNEL_TOKEN=  # from Zero Trust dashboard

Step 4: Configure LiveKit

Open livekit/config.prod.yaml and replace the key/secret placeholder so it matches your .env:

keys:
  letschat-prod: <paste LIVEKIT_API_SECRET here>

The rest of the file (ports, use_external_ip: true) can stay as-is for most home server setups.

Step 5: Pull Images and Start

docker compose -f docker-compose.prod.base.yml -f docker-compose.prod.tunnel.yml pull
docker compose -f docker-compose.prod.base.yml -f docker-compose.prod.tunnel.yml up -d
docker compose -f docker-compose.prod.base.yml -f docker-compose.prod.tunnel.yml ps

module-init automatically publishes the SpacetimeDB module once the database is healthy. Watch its progress with:

docker logs letschat-module-init

Step 6: Validate Public Endpoints

curl -i https://auth.example.com/health
curl -i https://files.example.com/minio/health/live
curl -i https://connect.example.com/.well-known/letschat.json

Confirm router forwarding to host: 44381/tcp, 44381/udp, 44382/udp.

Step 7: Client Onboarding

Users enter https://connect.example.com in startup and LetsChat will auto-discover backend URLs.

Discovery JSON example:

{
  "spacetimedb": "wss://chat.example.com",
  "auth": "https://auth.example.com",
  "livekit": "ws://lk.example.com:44380",
  "database": "letschat"
}

Use wss://lk... only if you add TLS in front of LiveKit signaling.