From f33ac1cad82004fb6c4a5b21720eb6245aa347cd Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Mon, 30 Mar 2026 10:00:47 +0400 Subject: [PATCH] deploy: Docker Compose stack with Caddy + Cloudflare TLS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .dockerignore | 12 +++ warzone/deploy/docker/.env.example | 12 +++ warzone/deploy/docker/Caddyfile | 21 ++++ warzone/deploy/docker/Dockerfile.server | 30 ++++++ warzone/deploy/docker/Dockerfile.wzp | 22 +++++ warzone/deploy/docker/docker-compose.ipv6.yml | 24 +++++ warzone/deploy/docker/docker-compose.yml | 99 +++++++++++++++++++ warzone/deploy/docker/test-stack.sh | 58 +++++++++++ 8 files changed, 278 insertions(+) create mode 100644 .dockerignore create mode 100644 warzone/deploy/docker/.env.example create mode 100644 warzone/deploy/docker/Caddyfile create mode 100644 warzone/deploy/docker/Dockerfile.server create mode 100644 warzone/deploy/docker/Dockerfile.wzp create mode 100644 warzone/deploy/docker/docker-compose.ipv6.yml create mode 100644 warzone/deploy/docker/docker-compose.yml create mode 100755 warzone/deploy/docker/test-stack.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d0df6b0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +**/target +**/node_modules +**/.git +**/.DS_Store +**/.claude +**/wasm-pkg +apache +nginx +nginx.txt +chat.py +tunnel.py +DESIGN.md diff --git a/warzone/deploy/docker/.env.example b/warzone/deploy/docker/.env.example new file mode 100644 index 0000000..d4d1430 --- /dev/null +++ b/warzone/deploy/docker/.env.example @@ -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) diff --git a/warzone/deploy/docker/Caddyfile b/warzone/deploy/docker/Caddyfile new file mode 100644 index 0000000..286f592 --- /dev/null +++ b/warzone/deploy/docker/Caddyfile @@ -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 +} diff --git a/warzone/deploy/docker/Dockerfile.server b/warzone/deploy/docker/Dockerfile.server new file mode 100644 index 0000000..12ed12f --- /dev/null +++ b/warzone/deploy/docker/Dockerfile.server @@ -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"] diff --git a/warzone/deploy/docker/Dockerfile.wzp b/warzone/deploy/docker/Dockerfile.wzp new file mode 100644 index 0000000..733fa95 --- /dev/null +++ b/warzone/deploy/docker/Dockerfile.wzp @@ -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 diff --git a/warzone/deploy/docker/docker-compose.ipv6.yml b/warzone/deploy/docker/docker-compose.ipv6.yml new file mode 100644 index 0000000..2c48c94 --- /dev/null +++ b/warzone/deploy/docker/docker-compose.ipv6.yml @@ -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 diff --git a/warzone/deploy/docker/docker-compose.yml b/warzone/deploy/docker/docker-compose.yml new file mode 100644 index 0000000..b3be844 --- /dev/null +++ b/warzone/deploy/docker/docker-compose.yml @@ -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: diff --git a/warzone/deploy/docker/test-stack.sh b/warzone/deploy/docker/test-stack.sh new file mode 100755 index 0000000..eec4554 --- /dev/null +++ b/warzone/deploy/docker/test-stack.sh @@ -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 ==="