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。
This commit is contained in:
HomeDash Bot
2026-09-12 09:37:45 +08:00
parent cb86557466
commit c0cbdb5efc
4 changed files with 272 additions and 2 deletions

View File

@ -16,8 +16,9 @@ RUN GOPROXY=https://goproxy.cn,direct go mod tidy && CGO_ENABLED=0 go build -tri
# ── Stage 2: SearXNG from source ──
FROM debian:bookworm-slim AS searxng-build
ARG HTTP_PROXY=http://172.17.0.1:7890
ARG HTTPS_PROXY=http://172.17.0.1:7890
# 需要代理时用 --build-arg 传入(默认不设,保证他人环境可直接构建)
ARG HTTP_PROXY=
ARG HTTPS_PROXY=
ENV DEBIAN_FRONTEND=noninteractive
ENV http_proxy=${HTTP_PROXY}

View File

@ -437,6 +437,85 @@ curl -s https://<域名>/ | grep -c '<!--HERO' || echo '门户页占位符已替
curl -s https://<域名>/login | grep -o '<h1>[^<]*</h1>' # 应为配置的标题或默认值
```
## 从零部署(干净机器)
前面的「快速开始」是最小试用形态(bridge 网络 + 端口映射)。要装成**统一入口**(一个容器同时
当反向代理 + 统一登录 + 导航 + 搜索),用 `deploy/install-host.sh`。
### 两条路径的分工(别混用)
| 场景 | 用什么 | 在哪执行 |
| --- | --- | --- |
| 干净机器首次安装 | `deploy/install-host.sh` | **目标机器上**(需要 docker) |
| 已有安装的升级/回滚 | `deploy/zd-host.sh <版本>` | 开发机(ssh 推送镜像,接受数秒中断) |
`zd-host.sh` 会拒绝在「没有 host 网络的 navi 容器」的机器上运行,这是刻意设计:它只做升级。
### 前置条件
| 项 | 要求 |
| --- | --- |
| 系统 | Linux + Docker(`docker info` 可用) |
| 端口 | 入口端口(默认 3080;TLS 时另加 8443)、跳转端口 3081 **必须独占** |
| 证书 | `NAVI_TLS=1`(默认)时,证书目录需有 `fullchain.pem` + `privkey.pem`;纯 HTTP 用 `NAVI_TLS=0` |
| 域名 | 各服务域名解析到本机入口端口(DNS 由你自行配置) |
| 可选依赖 | `openssl`(生成令牌;没有则用 /dev/urandom 兜底) |
| 远程执行 | 开发机需 `sshpass`(`zd-host.sh` 用;用密钥登录可自行改脚本) |
### 安装
```bash
# 在目标机器上(仓库目录内)
NAVI_DOMAIN=example.com \
NAVI_SYNC_TOKEN=<自定> NAVI_ADMIN_TOKEN=<自定> \
bash deploy/install-host.sh
# 远程执行
ssh root@<目标机> 'bash -s' < deploy/install-host.sh
```
脚本按 6 步执行并在任一步失败即停:前置检查 → 令牌 → 构建镜像 → 写初始 `gateway.yml` →
host 网络启动容器 → 应用配置 + 自检。**自检不全绿就退出**(容器保留,便于看日志)。
结束时打印一次令牌与后续步骤;之后到后台「服务转发」逐个添加服务域名并勾选统一认证。
### 环境变量(install-host.sh 全部可覆盖)
| 变量 | 默认 | 说明 |
| --- | --- | --- |
| `NAVI_SRC` | 脚本上级目录 | 仓库位置(用 `ssh 'bash -s'` 管道执行时需显式指定或先把仓库拷过去) |
| `NAVI_DATA_DIR` | `/vol1/docker/navi-data` | 数据卷(`conf.yml`、`gateway.yml`、历史、后台账号) |
| `NAVI_CERT_DIR` | `/vol1/docker/cert` | 证书目录,只读挂载到 `/etc/nginx/certs` |
| `NAVI_TAG` | `homedash:ui-install` | 构建出的镜像名 |
| `NAVI_ENTRY_PORT` / `NAVI_TLS_PORT` / `NAVI_REDIRECT_PORT` | `3080` / `8443` / `3081` | 入口、额外 TLS、http→https 跳转 |
| `NAVI_TLS` | `1` | `0` = 纯 HTTP(无需证书,适合内网先跑通) |
| `NAVI_DOMAIN` / `NAVI_LOGIN_DOMAIN` | 空 | 填了就自动生成门户路由 + 统一登录路由(否则装完在后台加) |
| `NAVI_SYNC_TOKEN` / `NAVI_ADMIN_TOKEN` | 空→自动生成 | 历史同步令牌 / 后台 API 令牌 |
| `NAVI_BUILD` / `NAVI_TAR` | `1` / 空 | `0` 表示用 `docker load -i $NAVI_TAR` 导入现成镜像 |
| `NAVI_SSH_PASS`(zd-host.sh) | 空 | ssh 密码;用密钥则留空 |
| `NAVI_HOST`(zd-host.sh) | `admin@10.0.0.10` | 开发机侧默认值,换机器需覆盖 |
### 证书与初始网关配置
`gateway.yml`(数据卷内)里的 `certFile` / `keyFile` 指向**容器内**路径,默认
`/etc/nginx/certs/fullchain.pem`、`/etc/nginx/certs/privkey.pem`,对应宿主机的
`$NAVI_CERT_DIR/fullchain.pem`、`$NAVI_CERT_DIR/privkey.pem`。
证书文件名不同时:改 `gateway.yml` 的这两个字段,或把证书按上述名字放好。
生成配置后由后台「转发与应用」的 **应用** 动作执行 `nginx -t` 校验,不通过则保留上一份可用配置
并把 nginx 报错原文返回给界面——所以改错配置不会打断入口。
### 装完自检(与脚本末尾一致)
```bash
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18080/healthz # 200
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18080/admin # 200
curl -sk -o /dev/null -w '%{http_code}\n' --resolve dashy.example.com:3080:127.0.0.1 \
https://dashy.example.com:3080/conf.yml # 受保护 → 302
curl -sk -o /dev/null -w '%{http_code}\n' --resolve nobody.invalid:3080:127.0.0.1 \
https://nobody.invalid:3080/ # 未知域名 → 404
```
## 部署需要的全部自定义文件
仓库即完整部署物料,没有额外依赖仓库外的文件:
@ -458,6 +537,7 @@ curl -s https://<域名>/login | grep -o '<h1>[^<]*</h1>' # 应为配置的标
| `deploy/verify-matrix.js` | 生产验收矩阵:6 尺寸 × 3 视图 × 深浅色,断言滚动/文字亮度/深色生效/零异常(需 `PORTAL_COOKIE`) |
| `deploy/verify-workspace.js` | 工作空间深色与"无法内嵌"弹窗验收(触发 blocked 态并读 computed style) |
| `deploy/verify-scroll.js` | 多视口纵向滚动复现/回归检查(含矮窗口) |
| `deploy/install-host.sh` | 干净机器首次安装(host 模式统一入口) |
| `deploy/zd-host.sh` | **合并后**的部署脚本(host 模式,接受数秒中断,失败自动回滚) |
| `portal/`(index.html、404.html、500.html、portal.css、favicon) | 门户静态页与自定义错误页,进镜像的 `/app/portal` |
| `admin-server/auth.go` | 容器内置统一登录(HMAC 会话 cookie、恒定时间比较、跳转白名单) |

186
deploy/install-host.sh Executable file
View File

@ -0,0 +1,186 @@
#!/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

View File

@ -1,3 +1,6 @@
# 试用/单机形态:桥接网络 + 端口映射,适合本机看效果。
# 统一入口形态(对外 3080/8443 + 3081 跳转、统一认证)请用 deploy/install-host.sh,
# 它是 host 网络单容器,不能与下面的端口映射共存。
version: "3.8"
services: