jshells/install_jsite.sh

1162 lines
37 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.

#!/bin/bash
# jsite前端自动化部署脚本
# 基于next.js + traefik开发
#
# 后端调用示例:
# 1. 基本部署: ./install_jsite.sh
# 2. 指定项目: ./install_jsite.sh --site-name myproject -r http://git.example.com/myproject
# 3. 跳过Docker: ./install_jsite.sh --skip-docker
# 4. 跳过Traefik: ./install_jsite.sh --skip-traefik
# 5. 强制更新: ./install_jsite.sh --force-update
# 6. 完整参数: ./install_jsite.sh --site-name myproject -r http://git.example.com/myproject -n 18 --skip-docker --force-update
#
# 返回码:
# 0 - 成功
# 1 - 参数错误
# 2 - 权限错误
# 3 - 其他错误
set -e # 遇到错误时退出
# 设置非交互式环境变量,避免交互式配置
export DEBIAN_FRONTEND=noninteractive
export DEBCONF_NONINTERACTIVE_SEEN=true
export UCF_FORCE_CONFNEW=1
export UCF_FORCE_CONFOLD=1
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 默认参数
GIT_REPO="http://git.jingrow.com:3000/jsite/jingrow"
NODE_VERSION="22"
SKIP_DOCKER=false
SKIP_TRAEFIK=false
SKIP_DEPENDENCIES=false
FORCE_UPDATE=false
SKIP_PM2=false
# 端口管理参数
START_PORT=3001
PORT_INCREMENT=1
# .env文件参数
SITE_URL="starrbud.com"
REVALIDATE_TOKEN="535bc122f3e364c"
SITE_NAME="jingrow"
SERVER_URL="https://admin.jingrow.com"
API_KEY="535bc122f3e364c"
API_SECRET="8629a3b12fc1cc2"
# 显示帮助信息
show_help() {
echo "用法: $0 [选项]"
echo ""
echo "选项:"
echo " -r, --repo URL Git仓库地址"
echo " -n, --node-version VERSION Node.js版本 (默认: 22)"
echo " --skip-docker 跳过Docker安装"
echo " --skip-traefik 跳过Traefik安装和启动"
echo " --skip-dependencies 跳过项目依赖安装"
echo " --skip-pm2 跳过PM2安装和启动"
echo " --force-update 强制更新项目(如果已存在)"
echo ""
echo "端口配置:"
echo " --start-port PORT 起始端口 (默认: 3001)"
echo " --port-increment INCREMENT 端口增量 (默认: 1)"
echo ""
echo ".env文件配置:"
echo " --site-url URL 网站URL (默认: http://192.168.2.200:3001)"
echo " --revalidate-token TOKEN 重新验证令牌 (默认: 535bc122f3e364c)"
echo " --site-name NAME 站点名称 (默认: jingrow)"
echo " --server-url URL 服务器URL (默认: https://admin.jingrow.com)"
echo " --api-key KEY API密钥 (默认: 535bc122f3e364c)"
echo " --api-secret SECRET API密钥 (默认: 8629a3b12fc1cc2)"
echo ""
echo " -h, --help 显示此帮助信息"
echo ""
echo "示例:"
echo " $0 --site-name myproject -r http://git.example.com/myproject"
echo " $0 --skip-docker --skip-traefik"
echo " $0 --force-update"
echo " $0 --site-url http://example.com --site-name myproject"
echo " $0 --start-port 3005 --port-increment 10"
}
# 解析命令行参数
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
-r|--repo)
GIT_REPO="$2"
shift 2
;;
-n|--node-version)
NODE_VERSION="$2"
shift 2
;;
--skip-docker)
SKIP_DOCKER=true
shift
;;
--skip-traefik)
SKIP_TRAEFIK=true
shift
;;
--skip-dependencies)
SKIP_DEPENDENCIES=true
shift
;;
--skip-pm2)
SKIP_PM2=true
shift
;;
--force-update)
FORCE_UPDATE=true
shift
;;
--start-port)
START_PORT="$2"
shift 2
;;
--port-increment)
PORT_INCREMENT="$2"
shift 2
;;
--site-url)
SITE_URL="$2"
shift 2
;;
--revalidate-token)
REVALIDATE_TOKEN="$2"
shift 2
;;
--site-name)
SITE_NAME="$2"
shift 2
;;
--server-url)
SERVER_URL="$2"
shift 2
;;
--api-key)
API_KEY="$2"
shift 2
;;
--api-secret)
API_SECRET="$2"
shift 2
;;
-h|--help)
show_help
exit 0
;;
*)
echo -e "${RED}[ERROR]${NC} 未知参数: $1"
show_help
exit 1
;;
esac
done
}
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
# 检查是否为root用户
check_root() {
if [[ $EUID -eq 0 ]]; then
log_info "以root用户身份运行脚本"
else
log_error "请以root用户身份运行此脚本"
exit 1
fi
}
# 1. 创建jingrow用户
create_jingrow_user() {
log_info "开始创建jingrow用户..."
if id "jingrow" &>/dev/null; then
log_warning "用户jingrow已存在"
else
useradd -m -s /bin/bash jingrow
log_success "用户jingrow创建成功"
fi
# 确保home目录存在
if [ ! -d "/home/jingrow" ]; then
mkdir -p /home/jingrow
chown jingrow:jingrow /home/jingrow
log_success "创建/home/jingrow目录"
fi
}
# 2. 安装nvm和node.js
install_nodejs() {
log_info "开始安装nvm和node.js..."
# 切换到jingrow用户目录
cd /home/jingrow
# 下载并安装nvm
if [ ! -d "/home/jingrow/.nvm" ]; then
log_info "下载并安装nvm..."
su - jingrow -c "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash"
log_success "nvm安装完成"
else
log_warning "nvm已存在"
fi
# 加载nvm并安装node.js
log_info "安装Node.js v$NODE_VERSION..."
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
nvm install $NODE_VERSION
nvm use $NODE_VERSION
nvm alias default $NODE_VERSION
"
# 验证安装
NODE_VERSION_OUTPUT=$(su - jingrow -c 'export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && node -v')
NPM_VERSION=$(su - jingrow -c 'export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && npm -v')
log_success "Node.js版本: $NODE_VERSION_OUTPUT"
log_success "npm版本: $NPM_VERSION"
}
# 2.5. 安装PM2
install_pm2() {
log_info "开始安装PM2..."
# 检查PM2是否已安装
PM2_VERSION=$(su - jingrow -c 'export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && pm2 -v 2>/dev/null || echo "not_installed"')
if [ "$PM2_VERSION" != "not_installed" ]; then
log_warning "PM2已安装版本: $PM2_VERSION"
else
log_info "安装PM2..."
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
npm install -g pm2
"
log_success "PM2安装完成"
fi
# 设置PM2开机自启避免交互式输出
log_info "配置PM2开机自启..."
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
pm2 startup 2>/dev/null || true
"
log_success "PM2开机自启配置完成"
}
# 2.6. 端口管理函数
get_available_port() {
local site_name="$1"
local base_port="$START_PORT"
local increment="$PORT_INCREMENT"
# 端口分配文件路径
local port_file="/home/jingrow/jsite/site_port.json"
# 确保jsite目录存在
if [ ! -d "/home/jingrow/jsite" ]; then
mkdir -p /home/jingrow/jsite
chown jingrow:jingrow /home/jingrow/jsite
fi
# 检查项目是否已有分配的端口
if [ -f "$port_file" ]; then
# 使用jq查询已分配的端口
if command -v jq &> /dev/null; then
local existing_port=$(jq -r ".$site_name // empty" "$port_file" 2>/dev/null || echo "")
if [ -n "$existing_port" ] && [ "$existing_port" != "null" ] && [ "$existing_port" != "empty" ]; then
echo "$existing_port"
return 0
fi
else
# 如果没有jq使用grep和cut来解析JSON
local existing_port=$(grep -o "\"$site_name\"[[:space:]]*:[[:space:]]*[0-9]*" "$port_file" 2>/dev/null | cut -d: -f2 | tr -d ' ' || echo "")
if [ -n "$existing_port" ]; then
echo "$existing_port"
return 0
fi
fi
fi
# 如果没有分配的端口,检查是否使用用户指定的起始端口
local next_port="$base_port"
# 检查用户是否传入了自定义起始端口通过比较START_PORT是否等于默认值3001
local user_specified_port=false
if [ "$base_port" != "3001" ]; then
user_specified_port=true
fi
if [ "$user_specified_port" = true ]; then
# 用户传入了自定义端口,优先使用用户指定的端口
# 检查用户指定的端口是否已被使用
local port_available=true
if [ -f "$port_file" ]; then
if command -v jq &> /dev/null; then
local used_ports=$(jq -r '.[]' "$port_file" 2>/dev/null || echo "")
if echo "$used_ports" | grep -q "^$base_port$"; then
port_available=false
fi
else
local used_ports=$(grep -o ":[[:space:]]*[0-9]*" "$port_file" 2>/dev/null | cut -d: -f2 | tr -d ' ' || echo "")
if echo "$used_ports" | grep -q "^$base_port$"; then
port_available=false
fi
fi
fi
if [ "$port_available" = true ]; then
# 用户指定的端口可用,直接使用
next_port="$base_port"
else
# 用户指定的端口已被使用,使用最大端口号 + 1
if [ -f "$port_file" ]; then
if command -v jq &> /dev/null; then
local max_port=$(jq -r 'max(.[])' "$port_file" 2>/dev/null || echo "$base_port")
if [ -n "$max_port" ] && [ "$max_port" != "null" ]; then
next_port=$((max_port + increment))
fi
else
local max_port=$(grep -o ":[[:space:]]*[0-9]*" "$port_file" 2>/dev/null | cut -d: -f2 | tr -d ' ' | sort -n | tail -1 || echo "$base_port")
if [ -n "$max_port" ]; then
next_port=$((max_port + increment))
fi
fi
fi
fi
else
# 用户没有传入自定义端口,使用自动分配逻辑
if [ -f "$port_file" ]; then
if command -v jq &> /dev/null; then
# 使用jq找到最大端口号
local max_port=$(jq -r 'max(.[])' "$port_file" 2>/dev/null || echo "$base_port")
if [ -n "$max_port" ] && [ "$max_port" != "null" ]; then
next_port=$((max_port + increment))
fi
else
# 使用grep和sort找到最大端口号
local max_port=$(grep -o ":[[:space:]]*[0-9]*" "$port_file" 2>/dev/null | cut -d: -f2 | tr -d ' ' | sort -n | tail -1 || echo "$base_port")
if [ -n "$max_port" ]; then
next_port=$((max_port + increment))
fi
fi
fi
fi
echo "$next_port"
return 0
}
# 2.7. 保存端口分配
save_port_assignment() {
local site_name="$1"
local port="$2"
local port_file="/home/jingrow/jsite/site_port.json"
# 确保jsite目录存在
if [ ! -d "/home/jingrow/jsite" ]; then
mkdir -p /home/jingrow/jsite
chown jingrow:jingrow /home/jingrow/jsite
fi
# 检查jq是否可用
if command -v jq &> /dev/null; then
# 使用jq保存到JSON文件
if [ -f "$port_file" ]; then
# 更新现有文件
jq ". + {\"$site_name\": $port}" "$port_file" > "${port_file}.tmp" && mv "${port_file}.tmp" "$port_file"
else
# 创建新文件
echo "{\"$site_name\": $port}" > "$port_file"
fi
else
# 如果没有jq使用简单的文本处理
if [ -f "$port_file" ]; then
# 移除已存在的条目(如果存在)
grep -v "\"$site_name\"" "$port_file" > "${port_file}.tmp" 2>/dev/null || true
mv "${port_file}.tmp" "$port_file"
# 在最后一个大括号前添加新条目
sed -i "s/}$/ \"$site_name\": $port\n}/" "$port_file" 2>/dev/null || echo "{\"$site_name\": $port}" > "$port_file"
else
# 创建新文件
echo "{\"$site_name\": $port}" > "$port_file"
fi
fi
# 设置文件权限
chown jingrow:jingrow "$port_file"
chmod 644 "$port_file"
log_success "端口 $port 已分配给项目 $site_name"
}
# 2.8. 获取或分配端口(确保一致性)
get_or_assign_port() {
local site_name="$1"
# 获取或分配端口
local port=$(get_available_port "$site_name")
if [ -n "$port" ]; then
# 检查是否已经保存过这个端口分配
local port_file="/home/jingrow/jsite/site_port.json"
local already_saved=false
if [ -f "$port_file" ]; then
if command -v jq &> /dev/null; then
local existing_port=$(jq -r ".$site_name // empty" "$port_file" 2>/dev/null || echo "")
if [ -n "$existing_port" ] && [ "$existing_port" != "null" ] && [ "$existing_port" != "empty" ]; then
already_saved=true
fi
else
local existing_port=$(grep -o "\"$site_name\"[[:space:]]*:[[:space:]]*[0-9]*" "$port_file" 2>/dev/null | cut -d: -f2 | tr -d ' ' || echo "")
if [ -n "$existing_port" ]; then
already_saved=true
fi
fi
fi
# 如果没有保存过,则保存端口分配(重定向日志输出)
if [ "$already_saved" = false ]; then
save_port_assignment "$site_name" "$port" >/dev/null 2>&1
fi
echo "$port"
return 0
else
log_error "无法为项目 $site_name 分配端口"
return 1
fi
}
# 2.9. 显示端口分配情况
show_port_assignments() {
local port_file="/home/jingrow/jsite/site_port.json"
if [ ! -f "$port_file" ]; then
log_info "暂无端口分配记录"
return 0
fi
log_info "当前端口分配情况:"
if command -v jq &> /dev/null; then
# 使用jq格式化输出
jq -r 'to_entries[] | " - \(.key): \(.value)"' "$port_file" 2>/dev/null || log_warning "无法解析端口分配文件"
else
# 使用grep和sed格式化输出
grep -o '"[^"]*"[[:space:]]*:[[:space:]]*[0-9]*' "$port_file" 2>/dev/null | sed 's/"//g' | sed 's/[[:space:]]*:[[:space:]]*/: /' | sed 's/^/ - /' || log_warning "无法解析端口分配文件"
fi
}
# 3. 克隆jsite项目
clone_jsite_project() {
log_info "开始克隆jsite项目: $SITE_NAME..."
cd /home/jingrow
# 创建jsite目录如果不存在
if [ ! -d "/home/jingrow/jsite" ]; then
mkdir -p /home/jingrow/jsite
chown jingrow:jingrow /home/jingrow/jsite
log_success "创建jsite目录"
fi
# 检查项目子目录是否已存在
if [ -d "/home/jingrow/jsite/$SITE_NAME" ]; then
if [ "$FORCE_UPDATE" = true ]; then
log_warning "项目目录已存在,强制更新..."
rm -rf "/home/jingrow/jsite/$SITE_NAME"
cd /home/jingrow/jsite
su - jingrow -c "cd /home/jingrow/jsite && git clone $GIT_REPO $SITE_NAME"
if [ $? -ne 0 ]; then
log_error "项目克隆失败: $GIT_REPO"
return 1
fi
log_success "jsite/$SITE_NAME项目更新完成"
else
log_warning "jsite/$SITE_NAME目录已存在,跳过克隆"
fi
else
cd /home/jingrow/jsite
log_info "克隆项目: $GIT_REPO"
su - jingrow -c "cd /home/jingrow/jsite && git clone $GIT_REPO $SITE_NAME"
if [ $? -ne 0 ]; then
log_error "项目克隆失败: $GIT_REPO"
return 1
fi
log_success "jsite/$SITE_NAME项目克隆完成"
fi
# 检查项目是否成功克隆
if [ ! -d "/home/jingrow/jsite/$SITE_NAME" ]; then
log_error "项目目录不存在,克隆可能失败"
return 1
fi
# 检查package.json是否存在
if [ ! -f "/home/jingrow/jsite/$SITE_NAME/package.json" ]; then
log_error "package.json文件不存在请检查项目是否正确克隆"
return 1
fi
log_success "项目克隆和验证完成"
}
# 4. 创建.env文件
create_env_file() {
log_info "创建.env文件..."
cd /home/jingrow/jsite/$SITE_NAME
# 获取项目端口
local project_port=$(get_or_assign_port "$SITE_NAME")
# 构建本地地址URL用于PUBLIC_SITE_URL
local public_site_url="http://127.0.0.1:$project_port"
# 检查.env文件是否已存在
if [ -f "/home/jingrow/jsite/$SITE_NAME/.env" ]; then
if [ "$FORCE_UPDATE" = true ]; then
log_warning ".env文件已存在强制更新..."
else
log_warning ".env文件已存在跳过创建"
return
fi
fi
# 创建.env文件
cat > "/home/jingrow/jsite/$SITE_NAME/.env" << EOF
PUBLIC_SITE_URL=$public_site_url
REVALIDATE_TOKEN=$REVALIDATE_TOKEN
JINGROW_SITE_NAME=$SITE_NAME
JINGROW_SERVER_URL=$SERVER_URL
JINGROW_API_KEY=$API_KEY
JINGROW_API_SECRET=$API_SECRET
EOF
# 设置文件权限
chown jingrow:jingrow "/home/jingrow/jsite/$SITE_NAME/.env"
chmod 600 "/home/jingrow/jsite/$SITE_NAME/.env"
log_success ".env文件创建完成 (端口: $project_port, PUBLIC_SITE_URL: $public_site_url)"
}
# 4.5. 创建PM2配置文件
create_pm2_config() {
log_info "创建PM2配置文件..."
cd /home/jingrow/jsite/$SITE_NAME
# 获取项目端口
local project_port=$(get_or_assign_port "$SITE_NAME")
# 创建logs目录
mkdir -p "/home/jingrow/jsite/$SITE_NAME/logs"
# 检查package.json中的scripts
local start_script="npm start"
if [ -f "/home/jingrow/jsite/$SITE_NAME/package.json" ]; then
if grep -q '"start"' "/home/jingrow/jsite/$SITE_NAME/package.json"; then
start_script="npm start"
elif grep -q '"dev"' "/home/jingrow/jsite/$SITE_NAME/package.json"; then
start_script="npm run dev"
else
log_warning "未找到start或dev脚本使用默认npm start"
fi
fi
# 检查项目是否使用ES模块
local is_es_module=false
if [ -f "/home/jingrow/jsite/$SITE_NAME/package.json" ]; then
if grep -q '"type": "module"' "/home/jingrow/jsite/$SITE_NAME/package.json"; then
is_es_module=true
fi
fi
# 使用.cjs扩展名来避免ES模块问题
cat > "/home/jingrow/jsite/$SITE_NAME/ecosystem.config.cjs" << EOF
module.exports = {
apps: [{
name: '$SITE_NAME',
script: 'npm',
args: 'start',
cwd: '/home/jingrow/jsite/$SITE_NAME',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PORT: $project_port
},
env_file: '.env',
log_file: './logs/combined.log',
out_file: './logs/out.log',
error_file: './logs/error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
time: true,
min_uptime: '10s',
max_restarts: 5
}]
};
EOF
# 设置文件权限
chown -R jingrow:jingrow "/home/jingrow/jsite/$SITE_NAME/ecosystem.config.cjs"
chown -R jingrow:jingrow "/home/jingrow/jsite/$SITE_NAME/logs"
log_success "PM2配置文件创建完成 (端口: $project_port, 启动脚本: $start_script, 模块类型: $([ "$is_es_module" = true ] && echo "ES模块" || echo "CommonJS"))"
}
# 4.6. 用PM2启动项目
start_project_with_pm2() {
log_info "使用PM2启动项目: $SITE_NAME..."
cd /home/jingrow/jsite/$SITE_NAME
# 检查项目目录是否存在
if [ ! -d "/home/jingrow/jsite/$SITE_NAME" ]; then
log_error "项目目录不存在: /home/jingrow/jsite/$SITE_NAME"
return 1
fi
# 检查package.json是否存在
if [ ! -f "/home/jingrow/jsite/$SITE_NAME/package.json" ]; then
log_error "package.json文件不存在请检查项目是否正确克隆"
return 1
fi
# 检查ecosystem.config.cjs是否存在
if [ ! -f "/home/jingrow/jsite/$SITE_NAME/ecosystem.config.cjs" ]; then
log_error "ecosystem.config.cjs文件不存在请检查PM2配置是否正确创建"
return 1
fi
# 检查项目是否已经在PM2中运行
PM2_STATUS=$(su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
pm2 list | grep -w '$SITE_NAME' | wc -l
")
if [ "$PM2_STATUS" != "0" ] && [ -n "$PM2_STATUS" ]; then
if [ "$FORCE_UPDATE" = true ]; then
log_warning "项目已在PM2中运行重新启动..."
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
cd /home/jingrow/jsite/$SITE_NAME
pm2 delete $SITE_NAME 2>/dev/null || true
pm2 start ecosystem.config.cjs
"
if [ $? -ne 0 ]; then
log_error "PM2启动失败请检查项目配置和依赖"
log_info "检查PM2日志"
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
pm2 logs $SITE_NAME --lines 10
"
return 1
fi
else
log_warning "项目已在PM2中运行跳过启动"
return 0
fi
else
log_info "启动新项目..."
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
cd /home/jingrow/jsite/$SITE_NAME
pm2 start ecosystem.config.cjs
"
if [ $? -ne 0 ]; then
log_error "PM2启动失败请检查项目配置和依赖"
log_info "检查PM2日志"
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
pm2 logs $SITE_NAME --lines 10
"
return 1
fi
fi
# 等待一下让PM2启动完成
sleep 5
# 检查PM2进程是否真的启动了
PM2_RUNNING=$(su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
pm2 list | grep -w '$SITE_NAME' | grep -c 'online' || echo '0'
")
if [ "$PM2_RUNNING" = "0" ]; then
log_error "PM2启动失败项目未正常运行"
log_info "检查PM2状态"
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
pm2 list
"
log_info "检查PM2日志"
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
pm2 logs $SITE_NAME --lines 20
"
return 1
fi
# 保存PM2配置
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
pm2 save
"
# 显示PM2状态
log_info "PM2项目状态"
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
pm2 list
"
log_success "项目已使用PM2启动完成"
}
# 5. 安装traefik
install_traefik() {
log_info "开始安装traefik..."
cd /home/jingrow
# 检查traefik目录是否已存在
if [ -d "/home/jingrow/traefik-docker" ]; then
if [ "$FORCE_UPDATE" = true ]; then
log_warning "traefik-docker目录已存在强制更新..."
rm -rf /home/jingrow/traefik-docker
else
log_warning "traefik-docker目录已存在跳过克隆"
return
fi
fi
# 从Git仓库克隆traefik配置
su - jingrow -c "cd /home/jingrow && git clone http://git.jingrow.com:3000/go/traefik-docker"
log_success "traefik项目克隆完成"
# 设置目录权限
chown -R jingrow:jingrow /home/jingrow/traefik-docker
log_success "设置traefik目录权限"
# 设置 acme.json 文件权限Let's Encrypt 要求 600 权限)
if [ -f "/home/jingrow/traefik-docker/acme.json" ]; then
chmod 600 /home/jingrow/traefik-docker/acme.json
chown jingrow:jingrow /home/jingrow/traefik-docker/acme.json
log_success "设置 acme.json 文件权限为 600"
else
log_warning "acme.json 文件不存在,请检查 Traefik 配置"
fi
}
# 6. 安装Docker如果未安装
install_docker() {
log_info "检查Docker安装状态..."
if command -v docker &> /dev/null; then
log_warning "Docker已安装"
else
log_info "开始安装Docker..."
# 设置非交互式环境
export DEBIAN_FRONTEND=noninteractive
export DEBCONF_NONINTERACTIVE_SEEN=true
# 更新包索引
apt-get update
# 安装必要的包(避免交互式配置)
apt-get install -y --force-yes apt-transport-https ca-certificates curl gnupg lsb-release
# 添加Docker官方GPG密钥非交互式
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --batch --yes --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# 设置稳定版仓库
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
# 更新包索引
apt-get update
# 安装Docker Engine避免交互式配置
apt-get install -y --force-yes docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 启动Docker服务
systemctl start docker
systemctl enable docker
log_success "Docker安装完成"
fi
# 将jingrow用户添加到docker组
usermod -aG docker jingrow
log_success "将jingrow用户添加到docker组"
}
# 7. 启动traefik
start_traefik() {
log_info "启动traefik服务..."
cd /home/jingrow/traefik-docker
# 检查docker-compose.yml文件是否存在
if [ ! -f "/home/jingrow/traefik-docker/docker-compose.yml" ]; then
log_error "docker-compose.yml文件不存在请检查traefik项目是否正确克隆"
return 1
fi
# 使用jingrow用户启动traefik
su - jingrow -c "cd /home/jingrow/traefik-docker && docker compose up -d"
log_success "traefik服务启动完成"
}
# 7.4. 解析SITE_URL并生成Host规则
generate_host_rule() {
local site_url="$1"
# 移除协议前缀(如果存在)
local domain=$(echo "$site_url" | sed -E 's|^https?://||')
# 移除端口号(如果存在)
domain=$(echo "$domain" | sed -E 's|:[0-9]+$||')
# 检查是否为一级域名(不包含子域名)
if [[ "$domain" =~ ^[^.]+\.[^.]+$ ]]; then
# 一级域名,添加 www 前缀
echo "Host(\`$domain\`) || Host(\`www.$domain\`)"
else
# 二级或更多级域名,只使用原域名
echo "Host(\`$domain\`)"
fi
}
# 7.5. 创建traefik网站配置文件
create_traefik_website_config() {
log_info "创建traefik网站配置文件..."
# 获取项目端口
local project_port=$(get_or_assign_port "$SITE_NAME")
# 生成Host规则
local host_rule=$(generate_host_rule "$SITE_URL")
# 创建conf.d/website目录如果不存在
local website_dir="/home/jingrow/traefik-docker/conf.d/website"
if [ ! -d "$website_dir" ]; then
mkdir -p "$website_dir"
chown jingrow:jingrow "$website_dir"
log_success "创建traefik website配置目录"
fi
# 创建网站配置文件
local config_file="$website_dir/$SITE_NAME.yml"
# 检查配置文件是否已存在
if [ -f "$config_file" ]; then
if [ "$FORCE_UPDATE" = true ]; then
log_warning "traefik网站配置文件已存在强制更新..."
else
log_warning "traefik网站配置文件已存在跳过创建"
return
fi
fi
# 创建网站配置文件
cat > "$config_file" << EOF
# $SITE_NAME 网站配置
http:
routers:
main-https:
rule: &host_rule $host_rule
entryPoints:
- websecure
service: main-service
tls:
certResolver: myresolver
main-http-redirect:
rule: *host_rule
entryPoints:
- web
middlewares:
- redirect-to-https
service: noop
services:
main-service:
loadBalancer:
servers:
- url: "http://localhost:$project_port"
noop:
loadBalancer:
servers:
- url: "http://127.0.0.1:65535" # 占位用无实际后端仅用于HTTP跳转
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
permanent: true
EOF
# 设置文件权限
chown jingrow:jingrow "$config_file"
chmod 644 "$config_file"
log_success "traefik网站配置文件创建完成: $config_file (端口: $project_port)"
log_info "Host规则: $host_rule"
# 重启traefik以加载新配置
log_info "重启traefik以加载新配置..."
su - jingrow -c "cd /home/jingrow/traefik-docker && docker compose restart"
log_success "traefik重启完成"
}
# 8. 安装项目依赖
install_project_dependencies() {
log_info "安装jsite/$SITE_NAME项目依赖..."
cd /home/jingrow/jsite/$SITE_NAME
# 检查package.json是否存在
if [ ! -f "/home/jingrow/jsite/$SITE_NAME/package.json" ]; then
log_error "package.json文件不存在请检查项目是否正确克隆"
return 1
fi
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
cd /home/jingrow/jsite/$SITE_NAME
npm install
"
if [ $? -ne 0 ]; then
log_error "项目依赖安装失败"
return 1
fi
log_success "项目依赖安装完成"
# 构建项目
log_info "构建jsite/$SITE_NAME项目..."
# 获取当前内存大小MB并减去200MB作为构建内存限制
local total_memory_mb=$(free -m | awk 'NR==2{print $2}')
local build_memory_mb=$((total_memory_mb - 300))
# 确保内存限制至少为512MB
if [ "$build_memory_mb" -lt 512 ]; then
build_memory_mb=512
log_warning "可用内存不足设置构建内存限制为512MB"
fi
log_info "设置构建内存限制为 ${build_memory_mb}MB (总内存: ${total_memory_mb}MB)"
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
export NODE_OPTIONS=\"--max-old-space-size=$build_memory_mb\"
cd /home/jingrow/jsite/$SITE_NAME
npm run build
"
if [ $? -ne 0 ]; then
log_error "项目构建失败"
return 1
fi
log_success "项目构建完成 (内存限制: ${build_memory_mb}MB)"
}
# 9. 显示部署信息
show_deployment_info() {
log_success "=== 部署完成 ==="
echo ""
log_info "部署信息:"
echo " - 用户: jingrow"
echo " - jsite目录: /home/jingrow/jsite"
echo " - 项目目录: /home/jingrow/jsite/$SITE_NAME"
echo " - 项目端口: $(get_or_assign_port "$SITE_NAME")"
echo " - Traefik目录: /home/jingrow/traefik-docker"
echo " - Traefik管理界面: http://localhost:8080"
echo " - Traefik网站配置: /home/jingrow/traefik-docker/conf.d/website/$SITE_NAME.yml"
echo " - PM2配置文件: /home/jingrow/jsite/$SITE_NAME/ecosystem.config.cjs"
echo " - PM2日志目录: /home/jingrow/jsite/$SITE_NAME/logs"
echo ""
log_info ".env文件配置"
echo " - PUBLIC_SITE_URL: $SITE_URL"
echo " - JINGROW_SITE_NAME: $SITE_NAME"
echo " - JINGROW_SERVER_URL: $SERVER_URL"
echo " - REVALIDATE_TOKEN: $REVALIDATE_TOKEN"
echo " - JINGROW_API_KEY: $API_KEY"
echo " - JINGROW_API_SECRET: $API_SECRET"
echo ""
log_info "PM2管理命令"
echo " - 查看状态: pm2 list"
echo " - 查看日志: pm2 logs $SITE_NAME"
echo " - 重启项目: pm2 restart $SITE_NAME"
echo " - 停止项目: pm2 stop $SITE_NAME"
echo " - 删除项目: pm2 delete $SITE_NAME"
echo " - 监控界面: pm2 monit"
echo ""
log_info "Traefik配置信息"
echo " - 网站配置文件: /home/jingrow/traefik-docker/conf.d/website/$SITE_NAME.yml"
echo " - 访问域名: $(generate_host_rule "$SITE_URL" | sed 's/Host(`//g' | sed 's/`)//g' | sed 's/ || / 或 /g')"
echo " - 后端端口: $(get_or_assign_port "$SITE_NAME")"
echo ""
# 显示端口分配情况
show_port_assignments
echo ""
log_info "下一步操作:"
echo " 1. 进入项目目录: cd /home/jingrow/jsite/$SITE_NAME"
echo " 2. 查看PM2状态: pm2 list"
echo " 3. 查看项目日志: pm2 logs $SITE_NAME"
echo " 4. 访问Traefik管理界面: http://localhost:8080"
echo " 5. 访问项目: $SITE_URL"
echo " 6. 检查traefik配置: cat /home/jingrow/traefik-docker/conf.d/website/$SITE_NAME.yml"
echo " 7. 查看端口分配: cat /home/jingrow/jsite/site_port.json"
echo ""
log_warning "注意请确保防火墙允许80、443、8080端口访问"
}
# 主函数
main() {
log_info "开始jsite前端自动化部署..."
echo ""
log_info "部署参数:"
echo " - 项目名称: $SITE_NAME"
echo " - Git仓库: $GIT_REPO"
echo " - Node.js版本: $NODE_VERSION"
echo " - 跳过Docker: $SKIP_DOCKER"
echo " - 跳过Traefik: $SKIP_TRAEFIK"
echo " - 跳过依赖安装: $SKIP_DEPENDENCIES"
echo " - 跳过PM2: $SKIP_PM2"
echo " - 强制更新: $FORCE_UPDATE"
echo ""
log_info "端口配置:"
echo " - 起始端口: $START_PORT"
echo " - 端口增量: $PORT_INCREMENT"
echo ""
log_info ".env文件参数:"
echo " - 网站URL: $SITE_URL"
echo " - 站点名称: $SITE_NAME"
echo " - 服务器URL: $SERVER_URL"
echo " - 重新验证令牌: $REVALIDATE_TOKEN"
echo " - API密钥: $API_KEY"
echo " - API密钥: $API_SECRET"
echo ""
check_root
create_jingrow_user
if [ "$SKIP_DOCKER" = false ]; then
install_docker
else
log_warning "跳过Docker安装"
fi
install_nodejs
if [ "$SKIP_PM2" = false ]; then
install_pm2
else
log_warning "跳过PM2安装"
fi
if ! clone_jsite_project; then
log_error "项目克隆失败,部署终止"
exit 1
fi
create_env_file
if [ "$SKIP_DEPENDENCIES" = false ]; then
if ! install_project_dependencies; then
log_error "项目依赖安装失败,部署终止"
exit 1
fi
else
log_warning "跳过项目依赖安装"
fi
if [ "$SKIP_PM2" = false ]; then
create_pm2_config
if ! start_project_with_pm2; then
log_error "PM2启动失败部署终止"
log_info "请检查以下内容:"
log_info "1. 项目依赖是否正确安装"
log_info "2. package.json中是否有start脚本"
log_info "3. 项目端口是否被占用"
log_info "4. 查看PM2日志: pm2 logs $SITE_NAME"
exit 1
fi
else
log_warning "跳过PM2配置和启动"
fi
if [ "$SKIP_TRAEFIK" = false ]; then
install_traefik
start_traefik
create_traefik_website_config
else
log_warning "跳过Traefik安装和启动"
fi
show_deployment_info
log_success "部署脚本执行完成!"
}
# 执行主函数
parse_arguments "$@"
main "$@"