预览图
介绍
每次改完代码都要手动部署,真的挺折磨人的。
打包、传文件、登服务器、解压、重启服务... 步骤多不说,还容易手滑传错文件。
后来干脆写了个 Zsh 脚本,配合 VS Code 的任务系统,现在部署变得特别简单:
- Command + Shift + P 呼出命令面板
- 选个环境(dev/prod),选个目标(后端/前端/全部)
- 回车,等着就行 脚本会自动完成打包、调用 1Panel API 上传、启停服务这一系列操作。还支持演练模式,先跑一遍看看要干啥,心里有个底。
这套方案最爽的是 简单配置就能用。
下面聊聊具体是怎么实现的,希望能给同样有部署烦恼的同学一些参考。
文件结构
代码
scripts/.onepanel-api-key
# 将这一行替换成你的 1Panel API 密钥
your-1panel-api-key
deploy-youlai-nest-1panel.dev.conf
# 1Panel 部署配置 - 开发/测试环境
# 复制此文件为 deploy-youlai-nest-1panel.dev.conf 并修改配置
# ==================== 1Panel 连接配置 ====================
ONEPANEL_BASE_URL="http://your-server-ip:port" # 1Panel 面板地址
ONEPANEL_API_KEY_FILE="./.onepanel-api-key" # API 密钥文件路径(相对 scripts 目录)
# ==================== 部署目标 ====================
DEPLOY_TARGET="${DEPLOY_TARGET:-all}" # 部署目标: nest | admin | lab | all
# ==================== 后端 Nest 配置 ====================
NEST_ENABLED=1 # 是否部署后端: 1=是, 0=否
NEST_PROJECT_DIR="/path/to/youlai-nest-master" # 后端项目本地目录
NEST_TARGET_DIR="/opt/1panel/www/sites/your-site" # 后端部署目标目录(服务器上)
NEST_RUNTIME_NAME="your-runtime-name" # 1Panel 运行环境名称
# ==================== 前端 Admin 配置 ====================
ADMIN_ENABLED=1 # 是否部署管理后台: 1=是, 0=否
ADMIN_PROJECT_DIR="/path/to/vue3-element-admin-master" # 管理后台项目本地目录
ADMIN_TARGET_DIR="/opt/1panel/www/sites/your-admin-site" # 管理后台部署目标目录
ADMIN_DIST_PATH="dist" # 管理后台打包输出目录
# ==================== 前端 Lab 配置 ====================
LAB_ENABLED=1 # 是否部署用户端: 1=是, 0=否
LAB_PROJECT_DIR="/path/to/lab-store-web" # 用户端项目本地目录
LAB_TARGET_DIR="/opt/1panel/www/sites/your-lab-site" # 用户端部署目标目录
LAB_DIST_PATH="dist" # 用户端打包输出目录
# ==================== 部署选项 ====================
DRY_RUN="${DRY_RUN:-0}" # 演练模式: 1=只显示不执行, 0=真正执行
START_AFTER_DEPLOY="${START_AFTER_DEPLOY:-1}" # 部署后启动服务: 1=启动, 0=不启动
# ==================== 压缩包保存选项 ====================
KEEP_ARCHIVES="${KEEP_ARCHIVES:-0}" # 是否保留压缩包: 1=保留, 0=删除
ARCHIVE_DIR="${ARCHIVE_DIR:-./dist-archives}" # 压缩包保存目录(相对项目根目录)
deploy-youlai-nest-1panel.prod.conf
一样的只是文件名称区别
deploy-youlai-nest-1panel.sh
#!/usr/bin/env zsh
# 用法:
# 1. 复制 scripts/deploy-youlai-nest-1panel.dev.conf.example 为 scripts/deploy-youlai-nest-1panel.dev.conf
# 2. 按需修改配置项
# 3. 复制 scripts/.onepanel-api-key.example 为 scripts/.onepanel-api-key,并填入 API 密钥
# 4. 执行:
# DEPLOY_ENV=dev ./scripts/deploy-youlai-nest-1panel.sh # 开发/测试环境
# DEPLOY_ENV=prod ./scripts/deploy-youlai-nest-1panel.sh # 正式环境
#
# 可选:
# DEPLOY_ENV=dev DEPLOY_TARGET=nest ./scripts/deploy-youlai-nest-1panel.sh # 仅部署后端 (dev)
# DEPLOY_ENV=prod DEPLOY_TARGET=nest ./scripts/deploy-youlai-nest-1panel.sh # 仅部署后端 (prod)
# DEPLOY_ENV=dev DEPLOY_TARGET=admin ./scripts/deploy-youlai-nest-1panel.sh # 仅部署管理后台 (dev)
# DEPLOY_ENV=prod DEPLOY_TARGET=admin ./scripts/deploy-youlai-nest-1panel.sh # 仅部署管理后台 (prod)
# DEPLOY_ENV=dev DEPLOY_TARGET=lab ./scripts/deploy-youlai-nest-1panel.sh # 仅部署用户端 (dev)
# DEPLOY_ENV=prod DEPLOY_TARGET=lab ./scripts/deploy-youlai-nest-1panel.sh # 仅部署用户端 (prod)
# DEPLOY_ENV=dev DRY_RUN=1 ./scripts/deploy-youlai-nest-1panel.sh # 演练模式 (dev)
# DEPLOY_ENV=prod DRY_RUN=1 ./scripts/deploy-youlai-nest-1panel.sh # 演练模式 (prod)
# 启用严格模式:遇到错误立即退出、未定义变量报错、管道错误检测
set -euo pipefail
# 获取脚本所在目录的绝对路径
SCRIPT_DIR="${0:A:h}"
# 获取项目根目录(脚本目录的父目录)
REPO_ROOT="${SCRIPT_DIR:h}"
# 部署环境,默认 dev,可通过环境变量覆盖
DEPLOY_ENV="${DEPLOY_ENV:-dev}"
# 配置文件路径,根据环境自动选择
CONFIG_FILE="${CONFIG_FILE:-$SCRIPT_DIR/deploy-youlai-nest-1panel.${DEPLOY_ENV}.conf}"
# 配置文件所在目录
CONFIG_DIR="${CONFIG_FILE:A:h}"
# 如果配置文件存在,则加载配置
if [[ -f "$CONFIG_FILE" ]]; then
source "$CONFIG_FILE"
fi
# ==================== 1Panel 连接配置 ====================
ONEPANEL_BASE_URL="${ONEPANEL_BASE_URL:-}" # 1Panel 面板地址
ONEPANEL_API_KEY_FILE="${ONEPANEL_API_KEY_FILE:-$SCRIPT_DIR/.onepanel-api-key}" # API 密钥文件路径
ONEPANEL_API_KEY="${ONEPANEL_API_KEY:-}" # API 密钥(从文件读取)
# ==================== 部署选项 ====================
DRY_RUN="${DRY_RUN:-0}" # 演练模式:1=只显示不执行,0=真正执行
START_AFTER_DEPLOY="${START_AFTER_DEPLOY:-1}" # 部署后是否启动服务:1=启动,0=不启动
DEPLOY_TARGET="${DEPLOY_TARGET:-all}" # 部署目标:nest|admin|lab|all
# ==================== 后端 Nest 项目配置 ====================
NEST_ENABLED="${NEST_ENABLED:-0}" # 是否启用后端部署
NEST_PROJECT_DIR="${NEST_PROJECT_DIR:-}" # 后端项目本地目录
NEST_TARGET_DIR="${NEST_TARGET_DIR:-}" # 后端部署目标目录(服务器上)
NEST_RUNTIME_NAME="${NEST_RUNTIME_NAME:-}" # 1Panel 运行环境名称
# ==================== 前端 Admin 项目配置 ====================
ADMIN_ENABLED="${ADMIN_ENABLED:-0}" # 是否启用管理后台部署
ADMIN_PROJECT_DIR="${ADMIN_PROJECT_DIR:-}" # 管理后台项目本地目录
ADMIN_TARGET_DIR="${ADMIN_TARGET_DIR:-}" # 管理后台部署目标目录
ADMIN_DIST_PATH="${ADMIN_DIST_PATH:-dist}" # 管理后台打包输出目录
# ==================== 前端 Lab 项目配置 ====================
LAB_ENABLED="${LAB_ENABLED:-0}" # 是否启用用户端部署
LAB_PROJECT_DIR="${LAB_PROJECT_DIR:-}" # 用户端项目本地目录
LAB_TARGET_DIR="${LAB_TARGET_DIR:-}" # 用户端部署目标目录
LAB_DIST_PATH="${LAB_DIST_PATH:-dist}" # 用户端打包输出目录
# ==================== 压缩包保存配置 ====================
KEEP_ARCHIVES="${KEEP_ARCHIVES:-0}" # 是否保留压缩包:1=保留,0=删除
ARCHIVE_DIR="${ARCHIVE_DIR:-$REPO_ROOT/dist-archives}" # 压缩包保存目录
# ==================== 临时文件和状态变量 ====================
TMP_DIR="$(mktemp -d)" # 创建临时目录用于存放压缩包
RUNTIME_ID="" # 1Panel 运行环境 ID
RUNTIME_STOPPED=0 # 标记运行环境是否已停止
# 清理函数:脚本退出时自动执行
cleanup() {
local exit_code=$?
# 如果脚本异常退出且已停止运行环境,则尝试恢复启动
if [[ "$exit_code" -ne 0 && "$RUNTIME_STOPPED" == "1" && "$START_AFTER_DEPLOY" == "1" && -n "$RUNTIME_ID" ]]; then
print -u2 -- "脚本异常退出,尝试恢复启动运行环境 ${NEST_RUNTIME_NAME} ..."
if runtime_operate "up" >/dev/null 2>&1; then
print -u2 -- "已尝试重新启动 ${NEST_RUNTIME_NAME}。"
else
print -u2 -- "自动恢复启动失败,请在 1Panel 中手动检查 ${NEST_RUNTIME_NAME}。"
fi
fi
# 如果不保留压缩包,删除临时目录
if [[ "$KEEP_ARCHIVES" != "1" ]]; then
rm -rf "$TMP_DIR"
fi
exit "$exit_code"
}
# 注册清理函数,在脚本退出时自动调用
trap cleanup EXIT
# 检查依赖命令是否存在
require_cmd() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
print -u2 -- "缺少依赖命令: $cmd"
exit 1
fi
}
# 检查必需的命令
require_cmd curl # HTTP 请求
require_cmd jq # JSON 处理
require_cmd pnpm # 包管理器
require_cmd tar # 压缩解压
require_cmd date # 日期时间
require_cmd awk # 文本处理
# 如果密钥文件路径是相对路径,转换为绝对路径
if [[ "$ONEPANEL_API_KEY_FILE" != /* ]]; then
ONEPANEL_API_KEY_FILE="${CONFIG_DIR}/${ONEPANEL_API_KEY_FILE}"
fi
# 验证必要配置
if [[ -z "$ONEPANEL_BASE_URL" ]]; then
print -u2 -- "请在配置文件中设置 ONEPANEL_BASE_URL: $CONFIG_FILE"
exit 1
fi
# 读取 API 密钥
if [[ -z "$ONEPANEL_API_KEY" ]]; then
if [[ ! -f "$ONEPANEL_API_KEY_FILE" ]]; then
print -u2 -- "未找到 API 密钥文件: $ONEPANEL_API_KEY_FILE"
exit 1
fi
# 读取密钥文件内容,去除换行符
ONEPANEL_API_KEY="$(tr -d '\r\n' < "$ONEPANEL_API_KEY_FILE")"
fi
if [[ -z "$ONEPANEL_API_KEY" ]]; then
print -u2 -- "API 密钥为空,请检查: $ONEPANEL_API_KEY_FILE"
exit 1
fi
# ==================== 1Panel API 工具函数 ====================
# MD5 加密函数(跨平台兼容 macOS 和 Linux)
md5_hex() {
local raw="$1"
if command -v md5 >/dev/null 2>&1; then
md5 -q -s "$raw"
else
printf '%s' "$raw" | md5sum | awk '{print $1}'
fi
}
# 生成 1Panel API 请求令牌
# 格式:md5("1panel" + API_KEY + 时间戳)
make_panel_token() {
local ts="$1"
md5_hex "1panel${ONEPANEL_API_KEY}${ts}"
}
# 发送 JSON 请求到 1Panel API
# 参数:method(GET/POST), endpoint, payload(JSON字符串)
panel_request_json() {
local method="$1"
local endpoint="$2"
local payload="${3:-}"
local ts token url response code
# 生成时间戳和令牌
ts="$(date +%s)"
token="$(make_panel_token "$ts")"
url="${ONEPANEL_BASE_URL%/}/api/v2/${endpoint}"
# 发送请求
if [[ "$method" == "GET" ]]; then
response="$(
curl --compressed -sS -X GET "$url" \
-H "1Panel-Token: $token" \
-H "1Panel-Timestamp: $ts"
)"
else
response="$(
curl --compressed -sS -X "$method" "$url" \
-H "1Panel-Token: $token" \
-H "1Panel-Timestamp: $ts" \
-H "Content-Type: application/json" \
--data "$payload"
)"
fi
# 验证响应是否为 JSON
if ! jq empty >/dev/null 2>&1 <<<"$response"; then
print -u2 -- "1Panel API 返回了非 JSON 响应:"
print -u2 -- "$response"
return 1
fi
# 检查响应状态码
code="$(jq -r '.code // empty' <<<"$response")"
if [[ "$code" != "200" && "$code" != "0" ]]; then
print -u2 -- "1Panel API 调用失败: ${endpoint}"
print -u2 -- "$(jq -r '.message // "unknown error"' <<<"$response")"
return 1
fi
print -r -- "$response"
}
# 上传文件到 1Panel
# 参数:remote_dir(远程目录), local_file(本地文件路径)
panel_upload_file() {
local remote_dir="$1"
local local_file="$2"
local ts token url response code
ts="$(date +%s)"
token="$(make_panel_token "$ts")"
url="${ONEPANEL_BASE_URL%/}/api/v2/files/upload"
# 使用 multipart/form-data 上传文件
response="$(
curl --compressed -sS -X POST "$url" \
-H "1Panel-Token: $token" \
-H "1Panel-Timestamp: $ts" \
-F "path=${remote_dir}" \
-F "overwrite=true" \
-F "file=@${local_file}"
)"
if ! jq empty >/dev/null 2>&1 <<<"$response"; then
print -u2 -- "1Panel 上传接口返回了非 JSON 响应:"
print -u2 -- "$response"
return 1
fi
code="$(jq -r '.code // empty' <<<"$response")"
if [[ "$code" != "200" && "$code" != "0" ]]; then
print -u2 -- "文件上传失败: $local_file"
print -u2 -- "$(jq -r '.message // "unknown error"' <<<"$response")"
return 1
fi
}
# 检查远程路径是否存在
panel_check_exists() {
local target_path="$1"
local payload response
payload="$(jq -nc --arg path "$target_path" '{path:$path,withInit:false}')"
response="$(panel_request_json POST "files/check" "$payload")"
[[ "$(jq -r '.data' <<<"$response")" == "true" ]]
}
# 删除远程路径(文件或目录)
# 参数:target_path, is_dir(true/false)
panel_delete_path() {
local target_path="$1"
local is_dir="$2"
local payload
payload="$(jq -nc \
--arg path "$target_path" \
--argjson isDir "$is_dir" \
'{path:$path,isDir:$isDir,forceDelete:true}')"
panel_request_json POST "files/del" "$payload" >/dev/null
}
# 解压远程压缩包
# 参数:archive_path(压缩包路径), dst_dir(目标目录)
panel_decompress_archive() {
local archive_path="$1"
local dst_dir="$2"
local payload
payload="$(jq -nc \
--arg path "$archive_path" \
--arg dst "$dst_dir" \
--arg type "tar.gz" \
'{path:$path,dst:$dst,type:$type,secret:""}')"
panel_request_json POST "files/decompress" "$payload" >/dev/null
}
# 修改远程文件/目录的所有者和组
# 参数:target_path, user, group, sub(true/false是否递归)
panel_chown_path() {
local target_path="$1"
local user="$2"
local group="$3"
local sub="${4:-true}"
local payload
payload="$(jq -nc \
--arg path "$target_path" \
--arg user "$user" \
--arg group "$group" \
--argjson sub "$sub" \
'{path:$path,user:$user,group:$group,sub:$sub}')"
panel_request_json POST "files/owner" "$payload" >/dev/null
}
# 查找运行环境 ID
find_runtime_id() {
local payload response
# 搜索运行环境列表
payload="$(jq -nc \
--arg name "$NEST_RUNTIME_NAME" \
'{page:1,pageSize:100,name:$name,status:"",type:""}')"
response="$(panel_request_json POST "runtimes/search" "$payload")"
# 从响应中提取匹配的运行环境 ID
jq -r \
--arg name "$NEST_RUNTIME_NAME" \
'.data.items[]? | select(.name == $name) | .id' <<<"$response" | head -n 1
}
# 操作运行环境(启动/停止)
# 参数:action(up/down)
runtime_operate() {
local action="$1"
local payload
payload="$(jq -nc --arg operate "$action" --argjson id "$RUNTIME_ID" '{operate:$operate,ID:$id}')"
panel_request_json POST "runtimes/operate" "$payload" >/dev/null
}
# ==================== 部署辅助函数 ====================
# 验证项目目录是否存在
validate_project_dir() {
local project_dir="$1"
local name="$2"
if [[ ! -d "$project_dir" ]]; then
print -u2 -- "${name} 项目目录不存在: $project_dir"
exit 1
fi
}
# 验证远程目标目录是否存在
validate_remote_target() {
local target_path="$1"
local name="$2"
if ! panel_check_exists "$target_path"; then
print -u2 -- "${name} 远端目标目录不存在: $target_path"
exit 1
fi
}
# 构建项目(执行 pnpm build)
build_project() {
local project_dir="$1"
local name="$2"
print -- "开始打包 ${name} ..."
(
cd "$project_dir"
pnpm build
)
}
# 部署前端项目
# 参数:project_dir, target_dir, dist_path, name
deploy_frontend() {
local project_dir="$1"
local target_dir="$2"
local dist_path="$3"
local name="$4"
local full_dist_path="${project_dir}/${dist_path}"
# 检查 dist 目录是否存在
if [[ ! -d "$full_dist_path" ]]; then
print -u2 -- "${name} dist 目录不存在: $full_dist_path"
return 1
fi
# 创建压缩包保存目录
if [[ "$KEEP_ARCHIVES" == "1" && ! -d "$ARCHIVE_DIR" ]]; then
mkdir -p "$ARCHIVE_DIR"
fi
# 生成压缩包文件名(带时间戳)
local archive_name="${name}-dist-$(date +%Y%m%d%H%M%S).tar.gz"
# 根据 KEEP_ARCHIVES 决定保存位置
if [[ "$KEEP_ARCHIVES" == "1" ]]; then
local archive_local_path="$ARCHIVE_DIR/$archive_name"
else
local archive_local_path="$TMP_DIR/$archive_name"
fi
local archive_remote_path="${target_dir%/}/$archive_name"
print -- "压缩 ${name} dist 目录内容..."
# 打包 dist 目录内的内容(不包含 dist 目录本身)
tar -C "$full_dist_path" -czf "$archive_local_path" .
# 如果保留压缩包,打印保存位置
if [[ "$KEEP_ARCHIVES" == "1" ]]; then
print -- "压缩包已保存: $archive_local_path"
fi
# 演练模式:只显示不执行
if [[ "$DRY_RUN" == "1" ]]; then
print -- "[DRY RUN] 上传 ${archive_local_path} -> ${target_dir}/"
print -- "[DRY RUN] 解压 ${archive_remote_path} -> ${target_dir}"
return 0
fi
# 上传压缩包
print -- "上传 ${name} 压缩包..."
panel_upload_file "$target_dir" "$archive_local_path"
# 修改压缩包权限为 1panel:1panel
print -- "修改 ${name} 压缩包权限..."
panel_chown_path "$archive_remote_path" "1panel" "1panel" false
# 解压压缩包
print -- "解压 ${name} 压缩包到 ${target_dir}..."
panel_decompress_archive "$archive_remote_path" "$target_dir"
# 修改解压后的文件权限为 1panel:1panel(递归)
print -- "修改 ${name} 文件权限..."
panel_chown_path "$target_dir" "1panel" "1panel" true
# 清理远程压缩包
if panel_check_exists "$archive_remote_path"; then
print -- "清理 ${name} 压缩包..."
panel_delete_path "$archive_remote_path" false
fi
print -- "${name} 部署完成"
}
# ==================== 主部署函数 ====================
# 部署后端 Nest 项目
deploy_nest() {
# 检查是否启用后端部署
if [[ "$NEST_ENABLED" != "1" ]]; then
print -- "跳过后端部署 (NEST_ENABLED=0)"
return 0
fi
# 检查部署目标是否匹配
if [[ "$DEPLOY_TARGET" != "all" && "$DEPLOY_TARGET" != "nest" ]]; then
print -- "跳过后端部署 (DEPLOY_TARGET=$DEPLOY_TARGET)"
return 0
fi
# 验证目录
validate_project_dir "$NEST_PROJECT_DIR" "后端 Nest"
validate_remote_target "$NEST_TARGET_DIR" "后端"
# 构建项目
build_project "$NEST_PROJECT_DIR" "后端 Nest"
# 创建压缩包保存目录
if [[ "$KEEP_ARCHIVES" == "1" && ! -d "$ARCHIVE_DIR" ]]; then
mkdir -p "$ARCHIVE_DIR"
fi
# 准备压缩包
local archive_name="nest-dist-$(date +%Y%m%d%H%M%S).tar.gz"
# 根据 KEEP_ARCHIVES 决定保存位置
if [[ "$KEEP_ARCHIVES" == "1" ]]; then
local archive_local_path="$ARCHIVE_DIR/$archive_name"
else
local archive_local_path="$TMP_DIR/$archive_name"
fi
local archive_remote_path="${NEST_TARGET_DIR%/}/$archive_name"
print -- "压缩后端 dist 目录..."
# 后端打包包含 dist 目录本身
tar -C "$NEST_PROJECT_DIR" -czf "$archive_local_path" dist
# 如果保留压缩包,打印保存位置
if [[ "$KEEP_ARCHIVES" == "1" ]]; then
print -- "压缩包已保存: $archive_local_path"
fi
# 演练模式
if [[ "$DRY_RUN" == "1" ]]; then
print -- "[DRY RUN] 停止运行环境 ${NEST_RUNTIME_NAME}"
print -- "[DRY RUN] 上传 ${archive_local_path} -> ${NEST_TARGET_DIR}/"
print -- "[DRY RUN] 解压 ${archive_remote_path} -> ${NEST_TARGET_DIR}"
print -- "[DRY RUN] 启动运行环境 ${NEST_RUNTIME_NAME}"
return 0
fi
# 查找运行环境 ID
RUNTIME_ID="$(find_runtime_id)"
if [[ -z "$RUNTIME_ID" ]]; then
print -u2 -- "未找到运行环境: $NEST_RUNTIME_NAME"
exit 1
fi
# 停止运行环境
print -- "停止运行环境 ${NEST_RUNTIME_NAME} ..."
runtime_operate "down"
RUNTIME_STOPPED=1
# 上传压缩包
print -- "上传后端压缩包..."
panel_upload_file "$NEST_TARGET_DIR" "$archive_local_path"
# 修改压缩包权限为 1panel:1panel
print -- "修改后端压缩包权限..."
panel_chown_path "$archive_remote_path" "1panel" "1panel" false
# 删除旧 dist 目录
local remote_dist_path="${NEST_TARGET_DIR%/}/dist"
if panel_check_exists "$remote_dist_path"; then
print -- "删除远端旧 dist 目录..."
panel_delete_path "$remote_dist_path" true
fi
# 解压新压缩包
print -- "解压后端压缩包..."
panel_decompress_archive "$archive_remote_path" "$NEST_TARGET_DIR"
# 修改解压后的文件权限为 1panel:1panel(递归)
print -- "修改后端文件权限..."
panel_chown_path "$remote_dist_path" "1panel" "1panel" true
# 清理压缩包
if panel_check_exists "$archive_remote_path"; then
print -- "清理后端压缩包..."
panel_delete_path "$archive_remote_path" false
fi
# 启动运行环境
print -- "启动运行环境 ${NEST_RUNTIME_NAME} ..."
runtime_operate "up"
RUNTIME_STOPPED=0
print -- "后端部署完成"
}
# 部署管理后台 Admin
deploy_admin() {
if [[ "$ADMIN_ENABLED" != "1" ]]; then
print -- "跳过管理后台部署 (ADMIN_ENABLED=0)"
return 0
fi
if [[ "$DEPLOY_TARGET" != "all" && "$DEPLOY_TARGET" != "admin" ]]; then
print -- "跳过管理后台部署 (DEPLOY_TARGET=$DEPLOY_TARGET)"
return 0
fi
validate_project_dir "$ADMIN_PROJECT_DIR" "管理后台 Admin"
validate_remote_target "$ADMIN_TARGET_DIR" "管理后台"
build_project "$ADMIN_PROJECT_DIR" "管理后台 Admin"
deploy_frontend "$ADMIN_PROJECT_DIR" "$ADMIN_TARGET_DIR" "$ADMIN_DIST_PATH" "管理后台"
}
# 部署用户端 Lab
deploy_lab() {
if [[ "$LAB_ENABLED" != "1" ]]; then
print -- "跳过用户端部署 (LAB_ENABLED=0)"
return 0
fi
if [[ "$DEPLOY_TARGET" != "all" && "$DEPLOY_TARGET" != "lab" ]]; then
print -- "跳过用户端部署 (DEPLOY_TARGET=$DEPLOY_TARGET)"
return 0
fi
validate_project_dir "$LAB_PROJECT_DIR" "用户端 Lab"
validate_remote_target "$LAB_TARGET_DIR" "用户端"
build_project "$LAB_PROJECT_DIR" "用户端 Lab"
deploy_frontend "$LAB_PROJECT_DIR" "$LAB_TARGET_DIR" "$LAB_DIST_PATH" "用户端"
}
# 主函数
main() {
print -- "=========================================="
print -- "1Panel 自动化部署脚本"
print -- "=========================================="
print -- "部署环境: $DEPLOY_ENV (dev | prod)"
print -- "部署目标: $DEPLOY_TARGET (nest | admin | lab | all)"
print -- "面板地址: $ONEPANEL_BASE_URL"
print -- "=========================================="
# 按顺序部署:后端 -> 管理后台 -> 用户端
deploy_nest
deploy_admin
deploy_lab
print -- "=========================================="
print -- "全部部署完成!"
print -- "=========================================="
}
# 执行主函数
main "$@"
1Panel 自动化部署说明
本文档说明 deploy-youlai-nest-1panel.sh 的使用方式。
支持的项目
| 项目 | 说明 | 部署方式 |
|---|---|---|
| 后端 Nest | youlai-nest-master | 停止服务 → 打包 dist 目录 → 上传 → 解压 → 启动服务 |
| 管理后台 Admin | vue3-element-admin-master | 打包 dist 内容 → 上传 → 解压 |
| 用户端 Lab | lab-store-web | 打包 dist 内容 → 上传 → 解压 |
目录说明
- deploy-youlai-nest-1panel.sh:部署脚本
- deploy-youlai-nest-1panel.dev.conf.example:开发/测试环境配置模板
- deploy-youlai-nest-1panel.dev.conf:开发/测试环境配置(需从模板创建)
- deploy-youlai-nest-1panel.prod.conf:正式环境配置
- .onepanel-api-key.example:密钥模板
.onepanel-api-key:实际密钥文件,已被 Git 忽略
配置文件
通过 DEPLOY_ENV 选择配置文件:
DEPLOY_ENV=dev对应deploy-youlai-nest-1panel.dev.confDEPLOY_ENV=prod对应deploy-youlai-nest-1panel.prod.conf
配置文件支持以下字段:
ONEPANEL_BASE_URL="http://your-server-ip:port" # 1Panel 面板地址
ONEPANEL_API_KEY_FILE="./.onepanel-api-key" # API 密钥文件路径
DEPLOY_TARGET="all" # 部署目标: nest | admin | lab | all
NEST_ENABLED=1 # 是否部署后端
NEST_PROJECT_DIR="/path/to/youlai-nest-master" # 后端项目目录
NEST_TARGET_DIR="/opt/1panel/www/sites/your-site" # 后端目标目录
NEST_RUNTIME_NAME="your-runtime-name" # 1Panel 运行环境名称
ADMIN_ENABLED=1 # 是否部署管理后台
ADMIN_PROJECT_DIR="/path/to/vue3-element-admin-master" # 管理后台项目目录
ADMIN_TARGET_DIR="/opt/1panel/www/sites/your-admin" # 管理后台目标目录
ADMIN_DIST_PATH="dist" # 管理后台 dist 目录
LAB_ENABLED=1 # 是否部署用户端
LAB_PROJECT_DIR="/path/to/lab-store-web" # 用户端项目目录
LAB_TARGET_DIR="/opt/1panel/www/sites/your-lab" # 用户端目标目录
LAB_DIST_PATH="dist" # 用户端 dist 目录
DRY_RUN=0 # 演练模式: 1=只显示不执行
START_AFTER_DEPLOY=1 # 部署后启动服务: 1=启动
KEEP_ARCHIVES=0 # 保留压缩包: 1=保留, 0=删除
ARCHIVE_DIR="./dist-archives" # 压缩包保存目录
部署命令
部署全部
# dev 环境
DEPLOY_ENV=dev ./scripts/deploy-youlai-nest-1panel.sh
# prod 环境
DEPLOY_ENV=prod ./scripts/deploy-youlai-nest-1panel.sh
仅部署后端
# dev 环境
DEPLOY_ENV=dev DEPLOY_TARGET=nest ./scripts/deploy-youlai-nest-1panel.sh
# prod 环境
DEPLOY_ENV=prod DEPLOY_TARGET=nest ./scripts/deploy-youlai-nest-1panel.sh
仅部署管理后台
# dev 环境
DEPLOY_ENV=dev DEPLOY_TARGET=admin ./scripts/deploy-youlai-nest-1panel.sh
# prod 环境
DEPLOY_ENV=prod DEPLOY_TARGET=admin ./scripts/deploy-youlai-nest-1panel.sh
仅部署用户端
# dev 环境
DEPLOY_ENV=dev DEPLOY_TARGET=lab ./scripts/deploy-youlai-nest-1panel.sh
# prod 环境
DEPLOY_ENV=prod DEPLOY_TARGET=lab ./scripts/deploy-youlai-nest-1panel.sh
常用选项
演练模式(不真正停服务和上传)
# dev 环境
DEPLOY_ENV=dev DRY_RUN=1 ./scripts/deploy-youlai-nest-1panel.sh
# prod 环境
DEPLOY_ENV=prod DRY_RUN=1 ./scripts/deploy-youlai-nest-1panel.sh
部署后不自动启动运行环境
DEPLOY_ENV=dev START_AFTER_DEPLOY=0 ./scripts/deploy-youlai-nest-1panel.sh
保留压缩包到本地
# 临时启用(仅本次部署)
DEPLOY_ENV=dev KEEP_ARCHIVES=1 ./scripts/deploy-youlai-nest-1panel.sh
# 或在配置文件中设置永久启用
# KEEP_ARCHIVES=1
压缩包将保存在项目根目录的 dist-archives/ 文件夹中。
注意事项
- 前端打包
dist目录内容,解压后直接覆盖目标目录 - 后端打包
dist目录本身,包含 dist 子目录 - 后端部署会停止/启动 1Panel 运行环境,前端部署无服务操作
- 脚本异常退出时,会自动尝试恢复启动后端运行环境
- 面板地址和目标目录都通过配置文件维护
- 配置文件包含敏感信息,请勿提交到 Git