Files
HomeDash/deploy/install-host.sh
HomeDash Bot 965c9cb41a fix(deploy): code_of 定义提到等待循环之前(自检退出码 127)
第二次练兵:路由表路径修复生效(apply routes: 2),但因等待监听的循环
调用了尚未定义的 code_of,脚本以 127 退出。把函数定义移到首次调用之前。
2026-09-12 09:41:01 +08:00

196 lines
8.6 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.

#!/usr/bin/env bash
# 首装脚本:在**干净机器**上把 HomeDash 装成统一入口(host 网络单容器)。
#
# 与 deploy/zd-host.sh 的分工:
# install-host.sh 首次安装(本机执行,需要 docker)
# zd-host.sh 已有安装的升级/回滚(从开发机通过 ssh 推送镜像)
#
# 用法(在目标机器上,仓库目录内):
# NAVI_DOMAIN=example.com NAVI_SYNC_TOKEN=xxx NAVI_ADMIN_TOKEN=yyy bash deploy/install-host.sh
# 远程执行:
# ssh root@host 'bash -s' < deploy/install-host.sh
#
# 常用环境变量(全部可覆盖):
# NAVI_SRC 仓库目录 默认:脚本上级目录
# NAVI_DATA_DIR 数据卷目录 默认:/vol1/docker/navi-data
# NAVI_CERT_DIR 证书目录(只读挂载)默认:/vol1/docker/cert
# NAVI_TAG 构建出的镜像名 默认:homedash:ui-install
# NAVI_ENTRY_PORT HTTPS/HTTP 入口端口 默认:3080
# NAVI_TLS_PORT 额外 TLS 端口 默认:8443
# NAVI_REDIRECT_PORT HTTP→HTTPS 跳转 默认:3081
# NAVI_TLS 1=TLS(需证书)0=纯 HTTP 默认:1
# NAVI_DOMAIN 门户域名(可选,会写入一条门户路由)
# NAVI_LOGIN_DOMAIN 统一登录域名(可选,默认 login.<NAVI_DOMAIN>)
# NAVI_SYNC_TOKEN / NAVI_ADMIN_TOKEN 留空则自动生成并打印
# NAVI_BUILD 1=本机构建 0=用 NAVI_TAR 导入 默认:1
# NAVI_TAR 已构建好的镜像 tar(NAVI_BUILD=0 时必填)
set -euo pipefail
SRC="${NAVI_SRC:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
DATA_DIR="${NAVI_DATA_DIR:-/vol1/docker/navi-data}"
CERT_DIR="${NAVI_CERT_DIR:-/vol1/docker/cert}"
TAG="${NAVI_TAG:-homedash:ui-install}"
ENTRY_PORT="${NAVI_ENTRY_PORT:-3080}"
TLS_PORT="${NAVI_TLS_PORT:-8443}"
REDIRECT_PORT="${NAVI_REDIRECT_PORT:-3081}"
TLS="${NAVI_TLS:-1}"
DOMAIN="${NAVI_DOMAIN:-}"
LOGIN_DOMAIN="${NAVI_LOGIN_DOMAIN:-}"
SYNC_TOKEN="${NAVI_SYNC_TOKEN:-}"
ADMIN_TOKEN="${NAVI_ADMIN_TOKEN:-}"
BUILD="${NAVI_BUILD:-1}"
TAR="${NAVI_TAR:-}"
CERT_FILE="/etc/nginx/certs/fullchain.pem"
KEY_FILE="/etc/nginx/certs/privkey.pem"
die() { echo "!! $*" >&2; exit 1; }
step() { echo "== $* =="; }
# ── 1. 前置检查 ──
step "1/6 前置检查"
command -v docker >/dev/null || die "需要 docker(本脚本在目标机器上执行)"
docker info >/dev/null 2>&1 || die "docker 不可用(权限或 daemon 未启动)"
[ -f "$SRC/Dockerfile" ] || die "找不到 $SRC/Dockerfile(用 NAVI_SRC 指定仓库目录)"
if docker inspect navi >/dev/null 2>&1; then
die "已存在名为 navi 的容器:这是一次升级,请用 deploy/zd-host.sh <版本>;如确要重装先 docker rm -f navi"
fi
for p in "$ENTRY_PORT" "$REDIRECT_PORT" "$TLS_PORT"; do
if ss -H -lnt 2>/dev/null | awk '{print $4}' | grep -q ":$p\$"; then
die "端口 $p 已被占用(统一入口需要独占;用 NAVI_*_PORT 换端口)"
fi
done
mkdir -p "$DATA_DIR" || die "无法创建数据目录 $DATA_DIR"
[ -d "$CERT_DIR" ] || mkdir -p "$CERT_DIR"
if [ "$TLS" = "1" ]; then
[ -s "$CERT_DIR/fullchain.pem" ] || die "缺少证书 $CERT_DIR/fullchain.pem(或用 NAVI_TLS=0 走纯 HTTP)"
[ -s "$CERT_DIR/privkey.pem" ] || die "缺少私钥 $CERT_DIR/privkey.pem"
fi
echo " 数据目录 $DATA_DIR"
echo " 证书目录 $CERT_DIR(TLS=$TLS)"
echo " 入口 ${ENTRY_PORT}$([ "$TLS" = 1 ] && echo " + ${TLS_PORT}" || echo ""),跳转 ${REDIRECT_PORT}"
# ── 2. 令牌 ──
step "2/6 令牌"
if [ -z "$SYNC_TOKEN" ]; then SYNC_TOKEN="$(openssl rand -hex 20 2>/dev/null || head -c 20 /dev/urandom | od -An -tx1 | tr -d ' \n')"; fi
if [ -z "$ADMIN_TOKEN" ]; then ADMIN_TOKEN="$(openssl rand -hex 20 2>/dev/null || head -c 20 /dev/urandom | od -An -tx1 | tr -d ' \n')"; fi
echo " 已就绪(只在这里打印一次,请自行保存)"
# ── 3. 构建镜像 ──
step "3/6 镜像"
if [ "$BUILD" = "1" ]; then
docker build -t "$TAG" -f "$SRC/Dockerfile" "$SRC"
else
[ -n "$TAR" ] && [ -s "$TAR" ] || die "NAVI_BUILD=0 需要 NAVI_TAR=<镜像 tar>"
docker load -i "$TAR"
fi
docker image inspect "$TAG" >/dev/null 2>&1 || die "镜像 $TAG 不存在(构建失败或 tar 里是别的 tag)"
# ── 4. 初始网关配置 ──
step "4/6 初始网关配置"
# 注意:路由表源文件是 <数据卷>/gateway.yml(容器按此读取);
# <数据卷>/gateway/ 只放生成产物 gateway.conf。写错位置会被静默当成空路由表。
GW_YML="$DATA_DIR/gateway.yml"
mkdir -p "$DATA_DIR/gateway"
if [ -s "$GW_YML" ]; then
echo " 已存在,保留不动:$GW_YML"
else
{
echo "listenPort: $ENTRY_PORT"
echo "listenTLSPort: $TLS_PORT"
echo "redirectPort: $REDIRECT_PORT"
echo "listenHTTPS: $([ "$TLS" = 1 ] && echo true || echo false)"
if [ "$TLS" = 1 ]; then
echo "certFile: $CERT_FILE"
echo "keyFile: $KEY_FILE"
fi
echo "authURL: http://127.0.0.1:3005/auth/verify"
if [ -n "$LOGIN_DOMAIN" ]; then
echo "loginURL: https://$LOGIN_DOMAIN/login"
elif [ -n "$DOMAIN" ]; then
echo "loginURL: https://login.$DOMAIN/login"
fi
echo "routes:"
if [ -n "$DOMAIN" ]; then
echo " - host: $DOMAIN"
echo " kind: portal"
echo " note: 门户(首装生成)"
fi
if [ -n "$LOGIN_DOMAIN" ]; then
echo " - host: $LOGIN_DOMAIN"
echo " kind: auth"
echo " note: 统一登录页"
elif [ -n "$DOMAIN" ]; then
echo " - host: login.$DOMAIN"
echo " kind: auth"
echo " note: 统一登录页"
fi
} > "$GW_YML"
echo " 已生成 $GW_YML($([ -n "$DOMAIN" ] && echo "含门户路由 $DOMAIN" || echo "暂无路由,装完在后台添加"))"
fi
# ── 5. 启动容器 ──
step "5/6 启动容器(host 网络)"
BASE_URL="${SEARXNG_BASE_URL:-}"
if [ -z "$BASE_URL" ]; then
if [ -n "$DOMAIN" ]; then BASE_URL="https://$DOMAIN/"; else BASE_URL="http://127.0.0.1:${ENTRY_PORT}/"; fi
fi
docker run -d --name navi --network host --restart unless-stopped \
-v "${DATA_DIR}:/app/data" -v "${CERT_DIR}:/etc/nginx/certs:ro" \
-e NAVI_SYNC_TOKEN="$SYNC_TOKEN" -e NAVI_ADMIN_TOKEN="$ADMIN_TOKEN" \
-e SEARXNG_BASE_URL="$BASE_URL" \
"$TAG" >/dev/null
echo " 容器已启动,等待健康检查…"
for i in $(seq 1 60); do
if curl -fs -m 3 http://127.0.0.1:18080/healthz >/dev/null 2>&1; then echo " healthz 就绪(${i}s)"; break; fi
[ "$i" = 60 ] && { docker logs --tail 40 navi >&2; die "健康检查超时"; }
sleep 1
done
# ── 6. 应用网关配置 + 自检 ──
step "6/6 应用配置与自检"
APPLY="$(curl -s -X POST -H "Authorization: Bearer $ADMIN_TOKEN" http://127.0.0.1:3005/api/gateway/apply || true)"
echo " apply: $APPLY"
case "$APPLY" in *applied*) ;; *) docker logs --tail 40 navi >&2; die "网关配置未通过校验(上面有 nginx 报错原文)";; esac
code_of() { curl -sk -o /dev/null -w '%{http_code}' -m 8 "$@" 2>/dev/null || true; }
# apply 会 reload nginx:等入口监听真正就绪再探测,避免把"还没起来"误判成失败
for i in $(seq 1 20); do
code="$(code_of "http://127.0.0.1:${ENTRY_PORT}/" )"
[ "$code" != "000" ] && break
sleep 0.5
done
fail=0
check() { # 名称 期望码 实际码
if [ "$2" = "$3" ]; then echo " ✓ $1 → $3"; else echo " ✗ $1 → $3(期望 $2)"; fail=1; fi
}
SCHEME=$([ "$TLS" = 1 ] && echo https || echo http)
check "后台 /admin" 200 "$(code_of "http://127.0.0.1:18080/admin")"
check "内部配置 /conf.yml" 200 "$(code_of "http://127.0.0.1:18080/conf.yml")"
check "未知域名 → 自定义 404" 404 "$(code_of -k --resolve nobody.invalid:${ENTRY_PORT}:127.0.0.1 "${SCHEME}://nobody.invalid:${ENTRY_PORT}/")"
if [ -n "$DOMAIN" ]; then
check "门户 $DOMAIN" 200 "$(code_of -k --resolve "${DOMAIN}:${ENTRY_PORT}:127.0.0.1" "${SCHEME}://${DOMAIN}:${ENTRY_PORT}/")"
fi
echo " 容器状态:$(docker inspect navi --format '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}')"
[ "$fail" = 0 ] || die "自检未全绿,请按上面 ✗ 项排查(容器保留未删除,便于查看日志)"
cat <<EOF
首装完成。
后台地址 http://127.0.0.1:18080/admin (外网:${SCHEME}://<域名>:${ENTRY_PORT}/admin)
统一登录 ${LOGIN_DOMAIN:-login.${DOMAIN:-<你的域名>}}
NAVI_SYNC_TOKEN=$SYNC_TOKEN
NAVI_ADMIN_TOKEN=$ADMIN_TOKEN
下一步:
1. 后台「服务转发」添加各服务域名,逐条勾选"统一认证";「统一登录账号」设置门户用户名/密码
2. 域名解析到本机 ${ENTRY_PORT}(TLS 时同时开放 ${TLS_PORT};${REDIRECT_PORT} 用于 http 跳 https)
3. 后台「站点信息」可改门户页/登录页文案、备案号、壁纸
4. 升级:在开发机执行 deploy/zd-host.sh <版本>(会保留 navi-prev-* 回滚点)
EOF