Files
xingrin/install.sh

491 lines
17 KiB
Bash
Raw Normal View History

2025-12-12 18:04:57 +08:00
#!/bin/bash
set -e
# ==============================================================================
# 用法:
# sudo ./install.sh 生产模式(拉取 Docker Hub 镜像)
# sudo ./install.sh --dev 开发模式(本地构建 + 调试日志)
# sudo ./install.sh --no-frontend 安装并只启动后端
# sudo ./install.sh --dev --no-frontend 开发模式 + 只启动后端
# ==============================================================================
# 解析参数
START_ARGS=""
DEV_MODE=false
for arg in "$@"; do
case $arg in
--dev)
DEV_MODE=true
START_ARGS="$START_ARGS --dev"
;;
--no-frontend)
START_ARGS="$START_ARGS --no-frontend"
;;
esac
done
# ==============================================================================
# 颜色定义
# ==============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
# ==============================================================================
# 日志函数
# ==============================================================================
info() {
echo -e "${BLUE}[INFO]${RESET} $1"
}
success() {
echo -e "${GREEN}[OK]${RESET} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${RESET} $1"
}
error() {
echo -e "${RED}[ERROR]${RESET} $1"
}
step() {
echo -e "\n${BOLD}${CYAN}>>> $1${RESET}"
}
header() {
echo -e "${BOLD}${BLUE}============================================================${RESET}"
echo -e "${BOLD}${BLUE} $1${RESET}"
echo -e "${BOLD}${BLUE}============================================================${RESET}"
}
# ==============================================================================
# 权限检查
# ==============================================================================
if [ "$EUID" -ne 0 ]; then
error "请使用 sudo 运行此脚本"
echo -e " 正确用法: ${BOLD}sudo ./install.sh${RESET}"
exit 1
fi
# 获取真实用户(通过 sudo 运行时 $SUDO_USER 是真实用户)
REAL_USER="${SUDO_USER:-$USER}"
# macOS 没有 getent使用 dscl 或 ~$USER 替代
if command -v getent &>/dev/null; then
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
else
REAL_HOME=$(eval echo "~$REAL_USER")
fi
2025-12-12 18:04:57 +08:00
# 项目根目录
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT_DIR"
# 从 VERSION 文件读取版本号
VERSION_FILE="$ROOT_DIR/VERSION"
if [ -f "$VERSION_FILE" ]; then
APP_VERSION=$(cat "$VERSION_FILE" | tr -d '[:space:]')
else
error "VERSION 文件不存在,无法确定安装版本"
exit 1
fi
2025-12-12 18:04:57 +08:00
# 显示标题
header "XingRin 一键安装脚本 (Ubuntu)"
info "当前用户: ${BOLD}$REAL_USER${RESET}"
info "项目路径: ${BOLD}$ROOT_DIR${RESET}"
info "安装版本: ${BOLD}$APP_VERSION${RESET}"
2025-12-12 18:04:57 +08:00
# ==============================================================================
# 工具函数
# ==============================================================================
# 生成随机字符串
generate_random_string() {
local length="${1:-32}"
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex "$length" 2>/dev/null | cut -c1-"$length"
else
date +%s%N | sha256sum | cut -c1-"$length"
fi
}
# 跨平台 sed -i兼容 macOS 和 Linux
sed_inplace() {
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "$@"
else
sed -i "$@"
fi
}
2025-12-12 18:04:57 +08:00
# 更新 .env 文件中的某个键
update_env_var() {
local file="$1"
local key="$2"
local value="$3"
if grep -q "^$key=" "$file"; then
sed_inplace "s|^$key=.*|$key=$value|" "$file"
2025-12-12 18:04:57 +08:00
else
echo "$key=$value" >> "$file"
fi
}
# 用于保存生成的密码,方便最后显示
GENERATED_DB_PASSWORD=""
GENERATED_DJANGO_KEY=""
2025-12-18 18:53:28 +08:00
# 生成自签 HTTPS 证书(使用容器,避免宿主机 openssl 兼容性问题)
2025-12-12 18:04:57 +08:00
generate_self_signed_cert() {
local ssl_dir="$DOCKER_DIR/nginx/ssl"
local fullchain="$ssl_dir/fullchain.pem"
local privkey="$ssl_dir/privkey.pem"
if [ -f "$fullchain" ] && [ -f "$privkey" ]; then
success "检测到已有 HTTPS 证书,跳过自签"
return
fi
info "未检测到 HTTPS 证书正在生成自签证书localhost..."
mkdir -p "$ssl_dir"
2025-12-18 18:53:28 +08:00
# 使用容器生成证书,避免依赖宿主机 openssl 版本
if docker run --rm -v "$ssl_dir:/ssl" alpine/openssl \
req -x509 -nodes -newkey rsa:2048 -days 365 \
-keyout /ssl/privkey.pem \
-out /ssl/fullchain.pem \
2025-12-12 18:04:57 +08:00
-subj "/C=CN/ST=NA/L=NA/O=XingRin/CN=localhost" \
2025-12-18 18:53:28 +08:00
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
>/dev/null 2>&1; then
2025-12-12 18:04:57 +08:00
success "自签证书已生成: $ssl_dir"
else
2025-12-18 18:53:28 +08:00
warn "自签证书生成失败,请手动放置证书到 $ssl_dir"
2025-12-12 18:04:57 +08:00
fi
}
# 自动为 docker/.env 填充敏感变量
auto_fill_docker_env_secrets() {
local env_file="$1"
info "自动生成 DJANGO_SECRET_KEY 和 DB_PASSWORD..."
GENERATED_DJANGO_KEY="$(generate_random_string 64)"
GENERATED_DB_PASSWORD="$(generate_random_string 32)"
update_env_var "$env_file" "DJANGO_SECRET_KEY" "$GENERATED_DJANGO_KEY"
update_env_var "$env_file" "DB_PASSWORD" "$GENERATED_DB_PASSWORD"
success "密钥生成完成"
}
# 显示安装总结信息
show_summary() {
echo
if [ "$1" == "success" ]; then
header "服务已成功启动!"
else
header "安装完成 Summary"
fi
if [ -f "$DOCKER_DIR/.env" ]; then
# 从 .env 读取配置用于显示
DB_HOST=$(grep "^DB_HOST=" "$DOCKER_DIR/.env" | cut -d= -f2)
DB_USER=$(grep "^DB_USER=" "$DOCKER_DIR/.env" | cut -d= -f2)
DB_PASSWORD=$(grep "^DB_PASSWORD=" "$DOCKER_DIR/.env" | cut -d= -f2)
echo -e "${YELLOW}数据库配置:${RESET}"
echo -e "------------------------------------------------------------"
echo -e " 服务器地址: ${DB_HOST:-未知}"
echo -e " 用户名: ${DB_USER:-未知}"
echo -e " 密码: ${DB_PASSWORD:-未知}"
echo -e "------------------------------------------------------------"
echo
fi
# 获取访问地址
PUBLIC_HOST=$(grep "^PUBLIC_HOST=" "$DOCKER_DIR/.env" 2>/dev/null | cut -d= -f2)
if [ -n "$PUBLIC_HOST" ] && [ "$PUBLIC_HOST" != "server" ]; then
ACCESS_HOST="$PUBLIC_HOST"
else
ACCESS_HOST="localhost"
fi
echo -e "${GREEN}访问地址:${RESET}"
printf " %-16s %s\n" "XingRin:" "https://${ACCESS_HOST}/"
echo -e " ${YELLOW}(HTTP 会自动跳转到 HTTPS)${RESET}"
echo
echo -e "${YELLOW}默认登录账号:${RESET}"
printf " %-16s %s\n" "用户名:" "admin"
printf " %-16s %s\n" "密码:" "admin"
echo -e "${YELLOW} [!] 请首次登录后修改密码!${RESET}"
echo
if [ "$1" != "success" ]; then
echo -e "${GREEN}后续启动命令:${RESET}"
echo -e " ./start.sh # 启动所有服务"
echo -e " ./start.sh --no-frontend # 只启动后端"
echo -e " ./stop.sh # 停止所有服务"
echo -e " ./update.sh # 更新系统"
echo
fi
2025-12-16 17:56:24 +08:00
echo -e "${YELLOW}[!] 云服务器某些厂商默认开启了安全策略(阿里云/腾讯云/华为云等):${RESET}"
echo -e " 端口未放行可能导致无法访问或无法扫描强烈推荐用国外vps或者在云控制台放行"
2025-12-19 18:37:05 +08:00
echo -e " ${RESET}80, 443, 5432, 6379"
2025-12-16 17:56:24 +08:00
echo
2025-12-12 18:04:57 +08:00
}
# ==============================================================================
# 安装流程
# ==============================================================================
step "[1/3] 检查基础命令"
MISSING_CMDS=()
2025-12-18 18:53:28 +08:00
for cmd in git curl; do
2025-12-12 18:04:57 +08:00
if ! command -v "$cmd" >/dev/null 2>&1; then
MISSING_CMDS+=("$cmd")
warn "未安装: $cmd"
else
success "已安装: $cmd"
fi
done
if [ ${#MISSING_CMDS[@]} -gt 0 ]; then
info "正在安装缺失命令: ${MISSING_CMDS[*]}..."
apt update -qq
apt install -y "${MISSING_CMDS[@]}"
success "基础命令安装完成"
fi
step "[2/3] 检查 Docker 环境"
if command -v docker >/dev/null 2>&1; then
success "已安装: docker"
else
info "正在安装 Docker..."
curl -fsSL https://get.docker.com | sh
usermod -aG docker "$REAL_USER"
success "Docker 安装完成"
fi
# 检查 docker compose
if docker compose version >/dev/null 2>&1; then
success "已安装: docker compose"
else
info "正在安装 docker-compose-plugin..."
apt install -y docker-compose-plugin
success "docker compose 安装完成"
fi
# ==============================================================================
# 交换分区配置(仅 Linux
# ==============================================================================
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
2025-12-19 19:02:43 +08:00
# 获取当前内存大小GB四舍五入
TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
2025-12-19 19:02:43 +08:00
TOTAL_MEM_GB=$(awk "BEGIN {printf \"%.0f\", $TOTAL_MEM_KB / 1024 / 1024}")
2025-12-19 12:33:48 +08:00
2025-12-19 19:02:43 +08:00
# 获取当前交换分区大小GB四舍五入
2025-12-19 12:33:48 +08:00
CURRENT_SWAP_KB=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
2025-12-19 19:02:43 +08:00
CURRENT_SWAP_GB=$(awk "BEGIN {printf \"%.0f\", $CURRENT_SWAP_KB / 1024 / 1024}")
# 推荐交换分区大小与内存相同最小1G最大8G
RECOMMENDED_SWAP=$TOTAL_MEM_GB
[ "$RECOMMENDED_SWAP" -lt 1 ] && RECOMMENDED_SWAP=1
[ "$RECOMMENDED_SWAP" -gt 8 ] && RECOMMENDED_SWAP=8
echo ""
info "系统内存: ${TOTAL_MEM_GB}GB当前交换分区: ${CURRENT_SWAP_GB}GB"
# 如果交换分区小于推荐值,提示用户
2025-12-19 12:33:48 +08:00
if [ "$CURRENT_SWAP_GB" -lt "$RECOMMENDED_SWAP" ]; then
echo -n -e "${BOLD}${CYAN}[?] 是否开启 ${RECOMMENDED_SWAP}GB 交换分区?可提升扫描稳定性 (Y/n) ${RESET}"
read -r setup_swap
echo
if [[ ! $setup_swap =~ ^[Nn]$ ]]; then
info "正在配置 ${RECOMMENDED_SWAP}GB 交换分区..."
if bash "$ROOT_DIR/docker/scripts/setup-swap.sh" "$RECOMMENDED_SWAP"; then
success "交换分区配置完成"
else
warn "交换分区配置失败,继续安装..."
fi
else
info "跳过交换分区配置"
fi
else
success "交换分区已足够: ${CURRENT_SWAP_GB}GB"
fi
fi
2025-12-12 18:04:57 +08:00
step "[3/3] 初始化配置"
DOCKER_DIR="$ROOT_DIR/docker"
if [ ! -d "$DOCKER_DIR" ]; then
error "未找到 docker 目录,请确认项目结构。"
exit 1
fi
if [ -f "$DOCKER_DIR/.env.example" ]; then
cp "$DOCKER_DIR/.env.example" "$DOCKER_DIR/.env"
success "已创建: docker/.env"
auto_fill_docker_env_secrets "$DOCKER_DIR/.env"
# 写入版本号(锁定安装时的版本)
update_env_var "$DOCKER_DIR/.env" "IMAGE_TAG" "$APP_VERSION"
success "已锁定版本: IMAGE_TAG=$APP_VERSION"
2025-12-12 18:04:57 +08:00
# 开发模式:开启调试日志
if [ "$DEV_MODE" = true ]; then
info "开发模式:开启调试配置..."
update_env_var "$DOCKER_DIR/.env" "DEBUG" "True"
update_env_var "$DOCKER_DIR/.env" "LOG_LEVEL" "INFO"
update_env_var "$DOCKER_DIR/.env" "ENABLE_COMMAND_LOGGING" "true"
success "已开启: DEBUG=True, LOG_LEVEL=INFO, ENABLE_COMMAND_LOGGING=true"
fi
# 询问数据库配置
echo ""
echo -n -e "${BOLD}${CYAN}[?] 是否使用远程 PostgreSQL 数据库?(y/N) ${RESET}"
read -r use_remote_db
echo
if [[ $use_remote_db =~ ^[Yy]$ ]]; then
echo -e "${CYAN} 请输入远程 PostgreSQL 配置:${RESET}"
# 服务器地址(必填)
echo -n -e " ${CYAN}服务器地址: ${RESET}"
read -r db_host
if [ -z "$db_host" ]; then
error "服务器地址不能为空"
exit 1
fi
# 端口(可选)
echo -n -e " ${CYAN}端口 [5432]: ${RESET}"
read -r db_port
db_port=${db_port:-5432}
# 用户名(必填)
echo -n -e " ${CYAN}用户名: ${RESET}"
read -r db_user
if [ -z "$db_user" ]; then
error "用户名不能为空"
exit 1
fi
# 密码(必填)
echo -n -e " ${CYAN}密码: ${RESET}"
read -r db_password
if [ -z "$db_password" ]; then
error "密码不能为空"
exit 1
fi
# 验证远程 PostgreSQL 连接(使用官方 postgres 镜像中的 psql
echo
info "正在验证远程 PostgreSQL 连接..."
# 使用 postgres 默认库验证连接(每个 PostgreSQL 都有这个库)
if ! docker run --rm \
-e PGPASSWORD="$db_password" \
postgres:15 \
psql "postgresql://$db_user@$db_host:$db_port/postgres" -c 'SELECT 1' >/dev/null 2>&1; then
echo
error "无法连接到远程 PostgreSQL请检查 IP/端口/用户名/密码是否正确"
echo " 尝试连接: postgresql://$db_user@$db_host:$db_port/postgres"
exit 1
fi
success "远程 PostgreSQL 连接验证通过"
# 尝试创建业务数据库(如果不存在)
info "检查并创建数据库..."
db_name=$(grep "^DB_NAME=" "$DOCKER_DIR/.env" | cut -d= -f2)
db_name=${db_name:-xingrin}
prefect_db=$(grep "^PREFECT_DB_NAME=" "$DOCKER_DIR/.env" | cut -d= -f2)
prefect_db=${prefect_db:-prefect}
docker run --rm -e PGPASSWORD="$db_password" postgres:15 \
psql "postgresql://$db_user@$db_host:$db_port/postgres" \
-c "CREATE DATABASE $db_name;" 2>/dev/null || true
docker run --rm -e PGPASSWORD="$db_password" postgres:15 \
psql "postgresql://$db_user@$db_host:$db_port/postgres" \
-c "CREATE DATABASE $prefect_db;" 2>/dev/null || true
success "数据库准备完成"
sed_inplace "s/^DB_HOST=.*/DB_HOST=$db_host/" "$DOCKER_DIR/.env"
sed_inplace "s/^DB_PORT=.*/DB_PORT=$db_port/" "$DOCKER_DIR/.env"
sed_inplace "s/^DB_USER=.*/DB_USER=$db_user/" "$DOCKER_DIR/.env"
sed_inplace "s/^DB_PASSWORD=.*/DB_PASSWORD=$db_password/" "$DOCKER_DIR/.env"
2025-12-12 18:04:57 +08:00
success "已配置远程数据库: $db_user@$db_host:$db_port"
else
info "使用本地 PostgreSQL 容器"
fi
# 是否为远程 VPS 部署(需要从其它机器 / Worker 访问本系统)
echo ""
echo -n -e "${BOLD}${CYAN}[?] 当前是否为远程 VPS 部署?(y/N) ${RESET}"
read -r set_public_host
echo
if [[ $set_public_host =~ ^[Yy]$ ]]; then
echo -n -e " ${CYAN}请输入当前远程 vps 的外网 IP 地址(例如 10.1.1.1: ${RESET}"
read -r public_host
if [ -z "$public_host" ]; then
warn "未输入外网ip地址将保持 .env 中已有的 PUBLIC_HOST请确保 Worker 能访问该地址)"
else
update_env_var "$DOCKER_DIR/.env" "PUBLIC_HOST" "$public_host"
success "已配置对外访问地址: $public_host"
fi
else
info "检测为本机 docker 部署,将 PUBLIC_HOST 设置为 server容器内部访问后端服务名"
update_env_var "$DOCKER_DIR/.env" "PUBLIC_HOST" "server"
fi
else
error "未找到 docker/.env.example"
exit 1
fi
# 准备 HTTPS 证书(无域名也可使用自签)
generate_self_signed_cert
2025-12-17 17:14:50 +08:00
# ==============================================================================
# 预拉取 Worker 镜像(避免扫描时等待)
# ==============================================================================
step "预拉取 Worker 镜像..."
DOCKER_USER=$(grep "^DOCKER_USER=" "$DOCKER_DIR/.env" 2>/dev/null | cut -d= -f2)
DOCKER_USER=${DOCKER_USER:-yyhuni}
WORKER_IMAGE="${DOCKER_USER}/xingrin-worker:${APP_VERSION}"
# 开发模式下构建本地 worker 镜像
if [ "$DEV_MODE" = true ]; then
info "开发模式:构建本地 Worker 镜像..."
if docker compose -f "$DOCKER_DIR/docker-compose.dev.yml" build worker; then
2025-12-18 22:55:33 +08:00
# 设置 TASK_EXECUTOR_IMAGE 环境变量指向本地构建的镜像(使用版本号-dev标识
update_env_var "$DOCKER_DIR/.env" "TASK_EXECUTOR_IMAGE" "docker-worker:${APP_VERSION}-dev"
success "本地 Worker 镜像构建完成: docker-worker:${APP_VERSION}-dev"
else
2025-12-18 22:55:33 +08:00
error "开发模式下本地 Worker 镜像构建失败!"
error "请检查构建错误并修复后重试"
exit 1
fi
2025-12-17 17:14:50 +08:00
else
info "正在拉取: $WORKER_IMAGE"
if docker pull "$WORKER_IMAGE"; then
success "Worker 镜像拉取完成"
else
2025-12-18 22:55:33 +08:00
error "Worker 镜像拉取失败,无法继续安装"
error "请检查网络连接或 Docker Hub 访问权限"
error "镜像地址: $WORKER_IMAGE"
exit 1
fi
2025-12-17 17:14:50 +08:00
fi
2025-12-12 18:04:57 +08:00
# ==============================================================================
# 启动服务
# ==============================================================================
step "正在启动服务..."
"$ROOT_DIR/start.sh" $START_ARGS
# ==============================================================================
# 完成总结
# ==============================================================================
show_summary "success"