470 lines
20 KiB
Bash
Raw Permalink Normal View History

#!/usr/bin/env bash
# ════════════════════════════════════════════════════════════════
# 光湖灯塔 · 国内主服务器 bootstrap · v0.1.0
# Sovereign: TCS-0002∞ · ICE-GL∞ · 国作登字-2026-A-00037559
# 守护: 铸渊 · ICE-GL-ZY001
# ════════════════════════════════════════════════════════════════
#
# 把一台格式化干净的 Ubuntu 22.04 国内 CVM 准备成"光湖灯塔":
# 1. 装基础工具链 (走阿里云镜像源, 国内可达)
# 2. 装 Docker + Compose (跳过 get.docker.com, 用阿里云镜像)
# 3. 在 /data 数据盘下建 Gitea/Postgres/Redis/Runner 标准目录
# 4. 起 Gitea + PostgreSQL + Redis + Gitea Actions Runner
# 5. 部署 Nginx 反代 (Gitea + 灯塔门户 + 总控 API 占位)
# 6. 装 PM2 / certbot / fail2ban / ufw 基础守护
#
# 服务器现状假设 (来自冰朔同步):
# - 实例: GH-CVM-MAIN-PROD-01 · 4C16G · Ubuntu 22.04
# - 数据盘: 100GB ext4 已挂载 /data, fstab 自动挂载
# - 安全组: 22/80/443/3000 已放行
# - 默认登录用户: ubuntu (本脚本需要 sudo -i 切到 root 后运行)
#
# 用法:
# sudo CN_LIGHTHOUSE_SERVER_ID=GH-CVM-MAIN-PROD-01 \
# GITEA_ADMIN_USER=bingshuo \
# GITEA_ADMIN_PASS=xxx \
# GITEA_ADMIN_EMAIL=you@example.com \
# GITEA_DB_PASS=xxx \
# bash bootstrap.sh
#
# 环境变量:
# CN_LIGHTHOUSE_SERVER_ID 必填 · 母编号 (GH-CVM-MAIN-PROD-01 / GH-CVM-BACKUP-01)
# DATA_ROOT 可选 · 默认 /data (持久数据根, 必须挂载)
# DEPLOY_ROOT 可选 · 默认 /opt/guanghu (代码 / pm2 进程根)
# GITEA_ADMIN_USER 必填 · Gitea 初始管理员账号
# GITEA_ADMIN_PASS 必填 · Gitea 初始管理员密码 (≥24 位)
# GITEA_ADMIN_EMAIL 必填 · 管理员邮箱
# GITEA_DB_PASS 必填 · PostgreSQL gitea 用户密码
# GITEA_HTTP_PORT 可选 · 默认 3000
# GITEA_SSH_PORT 可选 · 默认 2222 (避开系统 22)
# STAGE 可选 · bootstrap (默认) / update / rollback
# ════════════════════════════════════════════════════════════════
set -euo pipefail
# ─── 全局参数 ─────────────────────────────────────────────────
TEMPLATE_VERSION="0.2.0"
SERVER_ID="${CN_LIGHTHOUSE_SERVER_ID:-${SERVER_ID:-}}"
DATA_ROOT="${DATA_ROOT:-/data}"
DEPLOY_ROOT="${DEPLOY_ROOT:-/opt/guanghu}"
GITEA_HTTP_PORT="${GITEA_HTTP_PORT:-3000}"
GITEA_SSH_PORT="${GITEA_SSH_PORT:-2222}"
STAGE="${STAGE:-bootstrap}"
export DEBIAN_FRONTEND=noninteractive
# ─── 校验前置条件 ─────────────────────────────────────────────
if [ "$EUID" -ne 0 ]; then
echo "❌ 需要 root (请用 sudo -i 切到 root 后再跑)" >&2
exit 1
fi
if [ -z "$SERVER_ID" ]; then
echo "❌ 必须传入母编号: CN_LIGHTHOUSE_SERVER_ID=GH-CVM-MAIN-PROD-01" >&2
exit 1
fi
if ! [[ "$SERVER_ID" =~ ^GH-CVM-[A-Z0-9-]+$ ]]; then
echo "❌ 母编号格式不合法: $SERVER_ID (应为 GH-CVM-[A-Z0-9-]+)" >&2
exit 1
fi
if [ ! -d "$DATA_ROOT" ]; then
echo "❌ 数据盘根目录不存在: $DATA_ROOT (请先挂载数据盘)" >&2
exit 1
fi
# 检查 /data 是不是真的挂载点 (避免误装到系统盘)
if ! mountpoint -q "$DATA_ROOT"; then
echo "⚠️ $DATA_ROOT 不是挂载点, Gitea 数据会落到系统盘 (50G), 请确认" >&2
echo " 继续? (5秒后自动继续, Ctrl+C 中止)" >&2
sleep 5
fi
# ─── 自我感知: 探测真实硬件 + 动态调优 ────────────────────────
# 不再硬假设 4C16G, 适配 2C8G / 4C16G / 任意规格
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
mkdir -p "$DEPLOY_ROOT/_logs"
SERVER_ENV_JSON="$DEPLOY_ROOT/_logs/server-env.json"
if [ -x "$SCRIPT_DIR/detect-env.sh" ]; then
echo "[0/8] 探测当前服务器实际配置 (detect-env.sh)..."
DATA_ROOT="$DATA_ROOT" DEPLOY_ROOT="$DEPLOY_ROOT" \
bash "$SCRIPT_DIR/detect-env.sh" "$SERVER_ENV_JSON" || {
echo "⚠️ detect-env.sh 失败, 继续但不动态调优" >&2
}
fi
if [ "$STAGE" = "bootstrap" ]; then
for v in GITEA_ADMIN_USER GITEA_ADMIN_PASS GITEA_ADMIN_EMAIL GITEA_DB_PASS; do
if [ -z "${!v:-}" ]; then
echo "❌ bootstrap 阶段必填环境变量: $v" >&2
exit 1
fi
done
fi
echo "═══════════════════════════════════════════════════════════"
echo " 光湖灯塔 bootstrap"
echo " 母编号: $SERVER_ID"
echo " 数据根: $DATA_ROOT"
echo " 部署根: $DEPLOY_ROOT"
echo " 阶段: $STAGE"
echo " 模板版本: $TEMPLATE_VERSION"
echo "═══════════════════════════════════════════════════════════"
# ─── 阶段路由 ─────────────────────────────────────────────────
case "$STAGE" in
bootstrap) ;;
update) STAGE_UPDATE_ONLY=1 ;;
rollback)
echo "[rollback] 委派给 rollback.sh (快照式回滚)..."
if [ -x "$SCRIPT_DIR/rollback.sh" ]; then
# 修复: ROLLBACK_TS 为空时不能用 ${VAR:+--to} ${VAR:-} 拼参,
# 那会传两个空字符串 "" "" 给 rollback.sh, 触发 "未知参数" 退码 2.
# 改用条件分支显式调用; 这里不能写成 "${ROLLBACK_ARGS[@]}" 直接展开,
# 因为脚本顶部已 set -u, bash<4.4 在空数组展开会报 unbound variable.
if [ -n "${ROLLBACK_TS:-}" ]; then
DATA_ROOT="$DATA_ROOT" DEPLOY_ROOT="$DEPLOY_ROOT" \
bash "$SCRIPT_DIR/rollback.sh" --to "$ROLLBACK_TS"
else
DATA_ROOT="$DATA_ROOT" DEPLOY_ROOT="$DEPLOY_ROOT" \
bash "$SCRIPT_DIR/rollback.sh"
fi
exit $?
else
echo "[rollback fallback] 脚本未找到, 仅停服" >&2
cd "$DATA_ROOT/lighthouse" 2>/dev/null && docker compose down || true
exit 0
fi
;;
*)
echo "❌ 未知 STAGE: $STAGE (合法: bootstrap / update / rollback)" >&2
exit 1
;;
esac
# ─── update / bootstrap: 跑前先建快照 ────────────────────────
# 这样万一这次 update 把 compose / app.ini 改坏, rollback.sh 能回到这一刻
if [ -d "$DATA_ROOT/lighthouse" ] && [ -f "$DATA_ROOT/lighthouse/docker-compose.yml" ]; then
PRE_TS="$(date +%Y%m%d-%H%M%S)"
PRE_SNAP="$DATA_ROOT/lighthouse/snapshots/$PRE_TS-pre-$STAGE"
mkdir -p "$PRE_SNAP"
for f in docker-compose.yml .env .env.tune; do
[ -f "$DATA_ROOT/lighthouse/$f" ] && cp -a "$DATA_ROOT/lighthouse/$f" "$PRE_SNAP/" || true
done
if [ -f "$DATA_ROOT/lighthouse/gitea/conf/app.ini" ]; then
mkdir -p "$PRE_SNAP/gitea/conf"
cp -a "$DATA_ROOT/lighthouse/gitea/conf/app.ini" "$PRE_SNAP/gitea/conf/" || true
fi
echo "snapshot before $STAGE @ $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$PRE_SNAP/NOTE"
# 只保留最近 10 份, 防数据盘塞满
ls -1tr "$DATA_ROOT/lighthouse/snapshots" 2>/dev/null \
| head -n -10 \
| xargs -I{} rm -rf "$DATA_ROOT/lighthouse/snapshots/{}" 2>/dev/null || true
echo "[snap] pre-$STAGE 快照 -> $PRE_SNAP"
fi
# ─── 1. APT 镜像源 (阿里云) ──────────────────────────────────
if [ "${STAGE}" = "bootstrap" ]; then
echo "[1/8] 切换 APT 源到阿里云镜像 ..."
if ! grep -q "mirrors.aliyun.com" /etc/apt/sources.list 2>/dev/null; then
cp /etc/apt/sources.list "/etc/apt/sources.list.bak.$(date +%s)" || true
cat > /etc/apt/sources.list <<'EOF'
deb https://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse
EOF
fi
apt-get update -qq
fi
# ─── 2. 基础工具链 ───────────────────────────────────────────
echo "[2/8] 安装基础工具链 ..."
apt-get install -y -qq \
curl wget ca-certificates gnupg lsb-release \
git jq unzip vim htop net-tools \
nginx certbot python3-certbot-nginx \
fail2ban ufw \
rsync openssh-client \
build-essential
# ─── 3. Node 20 + PM2 (npmmirror 镜像) ───────────────────────
if ! command -v node >/dev/null 2>&1 || [ "$(node -v 2>/dev/null | sed 's/v//;s/\..*//')" -lt 20 ]; then
echo "[3/8] 安装 Node 20 (NodeSource 国内可达, 备用 nvm) ..."
if ! curl -fsSL --max-time 30 https://deb.nodesource.com/setup_20.x | bash - ; then
echo " NodeSource 不可达, 改用阿里云 nvm 源..."
export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node
curl -fsSL https://gitee.com/mirrors/nvm/raw/master/install.sh | bash || {
echo "❌ Node 安装失败, 请人工介入" >&2
exit 1
}
fi
apt-get install -y -qq nodejs || true
fi
if ! command -v pm2 >/dev/null 2>&1; then
npm config set registry https://registry.npmmirror.com
npm install -g pm2 --silent
fi
# ─── 4. Docker (阿里云镜像源, 跳过 get.docker.com) ─────────────
if ! command -v docker >/dev/null 2>&1; then
echo "[4/8] 安装 Docker (阿里云镜像源) ..."
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg \
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://mirrors.aliyun.com/docker-ce/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update -qq
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
fi
# Docker daemon 镜像加速
mkdir -p /etc/docker
if [ ! -f /etc/docker/daemon.json ]; then
cat > /etc/docker/daemon.json <<EOF
{
"registry-mirrors": [
"https://mirror.ccs.tencentyun.com",
"https://docker.mirrors.ustc.edu.cn",
"https://hub-mirror.c.163.com"
],
"data-root": "$DATA_ROOT/docker",
"log-driver": "json-file",
"log-opts": { "max-size": "50m", "max-file": "5" }
}
EOF
systemctl restart docker || systemctl start docker
fi
systemctl enable docker >/dev/null 2>&1 || true
# ─── 5. /data 标准目录 ───────────────────────────────────────
echo "[5/8] 在 $DATA_ROOT 下建标准目录 ..."
mkdir -p \
"$DATA_ROOT/lighthouse" \
"$DATA_ROOT/lighthouse/gitea" \
"$DATA_ROOT/lighthouse/postgres" \
"$DATA_ROOT/lighthouse/redis" \
"$DATA_ROOT/lighthouse/runner" \
"$DATA_ROOT/lighthouse/backup" \
"$DATA_ROOT/cos-cache" \
"$DATA_ROOT/pm2" \
"$DATA_ROOT/logs/gitea" \
"$DATA_ROOT/logs/nginx" \
"$DATA_ROOT/logs/runner"
mkdir -p \
"$DEPLOY_ROOT/_active" \
"$DEPLOY_ROOT/_archive" \
"$DEPLOY_ROOT/_logs" \
"$DEPLOY_ROOT/_manifest" \
"$DEPLOY_ROOT/_secrets" \
"$DEPLOY_ROOT/lighthouse-cn"
chmod 700 "$DEPLOY_ROOT/_secrets"
# pm2 默认日志/进程目录指向 /data/pm2, 系统盘只剩系统
export PM2_HOME="$DATA_ROOT/pm2"
# ─── 6. Gitea + Postgres + Redis (docker compose) ────────────
echo "[6/8] 部署 Gitea / PostgreSQL / Redis ..."
COMPOSE_DIR="$DATA_ROOT/lighthouse"
if [ ! -f "$COMPOSE_DIR/docker-compose.yml" ]; then
cp "$(dirname "$0")/docker-compose.yml" "$COMPOSE_DIR/docker-compose.yml"
fi
if [ ! -f "$COMPOSE_DIR/.env" ]; then
cat > "$COMPOSE_DIR/.env" <<EOF
# Generated by lighthouse-cn bootstrap @ $(date -u +%Y-%m-%dT%H:%M:%SZ)
SERVER_ID=$SERVER_ID
DATA_ROOT=$DATA_ROOT
GITEA_HTTP_PORT=$GITEA_HTTP_PORT
GITEA_SSH_PORT=$GITEA_SSH_PORT
GITEA_DB_NAME=gitea
GITEA_DB_USER=gitea
GITEA_DB_PASS=$GITEA_DB_PASS
TZ=Asia/Shanghai
EOF
chmod 600 "$COMPOSE_DIR/.env"
fi
# Gitea app.ini 模板渲染 (首次启动用)
APP_INI_TPL="$(dirname "$0")/gitea/app.ini.template"
if [ -f "$APP_INI_TPL" ] && [ ! -f "$DATA_ROOT/lighthouse/gitea/conf/app.ini" ]; then
mkdir -p "$DATA_ROOT/lighthouse/gitea/conf"
# 生成密码学安全的 SECRET_KEY (Gitea 推荐 ≥ 64 字符)
GITEA_SECRET_KEY="$(openssl rand -base64 48 | tr -d '\n=' | head -c 64)"
if [ -z "$GITEA_SECRET_KEY" ]; then
echo "❌ 无法生成 SECRET_KEY (openssl 缺失?)" >&2
exit 1
fi
# 用 awk 而非 sed (避免密码中特殊字符 / 引号歧义)
APP_INI_OUT="$DATA_ROOT/lighthouse/gitea/conf/app.ini"
# 先把 placeholder 替换值通过 env 传给 awk, 不进 shell 历史
GITEA_HTTP_PORT="$GITEA_HTTP_PORT" \
GITEA_SSH_PORT="$GITEA_SSH_PORT" \
GITEA_SECRET_KEY="$GITEA_SECRET_KEY" \
awk '
{
gsub(/__GITEA_HTTP_PORT__/, ENVIRON["GITEA_HTTP_PORT"])
gsub(/__GITEA_SSH_PORT__/, ENVIRON["GITEA_SSH_PORT"])
gsub(/__GITEA_SECRET_KEY__/, ENVIRON["GITEA_SECRET_KEY"])
print
}' "$APP_INI_TPL" > "$APP_INI_OUT"
# app.ini 含 SECRET_KEY, 必须 chmod 600
chmod 600 "$APP_INI_OUT"
unset GITEA_SECRET_KEY
fi
# ─── 6.5 动态调优 ─────────────────────────────────────────────
# 依据 detect-env 探到的真实硬件档位, 写 .env.tune
if [ -x "$SCRIPT_DIR/tune-from-env.sh" ] && [ -f "$SERVER_ENV_JSON" ]; then
ENV_FILE="$SERVER_ENV_JSON" DATA_ROOT="$DATA_ROOT" DEPLOY_ROOT="$DEPLOY_ROOT" \
bash "$SCRIPT_DIR/tune-from-env.sh" || echo "⚠️ tune-from-env.sh 失败, 用默认参数继续"
fi
# 加载档位决策, 决定是否在 update 阶段顺带启 runner profile
RUNNER_DEFAULT_ENABLED="false"
if [ -f "$DATA_ROOT/lighthouse/.env.tune" ]; then
# shellcheck disable=SC1091
. "$DATA_ROOT/lighthouse/.env.tune"
fi
cd "$COMPOSE_DIR"
docker compose pull
# update 阶段且档位允许 + token 已注入 → 顺带启 runner
COMPOSE_UP_ARGS=("up" "-d")
if [ "$STAGE" = "update" ] && [ "${RUNNER_DEFAULT_ENABLED:-false}" = "true" ] \
&& [ -n "${GITEA_RUNNER_TOKEN:-}" ]; then
echo " 档位允许 + RUNNER_TOKEN 已注入, 顺带启 runner"
COMPOSE_UP_ARGS=("--profile" "runner" "up" "-d")
fi
docker compose "${COMPOSE_UP_ARGS[@]}"
# 等 Gitea 起来
echo " 等待 Gitea 健康检查 ..."
GITEA_HEALTHY="false"
for i in $(seq 1 30); do
if curl -fs "http://127.0.0.1:$GITEA_HTTP_PORT/api/v1/version" >/dev/null 2>&1; then
echo " Gitea 已上线"
GITEA_HEALTHY="true"
break
fi
sleep 2
done
# ─── 自动兜底: update 失败 → 自动回滚到 pre-snapshot ──────────
# bootstrap (首次) 不触发自动回滚 (没有"上一个版本"可回)
if [ "$GITEA_HEALTHY" = "false" ] && [ "$STAGE" = "update" ] && [ -n "${PRE_SNAP:-}" ]; then
echo "❌ Gitea 60s 内未上线, 触发自动回滚到 pre-update 快照..." >&2
PRE_TS_BASENAME="$(basename "$PRE_SNAP")"
if [ -x "$SCRIPT_DIR/rollback.sh" ]; then
DATA_ROOT="$DATA_ROOT" DEPLOY_ROOT="$DEPLOY_ROOT" \
bash "$SCRIPT_DIR/rollback.sh" --to "$PRE_TS_BASENAME" || \
echo "⚠️ 自动回滚也失败了, 请人工介入. snapshot=$PRE_SNAP" >&2
fi
echo "[autorollback] 已触发. 仔细看上方日志." >&2
exit 1
fi
# 创建初始管理员 (idempotent: 已存在会报错并被忽略)
# 用临时文件 + stdin 避免密码进 ps / shell history
if [ "$STAGE" = "bootstrap" ]; then
ADMIN_PASS_FILE="$(mktemp)"
chmod 600 "$ADMIN_PASS_FILE"
printf '%s' "$GITEA_ADMIN_PASS" > "$ADMIN_PASS_FILE"
# 把密码文件 cp 进容器, 用 --password-file 读 (Gitea 1.21+ 支持)
docker cp "$ADMIN_PASS_FILE" lighthouse-gitea:/tmp/.adm-pass 2>/dev/null || true
docker exec lighthouse-gitea chown git:git /tmp/.adm-pass 2>/dev/null || true
docker exec lighthouse-gitea chmod 600 /tmp/.adm-pass 2>/dev/null || true
# Gitea 1.21 admin user create 仍需 --password 参数. 退而求其次: 用 env 透传
# (注: 进程参数仍可见, 但 GITEA_ADMIN_PASS 已通过 env 而非 cmd-line 传)
set +e
docker exec -u git -e GITEA_ADM_PASS="$(cat "$ADMIN_PASS_FILE")" \
lighthouse-gitea \
sh -c 'gitea admin user create --username "$1" --password "$GITEA_ADM_PASS" --email "$2" --admin --must-change-password=false' \
_ "$GITEA_ADMIN_USER" "$GITEA_ADMIN_EMAIL" 2>&1 | grep -v "$(cat "$ADMIN_PASS_FILE")" \
|| echo " 管理员创建跳过 (可能已存在)"
set -e
rm -f "$ADMIN_PASS_FILE"
docker exec lighthouse-gitea rm -f /tmp/.adm-pass 2>/dev/null || true
fi
# ─── 7. Nginx 反代 ───────────────────────────────────────────
echo "[7/8] 配置 Nginx 反代 ..."
NGX_TPL="$(dirname "$0")/nginx/lighthouse.conf"
if [ -f "$NGX_TPL" ]; then
cp "$NGX_TPL" /etc/nginx/sites-available/lighthouse.conf
ln -sf /etc/nginx/sites-available/lighthouse.conf /etc/nginx/sites-enabled/lighthouse.conf
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
fi
# ─── 8. ufw + fail2ban ────────────────────────────────────────
echo "[8/8] 加固防火墙 ..."
ufw --force reset >/dev/null
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow "$GITEA_HTTP_PORT/tcp"
ufw allow "$GITEA_SSH_PORT/tcp"
ufw --force enable
systemctl enable fail2ban >/dev/null 2>&1 || true
systemctl restart fail2ban || true
# ─── 收尾回执 ─────────────────────────────────────────────────
mkdir -p "$DEPLOY_ROOT/_logs"
RECEIPT_TS="$(date +%Y%m%d-%H%M%S)"
SIZE_TIER_VAL="${SIZE_TIER:-unknown}"
# 真实 runner 运行态 (跟 RUNNER_DEFAULT_ENABLED 区分):
# RUNNER_DEFAULT_ENABLED = 当前档位是否"默认允许"启 runner (来自 .env.tune)
# runner_running = 此刻 lighthouse-gitea-runner 容器是否真在跑
RUNNER_RUNNING="false"
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx 'lighthouse-gitea-runner'; then
RUNNER_RUNNING="true"
fi
cat > "$DEPLOY_ROOT/_logs/lighthouse-bootstrap-$RECEIPT_TS.json" <<EOF
{
"_sovereign": "TCS-0002∞ | $SERVER_ID",
"_copyright": "国作登字-2026-A-00037559",
"template_version": "$TEMPLATE_VERSION",
"stage": "$STAGE",
"completed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"data_root": "$DATA_ROOT",
"deploy_root": "$DEPLOY_ROOT",
"gitea_http_port": $GITEA_HTTP_PORT,
"gitea_ssh_port": $GITEA_SSH_PORT,
"size_tier": "$SIZE_TIER_VAL",
"runner_default_enabled": ${RUNNER_DEFAULT_ENABLED:-false},
"runner_running": $RUNNER_RUNNING,
"pre_snapshot": "${PRE_SNAP:-none}",
"server_env_file": "$SERVER_ENV_JSON",
"post_health_ok": ${GITEA_HEALTHY:-false}
}
EOF
echo
echo "═══════════════════════════════════════════════════════════"
echo " [OK] $SERVER_ID 灯塔 bootstrap 完成"
echo "═══════════════════════════════════════════════════════════"
echo " Gitea Web: http://<公网IP>:$GITEA_HTTP_PORT/"
echo " Gitea SSH: ssh://git@<公网IP>:$GITEA_SSH_PORT/"
echo " 数据根: $DATA_ROOT/lighthouse/"
echo " Compose 文件: $COMPOSE_DIR/docker-compose.yml"
echo " 下一步:"
echo " 1. 浏览器登录 Gitea, Site Admin → Actions → Runners 拿 token"
echo " 2. 把 token 配回 GitHub Secrets ZY_GITEA_RUNNER_TOKEN"
echo " 3. 跑 STAGE=update 注册 Gitea Actions Runner"
echo " 4. 从 COS 拉 guanghulab-snapshot.tar.gz, 跑 restore-from-cos.sh"
echo