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:
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
**/target
|
||||||
|
**/node_modules
|
||||||
|
**/.git
|
||||||
|
**/.DS_Store
|
||||||
|
**/.claude
|
||||||
|
**/wasm-pkg
|
||||||
|
apache
|
||||||
|
nginx
|
||||||
|
nginx.txt
|
||||||
|
chat.py
|
||||||
|
tunnel.py
|
||||||
|
DESIGN.md
|
||||||
12
warzone/deploy/docker/.env.example
Normal file
12
warzone/deploy/docker/.env.example
Normal 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)
|
||||||
21
warzone/deploy/docker/Caddyfile
Normal file
21
warzone/deploy/docker/Caddyfile
Normal 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
|
||||||
|
}
|
||||||
30
warzone/deploy/docker/Dockerfile.server
Normal file
30
warzone/deploy/docker/Dockerfile.server
Normal 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"]
|
||||||
22
warzone/deploy/docker/Dockerfile.wzp
Normal file
22
warzone/deploy/docker/Dockerfile.wzp
Normal 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
|
||||||
24
warzone/deploy/docker/docker-compose.ipv6.yml
Normal file
24
warzone/deploy/docker/docker-compose.ipv6.yml
Normal 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
|
||||||
99
warzone/deploy/docker/docker-compose.yml
Normal file
99
warzone/deploy/docker/docker-compose.yml
Normal 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:
|
||||||
58
warzone/deploy/docker/test-stack.sh
Executable file
58
warzone/deploy/docker/test-stack.sh
Executable 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 ==="
|
||||||
Reference in New Issue
Block a user