Files
ai-app-database/scripts/deploy.sh
huty ac8c49edc7
Some checks are pending
CI — Docker Build & Push / Build & Push Image (push) Waiting to run
feat: 添加 Linux 一键部署和更新脚本
scripts/deploy.sh — 一键部署:
  - 自动检测发行版(Ubuntu/Debian/CentOS/RHEL/Rocky/AlmaLinux/
    Fedora/Alpine/Arch/Manjaro/openSUSE)并安装 Docker + Compose
  - 兼容 Docker Compose v1(docker-compose)和 v2(docker compose)
  - 支持两种部署模式:新建 MySQL / 现有 MySQL
  - 支持镜像来源:Gitea 仓库拉取 / 本地构建
  - 交互式配置:端口、密钥、管理员账号、MySQL 密码等
  - 自动生成加密随机 SECRET_KEY
  - 可选启用 Nginx 反向代理(--profile nginx)
  - 启动后执行健康检查,访问 /auth/login 验证

scripts/update.sh — 一键更新:
  - 读取 .deploy-state 恢复上次部署配置,无需重新输入参数
  - 更新前自动备份当前镜像标签(rollback-<时间戳>)
  - 拉取新镜像时对比摘要,无变化时提示可跳过
  - 健康检查失败时自动回滚至备份标签并重启服务
  - 自动清理旧备份镜像(仅保留最近 3 个)
  - 支持 --yes 免交互、--tag 指定目标标签

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 00:58:05 +09:00

