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:
256
scripts/deploy-public.sh
Executable file
256
scripts/deploy-public.sh
Executable 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 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
|
||||
|
||||
# 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 "$@"
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# EchoChat 一键启动脚本
|
||||
# 用法:
|
||||
# ./scripts/start.sh # 启动全部(Docker 中间件 + Go 后端 + 前台 + 管理端)
|
||||
# ./scripts/start.sh # 启动应用全部(Docker 中间件 + Go/前端/管理端,本地进程跑)
|
||||
# ./scripts/start.sh --no-docker # 仅启动应用层(假设 Docker 容器已在运行)
|
||||
# ./scripts/start.sh backend # 仅启动指定服务(backend|frontend|admin|media|docker)
|
||||
# ./scripts/start.sh full # Phase 2e-2 Task 14:docker compose 全栈启动(含 media-server 容器)
|
||||
# # 会读 deploy/.env(如不存在提示 copy example),公网则用 deploy-public.sh
|
||||
# 注意:不使用 set -e,避免单个服务启动失败中断其他服务;
|
||||
# 失败时由 spawn_bg 内部打印日志末尾辅助排障
|
||||
set -uo pipefail
|
||||
@@ -12,6 +14,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
RUN_DIR="$ROOT_DIR/.run"
|
||||
LOG_DIR="$RUN_DIR/logs"
|
||||
DEPLOY_DIR="$ROOT_DIR/deploy"
|
||||
mkdir -p "$RUN_DIR" "$LOG_DIR"
|
||||
|
||||
COLOR_GREEN='\033[0;32m'
|
||||
@@ -66,6 +69,57 @@ start_docker() {
|
||||
log_ok "Docker 中间件已启动"
|
||||
}
|
||||
|
||||
# 确保 deploy/.env 存在:若缺失则从 .env.local.example 拷贝(等价于本机 Demo 默认值)
|
||||
# 公网部署走 scripts/deploy-public.sh 单独校验,不经此函数
|
||||
ensure_env_file() {
|
||||
local env_file="$DEPLOY_DIR/.env"
|
||||
if [[ -f "$env_file" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local tmpl="$DEPLOY_DIR/.env.local.example"
|
||||
if [[ -f "$tmpl" ]]; then
|
||||
cp "$tmpl" "$env_file"
|
||||
log_warn "deploy/.env 不存在,已从 .env.local.example 复制(本机 Demo 默认值)"
|
||||
else
|
||||
log_warn "deploy/.env 和 .env.local.example 都不存在,compose 将使用内置默认值"
|
||||
fi
|
||||
}
|
||||
|
||||
# Phase 2e-2 Task 14:docker compose 全栈启动
|
||||
# 默认只拉起非 public profile 的服务(5 个:postgres/redis/minio/go-service/media-server)
|
||||
# coturn 走 public profile,需要用 scripts/deploy-public.sh
|
||||
start_full() {
|
||||
ensure_env_file
|
||||
log_info "docker compose 全栈启动(postgres + redis + minio + go-service + media-server)..."
|
||||
cd "$DEPLOY_DIR"
|
||||
if ! docker compose -f docker-compose.dev.yml up -d --build; then
|
||||
log_err "docker compose 启动失败,请检查上方错误输出"
|
||||
return 1
|
||||
fi
|
||||
log_ok "全栈服务已发起启动,等待 healthcheck..."
|
||||
# 等待最多 180 秒让 media-server 进入 healthy
|
||||
local waited=0
|
||||
while (( waited < 180 )); do
|
||||
local health
|
||||
health="$(docker inspect -f '{{.State.Health.Status}}' echochat-media-server 2>/dev/null || echo 'starting')"
|
||||
case "$health" in
|
||||
healthy)
|
||||
log_ok "media-server 已 healthy($waited 秒)"
|
||||
break
|
||||
;;
|
||||
unhealthy)
|
||||
log_err "media-server unhealthy,请查看日志:docker logs echochat-media-server"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
sleep 3
|
||||
waited=$((waited + 3))
|
||||
done
|
||||
if (( waited >= 180 )); then
|
||||
log_warn "等待 media-server healthy 超过 180 秒,可能仍在 mediasoup worker 初始化中,请手动验证"
|
||||
fi
|
||||
}
|
||||
|
||||
start_backend() {
|
||||
if port_in_use 8085; then
|
||||
log_warn "端口 8085 已被占用,跳过 Go 后端启动"
|
||||
@@ -200,9 +254,21 @@ main() {
|
||||
frontend) start_frontend ;;
|
||||
admin) start_admin ;;
|
||||
media) start_media ;;
|
||||
full)
|
||||
start_full || log_err "compose 全栈启动异常"
|
||||
# full 模式下仍用本地进程跑前台 + 管理端(热更体验更好,符合开发机场景)
|
||||
start_frontend || true
|
||||
start_admin || true
|
||||
print_summary
|
||||
;;
|
||||
*)
|
||||
log_err "未知参数: $target"
|
||||
echo "用法: $0 [all|docker|backend|frontend|admin|media|--no-docker]"
|
||||
echo "用法: $0 [all|docker|backend|frontend|admin|media|full|--no-docker]"
|
||||
echo ""
|
||||
echo "模式说明:"
|
||||
echo " all 本地进程启动应用 + docker 只起中间件(默认,开发常用)"
|
||||
echo " full docker compose 全栈启动(含 media-server 容器,接近生产形态)"
|
||||
echo " 公网部署 使用 scripts/deploy-public.sh(校验 ANNOUNCED_IP + 启动 coturn)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -51,10 +51,17 @@ check_port "后台管理端" 3100 "http://localhost:3100"
|
||||
|
||||
echo ""
|
||||
echo "===== Docker 中间件 ====="
|
||||
check_container "echochat-postgres" 5432
|
||||
check_container "echochat-redis" 6379
|
||||
check_container "echochat-minio" 9000
|
||||
check_container "echochat-postgres" 5432
|
||||
check_container "echochat-redis" 6379
|
||||
check_container "echochat-minio" 9000
|
||||
|
||||
echo ""
|
||||
echo "===== 容器化应用(docker compose full) ====="
|
||||
check_container "echochat-go-service" 8085
|
||||
check_container "echochat-media-server" 3300
|
||||
check_container "echochat-coturn" 3478
|
||||
|
||||
echo ""
|
||||
echo "日志目录:$RUN_DIR/logs"
|
||||
echo "compose 日志:docker compose -f deploy/docker-compose.dev.yml logs -f [service]"
|
||||
echo ""
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# EchoChat 一键停止脚本
|
||||
# 用法:
|
||||
# ./scripts/stop.sh # 停止应用层(Go 后端 + 前台 + 管理端),保留 Docker 容器
|
||||
# ./scripts/stop.sh # 停止应用层(Go 后端 + 前台 + 管理端 + 媒体),保留 Docker 容器
|
||||
# ./scripts/stop.sh --all # 停止全部(含 Docker 中间件)
|
||||
# ./scripts/stop.sh full # 停止 docker compose 全栈(含 media-server 容器,含 public profile 的 coturn)
|
||||
# ./scripts/stop.sh backend # 仅停止指定服务(backend|frontend|admin|media|docker)
|
||||
# 不使用 set -e:即使某一项停止失败,其他项也要继续尝试
|
||||
set -uo pipefail
|
||||
@@ -10,6 +11,7 @@ set -uo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
RUN_DIR="$ROOT_DIR/.run"
|
||||
DEPLOY_DIR="$ROOT_DIR/deploy"
|
||||
|
||||
COLOR_GREEN='\033[0;32m'
|
||||
COLOR_YELLOW='\033[0;33m'
|
||||
@@ -88,6 +90,15 @@ stop_docker() {
|
||||
log_ok "Docker 中间件已停止(数据卷保留)"
|
||||
}
|
||||
|
||||
# Phase 2e-2 Task 14:停止 docker compose 全栈(含 media-server、coturn)
|
||||
# 使用 --profile public 确保即使 coturn 在 public profile 下运行也能被停掉
|
||||
stop_full() {
|
||||
log_info "停止 docker compose 全栈(含 media-server / coturn)..."
|
||||
cd "$DEPLOY_DIR"
|
||||
docker compose -f docker-compose.dev.yml --profile public stop || true
|
||||
log_ok "全栈服务已停止(数据卷保留)"
|
||||
}
|
||||
|
||||
main() {
|
||||
local target="${1:-app}"
|
||||
|
||||
@@ -112,8 +123,9 @@ main() {
|
||||
admin) stop_admin ;;
|
||||
media) stop_media ;;
|
||||
docker) stop_docker ;;
|
||||
full) stop_full ;;
|
||||
*)
|
||||
echo "用法: $0 [app|--all|backend|frontend|admin|media|docker]"
|
||||
echo "用法: $0 [app|--all|backend|frontend|admin|media|docker|full]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user