From 36b4e8e0c85a39a3391d736812f49a3db514978a Mon Sep 17 00:00:00 2001 From: JetSprow Date: Thu, 30 Apr 2026 09:18:05 +1000 Subject: [PATCH] feat: adapt panel docker build for low resource hosts --- Dockerfile | 7 +- README.md | 27 +++- scripts/install-jboard-panel.sh | 42 +++++- scripts/lib-resource-profile.sh | 238 ++++++++++++++++++++++++++++++++ scripts/upgrade-jboard-panel.sh | 71 ++++++++-- 5 files changed, 367 insertions(+), 18 deletions(-) create mode 100644 scripts/lib-resource-profile.sh diff --git a/Dockerfile b/Dockerfile index d863797..eb05fa2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,17 +3,20 @@ FROM node:20-alpine AS base # --- deps: install production + dev dependencies --- FROM base AS deps WORKDIR /app +ARG NPM_CONFIG_JOBS="" +ENV npm_config_jobs=${NPM_CONFIG_JOBS} RUN apk add --no-cache python3 make g++ COPY package.json package-lock.json ./ -RUN npm ci +RUN npm ci --no-audit --no-fund # --- builder: generate Prisma client & build Next.js --- FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . +ARG NEXT_BUILD_NODE_OPTIONS="" ENV NEXT_TELEMETRY_DISABLED=1 -ENV NODE_OPTIONS=--max-old-space-size=2048 +ENV NODE_OPTIONS=${NEXT_BUILD_NODE_OPTIONS} RUN npx prisma generate RUN npm run build diff --git a/README.md b/README.md index 35dcb14..5b4b589 100644 --- a/README.md +++ b/README.md @@ -128,9 +128,11 @@ SMTP 邮件服务、注册邮箱验证开关、支付方式、3x-ui 节点等业 适合全新 Linux 服务器。脚本会安装基础依赖、安装 Docker 与 Compose 插件、拉取代码、生成 `.env`、初始化数据库并启动面板。 ```bash -bash <(curl -fsSL https://raw.githubusercontent.com/JetSprow/J-Board/main/scripts/install-jboard-panel.sh) +bash <(curl -fsSL https://raw.githubusercontent.com/JetSprow/J-Board/lite/scripts/install-jboard-panel.sh) ``` +安装和更新脚本会自动检测 CPU、宿主机内存、Docker 可用内存和 Docker 数据盘空间。常规机器使用 Docker 默认构建策略;1C、低于 2GB 内存或 Docker 可用空间低于 8GB 的小机器会自动进入低资源模式,降低 Compose 并发、npm 编译并发和 Next 构建堆内存,并把镜像分步构建,构建会更慢但峰值占用更低。 + 脚本会交互询问: | 问题 | 如何填写 | @@ -146,7 +148,20 @@ bash <(curl -fsSL https://raw.githubusercontent.com/JetSprow/J-Board/main/script 也可以通过环境变量覆盖默认行为: ```bash -APP_DIR=/opt/jboard GH_REPO=JetSprow/J-Board BRANCH=main bash <(curl -fsSL https://raw.githubusercontent.com/JetSprow/J-Board/main/scripts/install-jboard-panel.sh) +APP_DIR=/opt/jboard GH_REPO=JetSprow/J-Board BRANCH=lite bash <(curl -fsSL https://raw.githubusercontent.com/JetSprow/J-Board/lite/scripts/install-jboard-panel.sh) +``` + +构建资源策略也可以手动覆盖: + +```bash +# 自动判断,默认值 +JBOARD_BUILD_PROFILE=auto ./scripts/upgrade-jboard-panel.sh + +# 强制低资源慢速构建 +JBOARD_BUILD_PROFILE=low ./scripts/upgrade-jboard-panel.sh + +# 强制常规构建,不额外限制并发或 Node heap +JBOARD_BUILD_PROFILE=normal ./scripts/upgrade-jboard-panel.sh ``` 脚本完成后会输出: @@ -188,6 +203,8 @@ docker compose up -d app ./scripts/upgrade-jboard-panel.sh ``` +更新脚本同样会自动判断机器配置,并在更新前备份 SQLite 数据库到 `backups/`。 + 常用排障: ```bash @@ -384,17 +401,17 @@ Cloudflare 场景建议在 Rules -> Settings -> Managed Transforms 开启 Add vi ## 备份与恢复 -下载 SQL 备份: +下载 SQLite 备份: ```bash -docker compose exec -T db pg_dump -U jboard jboard > backup_$(date +%Y%m%d_%H%M%S).sql +docker compose exec -T app sh -lc 'cd /app/storage && set -- jboard.db; [ -f jboard.db-wal ] && set -- "$@" jboard.db-wal; [ -f jboard.db-shm ] && set -- "$@" jboard.db-shm; tar -czf - "$@"' > backup_$(date +%Y%m%d_%H%M%S).tar.gz ``` 后台也可通过 `/admin/backups` 导出或恢复数据库。恢复前务必先保存当前数据库备份。 建议: -- 定期备份 PostgreSQL volume。 +- 定期备份 SQLite volume 或通过后台导出数据库。 - 将备份加密保存。 - 备份恢复后立即检查管理员登录、支付配置、节点密码、SMTP、订阅 URL 和 Agent 上报。 diff --git a/scripts/install-jboard-panel.sh b/scripts/install-jboard-panel.sh index c661056..e25aed1 100755 --- a/scripts/install-jboard-panel.sh +++ b/scripts/install-jboard-panel.sh @@ -2,7 +2,7 @@ set -euo pipefail GH_REPO="${GH_REPO:-JetSprow/J-Board}" -BRANCH="${BRANCH:-main}" +BRANCH="${BRANCH:-lite}" APP_DIR="${APP_DIR:-}" REWRITE_ENV="${REWRITE_ENV:-}" SKIP_DOCKER_INSTALL="${SKIP_DOCKER_INSTALL:-0}" @@ -228,6 +228,16 @@ git_in_repo() { fi } +load_resource_helpers() { + local helper="$APP_DIR/scripts/lib-resource-profile.sh" + if [ -f "$helper" ]; then + # shellcheck disable=SC1090 + . "$helper" + else + echo "未找到资源检测脚本:$helper,将使用 Docker 默认构建策略。" + fi +} + prepare_repo() { section "准备 J-Board 代码" @@ -345,12 +355,37 @@ configure_env() { } docker_compose() { - run_as_root docker compose "$@" + local env_args=() + if [ -n "${COMPOSE_PARALLEL_LIMIT:-}" ]; then + env_args+=("COMPOSE_PARALLEL_LIMIT=$COMPOSE_PARALLEL_LIMIT") + fi + + if [ "$(id -u)" -eq 0 ]; then + env "${env_args[@]}" docker compose "$@" + elif command -v sudo >/dev/null 2>&1; then + sudo env "${env_args[@]}" docker compose "$@" + else + echo "需要 root 权限。请使用 root 用户运行,或先安装 sudo。" >&2 + exit 1 + fi } start_panel() { section "构建并启动面板" - docker_compose build init app + + if command -v jboard_prepare_docker_build_env >/dev/null 2>&1; then + jboard_prepare_docker_build_env + jboard_print_build_profile + if jboard_is_low_resource_build; then + docker_compose build "${JBOARD_DOCKER_BUILD_ARGS[@]}" init + docker_compose build "${JBOARD_DOCKER_BUILD_ARGS[@]}" app + else + docker_compose build "${JBOARD_DOCKER_BUILD_ARGS[@]}" init app + fi + else + docker_compose build init app + fi + docker_compose --profile setup run --rm init docker_compose up -d app } @@ -425,6 +460,7 @@ main() { install_base_packages prepare_repo + load_resource_helpers configure_env install_docker start_panel diff --git a/scripts/lib-resource-profile.sh b/scripts/lib-resource-profile.sh new file mode 100644 index 0000000..7fefc7a --- /dev/null +++ b/scripts/lib-resource-profile.sh @@ -0,0 +1,238 @@ +#!/usr/bin/env bash + +# Shared build profile detection for panel install/upgrade scripts. +# Strong machines use Docker's default behavior. Small machines trade time for +# lower peak CPU and memory usage. + +JBOARD_BUILD_PROFILE_RESOLVED="" +JBOARD_CPU_COUNT="" +JBOARD_HOST_MEM_MB="" +JBOARD_DOCKER_MEM_MB="" +JBOARD_DOCKER_DISK_AVAIL_MB="" +JBOARD_EFFECTIVE_MEM_MB="" +JBOARD_NODE_HEAP_MB="" +JBOARD_DOCKER_BUILD_ARGS=() + +jboard_cpu_count() { + local value="" + + if command -v nproc >/dev/null 2>&1; then + value="$(nproc 2>/dev/null || true)" + elif command -v getconf >/dev/null 2>&1; then + value="$(getconf _NPROCESSORS_ONLN 2>/dev/null || true)" + elif command -v sysctl >/dev/null 2>&1; then + value="$(sysctl -n hw.ncpu 2>/dev/null || true)" + fi + + case "$value" in + ''|*[!0-9]*) echo 1 ;; + *) echo "$value" ;; + esac +} + +jboard_host_mem_mb() { + local value="" + + if [ -r /proc/meminfo ]; then + value="$(awk '/^MemTotal:/ {print int($2 / 1024)}' /proc/meminfo 2>/dev/null || true)" + elif command -v getconf >/dev/null 2>&1; then + local pages page_size + pages="$(getconf _PHYS_PAGES 2>/dev/null || true)" + page_size="$(getconf PAGE_SIZE 2>/dev/null || true)" + if [ -n "$pages" ] && [ -n "$page_size" ]; then + value="$((pages * page_size / 1024 / 1024))" + fi + elif command -v sysctl >/dev/null 2>&1; then + local bytes + bytes="$(sysctl -n hw.memsize 2>/dev/null || true)" + if [ -n "$bytes" ]; then + value="$((bytes / 1024 / 1024))" + fi + fi + + case "$value" in + ''|*[!0-9]*) echo 0 ;; + *) echo "$value" ;; + esac +} + +jboard_docker_info() { + if ! command -v docker >/dev/null 2>&1; then + return 1 + fi + + if command -v run_as_root >/dev/null 2>&1; then + run_as_root docker info "$@" 2>/dev/null + else + docker info "$@" 2>/dev/null + fi +} + +jboard_docker_mem_mb() { + local bytes="" + bytes="$(jboard_docker_info --format '{{.MemTotal}}' 2>/dev/null || true)" + + case "$bytes" in + ''|*[!0-9]*) echo 0 ;; + *) echo "$((bytes / 1024 / 1024))" ;; + esac +} + +jboard_path_avail_mb() { + local path="$1" + local value="" + + if [ -n "$path" ]; then + if command -v run_as_root >/dev/null 2>&1; then + value="$(run_as_root df -Pm "$path" 2>/dev/null | awk 'NR == 2 {print $4}' || true)" + else + value="$(df -Pm "$path" 2>/dev/null | awk 'NR == 2 {print $4}' || true)" + fi + fi + + case "$value" in + ''|*[!0-9]*) echo 0 ;; + *) echo "$value" ;; + esac +} + +jboard_docker_disk_avail_mb() { + local root_dir="" + root_dir="$(jboard_docker_info --format '{{.DockerRootDir}}' 2>/dev/null || true)" + + if [ -n "$root_dir" ]; then + jboard_path_avail_mb "$root_dir" + else + echo 0 + fi +} + +jboard_effective_mem_mb() { + local host="$1" + local docker_mem="$2" + + if [ "$docker_mem" -gt 0 ] && [ "$host" -gt 0 ]; then + if [ "$docker_mem" -lt "$host" ]; then + echo "$docker_mem" + else + echo "$host" + fi + elif [ "$docker_mem" -gt 0 ]; then + echo "$docker_mem" + else + echo "$host" + fi +} + +jboard_low_resource_heap_mb() { + local mem_mb="$1" + + if [ -n "${JBOARD_LOW_RESOURCE_NODE_MB:-}" ]; then + echo "$JBOARD_LOW_RESOURCE_NODE_MB" + return + fi + + if [ "$mem_mb" -gt 0 ] && [ "$mem_mb" -le 1200 ]; then + echo 640 + elif [ "$mem_mb" -gt 0 ] && [ "$mem_mb" -le 1700 ]; then + echo 768 + elif [ "$mem_mb" -gt 0 ] && [ "$mem_mb" -le 2400 ]; then + echo 1024 + else + echo 1536 + fi +} + +jboard_resolve_build_profile() { + local requested="${JBOARD_BUILD_PROFILE:-auto}" + local cpu="$1" + local mem_mb="$2" + local docker_disk_mb="$3" + + case "$requested" in + low|slow|small) + echo low + return + ;; + normal|fast|strong) + echo normal + return + ;; + auto|'') + ;; + *) + echo "未知 JBOARD_BUILD_PROFILE=$requested,回退为 auto。" >&2 + ;; + esac + + if [ "$cpu" -le 1 ]; then + echo low + elif [ "$mem_mb" -gt 0 ] && [ "$mem_mb" -lt 2048 ]; then + echo low + elif [ "$docker_disk_mb" -gt 0 ] && [ "$docker_disk_mb" -lt 8192 ]; then + echo low + else + echo normal + fi +} + +jboard_prepare_docker_build_env() { + JBOARD_CPU_COUNT="$(jboard_cpu_count)" + JBOARD_HOST_MEM_MB="$(jboard_host_mem_mb)" + JBOARD_DOCKER_MEM_MB="$(jboard_docker_mem_mb)" + JBOARD_DOCKER_DISK_AVAIL_MB="$(jboard_docker_disk_avail_mb)" + JBOARD_EFFECTIVE_MEM_MB="$(jboard_effective_mem_mb "$JBOARD_HOST_MEM_MB" "$JBOARD_DOCKER_MEM_MB")" + JBOARD_BUILD_PROFILE_RESOLVED="$(jboard_resolve_build_profile "$JBOARD_CPU_COUNT" "$JBOARD_EFFECTIVE_MEM_MB" "$JBOARD_DOCKER_DISK_AVAIL_MB")" + JBOARD_DOCKER_BUILD_ARGS=() + + export NEXT_TELEMETRY_DISABLED=1 + + if [ "$JBOARD_BUILD_PROFILE_RESOLVED" = "low" ]; then + JBOARD_NODE_HEAP_MB="$(jboard_low_resource_heap_mb "$JBOARD_EFFECTIVE_MEM_MB")" + export COMPOSE_PARALLEL_LIMIT="${COMPOSE_PARALLEL_LIMIT:-1}" + export NPM_CONFIG_JOBS="${NPM_CONFIG_JOBS:-1}" + export npm_config_jobs="${npm_config_jobs:-$NPM_CONFIG_JOBS}" + export NEXT_BUILD_NODE_OPTIONS="${NEXT_BUILD_NODE_OPTIONS:---max-old-space-size=${JBOARD_NODE_HEAP_MB}}" + else + JBOARD_NODE_HEAP_MB="" + export NEXT_BUILD_NODE_OPTIONS="${NEXT_BUILD_NODE_OPTIONS:-}" + export NPM_CONFIG_JOBS="${NPM_CONFIG_JOBS:-}" + fi + + if [ -n "${NEXT_BUILD_NODE_OPTIONS:-}" ]; then + JBOARD_DOCKER_BUILD_ARGS+=(--build-arg "NEXT_BUILD_NODE_OPTIONS=${NEXT_BUILD_NODE_OPTIONS}") + fi + if [ -n "${NPM_CONFIG_JOBS:-}" ]; then + JBOARD_DOCKER_BUILD_ARGS+=(--build-arg "NPM_CONFIG_JOBS=${NPM_CONFIG_JOBS}") + fi +} + +jboard_is_low_resource_build() { + [ "${JBOARD_BUILD_PROFILE_RESOLVED:-}" = "low" ] +} + +jboard_print_build_profile() { + local docker_mem="${JBOARD_DOCKER_MEM_MB:-0}" + local docker_disk="${JBOARD_DOCKER_DISK_AVAIL_MB:-0}" + local docker_text="unknown" + local disk_text="unknown" + + if [ "$docker_mem" -gt 0 ]; then + docker_text="${docker_mem}MB" + fi + if [ "$docker_disk" -gt 0 ]; then + disk_text="${docker_disk}MB" + fi + + if jboard_is_low_resource_build; then + echo "检测到低资源构建环境:CPU=${JBOARD_CPU_COUNT:-?},内存=${JBOARD_EFFECTIVE_MEM_MB:-0}MB,Docker内存=${docker_text},Docker可用空间=${disk_text}" + echo "启用慢速低占用模式:Compose 并发=1,npm jobs=${NPM_CONFIG_JOBS:-1},Node heap=${JBOARD_NODE_HEAP_MB:-?}MB。" + else + echo "检测到常规构建环境:CPU=${JBOARD_CPU_COUNT:-?},内存=${JBOARD_EFFECTIVE_MEM_MB:-0}MB,Docker内存=${docker_text},Docker可用空间=${disk_text}" + echo "使用 Docker 默认构建策略,不额外限制并发或 Node heap。" + fi + + if [ "$docker_disk" -gt 0 ] && [ "$docker_disk" -lt 8192 ]; then + echo "提示:Docker 可用空间低于 8GB,建议扩容 Docker 数据盘或清理未使用镜像/缓存。" + fi +} diff --git a/scripts/upgrade-jboard-panel.sh b/scripts/upgrade-jboard-panel.sh index d9f035f..559cdf0 100755 --- a/scripts/upgrade-jboard-panel.sh +++ b/scripts/upgrade-jboard-panel.sh @@ -10,31 +10,86 @@ HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:3000/api/public/app-info}" cd "$APP_DIR" +load_resource_helpers() { + if [ -f "$APP_DIR/scripts/lib-resource-profile.sh" ]; then + # shellcheck disable=SC1091 + . "$APP_DIR/scripts/lib-resource-profile.sh" + fi +} + +load_resource_helpers + +compose() { + $COMPOSE "$@" +} + +backup_sqlite_database() { + mkdir -p backups + + local backup_path="backups/jboard-sqlite-$(date +%F-%H%M%S).tar.gz" + local backed_up="0" + local backup_cmd='cd /app/storage 2>/dev/null && if [ -f jboard.db ]; then set -- jboard.db; [ -f jboard.db-wal ] && set -- "$@" jboard.db-wal; [ -f jboard.db-shm ] && set -- "$@" jboard.db-shm; tar -czf - "$@"; fi' + + if compose ps --services --filter status=running 2>/dev/null | grep -qx app; then + if compose exec -T app sh -lc "$backup_cmd" > "$backup_path"; then + backed_up="1" + fi + fi + + if [ "$backed_up" != "1" ] || [ ! -s "$backup_path" ]; then + if compose --profile setup run --rm -T --entrypoint sh init -lc "$backup_cmd" > "$backup_path"; then + backed_up="1" + fi + fi + + if [ "$backed_up" = "1" ] && [ -s "$backup_path" ]; then + echo "SQLite backup saved: $backup_path" + else + rm -f "$backup_path" + echo "No existing SQLite database found; skipping database backup." + fi +} + +build_updated_images() { + if command -v jboard_prepare_docker_build_env >/dev/null 2>&1; then + jboard_prepare_docker_build_env + jboard_print_build_profile + if jboard_is_low_resource_build; then + compose build "${JBOARD_DOCKER_BUILD_ARGS[@]}" init + compose build "${JBOARD_DOCKER_BUILD_ARGS[@]}" app + else + compose build "${JBOARD_DOCKER_BUILD_ARGS[@]}" init app + fi + else + compose build init app + fi +} + echo "[1/7] Pulling latest code..." git pull --ff-only +load_resource_helpers if [ "$BACKUP" = "1" ]; then - echo "[2/7] Backing up database..." - mkdir -p backups - $COMPOSE exec -T db pg_dump -U jboard jboard > "backups/jboard-db-$(date +%F-%H%M%S).sql" + echo "[2/7] Backing up SQLite database..." + backup_sqlite_database else echo "[2/7] Skipping database backup..." fi echo "[3/7] Building updated images..." -$COMPOSE build init app +build_updated_images echo "[4/7] Syncing Prisma schema inside Docker network..." -$COMPOSE --profile setup run --rm init sh -lc 'npm run db:push' +compose --profile setup run --rm init sh -lc 'npm run db:push' echo "[5/7] Restarting services..." -$COMPOSE up -d app +compose up -d app echo "[6/7] Waiting for app to boot..." sleep 8 echo "[7/7] Checking service status..." -$COMPOSE ps +compose ps echo echo "App health:" @@ -42,7 +97,7 @@ curl -fsS "$HEALTH_URL" || true echo echo "Recent app logs:" -$COMPOSE logs --tail=80 app || true +compose logs --tail=80 app || true echo echo "Upgrade complete."