deploy: Docker Compose stack with Caddy + Cloudflare TLS

Full production stack via docker compose:
- Caddy reverse proxy with Cloudflare DNS-01 TLS certs
- warzone-server (featherChat API + web UI)
- wzp-relay (QUIC audio SFU)
- wzp-web (browser WS ↔ QUIC bridge)

Architecture:
  Internet → Caddy (443/TLS) → voip.manko.yoga
    /*       → warzone-server:7700
    /audio/* → wzp-web:8080

Files:
- docker-compose.yml: main stack (4 services)
- docker-compose.ipv6.yml: IPv6 overlay
- Caddyfile: Cloudflare DNS challenge + reverse proxy
- Dockerfile.server: featherChat multi-stage build
- Dockerfile.wzp: wzp-relay + wzp-web multi-stage build
- .env.example: DNS records for dev/staging/prod
- test-stack.sh: smoke test (8 checks)
- .dockerignore: excludes target/, .git/, etc.

Deployment targets:
  dev:  172.16.81.135
  ipv6: 2a0d:3344:692c:2500:14f2:5885:d73c:b0a1
  prod: 63.250.54.239 / 2602:ff16:9:0:1:3d9:0:1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-30 10:00:47 +04:00
parent c2be68ca20
commit f33ac1cad8
8 changed files with 278 additions and 0 deletions

12
.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
**/target
**/node_modules
**/.git
**/.DS_Store
**/.claude
**/wasm-pkg
apache
nginx
nginx.txt
chat.py
tunnel.py
DESIGN.md

View File

@@ -0,0 +1,12 @@
# Copy to .env and fill in values
# Cloudflare API token (Zone:DNS:Edit permission for manko.yoga)
# Also create cf_api_token.txt with the same token for Docker secrets
# echo "YOUR_TOKEN" > cf_api_token.txt
CF_API_TOKEN=
# DNS records to create:
# voip.manko.yoga → A 172.16.81.135 (dev)
# voip.manko.yoga → AAAA 2a0d:3344:692c:2500:14f2:5885:d73c:b0a1 (ipv6 test)
# voip.manko.yoga → A 63.250.54.239 (production)
# voip.manko.yoga → AAAA 2602:ff16:9:0:1:3d9:0:1 (production ipv6)

View File

