jshells/jsite.sh

2391 lines
79 KiB
Bash
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前端自动化部署脚本
# ========================================
# 颜色定义
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:3000/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 ""
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
# 5. 清华大学镜像源(备用)
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ${codename} main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ${codename}-security main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ${codename}-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ${codename}-proposed main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.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进程状态
local pm2_status=$(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 -c 'online' || echo '0'
")
if [ "$pm2_status" != "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开机自启..."
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工具已安装
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
if [ "$user_specified_port" = true ]; then
# 用户传入了自定义端口,优先使用用户指定的端口
# 检查用户指定的端口是否已被使用
local port_available=true
if [ -f "$port_file" ]; then
# 确保jq工具已安装
ensure_jq_installed
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
# 确保jq工具已安装
ensure_jq_installed
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
# 确保jq工具已安装
ensure_jq_installed
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工具已安装
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:3000/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管理功能函数
# ========================================
# 生成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"
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_error "网站重启失败"
return 1
fi
else
# 如果未运行,则启动
start_site
return $?
fi
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开机自启..."
su - jingrow -c "
export NVM_DIR=\"\$HOME/.nvm\"
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
pm2 startup 2>/dev/null || true
"
# 保存当前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 自动启动配置完成"
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
;;
"autostartup")
# 配置网站自动启动
autostartup_site
;;
*)
log_error "不支持的mode: $MODE"
echo "支持的mode: deploy, create, create_and_start, delete, build, start, stop, restart, status, list, logs, autostartup"
exit 1
;;
esac
log_success "脚本执行完成!"
}
# 执行主函数
main "$@"