Files
xingrin/install.sh
2025-12-19 12:33:48 +08:00

491 lines
17 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
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
# 项目根目录
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
# 显示标题
header "XingRin 一键安装脚本 (Ubuntu)"
info "当前用户: ${BOLD}$REAL_USER${RESET}"
info "项目路径: ${BOLD}$ROOT_DIR${RESET}"
info "安装版本: ${BOLD}$APP_VERSION${RESET}"
# ==============================================================================
# 工具函数
# ==============================================================================
# 生成随机字符串
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
}
# 更新 .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"
else
echo "$key=$value" >> "$file"
fi
}
# 用于保存生成的密码,方便最后显示
GENERATED_DB_PASSWORD=""
GENERATED_DJANGO_KEY=""
# 生成自签 HTTPS 证书(使用容器,避免宿主机 openssl 兼容性问题)
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"
# 使用容器生成证书,避免依赖宿主机 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 \
-subj "/C=CN/ST=NA/L=NA/O=XingRin/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
>/dev/null 2>&1; then
success "自签证书已生成: $ssl_dir"
else
warn "自签证书生成失败,请手动放置证书到 $ssl_dir"
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
echo -e "${YELLOW}[!] 云服务器某些厂商默认开启了安全策略(阿里云/腾讯云/华为云等):${RESET}"
echo -e " 端口未放行可能导致无法访问或无法扫描强烈推荐用国外vps或者在云控制台放行"
echo -e " ${RESET}80, 443, 3000,8888, 5432, 6379"
echo
}
# ==============================================================================
# 安装流程
# ==============================================================================
step "[1/3] 检查基础命令"
MISSING_CMDS=()
for cmd in git curl; do
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
# 获取当前内存大小GB纯 bash 算术)
TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
TOTAL_MEM_GB=$((TOTAL_MEM_KB / 1024 / 1024))
# 获取当前交换分区大小GB
CURRENT_SWAP_KB=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
CURRENT_SWAP_GB=$((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"
# 如果交换分区小于推荐值,提示用户
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
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"
# 开发模式:开启调试日志
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"
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
# ==============================================================================
# 预拉取 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
# 设置 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
error "开发模式下本地 Worker 镜像构建失败!"
error "请检查构建错误并修复后重试"
exit 1
fi
else
info "正在拉取: $WORKER_IMAGE"
if docker pull "$WORKER_IMAGE"; then
success "Worker 镜像拉取完成"
else
error "Worker 镜像拉取失败,无法继续安装"
error "请检查网络连接或 Docker Hub 访问权限"
error "镜像地址: $WORKER_IMAGE"
exit 1
fi
fi
# ==============================================================================
# 启动服务
# ==============================================================================
step "正在启动服务..."
"$ROOT_DIR/start.sh" $START_ARGS
# ==============================================================================
# 完成总结
# ==============================================================================
show_summary "success"