monitor-ai/scripts/deploy-monitor.sh

432 lines
14 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# monitor-ai 部署脚本
#
# 用法:
# bash deploy-monitor.sh # 本地模式(默认)
# bash deploy-monitor.sh --dev # 开发模式(本地构建)
# bash deploy-monitor.sh --remote --host <IP> # 远程部署到指定服务器
#
# 首次部署自动:
# 1. 生成 OIDC_CLIENT_SECRET (openssl rand -hex 32)
# 2. 生成 LOCALADMIN_PASSWORD (openssl rand -hex 16)
# 3. 生成 pbkdf2 密码哈希(通过 Authelia 容器)
# 4. 更新 nginx 配置
#
# 支持系统:
# - macOS (本地): zsh + bash 兼容模式
# - Ubuntu/Debian (服务器): dash + bash
# - Rocky Linux/CentOS/RHEL (服务器): bash
# ============================================================
# 颜色和日志
# ============================================================
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; NC='\033[0m'
log() { printf "${GREEN}[✓]${NC} %s\n" "$1"; }
warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; }
info() { printf "${CYAN}[i]${NC} %s\n" "$1"; }
error() { printf "${RED}[✗]${NC} %s\n" "$1"; }
timing(){ printf " ${CYAN}${NC} %s\n" "$1"; }
# ============================================================
# 检测 macOS 和 Linux sed 差异
# ============================================================
is_mac() {
[[ "$(uname)" == "Darwin" ]]
}
SED_I_BACKUP='-i'
if is_mac; then
SED_I_BACKUP='-i ""'
fi
# ============================================================
# 解析参数
# ============================================================
MODE="local"
HOST=""
while [[ $# -gt 0 ]]; do
case "$1" in
--remote)
MODE="remote"
info "模式: 远程部署"
shift
;;
--host)
HOST="$2"
info "目标主机: $HOST"
shift 2
;;
--dev)
MODE="dev"
info "模式: 开发构建"
shift
;;
*)
warn "未知参数: $1"
shift
;;
esac
done
# ============================================================
# 路径配置
# ============================================================
LOCAL_DIR="/Users/niuniu/programs/docker"
MONITOR_DIR="$LOCAL_DIR/monitor-ai"
SHARED_DIR="$LOCAL_DIR/shared"
REMOTE_DIR="/root/docker/monitor-ai"
CONTAINER="monitor-ai"
SSH_TARGET="txjp"
SNAPSHOT_FILE="/tmp/.snapshot.monitor.md5"
# 远程模式需要 host 参数
if [[ "$MODE" == "remote" && -z "$HOST" ]]; then
error "远程部署需要指定 --host <IP>"
echo " 用法: bash deploy-monitor.sh --remote --host 43.133.38.210"
exit 1
fi
# ============================================================
# 打包源码
# ============================================================
package_source() {
log "打包源码..."
local pack_start
pack_start=$(date +%s)
COPYFILE_DISABLE=1 tar czf /tmp/monitor-ai.tar.gz \
-C "$LOCAL_DIR" \
--exclude='node_modules' --exclude='.next' --exclude='out' \
--exclude='data' --exclude='docs' \
--exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \
--exclude='.git' --exclude='._*' \
shared/ monitor-ai/ || {
error "打包失败"
return 1
}
local pack_end pkg_size pack_dur
pack_end=$(date +%s)
pack_dur=$((pack_end - pack_start))
pkg_size=$(du -h /tmp/monitor-ai.tar.gz 2>/dev/null | cut -f1 || echo "未知")
timing "打包耗时: ${pack_dur}s (${pkg_size})"
}
# ============================================================
# 首次部署:生成密钥和配置
# ============================================================
first_deploy_setup() {
local target="$1" # SSH target or "local"
local ssh_cmd=""
if [[ "$target" != "local" ]]; then
ssh_cmd="ssh $target"
fi
local run
if [[ -n "$ssh_cmd" ]]; then
run="$ssh_cmd"
else
run=""
fi
info "首次部署检查..."
# 检查 .env 是否存在
local env_exists
if [[ -n "$run" ]]; then
env_exists=$($run "test -f $REMOTE_DIR/.env && echo yes || echo no" 2>/dev/null)
else
env_exists=$(test -f "$MONITOR_DIR/.env" && echo yes || echo no)
fi
if [[ "$env_exists" == "yes" ]]; then
info "已存在 .env跳过密钥生成"
return 0
fi
log "首次部署:生成密钥和配置..."
local OIDC_SECRET LOCALADMIN_PASS JWT_SECRET PBKDF2_HASH
OIDC_SECRET=$(openssl rand -hex 32)
LOCALADMIN_PASS=$(openssl rand -hex 16)
JWT_SECRET=$(openssl rand -hex 32)
# 生成 pbkdf2 密码哈希(通过 Authelia 容器或本地 openssl
if [[ -n "$run" ]]; then
PBKDF2_HASH=$($run "docker exec authelia authelia crypto hash generate pbkdf2 --password '$LOCALADMIN_PASS' 2>/dev/null | grep 'Digest:' | awk '{print \$2}'" 2>/dev/null)
fi
# 如果 Authelia 不可用,使用 openssl 生成备用哈希
if [[ -z "$PBKDF2_HASH" ]]; then
warn "Authelia 不可用,使用 openssl 生成密码哈希"
PBKDF2_HASH=$(echo -n "$LOCALADMIN_PASS" | openssl dgst -sha256 -binary | openssl base64)
PBKDF2_HASH="pbkdf2:sha256:600000\$$(openssl rand -hex 16)\$$PBKDF2_HASH"
fi
# 写入 .env 文件
local env_content
env_content="DATABASE_PATH=/app/data/monitor.db
MONITOR_MODE=local
AUTHELIA_URL=https://sso.tlyq.ai
OIDC_CLIENT_ID=monitor-oidc
OIDC_CLIENT_SECRET=$OIDC_SECRET
OIDC_REDIRECT_URI=https://monitor.tlyq.ai/api/auth/callback
JWT_SECRET=$JWT_SECRET
COOKIE_DOMAIN=.tlyq.ai
LDAP_URL=ldap://ldap-ai:3890
LOCALADMIN_PASSWORD=$LOCALADMIN_PASS
NODE_ENV=production
NODE_TLS_REJECT_UNAUTHORIZED=0"
if [[ -n "$run" ]]; then
$run "cat > $REMOTE_DIR/.env << 'ENVEOF'
$env_content
ENVEOF"
else
echo "$env_content" > "$MONITOR_DIR/.env"
fi
log "密钥已生成"
info " OIDC_CLIENT_SECRET: $OIDC_SECRET"
info " LOCALADMIN_PASSWORD: $LOCALADMIN_PASS"
warn "请妥善保管以上密钥,.env 文件不会上传到版本控制"
}
# ============================================================
# 更新 nginx 配置
# ============================================================
update_nginx() {
local target="$1"
local nginx_conf="$LOCAL_DIR/nginx-proxy-ai/conf.d/monitor-ai.conf"
if [[ ! -f "$nginx_conf" ]]; then
warn "nginx 配置文件不存在: $nginx_conf"
return 0
fi
if [[ "$target" == "local" ]]; then
return 0
fi
log "更新 nginx 配置..."
scp "$nginx_conf" "$target:$REMOTE_DIR/../nginx-proxy-ai/conf.d/monitor-ai.conf" 2>/dev/null || true
ssh "$target" "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" && log "nginx 已重载" || warn "nginx 重载失败"
}
# ============================================================
# 服务器构建
# ============================================================
build_on_server() {
local target="$1"
# 1. 计算源码快照
log "检查源码是否有变化..."
local local_md5
local_md5=$(find "$MONITOR_DIR" "$SHARED_DIR" \
-not -path '*/node_modules/*' \
-not -path '*/.next/*' \
-not -path '*/out/*' \
-not -path '*/data/*' \
-not -path '*/docs/*' \
-not -path '*/.env*' \
-not -path '*/.git/*' \
-not -name '._*' \
-type f \
-exec md5 -q {} \; 2>/dev/null \
| sort \
| md5 -q)
echo " 源码快照: ${local_md5}"
local prev_md5
prev_md5=$(ssh "$target" "cat ${SNAPSHOT_FILE} 2>/dev/null" 2>/dev/null || echo "")
if [[ -z "$prev_md5" ]]; then
info "首次部署,执行完整构建"
elif [[ "$prev_md5" == "$local_md5" ]]; then
info "源码无变化,跳过构建,仅重启容器"
ssh "$target" "cd $REMOTE_DIR && docker compose down && docker compose up -d"
log "容器已重建"
return 0
else
info "检测到源码变化,执行增量构建"
fi
# 2. 打包源码
package_source
# 3. 上传
log "上传源码包..."
scp /tmp/monitor-ai.tar.gz "$target:/tmp/monitor-ai.tar.gz" || {
error "上传失败"
return 1
}
# 4. 解压源码到 /tmp然后 rsync 到目标目录
log "解压源码并准备依赖..."
ssh "$target" "\
rm -rf /tmp/deploy_monitor && mkdir -p /tmp/deploy_monitor && \
tar xzf /tmp/monitor-ai.tar.gz -C /tmp/deploy_monitor 2>/dev/null || true && \
rsync -a --delete \
--exclude='node_modules' --exclude='.next' --exclude='data' --exclude='docs' \
--exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \
--exclude='._*' \
/tmp/deploy_monitor/monitor-ai/ $REMOTE_DIR/ && \
rsync -a --delete \
--exclude='node_modules' --exclude='.git' \
/tmp/deploy_monitor/shared/ /root/docker/shared/ && \
rm -rf /tmp/deploy_monitor"
# 5. 首次部署:上传 docker-compose.yml 和 Dockerfile
ssh "$target" "cd $REMOTE_DIR && docker compose up -d"
# 如果 host 上没有 node_modules从容器内复制一份
ssh "$target" "if [ ! -d '${REMOTE_DIR}/node_modules' ]; then
echo ' 首次:复制容器内 node_modules 到主机...'
docker cp ${CONTAINER}:/app/node_modules ${REMOTE_DIR}/node_modules
echo ' 完成'
fi"
# 6. 安装可能新增的依赖
log "安装新增依赖..."
ssh "$target" "cd ${REMOTE_DIR} && npm install --prefer-offline 2>&1 | tail -5 || true"
# 7. 清理可能被上传的本地环境变量文件
ssh "$target" "rm -f $REMOTE_DIR/.env.local $REMOTE_DIR/.env.development $REMOTE_DIR/.env.production 2>/dev/null; echo '已清理本地环境文件'"
# 8. 服务器上执行 npm run build
log "服务器上执行 npm run build..."
local build_start build_end build_dur
build_start=$(date +%s)
ssh "$target" "cd ${REMOTE_DIR} && npm run build 2>&1" | \
grep -vE "^(info|warn|npm warn|audited|packages|funding|vulnerability|npm notice|New major)" | \
tail -15
local build_exit_code=${PIPESTATUS[0]}
build_end=$(date +%s)
build_dur=$((build_end - build_start))
timing "构建耗时: ${build_dur}s"
if [[ "$build_exit_code" -ne 0 ]]; then
warn "构建可能有警告,请检查上面的输出"
fi
# 9. 重建容器
log "重建容器..."
ssh "$target" "cd $REMOTE_DIR && docker compose up -d && docker compose restart"
# 10. 清理构建缓存
log "清理构建缓存..."
ssh "$target" "docker image prune -f 2>/dev/null || true"
local disk_usage
disk_usage=$(ssh "$target" "df / --output=pcent | tail -1 | tr -d ' %'" 2>/dev/null || echo "50")
if [ "$disk_usage" -gt 95 ]; then
warn "磁盘使用率 ${disk_usage}%,执行紧急清理"
ssh "$target" "docker builder prune -f 2>/dev/null || true"
elif [ "$disk_usage" -gt 85 ]; then
info "磁盘使用率 ${disk_usage}%执行激进清理6小时"
ssh "$target" "docker builder prune -f --filter 'until=6h' 2>/dev/null || true"
elif [ "$disk_usage" -gt 70 ]; then
info "磁盘使用率 ${disk_usage}%执行正常清理24小时"
ssh "$target" "docker builder prune -f --filter 'until=24h' 2>/dev/null || true"
else
info "磁盘使用率 ${disk_usage}%执行保守清理48小时"
ssh "$target" "docker builder prune -f --filter 'until=48h' 2>/dev/null || true"
fi
# 11. 保存快照
ssh "$target" "echo '$local_md5' > ${SNAPSHOT_FILE}"
}
# ============================================================
# 开发模式(本地构建)
# ============================================================
build_dev() {
log "本地开发模式构建..."
local build_start build_end build_dur
build_start=$(date +%s)
cd "$MONITOR_DIR" || { error "无法进入 $MONITOR_DIR"; exit 1; }
npm run build 2>&1 | tail -15
local build_exit_code=${PIPESTATUS[0]}
build_end=$(date +%s)
build_dur=$((build_end - build_start))
timing "构建耗时: ${build_dur}s"
if [[ "$build_exit_code" -ne 0 ]]; then
error "构建失败"
exit 1
fi
log "本地构建完成"
}
# ============================================================
# 执行部署
# ============================================================
echo ""
printf "${CYAN}=========================================${NC}\n"
printf "${CYAN} monitor-ai 部署${NC}\n"
printf "${CYAN}=========================================${NC}\n"
echo ""
BUILD_START=$(date +%s)
case "$MODE" in
local)
# 本地模式:部署到 txjp 服务器(默认行为)
log "部署目标: $MONITOR_DIR → txjp:$REMOTE_DIR"
echo ""
# 首次部署检查
first_deploy_setup "$SSH_TARGET"
# 构建
build_on_server "$SSH_TARGET"
# 更新 nginx
update_nginx "$SSH_TARGET"
;;
remote)
# 远程模式:部署到指定服务器
log "部署目标: $MONITOR_DIR$HOST:$REMOTE_DIR"
echo ""
# 首次部署检查
first_deploy_setup "$HOST"
# 构建
build_on_server "$HOST"
# 更新 nginx
update_nginx "$HOST"
;;
dev)
# 开发模式:仅本地构建
build_dev
;;
esac
# ============================================================
# 验证
# ============================================================
echo ""
log "验证部署..."
BUILD_END=$(date +%s)
TOTAL_DUR=$((BUILD_END - BUILD_START))
if [[ "$MODE" == "dev" ]]; then
log "开发构建完成,总耗时: ${TOTAL_DUR}s"
exit 0
fi
STATUS=$(ssh "$SSH_TARGET" "curl -s -o /dev/null -w '%{http_code}' -k 'https://monitor.tlyq.ai/api/health' 2>/dev/null" 2>/dev/null || echo "???")
if [[ "$STATUS" == "200" ]]; then
log "部署成功!总耗时: ${TOTAL_DUR}s | 访问 https://monitor.tlyq.ai"
exit 0
else
warn "返回状态码: $STATUS,请检查"
exit 1
fi