feat: deploy-chat.sh — full Hetzner deploy for chat.manko.yoga

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-30 11:51:49 +04:00
parent 02471b28ba
commit 9af5ec96b5

350
warzone/scripts/deploy-chat.sh Executable file
View File

@@ -0,0 +1,350 @@
#!/bin/bash
set -euo pipefail
# Deploy featherChat + WZP to chat.manko.yoga on Hetzner cx23.
# Clones from git, builds with Docker, sets up Caddy + TLS.
#
# Usage:
# ./scripts/deploy-chat.sh --create Create VPS + install Docker
# ./scripts/deploy-chat.sh --dns Update CF DNS (A + AAAA)
# ./scripts/deploy-chat.sh --deploy Clone repos + docker compose up
# ./scripts/deploy-chat.sh --redeploy Git pull + rebuild
# ./scripts/deploy-chat.sh --test Smoke test
# ./scripts/deploy-chat.sh --ssh SSH into VPS
# ./scripts/deploy-chat.sh --logs Tail logs
# ./scripts/deploy-chat.sh --destroy Delete VPS + DNS
# ./scripts/deploy-chat.sh --all create + dns + deploy + test
VM_NAME="fc-chat"
SSH_KEY_NAME="wz"
SSH_KEY_PATH="/Users/manwe/CascadeProjects/wzp"
SERVER_TYPE="cx23"
IMAGE="debian-12"
LOCATION="fsn1"
REMOTE_USER="root"
DOMAIN="chat.manko.yoga"
CF_ZONE="manko.yoga"
# Git repos (public)
GIT_FC="ssh://git@git.manko.yoga:222/manawenuz/featherChat.git"
GIT_WZP="ssh://git@git.manko.yoga:222/manawenuz/wz-phone.git"
GIT_BRANCH="feature/call-ring-group"
DEPLOY_DIR="/root/featherChat"
DOCKER_DIR="$DEPLOY_DIR/warzone/deploy/docker"
# Local paths
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
CF_TOKEN_FILE="$PROJECT_DIR/deploy/docker/cf_api_token.txt"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -q"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
get_cf_token() {
if [ -f "$CF_TOKEN_FILE" ]; then
cat "$CF_TOKEN_FILE" | tr -d '\n'
elif [ -n "${CF_API_TOKEN:-}" ]; then
echo "$CF_API_TOKEN"
else
echo "ERROR: No CF token. Create deploy/docker/cf_api_token.txt" >&2; exit 1
fi
}
get_vm_ip() {
hcloud server list -o columns=name,ipv4 -o noheader 2>/dev/null | grep "$VM_NAME" | awk '{print $2}'
}
get_vm_ipv6() {
hcloud server list -o columns=name,ipv6 -o noheader 2>/dev/null | grep "$VM_NAME" | awk '{print $2}' | sed 's|/64||'
}
ssh_cmd() {
local ip; ip=$(get_vm_ip)
[ -z "$ip" ] && { echo "ERROR: No VM '$VM_NAME'. Run --create first." >&2; exit 1; }
ssh $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip" "$@"
}
# ---------------------------------------------------------------------------
# --create
# ---------------------------------------------------------------------------
do_create() {
local existing
existing=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$VM_NAME" || true)
if [ -n "$existing" ]; then
echo "VM already exists: $VM_NAME ($(get_vm_ip))"
return
fi
echo "[1/3] Creating Hetzner VPS: $VM_NAME ($SERVER_TYPE, $LOCATION)..."
hcloud server create \
--name "$VM_NAME" \
--type "$SERVER_TYPE" \
--image "$IMAGE" \
--ssh-key "$SSH_KEY_NAME" \
--location "$LOCATION" \
--quiet
local ipv4 ipv6
ipv4=$(get_vm_ip)
ipv6=$(get_vm_ipv6)
echo " IPv4: $ipv4"
echo " IPv6: $ipv6"
echo "[2/3] Waiting for SSH..."
for i in $(seq 1 30); do
ssh $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ipv4" "echo ok" &>/dev/null && break
sleep 2
done
echo "[3/3] Installing Docker..."
ssh_cmd 'export DEBIAN_FRONTEND=noninteractive && \
apt-get update -qq > /dev/null && \
apt-get install -y -qq ca-certificates curl gnupg git > /dev/null && \
install -m 0755 -d /etc/apt/keyrings && \
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \
chmod a+r /etc/apt/keyrings/docker.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list && \
apt-get update -qq > /dev/null && \
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin > /dev/null && \
mkdir -p /etc/docker && echo "{\"ipv6\": true, \"fixed-cidr-v6\": \"fd00::/80\"}" > /etc/docker/daemon.json && \
systemctl restart docker'
echo ""
echo "=== VPS Ready ==="
echo "IPv4: $ipv4"
echo "IPv6: $ipv6"
echo "SSH: ssh -i $SSH_KEY_PATH root@$ipv4"
}
# ---------------------------------------------------------------------------
# --dns
# ---------------------------------------------------------------------------
do_dns() {
local ipv4 ipv6 cf_token zone_id
ipv4=$(get_vm_ip)
ipv6=$(get_vm_ipv6)
cf_token=$(get_cf_token)
[ -z "$ipv4" ] && { echo "ERROR: No VM." >&2; exit 1; }
echo "Updating DNS: $DOMAIN"
echo " A → $ipv4"
echo " AAAA → ${ipv6}1"
zone_id=$(curl -4 -s "https://api.cloudflare.com/client/v4/zones?name=$CF_ZONE" \
-H "Authorization: Bearer $cf_token" | python3 -c "import sys,json; print(json.load(sys.stdin)['result'][0]['id'])")
for type_content in "A:$ipv4" "AAAA:${ipv6}1"; do
local type="${type_content%%:*}" content="${type_content#*:}"
local rec_id
rec_id=$(curl -4 -s "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records?name=$DOMAIN&type=$type" \
-H "Authorization: Bearer $cf_token" | python3 -c "import sys,json; r=json.load(sys.stdin)['result']; print(r[0]['id'] if r else '')" 2>/dev/null || echo "")
if [ -n "$rec_id" ]; then
curl -4 -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$rec_id" \
-H "Authorization: Bearer $cf_token" -H "Content-Type: application/json" \
--data "{\"type\":\"$type\",\"name\":\"$DOMAIN\",\"content\":\"$content\",\"ttl\":120,\"proxied\":false}" > /dev/null
echo " $type updated"
else
curl -4 -s -X POST "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records" \
-H "Authorization: Bearer $cf_token" -H "Content-Type: application/json" \
--data "{\"type\":\"$type\",\"name\":\"$DOMAIN\",\"content\":\"$content\",\"ttl\":120,\"proxied\":false}" > /dev/null
echo " $type created"
fi
done
echo " Verify: dig $DOMAIN A +short && dig $DOMAIN AAAA +short"
}
# ---------------------------------------------------------------------------
# --deploy
# ---------------------------------------------------------------------------
do_deploy() {
local ip cf_token
ip=$(get_vm_ip)
cf_token=$(get_cf_token)
[ -z "$ip" ] && { echo "ERROR: No VM." >&2; exit 1; }
echo "[1/5] Uploading SSH key for git access..."
scp $SSH_OPTS -i "$SSH_KEY_PATH" "$SSH_KEY_PATH" "$REMOTE_USER@$ip:/root/.ssh/id_ed25519"
scp $SSH_OPTS -i "$SSH_KEY_PATH" "${SSH_KEY_PATH}.pub" "$REMOTE_USER@$ip:/root/.ssh/id_ed25519.pub"
ssh_cmd "chmod 600 /root/.ssh/id_ed25519"
echo "[2/5] Cloning repos on VPS..."
ssh_cmd "rm -rf $DEPLOY_DIR && \
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -p 222' git clone --depth 1 -b $GIT_BRANCH $GIT_FC $DEPLOY_DIR && \
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -p 222' git clone --depth 1 $GIT_WZP $DEPLOY_DIR/warzone-phone"
echo "[3/5] Updating Caddyfile domain..."
ssh_cmd "sed -i 's/voip.manko.yoga/$DOMAIN/g' $DOCKER_DIR/Caddyfile"
echo "[4/5] Setting up CF token..."
ssh_cmd "echo '$cf_token' > $DOCKER_DIR/cf_api_token.txt && chmod 600 $DOCKER_DIR/cf_api_token.txt"
echo "[5/5] Building + starting stack (takes a few minutes on first run)..."
ssh_cmd "cd $DOCKER_DIR && \
sed -i 's|voip.manko.yoga/audio|$DOMAIN/audio|g' docker-compose.yml && \
docker compose up -d --build 2>&1" | tail -30
echo ""
echo "=== Deployed ==="
echo "URL: https://$DOMAIN"
echo "Logs: $0 --logs"
echo "Test: $0 --test"
}
# ---------------------------------------------------------------------------
# --redeploy
# ---------------------------------------------------------------------------
do_redeploy() {
local ip; ip=$(get_vm_ip)
[ -z "$ip" ] && { echo "ERROR: No VM." >&2; exit 1; }
echo "[1/2] Pulling latest..."
ssh_cmd "cd $DEPLOY_DIR && GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -p 222' git pull && \
cd $DEPLOY_DIR/warzone-phone && GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -p 222' git pull"
echo "[2/2] Rebuilding..."
ssh_cmd "cd $DOCKER_DIR && \
sed -i 's/voip.manko.yoga/$DOMAIN/g' Caddyfile && \
sed -i 's|voip.manko.yoga/audio|$DOMAIN/audio|g' docker-compose.yml && \
docker compose up -d --build 2>&1" | tail -20
echo "=== Redeployed ==="
}
# ---------------------------------------------------------------------------
# --test
# ---------------------------------------------------------------------------
do_test() {
echo "=== Smoke Test: $DOMAIN ==="
local pass=0 fail=0
check() {
local name="$1" url="$2" expect="$3"
local status
status=$(curl -4 -s -o /dev/null -w "%{http_code}" --connect-timeout 10 "$url" 2>/dev/null || echo "000")
if [ "$status" = "$expect" ]; then
echo " OK $name ($status)"
pass=$((pass + 1))
else
echo " FAIL $name (got $status, expected $expect)"
fail=$((fail + 1))
fi
}
check "Web UI" "https://$DOMAIN/" "200"
check "Health" "https://$DOMAIN/v1/health" "200"
check "WASM" "https://$DOMAIN/wasm/warzone_wasm.js" "200"
check "Relay config" "https://$DOMAIN/v1/wzp/relay-config" "200"
check "Bot list" "https://$DOMAIN/v1/bot/list" "200"
check "Whoami" "https://$DOMAIN/v1/whoami" "200"
# TLS
local issuer
issuer=$(echo | openssl s_client -connect "$DOMAIN:443" -servername "$DOMAIN" 2>/dev/null | openssl x509 -noout -issuer 2>/dev/null || echo "?")
echo " TLS: $issuer"
# IPv4
local v4; v4=$(dig +short "$DOMAIN" A 2>/dev/null || echo "?")
echo " A: $v4"
# IPv6
local v6; v6=$(dig +short "$DOMAIN" AAAA 2>/dev/null || echo "?")
echo " AAAA: $v6"
# IPv6 connectivity
local v6_status
v6_status=$(curl -6 -s -o /dev/null -w "%{http_code}" --connect-timeout 5 "https://$DOMAIN/" 2>/dev/null || echo "000")
[ "$v6_status" = "200" ] && echo " IPv6: reachable ($v6_status)" && pass=$((pass + 1)) || echo " IPv6: not reachable ($v6_status)"
# Whoami content
local whoami
whoami=$(curl -4 -s "https://$DOMAIN/v1/whoami" 2>/dev/null)
echo " Whoami: $whoami"
echo ""
echo "Results: $pass passed, $fail failed"
}
# ---------------------------------------------------------------------------
# Utility
# ---------------------------------------------------------------------------
do_ssh() {
local ip; ip=$(get_vm_ip)
[ -z "$ip" ] && { echo "No VM." >&2; exit 1; }
exec ssh $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip"
}
do_logs() {
ssh_cmd "cd $DOCKER_DIR && docker compose logs -f --tail=50"
}
do_destroy() {
local existing
existing=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$VM_NAME" || true)
if [ -z "$existing" ]; then
echo "No VM '$VM_NAME'."
return
fi
echo "Destroying: $VM_NAME"
hcloud server delete "$VM_NAME"
echo "VM deleted."
read -p "Remove DNS records for $DOMAIN? [y/N] " -n 1 -r; echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
local cf_token zone_id
cf_token=$(get_cf_token)
zone_id=$(curl -4 -s "https://api.cloudflare.com/client/v4/zones?name=$CF_ZONE" \
-H "Authorization: Bearer $cf_token" | python3 -c "import sys,json; print(json.load(sys.stdin)['result'][0]['id'])")
for type in A AAAA; do
local rec_id
rec_id=$(curl -4 -s "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records?name=$DOMAIN&type=$type" \
-H "Authorization: Bearer $cf_token" | python3 -c "import sys,json; r=json.load(sys.stdin)['result']; print(r[0]['id'] if r else '')" 2>/dev/null || echo "")
[ -n "$rec_id" ] && curl -4 -s -X DELETE "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$rec_id" \
-H "Authorization: Bearer $cf_token" > /dev/null && echo " Deleted $type"
done
fi
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
case "${1:-}" in
--create) do_create ;;
--dns) do_dns ;;
--deploy) do_deploy ;;
--redeploy) do_redeploy ;;
--test) do_test ;;
--ssh) do_ssh ;;
--logs) do_logs ;;
--destroy) do_destroy ;;
--all) do_create; do_dns; do_deploy; echo ""; echo "Waiting 30s for TLS cert..."; sleep 30; do_test ;;
*)
echo "Deploy featherChat to chat.manko.yoga (Hetzner cx23)"
echo ""
echo "Usage: $0 <command>"
echo ""
echo " --create Create VPS + install Docker"
echo " --dns Update Cloudflare A + AAAA records"
echo " --deploy Clone repos + docker compose up"
echo " --redeploy Git pull + rebuild"
echo " --test Smoke test (6 checks + TLS + IPv6)"
echo " --ssh SSH into VPS"
echo " --logs Tail docker compose logs"
echo " --destroy Delete VPS + DNS"
echo " --all Full deploy (create + dns + deploy + test)"
;;
esac