# HomeDash — 自托管内网导航门户 将 **Dashy + SearXNG + 浏览器历史同步 + 管理后台**整合在单个 Docker 容器中,并附带**完整的内网服务发布方案**(带统一认证的反向代理 + webui4frpc 内网穿透),让外网通过域名安全访问内网服务。 ## 架构总览(生产全链路) ``` 公网用户 │ ▼ 阿里云 VPS (frps 服务端, :7000/8080/443) │ frps: vhost 端口 + tcp 隧道 ▼ frpc (由 webui4frpc 在内网主机上管理, WebUI :7500) │ configs/*.json → 定义各隧道 ▼ portal-nginx (统一认证反向代理, 容器) │ auth_request → portal-auth(:4001) 统一登录 │ map $host → 按子域名分流 ▼ navi 单容器 (Dashy+SearXNG+历史+管理, :3003) │ 内层 nginx :8080 按路径分流 ▼ Dashy / SearXNG(→Clash 代理出网) / 历史 API / 管理 UI ``` ### navi 容器内部 ``` ┌─────────────────────────────────────────────────────┐ │ nginx (:8080) — 统一入口,同域名同端口 │ │ ├─ / → Dashy 静态仪表盘 │ │ ├─ /search → SearXNG 搜索 (:8888) │ │ ├─ /static/ → SearXNG 静态资源 (:8888) │ │ ├─ /api/history/ → 历史同步 API (:3004) │ │ ├─ /admin/ → 管理后台 WebUI (:3005) │ │ ├─ /conf.yml → 运行时导航配置(数据卷) │ │ └─ /item-icons/ → 自定义图标 (数据卷) │ └─────────────────────────────────────────────────────┘ ``` **技术栈**:Go(历史 API + 管理 API)· SearXNG(Python) · Dashy/Vue · Nginx · Supervisord · frp/webui4frpc ## 快速开始 ### 前置条件 - Docker 20+ 与 Docker Compose v2 - 可选:公网域名 + 反向代理,用于外网访问(见下文「生产部署」) ### 方式 A:从本仓库拉取源码构建(推荐) ```bash # 1. 拉取仓库(把 换成你自己的 Git 托管地址) git clone HomeDash cd HomeDash # 2. 配置环境变量 cp .env.example .env # 编辑 .env: # - NAVI_SYNC_TOKEN / NAVI_ADMIN_TOKEN:鉴权令牌,openssl rand -hex 24 生成 # - SEARXNG_BASE_URL:对外访问地址(有域名填 https://nav.example.com) # - NAVI_PORT:对外端口(默认 3003) # 3. 首次启动(会拉取 golang/dashy/searxng 基础镜像并编译) docker compose up -d --build # 4. 访问 echo "打开 http://:3003" ``` ### 方式 B:直接构建镜像运行 ```bash docker build -t jianf-navi . docker run -d --name jianf-navi -p 3003:8080 \ -e SEARXNG_BASE_URL=https://nav.example.com \ -e NAVI_SYNC_TOKEN= \ -e NAVI_ADMIN_TOKEN= \ -v $PWD/data:/app/data \ jianf-navi ``` ### 首次配置导航项 容器启动后按需编辑 `data/conf.yml`(sections 列表),或直接用管理后台添加: ```bash # 先让 conf.yml 从模板生成 docker compose restart # 或用管理 WebUI 在浏览器添加:http://:3003/admin/ ``` > 首次没有 data/ 目录时,容器会用内置 `conf.example.yml` 生成一份最小配置。 ## 配置说明 ### conf.yml 核心字段 ```yaml appConfig: theme: default webSearch: searchEngine: custom customSearchEngine: /search?q= # 同域名反代到 SearXNG openingMethod: newtab sections: - name: 常用服务 # 按访问频次自动排序 icon: fas fa-star collapsed: false items: - title: Gitea icon: fas fa-code-branch url: http://192.168.1.100:3000 statusCheck: true ``` ### 图标 放入 `data/item-icons/`,conf.yml 引用文件名: ```yaml icon: my-icon.png # 本地图标 icon: fas fa-star # Font Awesome ``` ### 环境变量 | 变量 | 说明 | 默认 | |------|------|------| | `NAVI_SYNC_TOKEN` | 浏览器扩展同步历史令牌 | 空(不鉴权) | | `NAVI_ADMIN_TOKEN` | 管理后台/API 写操作令牌 | 空(不鉴权) | | `SEARXNG_BASE_URL` | 对外访问地址,影响搜索结果链接 | `http://localhost:3003` | | `NAVI_PORT` | compose 对外端口 | `3003` | ## 浏览器扩展 ### 安装 **Chrome / Edge**:`chrome://extensions/` → 开发者模式 → 加载 `extension/chrome/` **Firefox**:`about:debugging#/runtime/this-firefox` → 临时加载 `extension/firefox/manifest.json` > 扩展的「同步令牌」需填服务端 `.env` 里 `NAVI_SYNC_TOKEN` 的**同一个值**,否则同步被拒(401)。 ### 配置 | 字段 | 示例 | |------|------| | 服务地址 | `https://nav.example.com` | | 同步令牌 | `.env` 中的 `NAVI_SYNC_TOKEN` | | 设备名称 | `desktop-chrome` | ### 行为 - 每 30 秒检查本地队列,**仅在有新数据时 POST** - 搜索引擎 URL 在本地过滤,不上传 - 「常用服务」和「历史访问」按总访问频次自动排序 ## 管理后台 访问 `http://:3003/admin/` 可视化管理导航项。 API(需 `NAVI_ADMIN_TOKEN`): ```bash # 查看 curl -H "Authorization: Bearer " http://:3003/admin/api/sections # 添加 curl -H "Authorization: Bearer " -X POST -H 'Content-Type: application/json' \ -d '{"sectionIndex":0,"item":{"title":"New","url":"https://example.com"}}' \ http://:3003/admin/api/item # 删除 curl -H "Authorization: Bearer " -X DELETE -H 'Content-Type: application/json' \ -d '{"sectionIndex":0,"itemIndex":2}' \ http://:3003/admin/api/item/delete ``` ## 历史同步 API | 端点 | 方法 | 说明 | |------|------|------| | `/api/history/sync` | POST | 批量同步浏览记录 | | `/api/history/` | GET | 获取历史(按频次排序) | | `/api/history/` | DELETE | 清空全部历史 | | `/api/history/stats` | GET | 统计信息 | ```json // POST /api/history/sync { "items": [{ "url": "https://example.com/page", "title": "Page Title", "device": "desktop-chrome", "timestamp": 1725800000, "visit_count": 3 }] } ``` ## 生产部署:外层统一认证反向代理 > 内网门户通常不直接暴露 3003 端口,而是放在一个**带统一登录认证的反向代理**后面, > 所有子域名共用一个登录入口。下面是本项目参考的 portal-nginx 方案。 ### portal-nginx(统一入口容器) 结构:一个 nginx 容器负责所有 `*.example.com` 的 TLS 终止 + 按域名分流 + 统一认证。 ```nginx # 按域名映射到内网后端 map $host $backend { hostnames; default ""; navi.example.com http://127.0.0.1:3003; # 本导航容器 gitea.example.com http://127.0.0.1:3000; blog.example.com http://127.0.0.1:8080; } # 需登录的域名统一走 auth_request server { listen 443 ssl; http2 on; server_name navi.example.com gitea.example.com blog.example.com; auth_request /_auth; # 先问认证服务 error_page 401 = @login; # 未登录 → 跳登录页 location /_auth { internal; proxy_pass http://127.0.0.1:4001; # portal-auth 认证服务 proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header Cookie $http_cookie; } location @login { return 302 https://login.example.com/login?redirect=$scheme://$host$request_uri; } location / { if ($backend = "") { return 404; } proxy_pass $backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` ### portal-auth(统一认证服务) 一个极简 HTTP 认证服务(:4001),只做两件事: 1. `GET/POST /login` — 校验用户名密码,签发 `portal_session` cookie 2. `/_auth` 被 nginx `auth_request` 调用 — 校验 cookie 是否有效(200=通过,401=拒绝) ```python # app.py(示意) USERNAME = os.environ['PORTAL_USERNAME'] # admin PASSWORD = os.environ['PORTAL_PASSWORD'] # 强密码 sessions = {} # token -> 过期时间 # /login 成功 → uuid token 写入 sessions + Set-Cookie # /_auth 读 Cookie → 校验存在且未过期 → 200/401 ``` 部署: ```bash docker run -d --name portal-auth \ -p 4001:4001 \ -e PORTAL_USERNAME=admin \ -e PORTAL_PASSWORD=change-me \ -v /path/auth/app.py:/app/app.py:ro \ python:3.11-slim ``` ### 让搜索/管理子路径免登录(可选) Dashy 域名整体在认证后,登录用户能正常搜索。若希望搜索页**免登录公开访问**,在对应 server 块加: ```nginx location /search { auth_request off; proxy_pass $backend; } ``` > ⚠️ 管理后台 `/admin/` 与历史 API `/api/` **不应**免认证—— > 它们能修改导航与数据,建议保留登录墙,或改用 **Bearer Token**(见下文鉴权章节)。 ## 内网穿透:webui4frpc + frp > [webui4frpc](https://gitcode.com/JianFeeeee/webui4frpc) — 基于 Go 与 Web 的可视化 frpc 配置管理工具, > 提供 WebUI 创建/启停 frpc 隧道,无需手写 frpc.toml。 > 也可不用它,直接用原生 frpc(见文末「原生 frpc 方式」)。 ### 为什么要穿透 本导航容器跑在**内网**(如 192.168.x.x:3003)。要让**外网通过域名访问**,需要: ``` 外网用户 → 公网 VPS(有公网IP) 上的 frps [frp 服务端,监听控制端口 + 域名端口] → frpc(内网运行,webui4frpc 管理或原生) → 你内网的 portal-nginx / navi [把域名流量转发进来] ``` ### 第 1 步:公网 VPS 部署 frps 下载 frp 服务端二进制并写配置: ```toml # frps.toml bindPort = 7000 # frpc 连接 frps 的控制端口 vhostHTTPPort = 8080 # HTTP 类型代理的入口端口(雷池/WAF 也走它) auth.method = "token" auth.token = "<长随机token>" # frpc 必须带相同 token 才能连 tls.enable = true # frpc↔frps 加密 # 用 systemd 启动,或直接: # ./frps -c frps.toml ``` VPS 安全组/防火墙需放行:`7000`(frpc连接)、`8080`(http域名)、`443/80`(若 https 由 frps 终结)。 ### 第 2 步:内网主机部署 webui4frpc(可视化) 从 [webui4frpc](https://gitcode.com/JianFeeeee/webui4frpc) 下载二进制,启动: ```bash /opt/webui4frpc/webui4frpc \ -addr 0.0.0.0:7500 \ -user admin -password '<强密码>' \ -workdir /opt/webui4frpc/data # 打开 http://<内网IP>:7500 进入 WebUI ``` ### 第 3 步:在 WebUI 建「portal」隧道 在 WebUI 里新建配置,指向公网 frps,并添加代理。等价配置文件存于 `configs/*.json`: ```json { "serverAddr": "", "serverPort": 7000, "auth": { "method": "token", "token": "" }, "proxies": [ { "name": "portal_https", "type": "https", "localIP": "<内网portal-nginx主机IP>", "localPort": 3080, "customDomains": ["*.nav.example.com", "nav.example.com"] }, { "name": "portal_http", "type": "tcp", "localIP": "<内网portal-nginx主机IP>", "localPort": 3081, "remotePort": 8080 }, { "name": "navi_ssh", "type": "tcp", "localIP": "<内网目标主机IP>", "localPort": 22, "remotePort": 220 } ] } ``` webui4frpc 会自动下载并启动 `frpc`(存 `data/bin/`),配置变更即热加载。 ### 第 4 步:DNS 解析 把 `nav.example.com`、`*.nav.example.com` 的 DNS 指向**公网 VPS IP**。 - `type: https` 代理:frps 把域名 443 流量转发内网 portal-nginx:3080,**TLS 证书由你内网管理** - `type: tcp`(portal_http):把外网 8080 → 内网 3081(HTTP→HTTPS 跳转或备用入口) ### 原生 frpc 方式(不用 webui4frpc) ```toml # frpc.toml serverAddr = "" serverPort = 7000 auth.method = "token" auth.token = "" [[proxies]] name = "portal_https" type = "https" localIP = "192.168.1.100" localPort = 3080 customDomains = ["nav.example.com", "*.nav.example.com"] [[proxies]] name = "portal_http" type = "tcp" localIP = "192.168.1.100" localPort = 3081 remotePort = 8080 ``` ```bash ./frpc -c frpc.toml ``` ### 隧道规划对照 | 代理名 | type | 本地 | 远端 | 用途 | |--------|------|------|------|------| | portal_https | https | 内网:3080 | 443域名 | 统一反代 HTTPS 入口 | | portal_http | tcp | 内网:3081 | 8080 | HTTP 跳 HTTPS / 备用 | | gitea_ssh | tcp | 内网:22 | 220 | Git SSH | ## 鉴权(历史同步 + 管理 API) 导航门户的 `/api/` 和 `/admin/api/` 能读写数据,**不应裸奔**。推荐两层: 1. **外层**:portal-nginx 保留登录墙(`/admin/` 页面需登录) 2. **内层**:Go 服务校验 `Authorization: Bearer ` 部署时通过环境变量注入令牌: ```bash docker run -d ... \ -e NAVI_SYNC_TOKEN=<长随机串> \ # 浏览器扩展同步用 -e NAVI_ADMIN_TOKEN=<长随机串> \ # 管理 API 写操作用 jianf-navi:latest ``` 浏览器扩展在设置里填同一个 `NAVI_SYNC_TOKEN`,每次同步请求带 `Authorization: Bearer `。 ```bash # 管理 API 示例(写操作必须带 admin token) curl -H "Authorization: Bearer " \ -X POST -H 'Content-Type: application/json' \ -d '{"sectionIndex":0,"item":{"title":"New","url":"https://example.com"}}' \ http://:3003/admin/api/item ``` > 实现参考:`history-server` 的 `/sync` 校验 `NAVI_SYNC_TOKEN`, > `admin-server` 的写操作校验 `NAVI_ADMIN_TOKEN`。 ## 数据备份 ``` data/ ├── conf.yml # 导航配置 ├── history.json # 浏览历史 └── item-icons/ # 自定义图标 ``` ```bash # 自动备份 0 2 * * * docker exec navi tar czf /tmp/bak.tar.gz -C /app/data . && \ docker cp navi:/tmp/bak.tar.gz /backup/navi-$(date +\%Y\%m\%d).tar.gz ``` ## 项目结构 ``` ├── history-server/ # Go 历史同步 API (:3004) ├── admin-server/ # Go 管理后台 API (:3005) ├── admin/ # 管理后台前端 ├── web/ # Dashy 注入脚本 ├── extension/ # 浏览器扩展 (Chrome/Firefox) ├── nginx.conf # 容器内统一反代 ├── supervisord.conf # 进程管理 ├── conf.example.yml # 脱敏示例配置 ├── searxng-settings.yml ├── docker-compose.yml ├── Dockerfile └── .env.example ``` ## 常见问题 **搜索无结果**:确认容器能访问搜索引擎。 - 国内服务器通常需要代理才能出网:SearXNG 不读 `http_proxy` 环境变量, 必须在 `searxng-settings.yml` 的 `outgoing.proxies` 里配置(见文件内注释)。 - **Bing 结果异常少**:`www.bing.com` 会 302 跳到 `cn.bing.com`,而 SearXNG 引擎请求不跟随重定向,只会解析跳转页得到 0 条且不报错。已在默认配置里把 bing 的 `base_url` 指向 `https://cn.bing.com` 绕开该重定向。 - **引擎大面积超时**:`outgoing.max_request_timeout` 会覆盖每个引擎自己的 `timeout`。默认配置已放宽到 `request_timeout: 8.0` / `max_request_timeout: 15.0`, 经代理访问时不要再调回 3/4 秒。 - 百度与搜狗会返回 302 验证页(并触发引擎 1 小时封禁),默认已关闭。 **管理后台 401**:设置了 `NAVI_ADMIN_TOKEN` 时需携带 Bearer token。 **历史不显示**:检查扩展配置中的服务地址和同步令牌。 **内存占用**:正常约 100MB。 ## 许可 MIT