Files
fanqie-go/deploy.sh
T
chenxin 889c399a0b 🚀 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 说明
2026-10-09 15:53:21 +08:00

405 lines
13 KiB
Bash
Executable File
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
# ============================================================
# 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"