Files
Logan.LiandCursor cb1a9be2c4 chore: 增加 Docker 单容器部署
生产用 Dockerfile + compose 嵌入前端二进制,run.sh docker 一键构建启动。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 12:02:29 +08:00

468 lines
14 KiB
Bash
Executable File
Raw Permalink 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
# opsx-licence 启动脚本
# 一键启动前后端服务,支持日志轮转与单二进制部署
# 用法: ./run.sh [start|stop|restart|status|frontend|backend|build|binary|docker|help]
set -e
# ============================================================
# 配置区
# ============================================================
PROJECT_NAME="opsx-licence"
BACKEND_PORT=9001
FRONTEND_PORT=5181
BACKEND_DIR="backend"
FRONTEND_DIR="frontend"
BACKEND_BIN="backend"
STANDALONE_BIN="opsx-licence"
STANDALONE_PORT="${PORT:-9001}"
BACKEND_LAUNCH_LABEL="com.opsx.licence.backend"
FRONTEND_LAUNCH_LABEL="com.opsx.licence.frontend"
# 日志配置
MAX_LOG_FILES=3
MAX_LOG_SIZE=$((50 * 1024 * 1024)) # 50MB
# ============================================================
# 通用函数
# ============================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# PID 文件
BACKEND_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.backend.pid"
FRONTEND_PID_FILE="$SCRIPT_DIR/$FRONTEND_DIR/.frontend.pid"
STANDALONE_PID_FILE="$SCRIPT_DIR/.opsx-licence.pid"
STANDALONE_LOG_FILE="$SCRIPT_DIR/logs/opsx-licence.log"
# 检查端口是否被占用
check_port() {
lsof -ti:$1 2>/dev/null | head -1
}
# 释放端口
kill_port() {
local pids=$(lsof -ti:$1 2>/dev/null || echo "")
if [ -n "$pids" ]; then
info "释放端口 $1..."
echo "$pids" | xargs kill -9 2>/dev/null || true
sleep 1
fi
}
# 日志轮转(仅启动时调用)
rotate_log() {
local log_file=$1
[ ! -f "$log_file" ] && return 0
local file_size=$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file" 2>/dev/null || echo "0")
if [ "$file_size" -gt "$MAX_LOG_SIZE" ]; then
[ -f "${log_file}.${MAX_LOG_FILES}" ] && rm -f "${log_file}.${MAX_LOG_FILES}"
for i in $(seq $((MAX_LOG_FILES - 1)) -1 1); do
[ -f "${log_file}.${i}" ] && mv "${log_file}.${i}" "${log_file}.$((i + 1))"
done
mv "$log_file" "${log_file}.1"
info "日志已轮转: $log_file"
fi
}
# macOS 使用 launchctl 脱离当前终端,避免开发服务在脚本退出后被系统回收。 // Logan.Li
start_detached() {
local label="$1"
local log_file="$2"
local command="$3"
if [ "$(uname -s)" = "Darwin" ] && command -v launchctl >/dev/null 2>&1; then
launchctl remove "$label" 2>/dev/null || true
launchctl submit -l "$label" -o "$log_file" -e "$log_file" -- /bin/sh -c "$command"
else
nohup /bin/sh -c "$command" >> "$log_file" 2>&1 < /dev/null &
echo $!
fi
}
# macOS 下卸载 launchctl 任务,其他系统沿用 PID 与端口清理。 // Logan.Li
stop_detached() {
local label="$1"
if [ "$(uname -s)" = "Darwin" ] && command -v launchctl >/dev/null 2>&1; then
launchctl remove "$label" 2>/dev/null || true
fi
}
# ============================================================
# 后端服务
# ============================================================
start_backend() {
info "正在启动后端服务..."
cd "$SCRIPT_DIR/$BACKEND_DIR"
mkdir -p logs
rotate_log "logs/server.log"
# 释放被占用的端口
kill_port $BACKEND_PORT
export PORT=$BACKEND_PORT
export GIN_MODE=debug
# 编译并运行
info "编译 Go 程序..."
go build -o "$BACKEND_BIN" .
start_detached "$BACKEND_LAUNCH_LABEL" "$SCRIPT_DIR/$BACKEND_DIR/logs/server.log" \
"cd \"$SCRIPT_DIR/$BACKEND_DIR\" && exec env PORT=\"$BACKEND_PORT\" GIN_MODE=\"debug\" ./\"$BACKEND_BIN\""
# 等待服务启动
local retries=0
while ! curl -s "http://localhost:$BACKEND_PORT/health" > /dev/null 2>&1; do
retries=$((retries + 1))
if [ $retries -ge 30 ]; then
error "后端服务启动超时,请检查日志: $BACKEND_DIR/logs/server.log"
return 1
fi
sleep 1
done
echo "$(check_port $BACKEND_PORT)" > "$BACKEND_PID_FILE"
success "后端服务已启动 (PID: $(cat "$BACKEND_PID_FILE"), 端口: $BACKEND_PORT)"
return 0
}
stop_backend() {
stop_detached "$BACKEND_LAUNCH_LABEL"
if [ -f "$BACKEND_PID_FILE" ]; then
local pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || echo "")
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
success "后端服务已停止"
fi
rm -f "$BACKEND_PID_FILE"
fi
kill_port $BACKEND_PORT
}
# ============================================================
# 前端服务
# ============================================================
start_frontend() {
info "正在启动前端服务..."
cd "$SCRIPT_DIR/$FRONTEND_DIR"
mkdir -p logs
rotate_log "logs/dev.log"
kill_port $FRONTEND_PORT
if [ ! -d "node_modules" ]; then
warning "node_modules 不存在,正在安装依赖..."
npm install
fi
local npm_bin
npm_bin=$(command -v npm)
start_detached "$FRONTEND_LAUNCH_LABEL" "$SCRIPT_DIR/$FRONTEND_DIR/logs/dev.log" \
"cd \"$SCRIPT_DIR/$FRONTEND_DIR\" && exec env PATH=\"$PATH\" \"$npm_bin\" run dev"
# 等待服务启动
local retries=0
while ! curl -s "http://localhost:$FRONTEND_PORT" > /dev/null 2>&1; do
retries=$((retries + 1))
if [ $retries -ge 30 ]; then
error "前端服务启动超时,请检查日志: $FRONTEND_DIR/logs/dev.log"
return 1
fi
sleep 1
done
echo "$(check_port $FRONTEND_PORT)" > "$FRONTEND_PID_FILE"
success "前端服务已启动 (PID: $(cat "$FRONTEND_PID_FILE"), 端口: $FRONTEND_PORT)"
info "前端地址: http://localhost:$FRONTEND_PORT"
return 0
}
stop_frontend() {
stop_detached "$FRONTEND_LAUNCH_LABEL"
if [ -f "$FRONTEND_PID_FILE" ]; then
local pid=$(cat "$FRONTEND_PID_FILE" 2>/dev/null || echo "")
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
success "前端服务已停止"
fi
rm -f "$FRONTEND_PID_FILE"
fi
kill_port $FRONTEND_PORT
}
# ============================================================
# 单二进制部署模式
# ============================================================
build_binary() {
info "构建前端生产资源..."
cd "$SCRIPT_DIR/$FRONTEND_DIR"
npm run build
info "嵌入前端资源并编译 $STANDALONE_BIN..."
rm -rf "$SCRIPT_DIR/$BACKEND_DIR/static"
mkdir -p "$SCRIPT_DIR/$BACKEND_DIR/static"
cp -R dist/. "$SCRIPT_DIR/$BACKEND_DIR/static/"
cd "$SCRIPT_DIR/$BACKEND_DIR"
CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o "$SCRIPT_DIR/$STANDALONE_BIN" .
success "单二进制已生成: $SCRIPT_DIR/$STANDALONE_BIN"
}
start_binary() {
if [ ! -x "$SCRIPT_DIR/$STANDALONE_BIN" ]; then
error "未找到 $STANDALONE_BIN,请先执行 ./run.sh build"
return 1
fi
mkdir -p "$SCRIPT_DIR/logs"
rotate_log "$STANDALONE_LOG_FILE"
kill_port "$STANDALONE_PORT"
info "启动单二进制服务..."
PORT="$STANDALONE_PORT" GIN_MODE="${GIN_MODE:-debug}" nohup "$SCRIPT_DIR/$STANDALONE_BIN" >> "$STANDALONE_LOG_FILE" 2>&1 &
echo $! > "$STANDALONE_PID_FILE"
local retries=0
while ! curl -s "http://localhost:$STANDALONE_PORT/health" > /dev/null 2>&1; do
retries=$((retries + 1))
if [ $retries -ge 30 ]; then
error "单二进制启动超时,请检查日志: $STANDALONE_LOG_FILE"
return 1
fi
sleep 1
done
success "单二进制服务已启动: http://localhost:$STANDALONE_PORT (PID: $(cat "$STANDALONE_PID_FILE"))"
}
stop_binary() {
if [ -f "$STANDALONE_PID_FILE" ]; then
local pid=$(cat "$STANDALONE_PID_FILE" 2>/dev/null || echo "")
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
fi
rm -f "$STANDALONE_PID_FILE"
fi
kill_port "$STANDALONE_PORT"
}
binary_status() {
if [ -n "$(check_port $STANDALONE_PORT)" ]; then
success "单二进制服务: 运行中 (端口 $STANDALONE_PORT)"
else
warning "单二进制服务: 未运行"
fi
}
binary_command() {
case "${1:-start}" in
start) start_binary ;;
stop) stop_binary ;;
restart) stop_binary; sleep 1; start_binary ;;
status) binary_status ;;
upgrade) stop_binary; build_binary; start_binary ;;
*)
error "未知 binary 命令: $1"
echo "用法: ./run.sh binary [start|stop|restart|status|upgrade]"
return 1
;;
esac
}
# ============================================================
# Docker 部署(生产推荐)
# ============================================================
compose_cmd() {
if docker compose version >/dev/null 2>&1; then
echo "docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
echo "docker-compose"
else
error "未找到 docker compose / docker-compose"
return 1
fi
}
docker_command() {
local sub=${1:-up}
local dc
dc=$(compose_cmd) || return 1
if [ ! -f "$SCRIPT_DIR/.env" ]; then
warning "缺少 .env,正在从 .env.example 复制"
cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env"
warning "请编辑 .env 修改 JWT_SECRET / Waffo 配置后再部署"
fi
case "$sub" in
up|start)
info "Docker 构建并启动..."
$dc -f "$SCRIPT_DIR/docker-compose.yml" up -d --build
success "容器已启动: http://localhost:${HOST_PORT:-9001}"
;;
down|stop)
info "停止 Docker 容器..."
$dc -f "$SCRIPT_DIR/docker-compose.yml" down
;;
restart)
$dc -f "$SCRIPT_DIR/docker-compose.yml" restart
;;
rebuild|upgrade)
info "重新构建并滚动升级..."
$dc -f "$SCRIPT_DIR/docker-compose.yml" up -d --build --force-recreate
;;
logs)
$dc -f "$SCRIPT_DIR/docker-compose.yml" logs -f --tail=200
;;
status|ps)
$dc -f "$SCRIPT_DIR/docker-compose.yml" ps
;;
*)
error "未知 docker 命令: $sub"
echo "用法: ./run.sh docker [up|down|restart|rebuild|logs|status]"
return 1
;;
esac
}
# ============================================================
# 命令
# ============================================================
stop() {
info "正在停止服务..."
stop_backend
stop_frontend
info "所有服务已停止"
}
show_status() {
info "服务状态:"
if [ -n "$(check_port $BACKEND_PORT)" ]; then
success "后端服务: 运行中 (PID: $(check_port $BACKEND_PORT), 端口: $BACKEND_PORT)"
else
warning "后端服务: 未运行"
fi
if [ -n "$(check_port $FRONTEND_PORT)" ]; then
success "前端服务: 运行中 (端口: $FRONTEND_PORT)"
else
warning "前端服务: 未运行"
fi
}
show_help() {
echo "$PROJECT_NAME 启动脚本"
echo ""
echo "用法: ./run.sh [命令]"
echo ""
echo "命令:"
echo " start 启动前后端服务 (默认)"
echo " stop 停止前后端服务"
echo " restart 重启前后端服务"
echo " backend 仅启动后端服务"
echo " frontend 仅启动前端服务"
echo " build 构建嵌入前端的单二进制 $STANDALONE_BIN"
echo " binary 管理单二进制服务(start|stop|restart|status|upgrade)"
echo " docker Docker 部署(up|down|restart|rebuild|logs|status)"
echo " status 查看服务状态"
echo " help 显示帮助信息"
echo ""
echo "示例:"
echo " ./run.sh # 启动所有服务"
echo " ./run.sh stop # 停止服务"
echo " ./run.sh restart # 重启服务"
echo " ./run.sh backend # 仅启动后端"
echo " ./run.sh build # 生成 ./opsx-licence"
echo " ./run.sh binary start # 启动单二进制服务"
echo " ./run.sh docker up # Docker 构建并启动(生产推荐)"
}
# ============================================================
# 主入口
# ============================================================
main() {
local command=${1:-start}
case "$command" in
start)
info "========== $PROJECT_NAME 启动脚本 =========="
# 检查依赖
if ! command -v go &> /dev/null; then
error "Go 未安装,请先安装 Go"
exit 1
fi
if ! command -v npm &> /dev/null; then
error "npm 未安装,请先安装 Node.js"
exit 1
fi
if start_backend && start_frontend; then
echo ""
success "========== 所有服务已启动 =========="
info "前端地址: http://localhost:$FRONTEND_PORT"
info "后端地址: http://localhost:$BACKEND_PORT"
info "默认账号: admin / admin"
echo ""
info "使用 './run.sh stop' 停止服务"
else
error "服务启动失败"
exit 1
fi
;;
stop)
stop
;;
restart)
stop
sleep 2
main start
;;
backend)
info "========== $PROJECT_NAME (仅后端) =========="
start_backend
show_status
;;
frontend)
info "========== $PROJECT_NAME (仅前端) =========="
start_frontend
show_status
;;
build)
build_binary
;;
binary)
binary_command "${2:-start}"
;;
docker)
docker_command "${2:-up}"
;;
status)
show_status
;;
help|--help|-h)
show_help
;;
*)
error "未知命令: $command"
show_help
exit 1
;;
esac
}
main "$@"