Files
HomeDash/deploy/install-host.sh
HomeDash Bot c0cbdb5efc feat(deploy): 补首装路径,使他人/AI 可照教程从零部署
审计发现 5 处断点(3 处硬阻断):
1. Dockerfile 的 HTTP_PROXY/HTTPS_PROXY 默认指向内网代理 → 他人构建必失败
2. zd-host.sh 是升级脚本(要求已存在 host 模式 navi 容器)→ 新机器第一句过不去
3. docker-compose.yml 是桥接+端口映射,与'统一入口 host 模式'口径冲突
4. 证书文件名约定未文档化(实际来自 gateway.yml 的 certFile/keyFile)
5. 环境变量表不完整、缺 sshpass 依赖说明

本次:
- Dockerfile:代理默认置空,需要时 --build-arg 传入
- deploy/install-host.sh(新):干净机器首次安装,6 步并在任一步失败即停,
  含端口独占检查、证书检查、初始 gateway.yml 生成、apply、四项自检,
  令牌可自动生成并只打印一次;已存在 navi 容器时拒绝执行(引导去 zd-host.sh)
- docker-compose.yml:标注为试用形态,指向 install-host.sh
- README:新增「从零部署(干净机器)」——两条路径分工表、前置条件表、
  完整环境变量表、证书与初始网关配置说明、装完自检命令;自定义文件表登记新脚本

另修:脚本自检原用 /.navi-ping(该路径已按用户要求删除),改用 /conf.yml。
2026-09-12 09:37:45 +08:00

187 lines
8.2 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 初始网关配置"
GW_DIR="$DATA_DIR/gateway"
GW_YML="$GW_DIR/gateway.yml"
mkdir -p "$GW_DIR"
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
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)
code_of() { curl -sk -o /dev/null -w '%{http_code}' -m 8 "$@" || echo 000; }
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