Self-Hosting / Path B: Caddy Path B: Caddy Reverse Proxy
Caddy runs on your host, terminates HTTPS automatically, and routes by hostname to internal Docker services.
Simpler moving parts than a tunnel — but your host directly exposes ports 80 and 443.
Direct HTTPS Classic Reverse Proxy Auto TLS via ACME
Requirements (Read First) Ubuntu host with Docker Engine + Compose plugin. Cloudflare DNS records set to your host public IP (DNS-only mode for ACME issuance). Router/firewall allows 80/tcp and 443/tcp to host (for Caddy TLS/HTTP). LiveKit media forwarding still required: 44381/tcp+udp, 44382/udp. How This Path Works Caddy runs on your host, terminates HTTPS, and routes by hostname to internal Docker services.
This removes Cloudflare Tunnel complexity, but your host directly exposes 80/443 to the internet.
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 B Files Inside the letschat directory:
wget https://raw.githubusercontent.com/da-stoaz/letschat/main/docker-compose.prod.caddy.yml
wget -O .env \
https://raw.githubusercontent.com/da-stoaz/letschat/main/.env.production.caddy.example
mkdir -p deploy/caddy
wget -O deploy/caddy/Caddyfile \
https://raw.githubusercontent.com/da-stoaz/letschat/main/deploy/caddy/Caddyfile Step 2: DNS + Network Setup Point these A records to your host public IP (DNS-only, no Cloudflare proxy):
auth.example.com chat.example.com files.example.com lk.example.com connect.example.com Open on router/firewall: 80/tcp, 443/tcp (Caddy TLS), 44381/tcp+udp, 44382/udp (LiveKit media).
Step 3: Configure Secrets and Domains 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=wss://lk.example.com
AUTH_DOMAIN=auth.example.com
CHAT_DOMAIN=chat.example.com
FILES_DOMAIN=files.example.com
LIVEKIT_DOMAIN=lk.example.com
CONNECT_DOMAIN=connect.example.com 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.caddy.yml pull
docker compose -f docker-compose.prod.base.yml -f docker-compose.prod.caddy.yml up -d
docker compose -f docker-compose.prod.base.yml -f docker-compose.prod.caddy.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 Step 7: Client Onboarding Users enter https://connect.example.com in startup. Discovery JSON should point to your public endpoints:
{
"spacetimedb": "wss://chat.example.com",
"auth": "https://auth.example.com",
"livekit": "wss://lk.example.com",
"database": "letschat"
}