@@ -0,0 +1,21 @@
{
# Global ACME settings
email admin@manko.yoga
}
voip.manko.yoga {
# TLS via Cloudflare DNS-01 challenge
tls {
dns cloudflare {$CF_API_TOKEN}
}
# Audio bridge WebSocket (wzp-web)
# /audio/ws/* → wzp-web:8080/ws/*
handle_path /audio/* {
reverse_proxy wzp-web:8080
}
# Everything else → featherChat server
# Web UI (/), API (/v1/*), WebSocket (/v1/ws/*)
reverse_proxy warzone-server:7700
}

View File

@@ -0,0 +1,30 @@
# featherChat server — multi-stage build
# Build context: featherChat repo root (../../..)
FROM rust:1.83-bookworm AS builder
WORKDIR /build
# Copy warzone workspace
COPY warzone/Cargo.toml warzone/Cargo.lock ./warzone/
COPY warzone/crates ./warzone/crates
# Build server
WORKDIR /build/warzone
RUN cargo build --release --bin warzone-server
# Build WASM (for embedded web client)
RUN cargo install wasm-pack && \
wasm-pack build crates/warzone-wasm --target web --out-dir /build/wasm-pkg 2>&1 || true
# Runtime
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /build/warzone/target/release/warzone-server /usr/local/bin/
COPY --from=builder /build/wasm-pkg /srv/wasm-pkg
WORKDIR /data
EXPOSE 7700
ENTRYPOINT ["warzone-server"]
CMD ["--bind", "0.0.0.0:7700"]

View File

@@ -0,0 +1,22 @@
# WZP relay + web bridge — multi-stage build
# Build context: featherChat repo root (../../..)
FROM rust:1.83-bookworm AS builder
WORKDIR /build
# Copy warzone-phone workspace
COPY warzone-phone/Cargo.toml warzone-phone/Cargo.lock ./warzone-phone/
COPY warzone-phone/crates ./warzone-phone/crates
# Build both binaries
WORKDIR /build/warzone-phone
RUN cargo build --release --bin wzp-relay --bin wzp-web
# Runtime
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /build/warzone-phone/target/release/wzp-relay /usr/local/bin/
COPY --from=builder /build/warzone-phone/target/release/wzp-web /usr/local/bin/
WORKDIR /data

View File

@@ -0,0 +1,24 @@
# IPv6 overlay — use with:
# docker compose -f docker-compose.yml -f docker-compose.ipv6.yml up -d
#
# Requires Docker daemon IPv6 support:
# /etc/docker/daemon.json: {"ipv6": true, "fixed-cidr-v6": "fd00::/80"}
services:
caddy:
ports:
- "[::]:80:80"
- "[::]:443:443"
- "[::]:443:443/udp"
networks:
frontend:
enable_ipv6: true
ipam:
config:
- subnet: fd00:cafe:1::/64
backend:
enable_ipv6: true
ipam:
config:
- subnet: fd00:cafe:2::/64

View File

@@ -0,0 +1,99 @@
# featherChat + WZP full stack
# Usage:
# echo "YOUR_CF_API_TOKEN" > cf_api_token.txt
# docker compose up -d
#
# DNS: voip.manko.yoga → your IP
# Test: https://voip.manko.yoga
services:
# ─── Caddy reverse proxy (TLS termination) ───
caddy:
image: ghcr.io/caddy-dns/cloudflare:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3 (QUIC)
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
environment:
CF_API_TOKEN: /run/secrets/cf_api_token
secrets:
- cf_api_token
# Caddy reads CF_API_TOKEN; the caddy-cloudflare image supports file-based secrets
# via the {$CF_API_TOKEN} placeholder in Caddyfile.
# We mount the secret and set env to its content at runtime.
entrypoint: ["/bin/sh", "-c", "export CF_API_TOKEN=$(cat /run/secrets/cf_api_token) && caddy run --config /etc/caddy/Caddyfile --adapter caddyfile"]
depends_on:
- warzone-server
- wzp-web
networks:
- frontend
- backend
# ─── featherChat server ───
warzone-server:
build:
context: ../../..
dockerfile: warzone/deploy/docker/Dockerfile.server
restart: unless-stopped
environment:
# Browser connects to audio via Caddy: wss://voip.manko.yoga/audio/ws/ROOM
WZP_RELAY_ADDR: "voip.manko.yoga/audio"
RUST_LOG: "info"
volumes:
- server_data:/data
command: ["--bind", "0.0.0.0:7700", "--enable-bots"]
networks:
- backend
# ─── WZP QUIC relay (audio SFU) ───
wzp-relay:
build:
context: ../../..
dockerfile: warzone/deploy/docker/Dockerfile.wzp
restart: unless-stopped
entrypoint: ["wzp-relay"]
command:
- "--listen"
- "0.0.0.0:4433"
- "--auth-url"
- "http://warzone-server:7700/v1/auth/validate"
networks:
- backend
# ─── WZP web bridge (browser WS ↔ QUIC relay) ───
wzp-web:
build:
context: ../../..
dockerfile: warzone/deploy/docker/Dockerfile.wzp
restart: unless-stopped
entrypoint: ["wzp-web"]
command:
- "--port"
- "8080"
- "--relay"
- "wzp-relay:4433"
- "--auth-url"
- "http://warzone-server:7700/v1/auth/validate"
depends_on:
- wzp-relay
- warzone-server
networks:
- backend
secrets:
cf_api_token:
file: ./cf_api_token.txt
volumes:
caddy_data:
caddy_config:
server_data:
networks:
frontend:
backend:

View File

@@ -0,0 +1,58 @@
#!/bin/bash
set -e
HOST="${1:-voip.manko.yoga}"
SCHEME="${2:-https}"
echo "=== featherChat Stack Test ==="
echo "Host: $HOST ($SCHEME)"
echo ""
# 1. Web UI
echo -n "1. Web UI (GET /)... "
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$SCHEME://$HOST/")
[ "$STATUS" = "200" ] && echo "OK ($STATUS)" || echo "FAIL ($STATUS)"
# 2. API health
echo -n "2. API health... "
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$SCHEME://$HOST/v1/health")
[ "$STATUS" = "200" ] && echo "OK ($STATUS)" || echo "FAIL ($STATUS)"
# 3. WASM module
echo -n "3. WASM module... "
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$SCHEME://$HOST/wasm/warzone_wasm.js")
[ "$STATUS" = "200" ] && echo "OK ($STATUS)" || echo "FAIL ($STATUS)"
# 4. WZP relay config
echo -n "4. WZP relay config... "
RELAY=$(curl -s "$SCHEME://$HOST/v1/wzp/relay-config")
echo "$RELAY" | grep -q "relay_addr" && echo "OK ($(echo $RELAY | python3 -c 'import sys,json; print(json.load(sys.stdin).get("relay_addr","?"))' 2>/dev/null))" || echo "FAIL"
# 5. Audio bridge (wzp-web via Caddy /audio path)
echo -n "5. Audio bridge (GET /audio/)... "
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$SCHEME://$HOST/audio/")
# wzp-web returns 200 for its landing page
[ "$STATUS" = "200" ] && echo "OK ($STATUS)" || echo "WARN ($STATUS — wzp-web may not serve GET /)"
# 6. WebSocket upgrade test
echo -n "6. WS upgrade test... "
WS_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Upgrade: websocket" -H "Connection: Upgrade" "$SCHEME://$HOST/v1/ws/test")
echo "($WS_STATUS)"
# 7. TLS cert check
if [ "$SCHEME" = "https" ]; then
echo -n "7. TLS cert... "
ISSUER=$(echo | openssl s_client -connect "$HOST:443" -servername "$HOST" 2>/dev/null | openssl x509 -noout -issuer 2>/dev/null)
echo "$ISSUER" | grep -q "Let's Encrypt\|Cloudflare\|R3\|E1" && echo "OK ($ISSUER)" || echo "$ISSUER"
fi
# 8. IPv6 test
echo -n "8. IPv6... "
if curl -6 -s -o /dev/null -w "%{http_code}" --connect-timeout 3 "$SCHEME://$HOST/" 2>/dev/null; then
echo " (IPv6 reachable)"
else
echo "not available (IPv4 only)"
fi
echo ""
echo "=== Done ==="