#!/bin/bash # ======================================== # jsite前端自动化部署脚本 # ======================================== # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # ======================================== # 默认参数配置 # ======================================== # 基础配置 SITE_NAME="jingrow" GIT_REPO="http://git.jingrow.com/jsite/jingrow" NODE_VERSION="22" START_PORT=3001 PORT_INCREMENT=1 # jsite管理相关配置 JSITE_BASE_DIR="/home/jingrow/jsite" TRAEFIK_CONFIG_DIR="/home/jingrow/traefik-docker/conf.d/website" # 任务模式 MODE="deploy" # 默认模式:deploy, create, delete, build, start, stop, restart, status, list, logs # 跳过选项 SKIP_DOCKER=false SKIP_TRAEFIK=false SKIP_DEPENDENCIES=false SKIP_PM2=false FORCE_UPDATE=false # 网络配置 PUBLIC_IP="" # 公网IP地址 (用于内网IP不可用时) # .env文件参数 SITE_URL="example.com" SITE_EMAIL="email@example.com" REVALIDATE_TOKEN="" BACKEND_SERVER_URL="https://admin.example.com" BACKEND_API_KEY="" BACKEND_API_SECRET="" # ======================================== # 工具函数 # ======================================== # 智能IP选择:优先使用内网IP,没有内网IP时使用公网IP get_optimal_host_ip() { local host_ip="" # 方法1: 尝试获取内网IP local private_ip=$(ip route get 8.8.8.8 2>/dev/null | awk '{print $7}' | head -1) if [ -n "$private_ip" ] && [ "$private_ip" != "8.8.8.8" ]; then # 测试内网IP是否可达 if ping -c 2 -W 1 "$private_ip" >/dev/null 2>&1; then host_ip="$private_ip" # 注意:这里不输出日志,避免污染返回值 else # 注意:这里不输出日志,避免污染返回值 : fi fi # 方法2: 如果没有内网IP或内网IP不可达,使用公网IP if [ -z "$host_ip" ]; then if [ -n "$PUBLIC_IP" ]; then host_ip="$PUBLIC_IP" # 注意:这里不输出日志,避免污染返回值 else # 尝试自动获取公网IP local auto_public_ip=$(curl -s --max-time 5 ifconfig.me 2>/dev/null || curl -s --max-time 5 ipinfo.io/ip 2>/dev/null) if [ -n "$auto_public_ip" ]; then host_ip="$auto_public_ip" # 注意:这里不输出日志,避免污染返回值 else # 最后回退:使用第一个可用IP host_ip=$(hostname -I | awk '{print $1}') # 注意:这里不输出日志,避免污染返回值 fi fi fi echo "$host_ip" } # PM2命令执行函数(统一处理环境变量) execute_pm2_command() { local command="$1" su - jingrow -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" $command " 2>/dev/null } # 解析命令行参数 while [[ $# -gt 0 ]]; do case $1 in # 基础配置 --site-name) SITE_NAME="$2" shift 2 ;; --git-repo) GIT_REPO="$2" shift 2 ;; --node-version) NODE_VERSION="$2" shift 2 ;; --start-port) START_PORT="$2" shift 2 ;; --port-increment) PORT_INCREMENT="$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 ;; # 任务模式 --mode) MODE="$2" shift 2 ;; # 网络配置 --public-ip) PUBLIC_IP="$2" shift 2 ;; # .env文件参数 --site-url) SITE_URL="$2" shift 2 ;; --revalidate-token) REVALIDATE_TOKEN="$2" shift 2 ;; --backend-server-url) BACKEND_SERVER_URL="$2" shift 2 ;; --backend-api-key) BACKEND_API_KEY="$2" shift 2 ;; --backend-api-secret) BACKEND_API_SECRET="$2" shift 2 ;; --site-email) SITE_EMAIL="$2" shift 2 ;; # 帮助信息 -h|--help) echo "用法: $0 [选项]" echo "" echo "任务模式 (--mode):" echo " deploy 完整部署模式 (默认)" echo " create 创建新网站" echo " create_and_start 创建并启动网站(包含构建和启动)" echo " delete 删除网站" echo " build 构建网站" echo " start 启动网站" echo " stop 停止网站" echo " restart 重启网站" echo " status 查看网站状态" echo " list 列出所有网站" echo " logs 查看网站日志" echo " autostartup 配置网站自动启动" echo " setup_env 仅安装运行环境(Docker、jq、Node、PM2、Traefik)" echo "" echo "基础配置:" echo " --site-name NAME 项目名称 (默认: jingrow)" echo " --git-repo URL Git仓库地址" echo " --node-version VER Node.js版本 (默认: 22)" echo " --start-port PORT 起始端口 (默认: 3001)" echo " --port-increment INC 端口增量 (默认: 1)" echo "" echo "跳过选项:" echo " --skip-docker 跳过Docker安装" echo " --skip-traefik 跳过Traefik安装" echo " --skip-dependencies 跳过依赖安装" echo " --skip-pm2 跳过PM2安装" echo " --force-update 强制更新所有文件" echo "" echo "网络配置:" echo " --public-ip IP 公网IP地址 (用于内网IP不可用时)" echo "" echo ".env文件配置:" echo " --site-url URL 网站URL (默认: example.com)" echo " --site-email EMAIL 网站邮箱 (默认: email@example.com)" echo " --revalidate-token TK 重新验证令牌" echo " --backend-server-url URL 服务器URL (默认: https://admin.example.com)" echo " --backend-api-key KEY API密钥" echo " --backend-api-secret SECRET API密钥" echo "" echo " -h, --help 显示此帮助信息" echo "" echo "示例:" echo " # 完整部署" echo " $0 --mode deploy --site-name myproject --public-ip 8.217.167.199" echo " # 创建网站" echo " $0 --mode create --site-name myproject --git-repo http://git.example.com/project" echo " # 创建并启动网站" echo " $0 --mode create_and_start --site-name myproject --git-repo http://git.example.com/project" echo " # 管理网站" echo " $0 --mode start --site-name myproject" echo " $0 --mode stop --site-name myproject" echo " $0 --mode status --site-name myproject" echo " $0 --mode list" echo " # 配置自动启动" echo " $0 --mode autostartup --site-name myproject" exit 0 ;; *) echo "未知选项: $1" echo "使用 --help 查看帮助信息" exit 1 ;; esac done set -e # 遇到错误时退出 # 设置非交互式环境变量,避免交互式配置 export DEBIAN_FRONTEND=noninteractive export DEBCONF_NONINTERACTIVE_SEEN=true export UCF_FORCE_CONFNEW=1 export UCF_FORCE_CONFOLD=1 # 网络连通性检测 check_network_connectivity() { log_info "检测网络连通性..." local test_urls=( "https://mirrors.aliyun.com" "https://registry.npmmirror.com" "https://gitee.com" "https://mirrors.ustc.edu.cn" "https://raw.githubusercontent.com" ) local network_ok=false for url in "${test_urls[@]}"; do log_info "测试连接: $url" if curl -fsSL --connect-timeout 5 --max-time 10 "$url" >/dev/null 2>&1; then log_success "网络连接正常: $url" network_ok=true break else log_warning "网络连接失败: $url" fi done if [ "$network_ok" = false ]; then log_error "所有网络连接测试都失败,请检查网络配置" return 1 fi log_success "网络连通性检测完成" } # 配置APT使用国内镜像源,提高下载速度 configure_apt_mirrors() { log_info "配置APT使用国内镜像源..." # 备份原始源列表 if [ ! -f /etc/apt/sources.list.backup ]; then cp /etc/apt/sources.list /etc/apt/sources.list.backup log_info "已备份原始APT源列表" fi # 检测系统版本 local codename=$(lsb_release -cs 2>/dev/null || echo "jammy") # 创建新的源列表,使用多个国内镜像源(按优先级排序) cat > /etc/apt/sources.list << EOF # 1. 阿里云镜像源(国内最快) deb https://mirrors.aliyun.com/ubuntu/ ${codename} main restricted universe multiverse deb https://mirrors.aliyun.com/ubuntu/ ${codename}-security main restricted universe multiverse deb https://mirrors.aliyun.com/ubuntu/ ${codename}-updates main restricted universe multiverse deb https://mirrors.aliyun.com/ubuntu/ ${codename}-proposed main restricted universe multiverse deb https://mirrors.aliyun.com/ubuntu/ ${codename}-backports main restricted universe multiverse # 2. 腾讯云镜像源(备用) deb https://mirrors.cloud.tencent.com/ubuntu/ ${codename} main restricted universe multiverse deb https://mirrors.cloud.tencent.com/ubuntu/ ${codename}-security main restricted universe multiverse deb https://mirrors.cloud.tencent.com/ubuntu/ ${codename}-updates main restricted universe multiverse deb https://mirrors.cloud.tencent.com/ubuntu/ ${codename}-proposed main restricted universe multiverse deb https://mirrors.cloud.tencent.com/ubuntu/ ${codename}-backports main restricted universe multiverse # 3. 华为云镜像源(备用) deb https://mirrors.huaweicloud.com/ubuntu/ ${codename} main restricted universe multiverse deb https://mirrors.huaweicloud.com/ubuntu/ ${codename}-security main restricted universe multiverse deb https://mirrors.huaweicloud.com/ubuntu/ ${codename}-updates main restricted universe multiverse deb https://mirrors.huaweicloud.com/ubuntu/ ${codename}-proposed main restricted universe multiverse deb https://mirrors.huaweicloud.com/ubuntu/ ${codename}-backports main restricted universe multiverse # 4. 中科大镜像源(备用) deb https://mirrors.ustc.edu.cn/ubuntu/ ${codename} main restricted universe multiverse deb https://mirrors.ustc.edu.cn/ubuntu/ ${codename}-security main restricted universe multiverse deb https://mirrors.ustc.edu.cn/ubuntu/ ${codename}-updates main restricted universe multiverse deb https://mirrors.ustc.edu.cn/ubuntu/ ${codename}-proposed main restricted universe multiverse deb https://mirrors.ustc.edu.cn/ubuntu/ ${codename}-backports main restricted universe multiverse # 源码镜像源(使用阿里云,速度最快) deb-src https://mirrors.aliyun.com/ubuntu/ ${codename} main restricted universe multiverse deb-src https://mirrors.aliyun.com/ubuntu/ ${codename}-security main restricted universe multiverse deb-src https://mirrors.aliyun.com/ubuntu/ ${codename}-updates main restricted universe multiverse deb-src https://mirrors.aliyun.com/ubuntu/ ${codename}-proposed main restricted universe multiverse deb-src https://mirrors.aliyun.com/ubuntu/ ${codename}-backports main restricted universe multiverse EOF log_success "APT镜像源配置完成" } # 日志函数 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 } # 检查jsite基础目录 check_jsite_base_dir() { if [ ! -d "$JSITE_BASE_DIR" ]; then log_info "创建jsite基础目录: $JSITE_BASE_DIR" mkdir -p "$JSITE_BASE_DIR" chown "jingrow:jingrow" "$JSITE_BASE_DIR" fi } # 检查网站目录是否存在 check_site_exists() { local site_name="$1" [ -d "$JSITE_BASE_DIR/$site_name" ] } # 获取网站状态 get_site_status() { local site_name="$1" if ! check_site_exists "$site_name"; then echo "not_exists" return fi # 检查PM2进程状态(避免因为多次输出"0"导致误判) local pm2_count=$(su - "jingrow" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 list 2>/dev/null | grep -w '$site_name' | grep 'online' | wc -l " | tr -d '\r') if [ -n "$pm2_count" ] && [ "$pm2_count" -gt 0 ]; then echo "running" else echo "stopped" 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..." # 尝试多个NVM下载源,按优先级排序(国内镜像优先) local nvm_install_success=false local nvm_sources=( "https://gitee.com/mirrors/nvm/raw/v0.40.3/install.sh" "https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.40.3/install.sh" "https://npmmirror.com/mirrors/nvm/v0.40.3/install.sh" "https://mirrors.ustc.edu.cn/nvm/v0.40.3/install.sh" "https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh" ) for nvm_source in "${nvm_sources[@]}"; do log_info "尝试从 $nvm_source 下载nvm..." # 设置较短的超时时间,避免长时间等待 if su - jingrow -c "curl -fsSL --connect-timeout 10 --max-time 60 '$nvm_source' | bash"; then log_success "nvm安装完成 (来源: $nvm_source)" nvm_install_success=true break else log_warning "从 $nvm_source 下载失败,尝试下一个源..." fi done if [ "$nvm_install_success" = false ]; then log_error "所有NVM下载源都失败,请检查网络连接" return 1 fi 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 " # 配置npm使用国内镜像源,按优先级排序 log_info "配置npm使用国内镜像源..." su - jingrow -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" # 1. 淘宝NPM镜像(国内最快) npm config set registry https://registry.npmmirror.com # 2. 阿里云NPM镜像(备用) npm config set registry https://registry.npm.alibaba-inc.com # 3. 腾讯云NPM镜像(备用) npm config set registry https://mirrors.cloud.tencent.com/npm/ # 4. 华为云NPM镜像(备用) npm config set registry https://mirrors.huaweicloud.com/repository/npm/ # 5. 中科大NPM镜像(备用) npm config set registry https://registry.npm.ustc.edu.cn/ # 最终使用淘宝镜像(最快) npm config set registry https://registry.npmmirror.com echo 'npm镜像源配置完成(已配置5个备用源)' " # 验证安装 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" } # 智能NPM安装函数(支持多镜像源回退) smart_npm_install() { local package="$1" local install_dir="$2" local is_global="${3:-false}" local npm_registries=( "https://registry.npmmirror.com" # 1. 淘宝NPM镜像(国内最快) "https://registry.npm.alibaba-inc.com" # 2. 阿里云NPM镜像(备用) "https://mirrors.cloud.tencent.com/npm/" # 3. 腾讯云NPM镜像(备用) "https://mirrors.huaweicloud.com/repository/npm/" # 4. 华为云NPM镜像(备用) "https://registry.npm.ustc.edu.cn/" # 5. 中科大NPM镜像(备用) ) local install_success=false for registry in "${npm_registries[@]}"; do log_info "尝试从 $registry 安装 $package..." local install_cmd="" if [ "$is_global" = "true" ]; then install_cmd="npm install -g $package --registry=$registry" else if [ -n "$install_dir" ]; then install_cmd="cd '$install_dir' && npm install --registry=$registry" else install_cmd="npm install --registry=$registry" fi fi if su - jingrow -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" $install_cmd "; then log_success "$package 安装成功 (来源: $registry)" install_success=true break else log_warning "从 $registry 安装失败,尝试下一个源..." fi done if [ "$install_success" = false ]; then log_error "所有NPM镜像源都失败,无法安装 $package" return 1 fi return 0 } # 2.5. 安装PM2 install_pm2() { log_info "开始安装PM2..." # 确保NVM环境变量被正确配置到用户配置文件中 log_info "配置NVM环境变量..." # 检查 ~/.bashrc 中是否已有NVM配置 if ! su - jingrow -c "grep -q 'NVM_DIR' ~/.bashrc 2>/dev/null"; then log_info "在 ~/.bashrc 中添加NVM配置..." su - jingrow -c "echo 'export NVM_DIR=\"\$HOME/.nvm\"' >> ~/.bashrc" su - jingrow -c "echo '[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"' >> ~/.bashrc" su - jingrow -c "echo '[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\"' >> ~/.bashrc" fi # 检查 ~/.profile 中是否已有NVM配置(用于非交互式shell) if ! su - jingrow -c "grep -q 'NVM_DIR' ~/.profile 2>/dev/null"; then log_info "在 ~/.profile 中添加NVM配置..." su - jingrow -c "echo 'export NVM_DIR=\"\$HOME/.nvm\"' >> ~/.profile" su - jingrow -c "echo '[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"' >> ~/.profile" fi # 检查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..." if ! smart_npm_install "pm2" "" "true"; then log_error "PM2安装失败" return 1 fi log_success "PM2安装完成" fi # 设置PM2开机自启 log_info "配置PM2开机自启..." local node_path=$(ls -d /home/jingrow/.nvm/versions/node/v* 2>/dev/null | head -1) sudo env PATH="$PATH:$node_path/bin" pm2 startup systemd -u jingrow --hp /home/jingrow --service-name pm2-jingrow --silent 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工具已安装 ensure_jq_installed # 使用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 # 检查端口文件是否有效(包含有效的端口分配) local port_file_valid=false if [ -f "$port_file" ]; then # 确保jq工具已安装 ensure_jq_installed if command -v jq &> /dev/null; then # 使用jq检查文件是否包含有效的端口分配 local valid_ports=$(jq -r '.[] | select(type == "number" and . > 0)' "$port_file" 2>/dev/null || echo "") if [ -n "$valid_ports" ]; then port_file_valid=true fi else # 使用grep检查文件是否包含有效的端口分配 local valid_ports=$(grep -o '"[^"]*"[[:space:]]*:[[:space:]]*[0-9]\+' "$port_file" 2>/dev/null | cut -d: -f2 | tr -d ' ' | grep -E '^[0-9]+$' || echo "") if [ -n "$valid_ports" ]; then port_file_valid=true fi fi fi if [ "$user_specified_port" = true ]; then # 用户传入了自定义端口,优先使用用户指定的端口 # 检查用户指定的端口是否已被使用 local port_available=true if [ "$port_file_valid" = true ]; then if command -v jq &> /dev/null; then local used_ports=$(jq -r '.[] | select(type == "number" and . > 0)' "$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:]]*:[[: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 [ "$port_file_valid" = true ]; then if command -v jq &> /dev/null; then local max_port=$(jq -r 'max(.[] | select(type == "number" and . > 0))' "$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:]]*:[[: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 [ "$port_file_valid" = true ]; then if command -v jq &> /dev/null; then # 使用jq找到最大端口号 local max_port=$(jq -r 'max(.[] | select(type == "number" and . > 0))' "$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:]]*:[[: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 # 简单修复:确保分配的端口不重复 if [ "$port_file_valid" = true ]; then local used_ports="" if command -v jq &> /dev/null; then used_ports=$(jq -r '.[] | select(type == "number" and . > 0)' "$port_file" 2>/dev/null || echo "") else used_ports=$(grep -o '"[^"]*"[[:space:]]*:[[:space:]]*[0-9]\+' "$port_file" 2>/dev/null | cut -d: -f2 | tr -d ' ' || echo "") fi # 如果端口已被使用,递增直到找到可用端口 while echo "$used_ports" | grep -q "^$next_port$"; do next_port=$((next_port + increment)) done 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工具已安装 ensure_jq_installed # 检查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 # 提取现有条目并重新构建 local existing_entries=$(grep -o '"[^"]*"[[:space:]]*:[[:space:]]*[0-9]*' "$port_file" 2>/dev/null | grep -v "\"$site_name\"" || echo "") # 重新构建JSON文件 echo "{" > "$port_file" if [ -n "$existing_entries" ]; then echo "$existing_entries" | sed 's/$/,/' | head -n -1 >> "$port_file" echo "$existing_entries" | tail -n 1 >> "$port_file" echo "," >> "$port_file" fi echo " \"$site_name\": $port" >> "$port_file" echo "}" >> "$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 # 确保jq工具已安装 ensure_jq_installed 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 "当前端口分配情况:" # 确保jq工具已安装 ensure_jq_installed 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 # 配置Git使用更快的协议和超时设置 su - jingrow -c " git config --global http.lowSpeedLimit 0 git config --global http.lowSpeedTime 999999 git config --global http.postBuffer 524288000 git config --global core.compression 9 cd /home/jingrow/jsite && git clone --depth 1 $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" # 配置Git使用更快的协议和超时设置 su - jingrow -c " git config --global http.lowSpeedLimit 0 git config --global http.lowSpeedTime 999999 git config --global http.postBuffer 524288000 git config --global core.compression 9 cd /home/jingrow/jsite && git clone --depth 1 $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") # 智能IP选择:优先使用内网IP,没有内网IP时使用公网IP(与traefik配置保持一致) local host_ip=$(get_optimal_host_ip) # 输出IP选择结果 if [[ "$host_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then log_success "使用IP地址: $host_ip" else log_warning "IP地址格式异常: $host_ip" return 1 fi # 构建PUBLIC_SITE_URL(优先使用内网IP,没有内网IP时使用公网IP) local public_site_url="http://$host_ip:$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 # 项目配置 BACKEND_SITE_NAME=$SITE_NAME # 服务器配置 BACKEND_SERVER_URL=$BACKEND_SERVER_URL BACKEND_API_KEY=$BACKEND_API_KEY BACKEND_API_SECRET=$BACKEND_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 # 确保NVM环境变量被正确加载 log_info "加载NVM环境变量..." # 检查项目是否已经在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 ") # 检查项目是否真的在运行(状态为online) 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_STATUS" != "0" ] && [ -n "$PM2_STATUS" ]; then log_warning "项目已存在于PM2中,删除旧进程..." su - jingrow -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 delete $SITE_NAME 2>/dev/null || true " # 等待一下确保删除完成 sleep 2 fi # 启动新项目 log_info "启动项目: $SITE_NAME..." 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 # 等待PM2启动完成,最多等待60秒 log_info "等待PM2启动完成..." local wait_time=0 local max_wait=60 local pm2_running="0" local max_retries=3 local retry_count=0 while [ $wait_time -lt $max_wait ] && [ "$pm2_running" = "0" ]; do sleep 2 wait_time=$((wait_time + 2)) # 检查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_info "等待PM2启动... (${wait_time}s/${max_wait}s)" # 如果等待时间超过30秒,检查是否有错误 if [ $wait_time -gt 30 ] && [ $retry_count -lt $max_retries ]; then log_warning "启动时间较长,检查PM2状态和日志..." # 检查PM2状态 local 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' || echo 'not_found' ") if [ "$pm2_status" != "not_found" ]; then log_info "PM2状态: $pm2_status" fi # 检查PM2日志 local pm2_logs=$(su - jingrow -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 logs $SITE_NAME --lines 5 --nostream 2>/dev/null || echo 'no_logs' ") if [ "$pm2_logs" != "no_logs" ]; then log_info "PM2日志: $pm2_logs" fi retry_count=$((retry_count + 1)) fi fi done # 检查PM2进程是否真的启动了 if [ "$pm2_running" = "0" ]; then log_error "PM2启动失败,项目未正常运行(等待${wait_time}秒后仍未能启动)" 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 log_success "PM2进程已启动,开始健康检查..." # 获取项目端口 local project_port=$(get_or_assign_port "$SITE_NAME") # 等待应用完全启动 sleep 3 # 检查端口是否被监听(使用多种方法) log_info "检查端口 $project_port 是否被监听..." local port_check=0 local max_port_checks=10 local port_check_count=0 while [ $port_check_count -lt $max_port_checks ] && [ $port_check -eq 0 ]; do # 方法1: 使用netstat port_check=$(netstat -tlnp 2>/dev/null | grep ":$project_port " | wc -l || echo "0") # 方法2: 如果netstat失败,使用ss if [ $port_check -eq 0 ]; then port_check=$(ss -tlnp 2>/dev/null | grep ":$project_port " | wc -l || echo "0") fi # 方法3: 如果ss也失败,使用lsof if [ $port_check -eq 0 ]; then port_check=$(lsof -i :$project_port 2>/dev/null | wc -l || echo "0") fi if [ $port_check -eq 0 ]; then sleep 1 port_check_count=$((port_check_count + 1)) log_info "等待端口 $project_port 启动... (${port_check_count}/${max_port_checks})" fi done if [ $port_check -eq 0 ]; then log_error "项目启动失败:端口 $project_port 未被监听" 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 log_success "端口 $project_port 已被监听" # 检查PM2进程状态 log_info "检查PM2进程状态..." local 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' || echo 'not_found' ") if [ "$pm2_status" != "not_found" ]; then log_success "PM2进程状态正常" else log_error "PM2进程状态异常" return 1 fi log_success "项目已成功启动并监听端口 $project_port" # 保存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 " git config --global http.lowSpeedLimit 0 git config --global http.lowSpeedTime 999999 git config --global http.postBuffer 524288000 git config --global core.compression 9 cd /home/jingrow && git clone --depth 1 http://git.jingrow.com/go/traefik-docker " log_success "traefik项目克隆完成" # 设置目录权限 chown -R jingrow:jingrow /home/jingrow/traefik-docker log_success "设置traefik目录权限" # 删除已存在的traefik.yml配置文件(如果存在) rm -f "/home/jingrow/traefik-docker/traefik.yml" # 创建traefik.yml配置文件 log_info "创建traefik.yml配置文件..." cat > "/home/jingrow/traefik-docker/traefik.yml" << EOF entryPoints: web: address: ":80" websecure: address: ":443" traefik: address: ":8080" api: dashboard: true insecure: false # 生产环境建议关闭 providers: docker: exposedByDefault: false file: directory: /etc/traefik/conf.d watch: true certificatesResolvers: myresolver: acme: email: $SITE_EMAIL storage: /etc/traefik/acme.json httpChallenge: entryPoint: web EOF # 设置traefik.yml文件权限 chown jingrow:jingrow "/home/jingrow/traefik-docker/traefik.yml" chmod 664 "/home/jingrow/traefik-docker/traefik.yml" log_success "traefik.yml配置文件创建完成" # 设置 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 apt-transport-https ca-certificates curl gnupg lsb-release # 选择可用的 Docker APT 仓库与 GPG 地址(按优先级排序,国内镜像优先) local arch=$(dpkg --print-architecture) local codename=$(lsb_release -cs) local mirror_candidates=( "https://mirrors.aliyun.com/docker-ce" "https://mirrors.ccs.tencentyun.com/docker-ce" "https://mirrors.ustc.edu.cn/docker-ce" "https://mirrors.tuna.tsinghua.edu.cn/docker-ce" "https://download.docker.com" ) local chosen_base="" local chosen_gpg="" for base in "${mirror_candidates[@]}"; do log_info "尝试Docker镜像: ${base}" if curl -fsSL --connect-timeout 5 "${base}/linux/ubuntu/gpg" >/dev/null 2>&1; then chosen_base="$base" chosen_gpg="${base}/linux/ubuntu/gpg" log_success "选择Docker镜像: ${base}" break else log_warning "镜像不可达,继续回退: ${base}" fi done if [ -z "$chosen_base" ]; then log_error "无法访问任一 Docker APT 镜像,请检查网络(尤其是443端口)或稍后重试" return 1 fi # 添加Docker GPG密钥 curl -fsSL "$chosen_gpg" | gpg --batch --yes --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg || { log_error "下载或导入Docker GPG密钥失败: $chosen_gpg" return 1 } # 写入APT源 echo "deb [arch=${arch} signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] ${chosen_base}/linux/ubuntu ${codename} stable" \ > /etc/apt/sources.list.d/docker.list # 更新包索引 apt-get update # 安装Docker Engine(避免交互式配置) apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin # 启动Docker服务 systemctl start docker || true systemctl enable docker || true log_success "Docker安装完成" fi # 配置Docker镜像仓库地址 log_info "配置Docker镜像仓库地址..." # 创建Docker配置目录 mkdir -p /etc/docker # 创建或更新daemon.json配置文件(按优先级排序,国内镜像优先) cat > /etc/docker/daemon.json << EOF { "registry-mirrors": [ "https://k487mmwn.mirror.aliyuncs.com", "https://mirror.ccs.tencentyun.com", "https://docker.mirrors.ustc.edu.cn", "https://hub-mirror.c.163.com", "https://mirrors.tuna.tsinghua.edu.cn" ] } EOF # 重新加载systemd配置并重启Docker服务 log_info "重新加载systemd配置并重启Docker服务..." systemctl daemon-reload || true systemctl restart docker || true # 验证配置是否生效 log_info "验证Docker镜像仓库配置..." if docker info 2>/dev/null | grep -q "k487mmwn.mirror.aliyuncs.com"; then log_success "Docker镜像仓库配置成功" else log_warning "Docker镜像仓库配置可能未生效,请手动检查" fi # 将jingrow用户添加到docker组 usermod -aG docker jingrow || true log_success "将jingrow用户添加到docker组" } # 6.1 安装jq工具(如果未安装) install_jq() { log_info "检查jq工具安装状态..." if command -v jq &> /dev/null; then log_warning "jq工具已安装" else log_info "开始安装jq工具..." # 设置非交互式环境 export DEBIAN_FRONTEND=noninteractive export DEBCONF_NONINTERACTIVE_SEEN=true # 更新包索引 apt-get update # 安装jq工具 apt-get install -y jq if command -v jq &> /dev/null; then log_success "jq工具安装完成" else log_error "jq工具安装失败" return 1 fi fi } # 6.2 确保jq工具已安装 ensure_jq_installed() { if ! command -v jq &> /dev/null; then log_warning "jq工具未安装,正在安装..." if ! install_jq; then log_error "jq工具安装失败,无法处理JSON文件" return 1 fi fi } # 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 # 检查traefik是否已经在运行,如果运行则重启,否则启动 local traefik_running=$(su - jingrow -c "cd /home/jingrow/traefik-docker && docker compose ps -q" 2>/dev/null | wc -l) if [ "$traefik_running" -gt 0 ]; then log_info "traefik已在运行,重启服务以加载新配置..." su - jingrow -c "cd /home/jingrow/traefik-docker && docker compose restart" else log_info "启动新的traefik服务..." su - jingrow -c "cd /home/jingrow/traefik-docker && docker compose up -d" fi 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 if ! smart_npm_install "" "/home/jingrow/jsite/$SITE_NAME"; 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 --registry=https://registry.npmmirror.com " if [ $? -ne 0 ]; then log_error "项目构建失败" return 1 fi log_success "项目构建完成 (内存限制: ${build_memory_mb}MB)" } # 9. 显示部署信息 show_deployment_info() { log_success "=== 部署完成 ===" 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" if [ -n "$PUBLIC_IP" ]; then echo " - 指定公网IP: $PUBLIC_IP" fi log_info ".env文件配置:" if [ -f "/home/jingrow/jsite/$SITE_NAME/.env" ]; then echo " - PUBLIC_SITE_URL: $(grep '^PUBLIC_SITE_URL=' "/home/jingrow/jsite/$SITE_NAME/.env" | cut -d'=' -f2-)" echo " - BACKEND_SITE_NAME: $(grep '^BACKEND_SITE_NAME=' "/home/jingrow/jsite/$SITE_NAME/.env" | cut -d'=' -f2-)" echo " - BACKEND_SERVER_URL: $(grep '^BACKEND_SERVER_URL=' "/home/jingrow/jsite/$SITE_NAME/.env" | cut -d'=' -f2-)" echo " - REVALIDATE_TOKEN: $(grep '^REVALIDATE_TOKEN=' "/home/jingrow/jsite/$SITE_NAME/.env" | cut -d'=' -f2-)" echo " - BACKEND_API_KEY: $(grep '^BACKEND_API_KEY=' "/home/jingrow/jsite/$SITE_NAME/.env" | cut -d'=' -f2-)" echo " - BACKEND_API_SECRET: $(grep '^BACKEND_API_SECRET=' "/home/jingrow/jsite/$SITE_NAME/.env" | cut -d'=' -f2-)" else echo " - .env文件不存在" fi log_info "Traefik配置信息:" echo " - 网站配置文件: /home/jingrow/traefik-docker/conf.d/website/$SITE_NAME.yml" echo " - 主配置文件: /home/jingrow/traefik-docker/traefik.yml" echo " - SSL证书邮箱: $SITE_EMAIL" echo " - 访问域名: $SITE_URL" echo " - 后端端口: $(get_or_assign_port "$SITE_NAME")" log_info "PM2管理命令:" echo " - 查看状态: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 list'" echo " - 查看日志: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 logs $SITE_NAME'" echo " - 重启项目: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 restart $SITE_NAME'" echo " - 停止项目: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 stop $SITE_NAME'" echo " - 删除项目: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 delete $SITE_NAME'" echo " - 监控界面: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 monit'" log_info "Traefik配置信息:" echo " - 网站配置文件: /home/jingrow/traefik-docker/conf.d/website/$SITE_NAME.yml" echo " - 访问域名: $SITE_URL" echo " - 后端端口: $(get_or_assign_port "$SITE_NAME")" # 显示当前端口分配情况 show_port_assignments # 显示PM2状态 log_info "当前PM2状态:" su - jingrow -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 list " 2>/dev/null || log_warning "无法获取PM2状态,请手动执行: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 list'" log_info "下一步操作:" echo " 1. 进入项目目录: cd /home/jingrow/jsite/$SITE_NAME" echo " 2. 查看PM2状态: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 list'" echo " 3. 查看项目日志: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && 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" log_warning "注意:请确保防火墙允许80、443、8080端口访问" log_success "部署脚本执行完成!" } # ======================================== # jsite管理功能函数 # ======================================== # 仅安装运行环境(不创建/克隆项目) setup_env() { log_info "开始安装运行环境..." if [ "$SKIP_DOCKER" = false ]; then install_docker else log_warning "跳过Docker安装" fi install_jq install_nodejs if [ "$SKIP_PM2" = false ]; then install_pm2 else log_warning "跳过PM2安装" fi if [ "$SKIP_TRAEFIK" = false ]; then install_traefik start_traefik else log_warning "跳过Traefik安装和启动" fi log_success "运行环境安装完成" } # 生成Host规则 generate_host_rule() { local domain="$1" # 移除协议前缀 domain=$(echo "$domain" | sed -E 's|^https?://||') # 移除端口号 domain=$(echo "$domain" | sed -E 's|:[0-9]+$||') # 检查是否为一级域名 if [[ "$domain" =~ ^[^.]+\.[^.]+$ ]]; then echo "Host(\`$domain\`) || Host(\`www.$domain\`)" else echo "Host(\`$domain\`)" fi } # 创建traefik网站配置文件 create_traefik_website_config() { local site_name="$1" local domain="$2" local port="$3" # 自动创建traefik配置目录(如果不存在) if [ ! -d "$TRAEFIK_CONFIG_DIR" ]; then log_info "创建Traefik配置目录: $TRAEFIK_CONFIG_DIR" mkdir -p "$TRAEFIK_CONFIG_DIR" chown jingrow:jingrow "$TRAEFIK_CONFIG_DIR" chmod 755 "$TRAEFIK_CONFIG_DIR" fi local config_file="$TRAEFIK_CONFIG_DIR/$site_name.yml" local host_rule=$(generate_host_rule "$domain") # 获取本机IP地址 local host_ip=$(get_optimal_host_ip) # 创建配置文件 cat > "$config_file" << EOF 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://$host_ip:$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" 2>/dev/null || true chmod 644 "$config_file" log_success "Traefik配置文件已创建: $config_file" log_info "域名: $domain -> $host_ip:$port" } # 删除traefik网站配置文件 remove_traefik_website_config() { local site_name="$1" local config_file="$TRAEFIK_CONFIG_DIR/$site_name.yml" if [ -f "$config_file" ]; then rm -f "$config_file" log_success "Traefik配置文件已删除: $config_file" fi } # 重启traefik服务以加载新配置 restart_traefik() { local traefik_dir="/home/jingrow/traefik-docker" if [ -d "$traefik_dir" ] && [ -f "$traefik_dir/docker-compose.yml" ]; then log_info "重启Traefik服务以加载新配置..." su - "jingrow" -c "cd '$traefik_dir' && docker compose restart" 2>/dev/null || { log_warning "无法重启Traefik服务,请手动重启" } fi } # 创建新网站 create_site() { log_info "开始创建网站: $SITE_NAME" log_info "Git仓库: $GIT_REPO" log_info "域名: $SITE_URL" # 检查网站是否已存在 if check_site_exists "$SITE_NAME"; then log_error "网站 $SITE_NAME 已存在" return 1 fi # 获取并分配端口 local port=$(get_or_assign_port "$SITE_NAME") # 克隆项目 log_info "克隆项目: $GIT_REPO" if ! su - "jingrow" -c " git config --global http.lowSpeedLimit 0 git config --global http.lowSpeedTime 999999 git config --global http.postBuffer 524288000 git config --global core.compression 9 cd '$JSITE_BASE_DIR' && git clone --depth 1 '$GIT_REPO' '$SITE_NAME' "; then log_error "项目克隆失败" return 1 fi # 检查package.json是否存在 if [ ! -f "$JSITE_BASE_DIR/$SITE_NAME/package.json" ]; then log_error "package.json文件不存在,这可能不是一个有效的Node.js项目" rm -rf "$JSITE_BASE_DIR/$SITE_NAME" return 1 fi # 保存端口分配 save_port_assignment "$SITE_NAME" "$port" # 创建.env文件 create_env_file # 创建PM2配置文件 create_pm2_config # 创建traefik配置文件 if create_traefik_website_config "$SITE_NAME" "$SITE_URL" "$port"; then restart_traefik fi log_success "网站 $SITE_NAME 创建成功" log_info "项目路径: $JSITE_BASE_DIR/$SITE_NAME" log_info "分配端口: $port" log_info "绑定域名: $SITE_URL" # 提示下一步操作 log_info "下一步操作:" echo " 1. 构建项目: $0 --mode build --site-name $SITE_NAME" echo " 2. 启动项目: $0 --mode start --site-name $SITE_NAME" } # 创建并启动网站(包含构建和启动) create_and_start_site() { log_info "开始创建并启动网站: $SITE_NAME" log_info "Git仓库: $GIT_REPO" log_info "域名: $SITE_URL" # 检查网站是否已存在 if check_site_exists "$SITE_NAME"; then log_error "网站 $SITE_NAME 已存在" return 1 fi # 获取并分配端口 local port=$(get_or_assign_port "$SITE_NAME") # 克隆项目 log_info "克隆项目: $GIT_REPO" if ! su - jingrow -c " git config --global http.lowSpeedLimit 0 git config --global http.lowSpeedTime 999999 git config --global http.postBuffer 524288000 git config --global core.compression 9 cd '$JSITE_BASE_DIR' && git clone --depth 1 '$GIT_REPO' '$SITE_NAME' "; then log_error "项目克隆失败" return 1 fi # 检查package.json是否存在 if [ ! -f "$JSITE_BASE_DIR/$SITE_NAME/package.json" ]; then log_error "package.json文件不存在,这可能不是一个有效的Node.js项目" rm -rf "$JSITE_BASE_DIR/$SITE_NAME" return 1 fi # 保存端口分配 save_port_assignment "$SITE_NAME" "$port" # 创建.env文件 create_env_file # 创建PM2配置文件 create_pm2_config # 创建traefik配置文件 if create_traefik_website_config "$SITE_NAME" "$SITE_URL" "$port"; then restart_traefik fi # 安装项目依赖 log_info "安装项目依赖..." if ! smart_npm_install "" "$JSITE_BASE_DIR/$SITE_NAME"; then log_error "依赖安装失败" return 1 fi # 构建项目 log_info "构建项目..." if ! su - jingrow -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" cd '$JSITE_BASE_DIR/$SITE_NAME' npm run build "; then log_error "项目构建失败" return 1 fi # 启动项目 log_info "启动项目..." if ! start_project_with_pm2; then log_error "项目启动失败" return 1 fi log_success "网站 $SITE_NAME 创建、构建和启动成功" log_info "项目路径: $JSITE_BASE_DIR/$SITE_NAME" log_info "分配端口: $port" log_info "绑定域名: $SITE_URL" # 显示部署信息 log_info "部署信息:" echo " - 用户: jingrow" echo " - jsite目录: $JSITE_BASE_DIR" echo " - 项目目录: $JSITE_BASE_DIR/$SITE_NAME" echo " - 项目端口: $port" echo " - Traefik目录: /home/jingrow/traefik-docker" echo " - Traefik管理界面: http://localhost:8080" echo " - Traefik网站配置: $TRAEFIK_CONFIG_DIR/$SITE_NAME.yml" echo " - Traefik主配置: /home/jingrow/traefik-docker/traefik.yml" echo " - SSL证书邮箱: $SITE_EMAIL" echo " - PM2配置文件: $JSITE_BASE_DIR/$SITE_NAME/ecosystem.config.cjs" echo " - PM2日志目录: $JSITE_BASE_DIR/$SITE_NAME/logs" log_info "PM2管理命令:" echo " - 查看状态: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 list'" echo " - 查看日志: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 logs $SITE_NAME'" echo " - 重启项目: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 restart $SITE_NAME'" echo " - 停止项目: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 stop $SITE_NAME'" echo " - 删除项目: su - jingrow -c 'export NVM_DIR=\"\$HOME/.nvm\" && [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && pm2 delete $SITE_NAME'" log_info "访问信息:" echo " - 网站域名: $SITE_URL" echo " - 本地访问: http://localhost:$port" # 保存PM2进程列表,确保重启后能自动恢复 log_info "保存PM2进程列表..." su - jingrow -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 save " log_success "网站创建、构建和启动完成!" } # 删除网站 delete_site() { log_info "开始删除网站: $SITE_NAME" # 停止PM2进程 local status=$(get_site_status "$SITE_NAME") if [ "$status" = "running" ]; then log_info "停止运行中的进程..." if ! stop_site; then log_warning "停止进程失败,继续删除操作" fi elif [ "$status" = "not_exists" ]; then log_warning "网站 $SITE_NAME 不存在,跳过停止操作" else log_info "网站已停止,无需停止操作" fi # 删除PM2配置 log_info "删除PM2配置..." su - "jingrow" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 delete '$SITE_NAME' 2>/dev/null || true " log_success "PM2配置已清理" # 删除traefik配置文件 log_info "删除Traefik配置文件..." if remove_traefik_website_config "$SITE_NAME"; then log_success "Traefik配置文件已删除" else log_warning "Traefik配置文件删除失败或不存在" fi # 删除端口分配记录 log_info "删除端口分配记录..." local port_file="$JSITE_BASE_DIR/site_port.json" # 确保jq工具已安装 ensure_jq_installed if [ -f "$port_file" ] && command -v jq &> /dev/null; then if jq "del(.$SITE_NAME)" "$port_file" > "${port_file}.tmp" 2>/dev/null; then mv "${port_file}.tmp" "$port_file" chown "jingrow:jingrow" "$port_file" 2>/dev/null || true log_success "端口分配记录已删除" else log_warning "端口分配记录删除失败" fi else log_warning "端口分配文件不存在或jq命令不可用" fi # 删除项目目录 log_info "删除项目目录: $JSITE_BASE_DIR/$SITE_NAME" if [ -d "$JSITE_BASE_DIR/$SITE_NAME" ]; then if rm -rf "$JSITE_BASE_DIR/$SITE_NAME"; then log_success "项目目录已删除" else log_error "删除项目目录失败" return 1 fi else log_warning "项目目录不存在" fi # 重启traefik以移除配置 log_info "重启Traefik服务..." if restart_traefik; then log_success "Traefik服务已重启" else log_warning "重启Traefik失败,请手动重启" fi log_success "网站 $SITE_NAME 已删除" } # 构建网站 build_site() { log_info "构建网站: $SITE_NAME" # 安装依赖 log_info "安装依赖..." if ! smart_npm_install "" "$JSITE_BASE_DIR/$SITE_NAME"; then log_error "依赖安装失败" return 1 fi # 构建项目 log_info "构建项目..." if ! su - "jingrow" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" cd '$JSITE_BASE_DIR/$SITE_NAME' npm run build "; then log_error "项目构建失败" return 1 fi log_success "网站 $SITE_NAME 构建完成" } # 启动网站 start_site() { log_info "启动网站: $SITE_NAME" restart_site return $? } # 停止网站 stop_site() { local status=$(get_site_status "$SITE_NAME") if [ "$status" = "not_exists" ]; then log_error "网站 $SITE_NAME 不存在" return 1 fi if [ "$status" = "stopped" ]; then log_warning "网站 $SITE_NAME 已停止" return 0 fi log_info "停止网站: $SITE_NAME" if ! su - "jingrow" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 stop '$SITE_NAME' "; then log_error "网站停止失败" return 1 fi log_success "网站 $SITE_NAME 已停止" } # 重启网站 restart_site() { local status=$(get_site_status "$SITE_NAME") if [ "$status" = "not_exists" ]; then log_error "网站 $SITE_NAME 不存在" return 1 fi log_info "重启网站: $SITE_NAME" if [ "$status" = "running" ]; then if ! su - "jingrow" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 restart '$SITE_NAME' "; then log_warning "pm2 restart 失败,尝试重新创建并启动..." if ! start_project_with_pm2; then log_error "网站重启失败" return 1 fi fi else # 若未运行,则用PM2创建并启动 if ! start_project_with_pm2; then log_error "网站启动失败" return 1 fi fi # 保存PM2进程列表,确保重启后能自动恢复 log_info "保存PM2进程列表..." su - jingrow -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 save " log_success "网站 $SITE_NAME 重启成功" } # 查看网站状态 show_site_status() { local status=$(get_site_status "$SITE_NAME") case "$status" in "not_exists") log_error "网站 $SITE_NAME 不存在" return 1 ;; "running") log_success "网站 $SITE_NAME 正在运行" ;; "stopped") log_warning "网站 $SITE_NAME 已停止" ;; esac # 显示PM2详细状态 if [ "$status" != "not_exists" ]; then log_info "PM2状态:" su - "jingrow" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 list | grep -E '(App name|$SITE_NAME)' || echo '未找到PM2进程' " fi } # 列出所有网站 list_sites() { log_info "jsite网站列表:" if [ ! -d "$JSITE_BASE_DIR" ] || [ -z "$(ls -A "$JSITE_BASE_DIR" 2>/dev/null)" ]; then echo " 暂无网站" return 0 fi for site_dir in "$JSITE_BASE_DIR"/*; do if [ -d "$site_dir" ]; then local site_name=$(basename "$site_dir") local status=$(get_site_status "$site_name") case "$status" in "running") echo " - $site_name (运行中)" ;; "stopped") echo " - $site_name (已停止)" ;; *) echo " - $site_name (未知状态)" ;; esac fi done } # 查看网站日志 show_site_logs() { local status=$(get_site_status "$SITE_NAME") if [ "$status" = "not_exists" ]; then log_error "网站 $SITE_NAME 不存在" return 1 fi log_info "网站 $SITE_NAME 的日志:" su - "jingrow" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 logs '$SITE_NAME' " } # 配置网站自动启动 autostartup_site() { log_info "配置网站 $SITE_NAME 自动启动..." # 检查网站是否存在 if ! check_site_exists "$SITE_NAME"; then log_error "网站 $SITE_NAME 不存在,请先创建网站" return 1 fi # 保存PM2进程列表 log_info "保存PM2进程列表..." local node_path=$(ls -d /home/jingrow/.nvm/versions/node/v* 2>/dev/null | head -1) sudo env PATH="$PATH:$node_path/bin" su - jingrow -c "pm2 save --silent" log_success "PM2进程列表保存完成" log_success "网站 $SITE_NAME 自动启动配置完成" log_info "服务器重启后,网站将自动启动" log_info "如需禁用自动启动,请使用: pm2 unstartup" } # ======================================== # 主函数 # ======================================== # 主函数 main() { log_info "开始jsite管理脚本..." echo "" log_info "执行模式: $MODE" log_info "目标网站: $SITE_NAME" echo "" # 检查用户和基础目录 check_root create_jingrow_user check_jsite_base_dir # 检测网络连通性 check_network_connectivity # 配置APT镜像源,提高下载速度 configure_apt_mirrors # 根据mode执行不同的任务 case "$MODE" in "deploy") # 完整部署模式 log_info "执行完整部署模式..." 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_EMAIL" echo " - 站点名称: $SITE_NAME" echo " - 服务器URL: $BACKEND_SERVER_URL" echo " - 重新验证令牌: $REVALIDATE_TOKEN" echo " - API密钥: $BACKEND_API_KEY" echo " - API密钥: $BACKEND_API_SECRET" echo "" if [ "$SKIP_DOCKER" = false ]; then install_docker else log_warning "跳过Docker安装" fi # 安装jq工具(用于处理JSON文件) install_jq install_nodejs 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 # PM2安装和项目启动(在项目准备完成后) if [ "$SKIP_PM2" = false ]; then log_info "开始PM2安装和项目启动..." install_pm2 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 # 直接创建traefik配置 local project_port=$(get_or_assign_port "$SITE_NAME") create_traefik_website_config "$SITE_NAME" "$SITE_URL" "$project_port" start_traefik else log_warning "跳过Traefik安装和启动" fi show_deployment_info ;; "create") # 创建新网站 create_site ;; "create_and_start") # 创建并启动网站(包含构建和启动) create_and_start_site ;; "delete") # 删除网站 delete_site ;; "build") # 构建网站 build_site ;; "start") # 启动网站 start_site ;; "stop") # 停止网站 stop_site ;; "restart") # 重启网站 restart_site ;; "status") # 查看网站状态 show_site_status ;; "list") # 列出所有网站 list_sites ;; "logs") # 查看网站日志 show_site_logs ;; "setup_env") # 仅安装运行环境 setup_env ;; "autostartup") # 配置网站自动启动 autostartup_site ;; *) log_error "不支持的mode: $MODE" echo "支持的mode: deploy, create, create_and_start, delete, build, start, stop, restart, status, list, logs, autostartup, setup_env" exit 1 ;; esac log_success "脚本执行完成!" } # 执行主函数 main "$@"