🚀 deploy(build): 新增一键部署脚本与构建工具链
- 新增 deploy.sh:编译部署、systemd/Nginx 初始化、软链接版本管理与交互式回滚、部署后 /healthz 健康自检、certbot 自动申请 HTTPS 证书与续期重载 Hook - 新增 .deploy.env.example 部署配置模板,.deploy.env(不入库)覆盖脚本默认值,路径默认由 APP_NAME 派生,CERT_EMAIL 默认 admin@域名 - 新增 Makefile:build / build-linux 交叉编译 / test(含 go vet)/ clean - main.go 新增 Version/BuildTime 变量,使 -ldflags -X 注入生效并在启动日志打印 - systemd 工作目录改为代码目录,修复 config.yaml/.env/cache.db 相对路径加载失败 - .gitignore 忽略 .deploy.env 与 fanqie-* 编译产物;README 新增部署章节与 HTTPS 说明
This commit is contained in:
1 parent
6a473aea3e
commit
889c399a0b
6 files changed
+506
-1
No files matched your search
@@ -0,0 +1,42 @@
|
|||||||
|
# ============================================================
|
||||||
|
# 部署配置模板(deploy.sh 使用)
|
||||||
|
#
|
||||||
|
# 使用方法(在服务器上):
|
||||||
|
# cp .deploy.env.example .deploy.env && vi .deploy.env
|
||||||
|
#
|
||||||
|
# 说明:
|
||||||
|
# - .deploy.env 不入库(.gitignore 已忽略),只写要覆盖的项
|
||||||
|
# - 路径类默认值由 APP_NAME 派生(如 /opt/${APP_NAME}/bin),改 APP_NAME 即全部跟随
|
||||||
|
# - 应用环境变量固定读取 ${SRC_DIR}/.env(模板见 .env.example,敏感密码放那里)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# ---------- 应用信息 ----------
|
||||||
|
# APP_NAME="fanqie"
|
||||||
|
# APP_DESC="番茄小说api接口"
|
||||||
|
|
||||||
|
# ---------- 运行用户 ----------
|
||||||
|
# 留空 = 执行 deploy.sh 的当前用户
|
||||||
|
RUN_USER=""
|
||||||
|
|
||||||
|
# ---------- 路径 ----------
|
||||||
|
# 代码目录,同时也是服务的工作目录(config/config.yaml、.env、cache.db 都在这里)
|
||||||
|
SRC_DIR="/root/code/novel/fanqie-go"
|
||||||
|
|
||||||
|
# 以下路径默认由 APP_NAME 派生,一般无需修改:
|
||||||
|
# BIN_DIR="/opt/fanqie/bin" # 默认 /opt/${APP_NAME}/bin
|
||||||
|
# SERVICE_FILE="/etc/systemd/system/fanqie.service" # 默认 /etc/systemd/system/${APP_NAME}.service
|
||||||
|
# NGINX_CONF="/etc/nginx/conf.d/fanqie.conf" # 默认 /etc/nginx/conf.d/${APP_NAME}.conf
|
||||||
|
|
||||||
|
# ---------- 网络 ----------
|
||||||
|
# APP_PORT="40180"
|
||||||
|
DOMAIN="fq.20010117.xyz"
|
||||||
|
|
||||||
|
# ---------- 证书 ----------
|
||||||
|
# Certbot 申请证书的邮箱,用于接收过期提醒
|
||||||
|
CERT_EMAIL="you@example.com"
|
||||||
|
|
||||||
|
# ---------- 编译 ----------
|
||||||
|
# BUILD_TARGET="./cmd/fanqie"
|
||||||
|
|
||||||
|
# ---------- 其他 ----------
|
||||||
|
# KEEP_VERSIONS=5
|
||||||
@@ -28,6 +28,7 @@ go.work
|
|||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
.deploy.env
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
@@ -47,5 +48,9 @@ __pycache__
|
|||||||
|
|
||||||
cache.db
|
cache.db
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
fanqie-*
|
||||||
|
/fanqie
|
||||||
|
|
||||||
# 环境变量(含真实密码,勿提交)
|
# 环境变量(含真实密码,勿提交)
|
||||||
.env
|
.env
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
BINARY := fanqie
|
||||||
|
VERSION ?= $(shell date +%Y%m%d-%H%M%S)
|
||||||
|
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
BUILD_TARGET := ./cmd/fanqie
|
||||||
|
|
||||||
|
LDFLAGS := -s -w -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)
|
||||||
|
|
||||||
|
.PHONY: build build-linux test clean
|
||||||
|
|
||||||
|
build:
|
||||||
|
CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o $(BINARY)-$(VERSION) $(BUILD_TARGET)
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o $(BINARY)-$(VERSION)-linux $(BUILD_TARGET)
|
||||||
|
|
||||||
|
test:
|
||||||
|
go vet ./... && go test ./...
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BINARY)-*
|
||||||
@@ -395,6 +395,36 @@ cp .env.example .env
|
|||||||
|
|
||||||
失败响应不缓存;评论类接口为动态数据,不缓存。
|
失败响应不缓存;评论类接口为动态数据,不缓存。
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
### Linux 服务器(deploy.sh 一键部署)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 首次:编译 + 部署 + 写入 systemd/nginx 配置
|
||||||
|
./deploy.sh v1.0.0 --init
|
||||||
|
|
||||||
|
# 日常:拉取代码 + 编译 + 部署 + 重启
|
||||||
|
./deploy.sh v1.0.0
|
||||||
|
|
||||||
|
# 版本管理
|
||||||
|
./deploy.sh --list # 列出已部署版本
|
||||||
|
./deploy.sh --rollback v1.0.0 # 回滚到指定版本
|
||||||
|
|
||||||
|
# 服务运维
|
||||||
|
sudo systemctl status fanqie
|
||||||
|
sudo journalctl -u fanqie -f
|
||||||
|
```
|
||||||
|
|
||||||
|
部署结构:二进制存于 `/opt/fanqie/bin/`,`fanqie-current` 软链接指向当前版本(保留最近 5 个),systemd 守护 + Nginx 反代,部署后自动做 `/healthz` 健康检查。
|
||||||
|
|
||||||
|
首次 `--init` 时若服务器装有 certbot,会自动申请 HTTPS 证书并配置 80→443 跳转(邮箱默认 `admin@域名`,可用 `.deploy.env` 的 `CERT_EMAIL` 覆盖);续期由 `certbot.timer` 自动完成,续期后经 deploy-hook 重载 Nginx。
|
||||||
|
|
||||||
|
### 本地交叉编译
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build-linux VERSION=v1.0.0 # 产物 fanqie-v1.0.0-linux,scp 到服务器即可运行
|
||||||
|
```
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- **Go 1.25** - 编程语言
|
- **Go 1.25** - 编程语言
|
||||||
|
|||||||
+5
-1
@@ -16,13 +16,17 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Version / BuildTime 由编译时 -ldflags "-X main.Version=... -X main.BuildTime=..." 注入
|
||||||
var (
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
BuildTime = "unknown"
|
||||||
|
|
||||||
logger = common.NewLogger("main", "INFO", "")
|
logger = common.NewLogger("main", "INFO", "")
|
||||||
configWatcher *config.ConfigWatcher
|
configWatcher *config.ConfigWatcher
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
logger.Info("番茄小说API服务 - v2.0.0")
|
logger.Info(fmt.Sprintf("番茄小说API服务 版本=%s 编译时间=%s", Version, BuildTime))
|
||||||
|
|
||||||
// 加载配置文件
|
// 加载配置文件
|
||||||
initConfig()
|
initConfig()
|
||||||
|
|||||||
@@ -0,0 +1,404 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# Go 应用一键部署脚本
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# ./deploy.sh # 编译 + 部署(自动版本号)
|
||||||
|
# ./deploy.sh v1.2.0 # 编译 + 部署(指定版本)
|
||||||
|
# ./deploy.sh v1.2.0 --init # 首次初始化(写 systemd/nginx 配置)
|
||||||
|
# ./deploy.sh v1.2.0 --no-pull # 跳过 git pull
|
||||||
|
# ./deploy.sh --build-only # 只编译,不部署
|
||||||
|
# ./deploy.sh --rollback # 交互式回滚到指定版本
|
||||||
|
# ./deploy.sh --rollback v1.0.0 # 直接回滚到指定版本
|
||||||
|
# ./deploy.sh --list # 列出所有已部署版本
|
||||||
|
# ./deploy.sh --help # 显示帮助
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ==================== 加载本地配置 ====================
|
||||||
|
# .deploy.env(不入库)覆盖下方默认值;模板见 .deploy.env.example
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
if [ -f "${SCRIPT_DIR}/.deploy.env" ]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "${SCRIPT_DIR}/.deploy.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==================== 配置区(默认值,可被 .deploy.env 覆盖) ====================
|
||||||
|
APP_NAME="${APP_NAME:-fanqie}" # 应用名(用于服务名、目录名)
|
||||||
|
APP_DESC="${APP_DESC:-番茄小说api接口}" # 服务描述
|
||||||
|
RUN_USER="${RUN_USER:-$(whoami)}" # 运行用户(默认当前用户)
|
||||||
|
SRC_DIR="${SRC_DIR:-/root/code/novel/fanqie-go}" # 代码目录
|
||||||
|
BIN_DIR="${BIN_DIR:-/opt/${APP_NAME}/bin}" # 二进制存放目录
|
||||||
|
SERVICE_FILE="${SERVICE_FILE:-/etc/systemd/system/${APP_NAME}.service}"
|
||||||
|
NGINX_CONF="${NGINX_CONF:-/etc/nginx/conf.d/${APP_NAME}.conf}"
|
||||||
|
APP_PORT="${APP_PORT:-40180}" # 程序监听端口
|
||||||
|
DOMAIN="${DOMAIN:-fq.20010117.xyz}" # 域名
|
||||||
|
CERT_EMAIL="${CERT_EMAIL:-admin@${DOMAIN}}" # 证书申请邮箱(过期提醒)
|
||||||
|
BUILD_TARGET="${BUILD_TARGET:-./cmd/fanqie}" # go build 的包路径
|
||||||
|
ENV_FILE="${ENV_FILE:-${SRC_DIR}/.env}" # 应用环境变量(.env.example 为模板)
|
||||||
|
KEEP_VERSIONS="${KEEP_VERSIONS:-5}" # 保留最近几个版本
|
||||||
|
|
||||||
|
# ==================== 颜色与日志 ====================
|
||||||
|
C_GREEN='\033[1;32m'; C_YELLOW='\033[1;33m'; C_RED='\033[1;31m'; C_BLUE='\033[1;34m'; C_RESET='\033[0m'
|
||||||
|
log() { echo -e "${C_GREEN}[$(date '+%H:%M:%S')]${C_RESET} $*"; }
|
||||||
|
warn() { echo -e "${C_YELLOW}[警告]${C_RESET} $*"; }
|
||||||
|
err() { echo -e "${C_RED}[错误]${C_RESET} $*" >&2; }
|
||||||
|
info() { echo -e "${C_BLUE}[信息]${C_RESET} $*"; }
|
||||||
|
|
||||||
|
# 非 root 时自动加 sudo
|
||||||
|
SUDO=""
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
SUDO="sudo"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==================== 参数解析 ====================
|
||||||
|
VERSION=""
|
||||||
|
INIT=false
|
||||||
|
NO_PULL=false
|
||||||
|
BUILD_ONLY=false
|
||||||
|
ROLLBACK=false
|
||||||
|
ROLLBACK_VERSION=""
|
||||||
|
LIST=false
|
||||||
|
|
||||||
|
print_help() {
|
||||||
|
cat <<EOF
|
||||||
|
用法: $0 [选项] [版本号]
|
||||||
|
|
||||||
|
选项:
|
||||||
|
--init 首次初始化,写入 systemd 和 nginx 配置
|
||||||
|
--no-pull 跳过 git pull
|
||||||
|
--build-only 只编译,不部署、不重启服务
|
||||||
|
--rollback [版本] 回滚到指定版本(不带版本号则交互式选择)
|
||||||
|
--list 列出所有已部署的版本
|
||||||
|
--help, -h 显示此帮助
|
||||||
|
|
||||||
|
示例:
|
||||||
|
$0 # 编译 + 部署(自动版本号)
|
||||||
|
$0 v1.2.0 # 编译 + 部署(指定版本)
|
||||||
|
$0 v1.2.0 --init # 首次初始化部署
|
||||||
|
$0 --build-only # 只编译
|
||||||
|
$0 --rollback # 交互式回滚
|
||||||
|
$0 --rollback v1.0.0 # 回滚到 v1.0.0
|
||||||
|
$0 --list # 查看所有版本
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
i=0
|
||||||
|
ARGS=("$@")
|
||||||
|
while [ $i -lt ${#ARGS[@]} ]; do
|
||||||
|
arg="${ARGS[$i]}"
|
||||||
|
case "$arg" in
|
||||||
|
--init) INIT=true ;;
|
||||||
|
--no-pull) NO_PULL=true ;;
|
||||||
|
--build-only) BUILD_ONLY=true ;;
|
||||||
|
--rollback)
|
||||||
|
ROLLBACK=true
|
||||||
|
# 看下一个参数是不是版本号
|
||||||
|
if [ $((i+1)) -lt ${#ARGS[@]} ] && [[ "${ARGS[$((i+1))]}" != --* ]]; then
|
||||||
|
ROLLBACK_VERSION="${ARGS[$((i+1))]}"
|
||||||
|
i=$((i+1))
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
--list) LIST=true ;;
|
||||||
|
--help|-h) print_help; exit 0 ;;
|
||||||
|
v*) VERSION="$arg" ;;
|
||||||
|
*) err "未知参数: $arg"; print_help; exit 1 ;;
|
||||||
|
esac
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
|
||||||
|
VERSION="${VERSION:-v$(date +%Y%m%d-%H%M%S)}"
|
||||||
|
BUILD_TIME=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
|
||||||
|
# ==================== 辅助函数 ====================
|
||||||
|
|
||||||
|
# 原子切换软链接
|
||||||
|
switch_symlink() {
|
||||||
|
local target="$1"
|
||||||
|
$SUDO ln -sfn "$target" "${BIN_DIR}/${APP_NAME}-current.tmp"
|
||||||
|
$SUDO mv -Tf "${BIN_DIR}/${APP_NAME}-current.tmp" "${BIN_DIR}/${APP_NAME}-current"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 列出所有版本
|
||||||
|
list_versions() {
|
||||||
|
if [ ! -d "${BIN_DIR}" ]; then
|
||||||
|
warn "二进制目录不存在: ${BIN_DIR}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local current
|
||||||
|
current=$(readlink -f "${BIN_DIR}/${APP_NAME}-current" 2>/dev/null | xargs -r basename || echo "")
|
||||||
|
info "已部署版本(${BIN_DIR}):"
|
||||||
|
echo ""
|
||||||
|
local i=1
|
||||||
|
for f in $(ls -1t ${BIN_DIR}/${APP_NAME}-v* 2>/dev/null); do
|
||||||
|
local name
|
||||||
|
name=$(basename "$f")
|
||||||
|
if [ "$name" = "$current" ]; then
|
||||||
|
echo -e " ${C_GREEN}* ${name} (当前版本)${C_RESET}"
|
||||||
|
else
|
||||||
|
echo " ${name}"
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
if [ $i -eq 1 ]; then
|
||||||
|
warn "没有找到任何版本"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 清理旧版本
|
||||||
|
cleanup_old_versions() {
|
||||||
|
log "清理旧版本(保留最近 ${KEEP_VERSIONS} 个)..."
|
||||||
|
local current
|
||||||
|
current=$(readlink -f "${BIN_DIR}/${APP_NAME}-current" 2>/dev/null | xargs -r basename || echo "")
|
||||||
|
local count=0
|
||||||
|
# 按时间倒序列出,保留前 N 个,且不删当前版本
|
||||||
|
for f in $(ls -1t ${BIN_DIR}/${APP_NAME}-v* 2>/dev/null); do
|
||||||
|
local name
|
||||||
|
name=$(basename "$f")
|
||||||
|
count=$((count+1))
|
||||||
|
if [ $count -gt ${KEEP_VERSIONS} ] && [ "$name" != "$current" ]; then
|
||||||
|
$SUDO rm -f "$f"
|
||||||
|
log " 已删除: $name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# 重启服务 + 验证
|
||||||
|
restart_and_verify() {
|
||||||
|
log "重启 ${APP_NAME} 服务..."
|
||||||
|
if systemctl list-unit-files 2>/dev/null | grep -q "^${APP_NAME}.service"; then
|
||||||
|
$SUDO systemctl restart "${APP_NAME}"
|
||||||
|
$SUDO systemctl enable "${APP_NAME}" >/dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
warn "服务 ${APP_NAME}.service 不存在,请先执行 $0 --init"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v nginx >/dev/null 2>&1; then
|
||||||
|
$SUDO systemctl reload nginx 2>/dev/null || $SUDO nginx -s reload 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 健康自检:确认服务真正响应,而非仅进程存活
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
if curl -fsS --max-time 5 "http://127.0.0.1:${APP_PORT}/healthz" >/dev/null 2>&1; then
|
||||||
|
log "健康检查通过: /healthz"
|
||||||
|
else
|
||||||
|
err "健康检查失败: http://127.0.0.1:${APP_PORT}/healthz"
|
||||||
|
warn "服务可能启动异常,如需回滚执行: $0 --rollback"
|
||||||
|
$SUDO journalctl -u "${APP_NAME}" -n 30 --no-pager || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "服务状态:"
|
||||||
|
$SUDO systemctl status "${APP_NAME}" --no-pager -l | head -n 15 || true
|
||||||
|
echo ""
|
||||||
|
log "最近日志:"
|
||||||
|
$SUDO journalctl -u "${APP_NAME}" -n 15 --no-pager || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== --list ====================
|
||||||
|
if [ "$LIST" = true ]; then
|
||||||
|
list_versions
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==================== --rollback ====================
|
||||||
|
if [ "$ROLLBACK" = true ]; then
|
||||||
|
if [ ! -d "${BIN_DIR}" ]; then
|
||||||
|
err "二进制目录不存在: ${BIN_DIR}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 没指定版本,交互式选择
|
||||||
|
if [ -z "$ROLLBACK_VERSION" ]; then
|
||||||
|
list_versions
|
||||||
|
echo ""
|
||||||
|
read -rp "输入要回滚到的版本号(如 ${APP_NAME}-v1.0.0,直接回车取消): " TARGET
|
||||||
|
[ -z "$TARGET" ] && { info "已取消"; exit 0; }
|
||||||
|
ROLLBACK_VERSION="$TARGET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 补全前缀
|
||||||
|
if [[ "$ROLLBACK_VERSION" != ${APP_NAME}-* ]]; then
|
||||||
|
ROLLBACK_VERSION="${APP_NAME}-${ROLLBACK_VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TARGET_PATH="${BIN_DIR}/${ROLLBACK_VERSION}"
|
||||||
|
if [ ! -f "$TARGET_PATH" ]; then
|
||||||
|
err "版本不存在: $TARGET_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "回滚到: ${ROLLBACK_VERSION}"
|
||||||
|
switch_symlink "${ROLLBACK_VERSION}"
|
||||||
|
restart_and_verify
|
||||||
|
echo ""
|
||||||
|
log "✅ 已回滚到 ${ROLLBACK_VERSION}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==================== 1. 前置检查 ====================
|
||||||
|
log "检查环境..."
|
||||||
|
command -v go >/dev/null 2>&1 || { err "未找到 go,请先安装 Go"; exit 1; }
|
||||||
|
command -v nginx >/dev/null 2>&1 || warn "未检测到 nginx"
|
||||||
|
|
||||||
|
cd "$SRC_DIR" || { err "代码目录不存在: $SRC_DIR"; exit 1; }
|
||||||
|
|
||||||
|
# ==================== 2. 拉取代码 ====================
|
||||||
|
if [ "$NO_PULL" = false ] && [ -d .git ]; then
|
||||||
|
log "拉取最新代码..."
|
||||||
|
git pull --ff-only || warn "git pull 失败,继续使用当前代码"
|
||||||
|
else
|
||||||
|
log "跳过 git pull"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==================== 3. 编译 ====================
|
||||||
|
log "编译 ${APP_NAME} (版本: ${VERSION})..."
|
||||||
|
CGO_ENABLED=0 go build \
|
||||||
|
-ldflags="-s -w -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME}" \
|
||||||
|
-o "${APP_NAME}-${VERSION}" \
|
||||||
|
"${BUILD_TARGET}"
|
||||||
|
|
||||||
|
[ -f "${APP_NAME}-${VERSION}" ] || { err "编译产物不存在"; exit 1; }
|
||||||
|
log "编译完成: ${APP_NAME}-${VERSION}"
|
||||||
|
|
||||||
|
# ==================== --build-only ====================
|
||||||
|
if [ "$BUILD_ONLY" = true ]; then
|
||||||
|
echo ""
|
||||||
|
log "✅ 仅编译模式,产物:$(pwd)/${APP_NAME}-${VERSION}"
|
||||||
|
echo " 如需部署,请执行: $0 ${VERSION} --no-pull"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==================== 4. 部署二进制 ====================
|
||||||
|
log "部署到 ${BIN_DIR}..."
|
||||||
|
$SUDO mkdir -p "${BIN_DIR}"
|
||||||
|
$SUDO mv "${APP_NAME}-${VERSION}" "${BIN_DIR}/"
|
||||||
|
$SUDO chmod +x "${BIN_DIR}/${APP_NAME}-${VERSION}"
|
||||||
|
|
||||||
|
switch_symlink "${APP_NAME}-${VERSION}"
|
||||||
|
log "软链接已切换到: ${APP_NAME}-${VERSION}"
|
||||||
|
|
||||||
|
cleanup_old_versions
|
||||||
|
|
||||||
|
# ==================== 5. 首次初始化 ====================
|
||||||
|
if [ "$INIT" = true ]; then
|
||||||
|
log "初始化 systemd 服务: ${SERVICE_FILE}"
|
||||||
|
$SUDO tee "${SERVICE_FILE}" >/dev/null <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=${APP_DESC}
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=${RUN_USER}
|
||||||
|
Group=${RUN_USER}
|
||||||
|
WorkingDirectory=${SRC_DIR}
|
||||||
|
ExecStart=${BIN_DIR}/${APP_NAME}-current
|
||||||
|
EnvironmentFile=-${ENV_FILE}
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "初始化 Nginx 配置: ${NGINX_CONF}"
|
||||||
|
$SUDO tee "${NGINX_CONF}" >/dev/null <<EOF
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ${DOMAIN};
|
||||||
|
|
||||||
|
access_log /var/log/nginx/${APP_NAME}.access.log;
|
||||||
|
error_log /var/log/nginx/${APP_NAME}.error.log;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:${APP_PORT};
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Certbot 续期后自动重载 Nginx(deploy-hook 只在续期成功时执行)----
|
||||||
|
HOOK_FILE="/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh"
|
||||||
|
if [ ! -f "${HOOK_FILE}" ]; then
|
||||||
|
log "创建 Certbot 续期重载 Hook: ${HOOK_FILE}"
|
||||||
|
$SUDO mkdir -p /etc/letsencrypt/renewal-hooks/deploy/
|
||||||
|
$SUDO tee "${HOOK_FILE}" >/dev/null <<'HOOK'
|
||||||
|
#!/bin/bash
|
||||||
|
if /usr/sbin/nginx -t 2>/dev/null; then
|
||||||
|
systemctl reload nginx
|
||||||
|
fi
|
||||||
|
HOOK
|
||||||
|
$SUDO chmod +x "${HOOK_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 首次初始化自动申请 HTTPS 证书(一次性;续期由 certbot.timer 自动处理)----
|
||||||
|
if [ ! -d "/etc/letsencrypt/live/${DOMAIN}" ]; then
|
||||||
|
if command -v certbot >/dev/null 2>&1; then
|
||||||
|
log "未检测到证书,尝试自动申请: ${DOMAIN}"
|
||||||
|
$SUDO certbot --nginx -d "${DOMAIN}" --non-interactive --agree-tos -m "${CERT_EMAIL}" --redirect \
|
||||||
|
|| warn "证书申请失败,请检查域名解析与 80 端口;可稍后手动执行 certbot --nginx -d ${DOMAIN}"
|
||||||
|
else
|
||||||
|
warn "未安装 certbot,跳过证书申请。可手动执行: dnf install certbot python3-certbot-nginx"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "证书已存在,跳过申请"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then
|
||||||
|
log "检测到 SELinux,放行 Nginx 网络连接..."
|
||||||
|
$SUDO setsebool -P httpd_can_network_connect 1 || warn "setsebool 失败"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld; then
|
||||||
|
log "放行防火墙 80/443..."
|
||||||
|
$SUDO firewall-cmd --permanent --add-service=http >/dev/null
|
||||||
|
$SUDO firewall-cmd --permanent --add-service=https >/dev/null
|
||||||
|
$SUDO firewall-cmd --reload >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "重载 systemd..."
|
||||||
|
$SUDO systemctl daemon-reload
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==================== 6. 校验 Nginx ====================
|
||||||
|
if [ -f "${NGINX_CONF}" ]; then
|
||||||
|
log "校验 Nginx 配置..."
|
||||||
|
$SUDO nginx -t || { err "Nginx 配置有误"; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==================== 7. 重启服务 + 验证 ====================
|
||||||
|
restart_and_verify
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "✅ 部署完成!"
|
||||||
|
echo " 版本: ${VERSION}"
|
||||||
|
echo " 二进制: ${BIN_DIR}/${APP_NAME}-current"
|
||||||
|
echo " 端口: ${APP_PORT}"
|
||||||
|
echo " 域名: http://${DOMAIN}"
|
||||||
|
echo ""
|
||||||
|
echo "常用命令:"
|
||||||
|
echo " 查看状态: sudo systemctl status ${APP_NAME}"
|
||||||
|
echo " 查看日志: sudo journalctl -u ${APP_NAME} -f"
|
||||||
|
echo " 重启服务: sudo systemctl restart ${APP_NAME}"
|
||||||
|
echo " 列出版本: $0 --list"
|
||||||
|
echo " 回滚版本: $0 --rollback"
|
||||||
Reference in new issue
Block a user