#!/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_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 <:${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