#!/bin/sh echo "==========================================" echo " NOWEN - Starting..." echo "==========================================" echo "Architecture: $(uname -m)" echo "" # ============================================================ # 🔑 防呆自动配置 — 自动检测 Docker 环境并设置关键环境变量 # ============================================================ # 绿联/飞牛等 NAS 的 Docker GUI 用户通常不会手动设置环境变量 # 所以我们在启动时自动检测并设置,确保即使什么都不配也能正常工作 # 自动设置 NODE_ENV=production(如果未设置且检测到在 Docker 容器内) if [ -z "${NODE_ENV}" ]; then if [ -f "/.dockerenv" ] || [ -f "/app/start.sh" ] || grep -qsE 'docker|containerd|kubepods' /proc/1/cgroup 2>/dev/null; then export NODE_ENV=production echo "🔧 Auto-set NODE_ENV=production (Docker detected)" fi fi # 自动设置 SI_FILESYSTEM_DISK_PREFIX(如果未设置且 /host 挂载存在) if [ -z "${SI_FILESYSTEM_DISK_PREFIX}" ] && [ -d "/host/proc" ]; then export SI_FILESYSTEM_DISK_PREFIX=/host echo "🔧 Auto-set SI_FILESYSTEM_DISK_PREFIX=/host" fi # 自动设置 PROC_PATH(如果未设置且 /host/proc 存在) if [ -z "${PROC_PATH}" ] && [ -d "/host/proc" ]; then export PROC_PATH=/host/proc echo "🔧 Auto-set PROC_PATH=/host/proc" fi # 自动设置 SYS_PATH(如果未设置且 /host/sys 存在) if [ -z "${SYS_PATH}" ] && [ -d "/host/sys" ]; then export SYS_PATH=/host/sys echo "🔧 Auto-set SYS_PATH=/host/sys" fi # 自动设置 FS_PATH(如果未设置且 /host 存在) if [ -z "${FS_PATH}" ] && [ -d "/host" ]; then export FS_PATH=/host echo "🔧 Auto-set FS_PATH=/host" fi echo " NODE_ENV=${NODE_ENV:-not set}" echo "" # ============================================================ # 🔑 数据持久化 — 终极防呆设计(兼容绿联/飞牛等 NAS) # ============================================================ # # ⚠️ 问题根因: # 绿联/飞牛等 NAS 的 Docker GUI 会为每个容器创建独立的存储目录, # 路径格式为: 共享文件夹/docker/<容器名>/app_server_data # 当更新镜像重建容器时,容器名从 xxx-1 变成 xxx-2, # 新的存储目录是空的,导致数据丢失。 # 更关键的是:如果 Dockerfile 声明了 VOLUME,NAS 会为 VOLUME # 路径也创建独立目录,安全备份卷也一起丢失。 # # 解决策略: # 1. Dockerfile 中不声明 VOLUME(避免 NAS 自动创建新目录) # 2. 安全备份放在 /app/.nowen-safe(镜像层内路径,NAS 不挂载) # 3. 启动时扫描宿主机旧容器目录,自动找回数据 # 4. 同时兼容用户手动配置的 bind mount / named volume # ============================================================ DATA_DIR="/app/server/data" DB_FILE="${DATA_DIR}/zen-garden.db" BACKUP_DIR="${DATA_DIR}/backups" # 🔑 安全备份使用镜像层内路径(不是 VOLUME,NAS 不会挂载它) SAFE_BACKUP_DIR="/app/.nowen-safe" SAFE_DB_FILE="${SAFE_BACKUP_DIR}/zen-garden.db.safe" # 确保目录存在 mkdir -p "${BACKUP_DIR}" mkdir -p "${SAFE_BACKUP_DIR}" # ============================================================ # 第一步:挂载检测 # ============================================================ check_mount() { MOUNT_MARKER="${DATA_DIR}/.mount-marker" if mount | grep -q "/app/server/data"; then MOUNT_TYPE=$(mount | grep "/app/server/data" | head -1) echo "✅ Data volume detected: mounted" echo " Mount info: ${MOUNT_TYPE}" return 0 else if [ -f "${MOUNT_MARKER}" ]; then echo "✅ Data volume detected: persistent (marker found)" return 0 else echo "⚠️ WARNING: Data directory may not be properly mounted!" echo "" echo "┌───────────────────────────────────────────────────┐" echo "│ ⚠️ 数据持久化警告 / Data Persistence Warning │" echo "├───────────────────────────────────────────────────┤" echo "│ │" echo "│ 未检测到数据卷挂载。 │" echo "│ NOWEN 会自动在镜像内保留备份,但强烈建议 │" echo "│ 在 NAS Docker 设置中手动指定宿主机存储路径: │" echo "│ │" echo "│ 宿主机路径(固定) → /app/server/data │" echo "│ │" echo "└───────────────────────────────────────────────────┘" echo "" echo "$(date -Iseconds)" > "${MOUNT_MARKER}" return 1 fi fi } # ============================================================ # 第二步:数据恢复(优先级从高到低) # ============================================================ recover_database() { if [ -f "${DB_FILE}" ]; then DB_SIZE=$(ls -lh "${DB_FILE}" | awk '{print $5}') echo "✅ Database found: zen-garden.db (${DB_SIZE})" # 启动前备份到 data/backups/ TIMESTAMP=$(date +%Y%m%d_%H%M%S) cp "${DB_FILE}" "${BACKUP_DIR}/zen-garden_${TIMESTAMP}.db.bak" echo " Auto backup: zen-garden_${TIMESTAMP}.db.bak" # 同步到镜像层内的安全备份 cp "${DB_FILE}" "${SAFE_DB_FILE}" echo " Safe backup synced to image layer" # 清理旧备份,只保留最近 5 个 cd "${BACKUP_DIR}" && ls -1t *.db.bak 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null cd /app return 0 fi echo "⚠️ Database not found in data directory!" echo "" # ============================================================ # 恢复源 1: 镜像层内安全备份(/app/.nowen-safe/) # 这个路径在镜像层内,NAS 不会为它创建新目录 # ============================================================ if [ -f "${SAFE_DB_FILE}" ]; then SAFE_SIZE=$(ls -lh "${SAFE_DB_FILE}" | awk '{print $5}') echo "🔄 Found safe backup in image layer (${SAFE_SIZE}), restoring..." cp "${SAFE_DB_FILE}" "${DB_FILE}" if [ -f "${DB_FILE}" ]; then echo "✅ Database restored from image-layer safe backup!" return 0 fi fi # ============================================================ # 恢复源 1.5: 兼容旧版安全备份路径(/app/.data-backup/) # ============================================================ OLD_SAFE_FILE="/app/.data-backup/zen-garden.db.safe" if [ -f "${OLD_SAFE_FILE}" ]; then OLD_SAFE_SIZE=$(ls -lh "${OLD_SAFE_FILE}" | awk '{print $5}') echo "🔄 Found legacy safe backup (${OLD_SAFE_SIZE}), restoring..." cp "${OLD_SAFE_FILE}" "${DB_FILE}" if [ -f "${DB_FILE}" ]; then echo "✅ Database restored from legacy safe backup!" return 0 fi fi # ============================================================ # 恢复源 2: data/backups/ 目录 # ============================================================ LATEST_BACKUP=$(ls -1t "${BACKUP_DIR}"/*.db.bak 2>/dev/null | head -1) if [ -n "${LATEST_BACKUP}" ]; then echo "🔄 Found backup: ${LATEST_BACKUP}" echo " Restoring from backup..." cp "${LATEST_BACKUP}" "${DB_FILE}" if [ -f "${DB_FILE}" ]; then echo "✅ Database restored from backup!" return 0 fi fi # ============================================================ # 恢复源 3: 扫描宿主机常见路径 + 绿联/飞牛旧容器目录 # 这是关键!绿联 NAS 的 Docker 数据存储在宿主机的 # /docker/<容器名>/ 目录下,通过 /host 只读挂载可以访问 # ============================================================ echo "🔍 Scanning host filesystem for existing NOWEN data..." # 3a: 扫描绿联/飞牛等 NAS 的旧容器目录 # 绿联挂载格式: 共享文件夹/docker/<容器名>/app_server_data/zen-garden.db # 在 /host 挂载下查找 if [ -d "/host" ]; then echo " Scanning NAS old container directories..." # 搜索所有可能的 docker 目录 for DOCKER_BASE in \ "/host/docker" \ "/host/DATA/docker" \ "/host/volume1/docker" \ "/host/volume2/docker" \ "/host/mnt/docker" \ "/host/opt/docker" \ "/host/home/docker" \ "/host/mnt/user1/docker" \ "/host/mnt/media_rw"; do if [ -d "${DOCKER_BASE}" ]; then # 搜索该 docker 目录下所有包含 nowen 的容器目录 for CONTAINER_DIR in $(find "${DOCKER_BASE}" -maxdepth 2 -type d -iname "*nowen*" 2>/dev/null); do # 搜索该容器目录下的所有 zen-garden.db 文件 for DB_FOUND in $(find "${CONTAINER_DIR}" -name "zen-garden.db" -type f 2>/dev/null); do FOUND_SIZE=$(ls -lh "${DB_FOUND}" 2>/dev/null | awk '{print $5}') # 验证文件大小(大于 100 字节才是有效数据库) FOUND_BYTES=$(stat -c%s "${DB_FOUND}" 2>/dev/null || stat -f%z "${DB_FOUND}" 2>/dev/null || echo "0") if [ "${FOUND_BYTES}" -gt 100 ] 2>/dev/null; then echo "🎉 Found database in old container: ${DB_FOUND} (${FOUND_SIZE})" cp "${DB_FOUND}" "${DB_FILE}" 2>/dev/null if [ -f "${DB_FILE}" ]; then echo "✅ Database restored from NAS old container directory!" # 同步到安全备份 cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null return 0 fi fi done done # 也搜索非 nowen 名称但包含 cropflre 的目录 for CONTAINER_DIR in $(find "${DOCKER_BASE}" -maxdepth 2 -type d -iname "*cropflre*" 2>/dev/null); do for DB_FOUND in $(find "${CONTAINER_DIR}" -name "zen-garden.db" -type f 2>/dev/null); do FOUND_SIZE=$(ls -lh "${DB_FOUND}" 2>/dev/null | awk '{print $5}') FOUND_BYTES=$(stat -c%s "${DB_FOUND}" 2>/dev/null || stat -f%z "${DB_FOUND}" 2>/dev/null || echo "0") if [ "${FOUND_BYTES}" -gt 100 ] 2>/dev/null; then echo "🎉 Found database in old container: ${DB_FOUND} (${FOUND_SIZE})" cp "${DB_FOUND}" "${DB_FILE}" 2>/dev/null if [ -f "${DB_FILE}" ]; then echo "✅ Database restored from NAS old container directory!" cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null return 0 fi fi done done fi done # 3b: 扫描传统绑定挂载路径 for SCAN_PATH in \ "/host/volume1/docker/nowen/data/zen-garden.db" \ "/host/volume2/docker/nowen/data/zen-garden.db" \ "/host/DATA/docker/nowen/data/zen-garden.db" \ "/host/mnt/docker/nowen/data/zen-garden.db" \ "/host/opt/docker/nowen/data/zen-garden.db" \ "/host/home/docker/nowen/data/zen-garden.db" \ "/host/root/nowen-data/zen-garden.db" \ "/host/home/nowen-data/zen-garden.db"; do if [ -f "${SCAN_PATH}" ]; then FOUND_SIZE=$(ls -lh "${SCAN_PATH}" | awk '{print $5}') echo "🎉 Found existing database at: ${SCAN_PATH} (${FOUND_SIZE})" cp "${SCAN_PATH}" "${DB_FILE}" if [ -f "${DB_FILE}" ]; then echo "✅ Database restored from host filesystem!" cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null return 0 fi fi done # 3c: 最后的搜索 — 全局搜索 /host 下所有 zen-garden.db(限时 30 秒) echo " Deep scanning for zen-garden.db on host (timeout 30s)..." DEEP_FOUND=$(timeout 30 find /host -name "zen-garden.db" -type f -size +100c 2>/dev/null | head -5) for DB_FOUND in ${DEEP_FOUND}; do FOUND_SIZE=$(ls -lh "${DB_FOUND}" 2>/dev/null | awk '{print $5}') echo "🎉 Found database: ${DB_FOUND} (${FOUND_SIZE})" cp "${DB_FOUND}" "${DB_FILE}" 2>/dev/null if [ -f "${DB_FILE}" ]; then echo "✅ Database restored from deep host scan!" cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null return 0 fi done fi # ============================================================ # 恢复源 4: Docker 卷扫描 # ============================================================ if command -v docker > /dev/null 2>&1; then echo "🔍 Scanning Docker volumes for orphaned data..." for VOL in $(docker volume ls -q 2>/dev/null); do INSPECT_PATH=$(docker volume inspect "${VOL}" --format '{{ .Mountpoint }}' 2>/dev/null) if [ -n "${INSPECT_PATH}" ] && [ -f "${INSPECT_PATH}/zen-garden.db" ]; then FOUND_SIZE=$(ls -lh "${INSPECT_PATH}/zen-garden.db" 2>/dev/null | awk '{print $5}') echo "🎉 Found orphaned data in volume '${VOL}' (${FOUND_SIZE})" cp "${INSPECT_PATH}/zen-garden.db" "${DB_FILE}" 2>/dev/null if [ -f "${DB_FILE}" ]; then echo "✅ Database restored from orphaned Docker volume!" cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null return 0 fi fi done fi echo "📝 No existing data found anywhere — creating fresh database" echo "" return 1 } # ============================================================ # 第三步:运行时持续备份(后台进程) # ============================================================ start_safe_backup_daemon() { # 每 3 分钟将 db 同步到镜像层内的安全备份 while true; do sleep 180 if [ -f "${DB_FILE}" ]; then cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null # 也尝试备份到旧路径(兼容已挂载 /app/.data-backup 的用户) if [ -d "/app/.data-backup" ]; then cp "${DB_FILE}" "/app/.data-backup/zen-garden.db.safe" 2>/dev/null fi fi done & BACKUP_DAEMON_PID=$! echo " Safe backup daemon: PID ${BACKUP_DAEMON_PID} (every 3min)" } # ============================================================ # 第四步:优雅停止时保存 # ============================================================ graceful_shutdown() { echo "" echo "🛑 Shutting down NOWEN..." # 同步最新数据到安全备份(镜像层内) if [ -f "${DB_FILE}" ]; then cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null echo "✅ Final safe backup saved to image layer" # 也备份到旧路径 if [ -d "/app/.data-backup" ]; then cp "${DB_FILE}" "/app/.data-backup/zen-garden.db.safe" 2>/dev/null fi fi # 停止后台进程 kill "${BACKUP_DAEMON_PID}" 2>/dev/null kill "${BACKEND_PID}" 2>/dev/null # 停止 nginx nginx -s quit 2>/dev/null echo "👋 NOWEN stopped" exit 0 } # 注册信号处理 trap graceful_shutdown SIGTERM SIGINT # ============================================================ # 执行启动流程 # ============================================================ echo "--- Data Persistence Check ---" check_mount echo "" echo "--- Database Recovery ---" recover_database RECOVERY_RESULT=$? echo "" # 显示数据目录内容 echo "📁 Data directory contents:" ls -la "${DATA_DIR}/" 2>/dev/null echo "" # 启动安全备份守护进程 start_safe_backup_daemon # 启动后端 echo "Starting backend on port 3001..." cd /app/server && tsx src/index.ts & BACKEND_PID=$! sleep 2 # 启动后再次同步(initDatabase 可能创建了新库) if [ -f "${DB_FILE}" ]; then cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null fi # 启动 nginx(前台运行) echo "Starting nginx on port 3000..." nginx -g "daemon off;" & NGINX_PID=$! # 等待任意子进程退出 wait ${NGINX_PID} ${BACKEND_PID}