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"
}