498 lines
20 KiB
Bash
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.
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════════
# 个人资料库 — 一键部署脚本
# 支持发行版Ubuntu / Debian / CentOS / RHEL / Rocky / AlmaLinux /
# Fedora / Alpine / Arch / Manjaro
#
# 用法:
# curl -fsSL https://your-gitea/raw/.../scripts/deploy.sh | bash
# 或bash scripts/deploy.sh
# ═══════════════════════════════════════════════════════════════════
set -euo pipefail
IFS=$'\n\t'
# ── 脚本自身位置 & 项目根目录 ─────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
STATE_FILE="$PROJECT_DIR/.deploy-state"
LOG_FILE="/tmp/resource-library-deploy-$(date +%Y%m%d%H%M%S).log"
# ── 颜色 & 输出 ──────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
log() { echo -e "$(date '+%H:%M:%S') $*" | tee -a "$LOG_FILE"; }
info() { echo -e "${CYAN}[INFO]${NC} $*" | tee -a "$LOG_FILE"; }
ok() { echo -e "${GREEN}[ OK ]${NC} $*" | tee -a "$LOG_FILE"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*" | tee -a "$LOG_FILE"; }
err() { echo -e "${RED}[ERR ]${NC} $*" | tee -a "$LOG_FILE" >&2; }
die() { err "$*"; exit 1; }
section() { echo -e "\n${BOLD}${BLUE}── $* ─────────────────────────────────────${NC}" | tee -a "$LOG_FILE"; }
ask() { echo -e "${YELLOW}[?]${NC} $*"; }
banner() {
cat << 'EOF'
╔═══════════════════════════════════════════╗
║ 个人资料库 一键部署脚本 ║
║ Personal Resource Library Deployer ║
╚═══════════════════════════════════════════╝
EOF
}
# ── 工具函数 ──────────────────────────────────────────────────────
command_exists() { command -v "$1" &>/dev/null; }
# 生成安全随机字符串
gen_secret() {
python3 -c "import secrets; print(secrets.token_hex(32))" 2>/dev/null ||
openssl rand -hex 32 2>/dev/null ||
tr -dc 'A-Za-z0-9@#$%' < /dev/urandom | head -c 64
}
# 读取用户输入,支持默认值和可选的不回显(密码)
prompt() {
local var_name="$1" prompt_text="$2" default="${3:-}" secret="${4:-no}"
local value=""
if [ -n "$default" ]; then
ask "$prompt_text [默认: ${secret:+***}${default:-}]: "
else
ask "$prompt_text: "
fi
if [ "$secret" = "yes" ]; then
read -r -s value
echo
else
read -r value
fi
value="${value:-$default}"
eval "$var_name='$value'"
}
# ── 环境检测 ──────────────────────────────────────────────────────
check_bash_version() {
local ver="${BASH_VERSINFO[0]:-0}"
[ "$ver" -ge 4 ] || die "需要 Bash 4.0+,当前版本: $BASH_VERSION"
}
check_root() {
if [ "$(id -u)" -ne 0 ]; then
if command_exists sudo; then
warn "建议以 root 或 sudo 运行;继续尝试…"
SUDO="sudo"
else
die "需要 root 权限(或安装 sudo"
fi
else
SUDO=""
fi
}
check_internet() {
info "检测网络连通性…"
if ! curl -fsSL --max-time 8 https://get.docker.com > /dev/null 2>&1 && \
! wget -q --timeout=8 -O /dev/null https://get.docker.com 2>/dev/null; then
warn "无法访问 https://get.docker.comDocker 自动安装可能失败"
warn "请手动安装 Docker 后重新运行此脚本"
else
ok "网络正常"
fi
}
# ── 发行版检测 ────────────────────────────────────────────────────
detect_os() {
OS_ID="unknown"; OS_PRETTY="Unknown"; PKG_MGR="unknown"
if [ -f /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
OS_ID="${ID:-unknown}"
OS_PRETTY="${PRETTY_NAME:-$OS_ID}"
OS_ID_LIKE="${ID_LIKE:-}"
fi
# 根据 ID 或 ID_LIKE 映射包管理器
case "$OS_ID" in
ubuntu|debian|linuxmint|pop|raspbian|kali)
PKG_MGR="apt" ;;
centos|rhel|ol|scientific)
PKG_MGR="yum_or_dnf" ;;
rocky|almalinux|eurolinux|springdale)
PKG_MGR="dnf" ;;
fedora)
PKG_MGR="dnf" ;;
alpine)
PKG_MGR="apk" ;;
arch|manjaro|endeavouros|garuda|artix)
PKG_MGR="pacman" ;;
opensuse*|sles)
PKG_MGR="zypper" ;;
*)
if echo "${OS_ID_LIKE:-}" | grep -qiE "debian|ubuntu"; then
PKG_MGR="apt"
elif echo "${OS_ID_LIKE:-}" | grep -qiE "rhel|centos|fedora"; then
PKG_MGR="yum_or_dnf"
elif echo "${OS_ID_LIKE:-}" | grep -qiE "suse"; then
PKG_MGR="zypper"
fi
;;
esac
info "操作系统: $OS_PRETTY (ID=$OS_ID, PKG=$PKG_MGR)"
}
# ── Docker 安装 ───────────────────────────────────────────────────
# 使用 Docker 官方安装脚本支持大多数发行版Alpine/Arch 走包管理器
install_docker() {
section "安装 Docker"
if command_exists docker; then
local ver
ver=$(docker --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1)
ok "Docker 已安装: $ver"
return 0
fi
info "开始安装 Docker…"
case "$PKG_MGR" in
apk)
$SUDO apk add --no-cache docker docker-cli-compose
$SUDO rc-update add docker boot 2>/dev/null || true
$SUDO service docker start 2>/dev/null || true
;;
pacman)
$SUDO pacman -Sy --noconfirm docker docker-compose
$SUDO systemctl enable --now docker
;;
zypper)
$SUDO zypper install -y docker docker-compose
$SUDO systemctl enable --now docker
;;
*)
# 官方安装脚本Ubuntu/Debian/CentOS/RHEL/Rocky/Fedora 等)
local tmp
tmp=$(mktemp)
curl -fsSL https://get.docker.com -o "$tmp" || \
wget -qO "$tmp" https://get.docker.com || \
die "下载 Docker 安装脚本失败"
$SUDO sh "$tmp"
rm -f "$tmp"
;;
esac
# 启动并设置开机自启systemd 系统)
if command_exists systemctl && [ "$PKG_MGR" != "apk" ]; then
$SUDO systemctl enable docker 2>/dev/null || true
$SUDO systemctl start docker 2>/dev/null || true
fi
# 将当前用户加入 docker 组(后续免 sudo
if [ -n "${SUDO_USER:-}" ] && getent group docker &>/dev/null; then
$SUDO usermod -aG docker "$SUDO_USER"
warn "已将 $SUDO_USER 加入 docker 组,需重新登录后生效"
fi
ok "Docker 安装完成: $(docker --version 2>/dev/null || echo '(请重新登录后验证)')"
}
# ── Docker Compose 检测 ───────────────────────────────────────────
detect_compose() {
if docker compose version &>/dev/null 2>&1; then
COMPOSE_CMD="docker compose"
ok "Docker Compose v2 (plugin): $(docker compose version --short 2>/dev/null)"
elif command_exists docker-compose; then
COMPOSE_CMD="docker-compose"
ok "Docker Compose v1 (standalone): $(docker-compose --version 2>/dev/null)"
else
info "未找到 Docker Compose尝试安装 Compose 插件…"
install_compose_plugin
COMPOSE_CMD="docker compose"
fi
}
install_compose_plugin() {
local COMPOSE_VER="v2.27.1"
local ARCH; ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="x86_64" ;;
aarch64|arm64) ARCH="aarch64" ;;
armv7l) ARCH="armv7" ;;
*) die "不支持的 CPU 架构: $ARCH" ;;
esac
local URL="https://github.com/docker/compose/releases/download/${COMPOSE_VER}/docker-compose-linux-${ARCH}"
local DEST="/usr/local/lib/docker/cli-plugins/docker-compose"
$SUDO mkdir -p "$(dirname "$DEST")"
$SUDO curl -fsSL "$URL" -o "$DEST" || $SUDO wget -qO "$DEST" "$URL" || \
die "下载 Docker Compose 插件失败"
$SUDO chmod +x "$DEST"
ok "Docker Compose 插件安装完成"
}
# ── 交互配置 ──────────────────────────────────────────────────────
collect_config() {
section "部署配置"
echo
echo " 请选择部署模式:"
echo " 1) 新建 MySQL全栈适合新服务器"
echo " 2) 使用现有 MySQL适合已有数据库"
echo
ask "请输入选项 [1/2](默认: 1: "
read -r DEPLOY_MODE_INPUT
DEPLOY_MODE_INPUT="${DEPLOY_MODE_INPUT:-1}"
case "$DEPLOY_MODE_INPUT" in
1) DEPLOY_MODE="new-mysql" ; COMPOSE_FILE="docker-compose.yml" ; ENV_FILE=".env.docker" ;;
2) DEPLOY_MODE="external-db" ; COMPOSE_FILE="docker-compose.external-db.yml" ; ENV_FILE=".env.external-db" ;;
*) die "无效选项: $DEPLOY_MODE_INPUT" ;;
esac
ok "部署模式: $DEPLOY_MODE"
echo
echo " 请选择镜像来源:"
echo " 1) 从 Gitea 镜像仓库拉取(需已完成 CI 构建)"
echo " 2) 本地构建(无需 CI首次部署推荐"
echo
ask "请输入选项 [1/2](默认: 2: "
read -r IMG_SRC_INPUT
IMG_SRC_INPUT="${IMG_SRC_INPUT:-2}"
case "$IMG_SRC_INPUT" in
1) IMAGE_SOURCE="registry" ;;
2) IMAGE_SOURCE="build" ;;
*) die "无效选项: $IMG_SRC_INPUT" ;;
esac
if [ "$IMAGE_SOURCE" = "registry" ]; then
prompt REGISTRY_HOST "Gitea 镜像仓库地址" "git.hty1024.com"
prompt REGISTRY_IMAGE "镜像名(小写)" "${REGISTRY_HOST}/hty1024/ai-app-database"
prompt REGISTRY_USER "登录用户名" ""
prompt REGISTRY_PASS "登录密码 / Token" "" yes
info "登录镜像仓库…"
echo "$REGISTRY_PASS" | docker login "$REGISTRY_HOST" \
-u "$REGISTRY_USER" --password-stdin || die "登录失败,请检查凭据"
ok "镜像仓库登录成功"
else
REGISTRY_IMAGE=""
fi
echo
# ── 公共配置 ──────────────────────────────────────────────────
local auto_secret; auto_secret=$(gen_secret)
prompt APP_PORT "应用端口" "5000"
prompt SECRET_KEY "应用密钥(留空自动生成)" "$auto_secret" yes
SECRET_KEY="${SECRET_KEY:-$auto_secret}"
prompt ADMIN_USERNAME "管理员用户名" "admin"
prompt ADMIN_PASSWORD "管理员密码" "Admin@123456" yes
prompt ADMIN_EMAIL "管理员邮箱" "admin@example.com"
prompt MAX_UPLOAD_MB "最大上传大小(MB)" "500"
prompt GUNICORN_WORKERS "Gunicorn worker 数量" "4"
# ── 模式专属配置 ──────────────────────────────────────────────
if [ "$DEPLOY_MODE" = "new-mysql" ]; then
echo
info "MySQL 配置(新建数据库)"
local auto_root_pw; auto_root_pw=$(gen_secret | cut -c1-24)
local auto_user_pw; auto_user_pw=$(gen_secret | cut -c1-24)
prompt MYSQL_ROOT_PASSWORD "MySQL root 密码(留空自动生成)" "$auto_root_pw" yes
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-$auto_root_pw}"
prompt MYSQL_PASSWORD "MySQL 应用用户密码(留空自动生成)" "$auto_user_pw" yes
MYSQL_PASSWORD="${MYSQL_PASSWORD:-$auto_user_pw}"
prompt MYSQL_EXPOSE_PORT "MySQL 对外端口(留空=不暴露)" "127.0.0.1:3306"
else
echo
info "外部数据库配置"
echo " 格式: mysql+pymysql://用户名:密码@主机:端口/数据库名"
prompt DATABASE_URL "数据库连接串" ""
[ -n "$DATABASE_URL" ] || die "外部 MySQL 模式必须提供 DATABASE_URL"
prompt DB_WAIT_SECONDS "等待数据库就绪超时(秒)" "30"
fi
# ── 是否启用 Nginx ──────────────────────────────────────────
echo
ask "是否启用 Nginx 反向代理?[y/N](默认: N: "
read -r NGINX_INPUT
USE_NGINX="${NGINX_INPUT:-N}"
if echo "$USE_NGINX" | grep -qiE "^y(es)?$"; then
USE_NGINX="yes"
prompt NGINX_HTTP_PORT "Nginx HTTP 端口" "80"
prompt NGINX_HTTPS_PORT "Nginx HTTPS 端口" "443"
else
USE_NGINX="no"
fi
}
# ── 写入 env 文件 ─────────────────────────────────────────────────
write_env_file() {
section "写入配置文件"
local env_path="$PROJECT_DIR/$ENV_FILE"
# 备份旧文件
if [ -f "$env_path" ]; then
cp "$env_path" "${env_path}.bak.$(date +%Y%m%d%H%M%S)"
warn "已备份旧配置: ${env_path}.bak.*"
fi
{
echo "# 个人资料库部署配置 — 生成于 $(date '+%Y-%m-%d %H:%M:%S')"
echo "# 请勿提交此文件到版本控制系统!"
echo ""
echo "SECRET_KEY=${SECRET_KEY}"
echo "FLASK_ENV=production"
echo "APP_PORT=${APP_PORT}"
echo "MAX_UPLOAD_SIZE_MB=${MAX_UPLOAD_MB}"
echo "GUNICORN_WORKERS=${GUNICORN_WORKERS}"
echo "GUNICORN_TIMEOUT=120"
echo "LOG_LEVEL=info"
echo ""
echo "ADMIN_USERNAME=${ADMIN_USERNAME}"
echo "ADMIN_PASSWORD=${ADMIN_PASSWORD}"
echo "ADMIN_EMAIL=${ADMIN_EMAIL}"
echo ""
if [ "$DEPLOY_MODE" = "new-mysql" ]; then
echo "MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}"
echo "MYSQL_DATABASE=resource_library"
echo "MYSQL_USER=resource_library"
echo "MYSQL_PASSWORD=${MYSQL_PASSWORD}"
echo "MYSQL_EXPOSE_PORT=${MYSQL_EXPOSE_PORT:-127.0.0.1:3306}"
else
echo "DATABASE_URL=${DATABASE_URL}"
echo "DB_WAIT_SECONDS=${DB_WAIT_SECONDS:-30}"
fi
if [ "${USE_NGINX}" = "yes" ]; then
echo ""
echo "NGINX_HTTP_PORT=${NGINX_HTTP_PORT:-80}"
echo "NGINX_HTTPS_PORT=${NGINX_HTTPS_PORT:-443}"
fi
} > "$env_path"
chmod 600 "$env_path"
ok "配置文件已写入: $env_path"
}
# ── 写入部署状态文件 ──────────────────────────────────────────────
write_state_file() {
{
echo "DEPLOY_MODE=${DEPLOY_MODE}"
echo "COMPOSE_FILE=${COMPOSE_FILE}"
echo "ENV_FILE=${ENV_FILE}"
echo "IMAGE_SOURCE=${IMAGE_SOURCE}"
echo "REGISTRY_IMAGE=${REGISTRY_IMAGE:-}"
echo "APP_PORT=${APP_PORT}"
echo "USE_NGINX=${USE_NGINX}"
echo "DEPLOYED_AT=$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
} > "$STATE_FILE"
ok "状态文件已写入: $STATE_FILE"
}
# ── 构建或拉取镜像 ────────────────────────────────────────────────
prepare_image() {
section "准备镜像"
cd "$PROJECT_DIR"
if [ "$IMAGE_SOURCE" = "registry" ]; then
info "从镜像仓库拉取: ${REGISTRY_IMAGE}:latest …"
docker pull "${REGISTRY_IMAGE}:latest" || die "镜像拉取失败"
ok "镜像拉取完成"
else
info "本地构建镜像(首次构建可能需要几分钟)…"
docker build -t resource-library:latest . 2>&1 | tee -a "$LOG_FILE"
ok "本地构建完成"
fi
}
# ── 启动服务 ──────────────────────────────────────────────────────
start_services() {
section "启动服务"
cd "$PROJECT_DIR"
local compose_args=(--env-file "$ENV_FILE" -f "$COMPOSE_FILE")
if [ "$USE_NGINX" = "yes" ]; then
compose_args+=(--profile nginx)
fi
info "执行: $COMPOSE_CMD ${compose_args[*]} up -d"
$COMPOSE_CMD "${compose_args[@]}" up -d 2>&1 | tee -a "$LOG_FILE"
ok "容器已启动"
}
# ── 健康检查 ──────────────────────────────────────────────────────
health_check() {
section "健康检查"
local max_wait=120 elapsed=0 interval=5
local url="http://localhost:${APP_PORT}/auth/login"
info "等待服务就绪(最长 ${max_wait}s"
while [ "$elapsed" -lt "$max_wait" ]; do
if curl -fsSL --max-time 5 "$url" > /dev/null 2>&1; then
ok "服务已就绪!耗时: ${elapsed}s"
return 0
fi
printf '.'
sleep "$interval"
elapsed=$((elapsed + interval))
done
echo
err "健康检查超时(${max_wait}s"
warn "查看日志: $COMPOSE_CMD -f $COMPOSE_FILE logs --tail=50"
return 1
}
# ── 部署完成提示 ──────────────────────────────────────────────────
print_success() {
local local_ip
local_ip=$(hostname -I 2>/dev/null | awk '{print $1}') || local_ip="<服务器IP>"
echo
echo -e "${GREEN}${BOLD}═══════════════════════════════════════════════${NC}"
echo -e "${GREEN}${BOLD} 🎉 部署成功! ${NC}"
echo -e "${GREEN}${BOLD}═══════════════════════════════════════════════${NC}"
echo
echo -e " 访问地址: ${CYAN}http://${local_ip}:${APP_PORT}${NC}"
if [ "$USE_NGINX" = "yes" ]; then
echo -e " Nginx: ${CYAN}http://${local_ip}:${NGINX_HTTP_PORT:-80}${NC}"
fi
echo
echo -e " 管理员账号: ${YELLOW}${ADMIN_USERNAME}${NC}"
echo -e " 管理员密码: ${YELLOW}(部署时设置)${NC}"
echo
echo -e " 查看日志: ${BLUE}$COMPOSE_CMD -f $COMPOSE_FILE logs -f${NC}"
echo -e " 更新应用: ${BLUE}bash scripts/update.sh${NC}"
echo -e " 停止服务: ${BLUE}$COMPOSE_CMD -f $COMPOSE_FILE down${NC}"
echo
echo -e " 部署日志: $LOG_FILE"
echo -e "${GREEN}${BOLD}═══════════════════════════════════════════════${NC}"
echo
}
# ── 主流程 ────────────────────────────────────────────────────────
main() {
banner
log "部署日志: $LOG_FILE"
check_bash_version
check_root
check_internet
detect_os
install_docker
detect_compose
collect_config
write_env_file
write_state_file
prepare_image
start_services
health_check
print_success
}
main "$@"