280 lines
10 KiB
Bash
Executable File
280 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ============================================================
|
||
# EchoChat 公网部署脚本(Phase 2e-2 Task 14)
|
||
# ------------------------------------------------------------
|
||
# 职责:
|
||
# 1. 校验 deploy/.env 内关键参数(ANNOUNCED_IP / 密码替换等)
|
||
# 2. 给出防火墙/安全组端口放通 checklist(本地用 nc 探测)
|
||
# 3. 启动 docker compose --profile public(含 media-server + coturn)
|
||
#
|
||
# 不做的事:
|
||
# - 不替你改 iptables / ufw(不同云/OS 命令不一,避免误操作)
|
||
# - 不替你改 hosts / DNS(域名解析请预先完成)
|
||
#
|
||
# 用法:
|
||
# cp deploy/.env.public.example deploy/.env
|
||
# vim deploy/.env # 按 checklist 替换所有 _REPLACE_WITH_*_
|
||
# ./scripts/deploy-public.sh
|
||
# ============================================================
|
||
set -uo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
DEPLOY_DIR="$ROOT_DIR/deploy"
|
||
ENV_FILE="$DEPLOY_DIR/.env"
|
||
|
||
COLOR_GREEN='\033[0;32m'
|
||
COLOR_YELLOW='\033[0;33m'
|
||
COLOR_RED='\033[0;31m'
|
||
COLOR_CYAN='\033[0;36m'
|
||
COLOR_RESET='\033[0m'
|
||
|
||
log_info() { printf "${COLOR_CYAN}[INFO]${COLOR_RESET} %s\n" "$*"; }
|
||
log_ok() { printf "${COLOR_GREEN}[OK]${COLOR_RESET} %s\n" "$*"; }
|
||
log_warn() { printf "${COLOR_YELLOW}[WARN]${COLOR_RESET} %s\n" "$*"; }
|
||
log_err() { printf "${COLOR_RED}[ERR]${COLOR_RESET} %s\n" "$*"; }
|
||
|
||
# 计数器
|
||
ERR_COUNT=0
|
||
WARN_COUNT=0
|
||
err() { log_err "$*"; ERR_COUNT=$((ERR_COUNT + 1)); }
|
||
warn() { log_warn "$*"; WARN_COUNT=$((WARN_COUNT + 1)); }
|
||
|
||
# ------------------------------------------------------------
|
||
# Step 1:env 文件校验
|
||
# ------------------------------------------------------------
|
||
step_validate_env() {
|
||
log_info "=== Step 1/4: 校验 deploy/.env ==="
|
||
|
||
if [[ ! -f "$ENV_FILE" ]]; then
|
||
err "deploy/.env 不存在"
|
||
log_info "请先执行:cp deploy/.env.public.example deploy/.env 并填写关键参数"
|
||
return 1
|
||
fi
|
||
|
||
# shellcheck disable=SC1090
|
||
set -a
|
||
source "$ENV_FILE"
|
||
set +a
|
||
|
||
# DEPLOY_MODE 必须是 public(防止误把 local 的 env 复制过来)
|
||
if [[ "${DEPLOY_MODE:-}" != "public" ]]; then
|
||
warn "deploy/.env 中 DEPLOY_MODE=\"${DEPLOY_MODE:-<unset>}\",不是 public;确认你要走公网部署?"
|
||
fi
|
||
|
||
# MEDIASOUP_ANNOUNCED_IP 必填
|
||
if [[ -z "${MEDIASOUP_ANNOUNCED_IP:-}" ]]; then
|
||
err "MEDIASOUP_ANNOUNCED_IP 未设置,远端浏览器将无法建连 mediasoup"
|
||
log_info "请填写服务器的公网 IP,例:MEDIASOUP_ANNOUNCED_IP=203.0.113.42"
|
||
else
|
||
log_ok "MEDIASOUP_ANNOUNCED_IP=${MEDIASOUP_ANNOUNCED_IP}"
|
||
fi
|
||
|
||
# 检查占位符没被替换
|
||
local placeholders=(
|
||
POSTGRES_PASSWORD
|
||
REDIS_PASSWORD
|
||
MINIO_ROOT_PASSWORD
|
||
JWT_SECRET
|
||
MEDIA_INTERNAL_TOKEN
|
||
TURN_USERNAME
|
||
TURN_PASSWORD
|
||
)
|
||
for var in "${placeholders[@]}"; do
|
||
local val="${!var:-}"
|
||
if [[ -z "$val" ]]; then
|
||
# REDIS_PASSWORD 允许为空(仅内网访问时)
|
||
if [[ "$var" == "REDIS_PASSWORD" ]]; then
|
||
warn "$var 为空(仅当 Redis 不对公网暴露时可接受)"
|
||
else
|
||
err "$var 为空"
|
||
fi
|
||
elif [[ "$val" == _REPLACE_WITH_* ]]; then
|
||
err "$var 仍是占位符(${val}),请替换为真实值"
|
||
fi
|
||
done
|
||
|
||
# JWT_SECRET 长度
|
||
if [[ -n "${JWT_SECRET:-}" && ${#JWT_SECRET} -lt 32 ]]; then
|
||
warn "JWT_SECRET 长度仅 ${#JWT_SECRET} 字符,推荐 >= 64 字符(openssl rand -hex 32)"
|
||
fi
|
||
|
||
# Task 16 Nit(代码审查 2026-04-23):REDIS_PASSWORD 与 redis.conf requirepass 联动校验
|
||
# 背景:REDIS_PASSWORD 只影响 go-service / media-server 的连接侧,
|
||
# 若 deploy/docker/redis/redis.conf 未设置 requirepass,Redis 本身会接受无密码连接(相当于裸奔)
|
||
local redis_conf="$DEPLOY_DIR/docker/redis/redis.conf"
|
||
if [[ -f "$redis_conf" ]]; then
|
||
local has_requirepass
|
||
has_requirepass=$(grep -E '^\s*requirepass\s+\S+' "$redis_conf" || true)
|
||
if [[ -n "${REDIS_PASSWORD:-}" ]]; then
|
||
if [[ -z "$has_requirepass" ]]; then
|
||
err "REDIS_PASSWORD 已设置,但 $redis_conf 未启用 requirepass —— Redis 仍允许空密码登录,公网暴露时务必同步开启"
|
||
log_info "建议:在 redis.conf 追加 requirepass \$REDIS_PASSWORD(或在 docker-compose command 中传入 --requirepass)"
|
||
else
|
||
log_ok "Redis requirepass 已在 redis.conf 启用,与 REDIS_PASSWORD 联动"
|
||
fi
|
||
else
|
||
if [[ -n "$has_requirepass" ]]; then
|
||
warn "redis.conf 启用了 requirepass,但 deploy/.env 的 REDIS_PASSWORD 为空 —— go-service/media-server 连接会失败"
|
||
fi
|
||
fi
|
||
else
|
||
warn "未找到 $redis_conf,跳过 Redis 密码联动校验"
|
||
fi
|
||
|
||
# TURN 开关一致性
|
||
if [[ "${TURN_ENABLED:-false}" != "true" ]]; then
|
||
warn "TURN_ENABLED=${TURN_ENABLED:-false},公网部署下建议开启 TURN 作为对称 NAT fallback"
|
||
fi
|
||
|
||
if (( ERR_COUNT > 0 )); then
|
||
log_err "env 校验失败:$ERR_COUNT 个错误,$WARN_COUNT 个警告"
|
||
return 1
|
||
fi
|
||
log_ok "env 校验通过($WARN_COUNT 个警告)"
|
||
}
|
||
|
||
# ------------------------------------------------------------
|
||
# Step 2:防火墙/端口占用检查(本机视角)
|
||
# ------------------------------------------------------------
|
||
# 说明:云安全组需要在云控制台放通,本脚本只能检测本机 iptables + 当前占用
|
||
step_check_ports() {
|
||
log_info "=== Step 2/4: 本机端口占用检查 ==="
|
||
|
||
local ports=(
|
||
"8085/tcp|Go backend API"
|
||
"3300/tcp|media-server (internal, 不建议暴露公网)"
|
||
"40000/udp|mediasoup RTC (端口范围起点)"
|
||
"40199/udp|mediasoup RTC (端口范围终点)"
|
||
)
|
||
|
||
if [[ "${TURN_ENABLED:-false}" == "true" ]]; then
|
||
ports+=(
|
||
"${TURN_LISTEN_PORT:-3478}/udp|coturn STUN/TURN"
|
||
"${TURN_LISTEN_PORT:-3478}/tcp|coturn STUN/TURN"
|
||
"${TURN_TLS_PORT:-5349}/tcp|coturn TLS"
|
||
)
|
||
fi
|
||
|
||
for entry in "${ports[@]}"; do
|
||
local port="${entry%%|*}"
|
||
local desc="${entry##*|}"
|
||
local port_num="${port%/*}"
|
||
local proto="${port##*/}"
|
||
if [[ "$proto" == "tcp" ]]; then
|
||
if lsof -iTCP:"$port_num" -sTCP:LISTEN -n -P >/dev/null 2>&1; then
|
||
warn "TCP:$port_num 已被占用(${desc}),compose 启动可能失败"
|
||
else
|
||
log_ok "TCP:$port_num 空闲(${desc})"
|
||
fi
|
||
else
|
||
# UDP 只做提示,不中断
|
||
log_ok "UDP:$port_num 待 docker compose 绑定(${desc})"
|
||
fi
|
||
done
|
||
|
||
printf "\n${COLOR_YELLOW}⚠️ 云安全组/iptables 放通清单${COLOR_RESET}(本脚本无法自动执行,请手动确认):\n"
|
||
printf " TCP:8085 Go backend API\n"
|
||
printf " UDP:40000-40199 mediasoup RTC 流量\n"
|
||
printf " TCP:40000-40199 mediasoup ICE-TCP fallback\n"
|
||
if [[ "${TURN_ENABLED:-false}" == "true" ]]; then
|
||
printf " UDP:${TURN_LISTEN_PORT:-3478} coturn STUN/TURN\n"
|
||
printf " TCP:${TURN_LISTEN_PORT:-3478} coturn STUN/TURN\n"
|
||
printf " TCP:${TURN_TLS_PORT:-5349} coturn TLS\n"
|
||
printf " UDP:${TURN_MIN_PORT:-49160}-${TURN_MAX_PORT:-49200} coturn 中继流量\n"
|
||
fi
|
||
echo ""
|
||
}
|
||
|
||
# ------------------------------------------------------------
|
||
# Step 3:Docker 环境自检
|
||
# ------------------------------------------------------------
|
||
step_check_docker() {
|
||
log_info "=== Step 3/4: Docker 环境自检 ==="
|
||
|
||
if ! command -v docker >/dev/null 2>&1; then
|
||
err "未检测到 docker 命令"
|
||
return 1
|
||
fi
|
||
log_ok "docker: $(docker --version)"
|
||
|
||
if ! docker info >/dev/null 2>&1; then
|
||
err "docker daemon 无法连接(sudo systemctl start docker?)"
|
||
return 1
|
||
fi
|
||
log_ok "docker daemon 可访问"
|
||
|
||
if ! docker-compose version >/dev/null 2>&1; then
|
||
err "未检测到 docker-compose(需 Compose V2 plugin)"
|
||
return 1
|
||
fi
|
||
log_ok "docker-compose: $(docker-compose version | head -n1)"
|
||
}
|
||
|
||
# ------------------------------------------------------------
|
||
# Step 4:启动 public profile
|
||
# ------------------------------------------------------------
|
||
step_launch() {
|
||
log_info "=== Step 4/4: 启动 docker-compose (public profile) ==="
|
||
|
||
if (( ERR_COUNT > 0 )); then
|
||
log_err "前面校验有错误,拒绝启动(先修正后再跑脚本)"
|
||
return 1
|
||
fi
|
||
|
||
cd "$DEPLOY_DIR"
|
||
if [[ "${TURN_ENABLED:-false}" == "true" ]]; then
|
||
log_info "TURN_ENABLED=true,使用 --profile public 启动(含 coturn)"
|
||
docker-compose -f docker-compose.dev.yml --profile public up -d --build
|
||
else
|
||
log_info "TURN_ENABLED=false,跳过 coturn(仅默认服务)"
|
||
docker-compose -f docker-compose.dev.yml up -d --build
|
||
fi
|
||
|
||
log_ok "启动指令已发出,等待 healthcheck(最多 180 秒)..."
|
||
local waited=0
|
||
while (( waited < 180 )); do
|
||
local health
|
||
health="$(docker inspect -f '{{.State.Health.Status}}' echochat-media-server 2>/dev/null || echo 'starting')"
|
||
if [[ "$health" == "healthy" ]]; then
|
||
log_ok "media-server healthy($waited 秒)"
|
||
break
|
||
fi
|
||
if [[ "$health" == "unhealthy" ]]; then
|
||
log_err "media-server unhealthy,查看日志:docker logs echochat-media-server"
|
||
return 1
|
||
fi
|
||
sleep 3
|
||
waited=$((waited + 3))
|
||
done
|
||
|
||
printf "\n${COLOR_GREEN}=============== 公网部署完成 ===============${COLOR_RESET}\n"
|
||
printf " Go 后端 API: http://${MEDIASOUP_ANNOUNCED_IP}:8085\n"
|
||
printf " media-server: http://127.0.0.1:3300 (仅内网,勿对外暴露)\n"
|
||
if [[ "${TURN_ENABLED:-false}" == "true" ]]; then
|
||
printf " coturn TURN: ${MEDIASOUP_ANNOUNCED_IP}:${TURN_LISTEN_PORT:-3478}\n"
|
||
fi
|
||
printf "\n 查看状态: docker compose -f deploy/docker-compose.dev.yml ps\n"
|
||
printf " 查看日志: docker compose -f deploy/docker-compose.dev.yml logs -f [service]\n"
|
||
printf " 停止全栈: ./scripts/stop.sh full\n"
|
||
printf "${COLOR_GREEN}============================================${COLOR_RESET}\n"
|
||
}
|
||
|
||
main() {
|
||
step_validate_env || exit 1
|
||
step_check_ports # Step 2,不会 return 1,只收集 warn
|
||
step_check_docker || exit 1 # Step 3
|
||
# 二次确认(至少暴露 1 个明显错误时已退出,这里只对警告放行)
|
||
if [[ "${1:-}" != "--yes" ]] && (( WARN_COUNT > 0 )); then
|
||
printf "\n${COLOR_YELLOW}上述 %d 个警告仅提示,不阻塞启动。继续部署?(y/N): ${COLOR_RESET}" "$WARN_COUNT"
|
||
read -r confirm
|
||
case "$confirm" in
|
||
y|Y|yes|YES) ;;
|
||
*) log_info "已取消部署"; exit 0 ;;
|
||
esac
|
||
fi
|
||
step_launch
|
||
}
|
||
|
||
main "$@"
|