feat(deploy): Task 14 完成 docker-compose 扩展 + 环境变量双态部署

- deploy/docker-compose.dev.yml 新增 media-server 服务 + coturn(profiles:public),
  全部服务参数改为 env 变量注入,go-service 依赖 media-server
- deploy/.env.example / .env.local.example / .env.public.example 三份模板,
  公网模板用 _REPLACE_WITH_*_ 占位符防"示例值上生产"
- scripts/start.sh / stop.sh / status.sh 新增 full 子命令对接 docker compose --profile public
- scripts/deploy-public.sh 新建:env 校验 + 端口 checklist + Docker 自检 +
  --profile public up -d --build + 健康检查闭环
- docs/deployment/meeting-mvp.md 新建:双态部署全流程 + 强密码生成 + 防火墙 + FAQ
- 双模式 docker compose config 校验通过;deploy-public.sh 三种错误场景按预期退出

同步更新 CURRENT_STATUS / implementation.plan / project-context
Task 0-14  / 剩余 Task 15-16

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-04-22 17:59:48 +08:00
parent ba8c72aae0
commit a01e973335
12 changed files with 946 additions and 33 deletions

256
scripts/deploy-public.sh Executable file
View File

@@ -0,0 +1,256 @@
#!/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 1env 文件校验
# ------------------------------------------------------------
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 "请填写服务器的公网 IPMEDIASOUP_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
# 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 3Docker 环境自检
# ------------------------------------------------------------
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 "$@"