# 配合 nginx 部署 > 中文文档 | [English](../en/NGINX-DEPLOYMENT.md) `dsh-auth-gateway` 本身是一个独立监听的网关进程(运行在 dsh 进程内),可以直接对外提供服务,也可以放在 nginx 反向代理之后。本文覆盖四种典型拓扑,均给出完整配置示例。 > 本文所有示例使用占位符:域名 `dsh.example.com`、公网 IP `203.0.113.10`、网关端口 `8080`。请替换为你自己的值。 --- ## 拓扑总览 ``` 拓扑 A:裸金属直连 拓扑 B:子域名部署(推荐) 拓扑 C:子路径部署 拓扑 D:Docker nginx 容器 浏览器 ──> 网关:8080 浏览器 ──> nginx:443 浏览器 ──> nginx:443 浏览器 ──> nginx:443 │ dsh.example.com │ example.com/dsh/ │ └─> 网关:8080 ├─> /dsh/ → 网关:8080 └─> 网关:8080 (独立域名,零冲突) ├─> /assets/ → 网关:8080 (nginx 在容器里) ├─> /api/ → 网关:8080 └─> 其他 → 博客应用 ``` | 拓扑 | 适用场景 | basePath | 复杂度 | |---|---|---|---| | A. 裸金属直连 | 内网/可信网络 | `/` | 最低 | | **B. 子域名部署** | **与其他应用共存(推荐)** | **`/`** | **低** | | C. 子路径部署 | 同域名、无法加子域 | `/dsh` | 中(需代理剥离前缀) | | D. Docker nginx | nginx 在容器中 | `/` | 中 | **推荐方案:子域名(拓扑 B)**。dsh 0.1.7 起页面使用文档相对路由,子路径部署(拓扑 C)只需 nginx 剥离 `/dsh/` 前缀并给网关配置 `basePath`,不再维护根路径白名单;子域名则连前缀处理都不需要,与其他应用彻底隔离,配置最简。 --- ## 0. 网关侧公共配置 无论哪种拓扑,`dsh-auth-gateway` 的 profile patch(或 bundle patch)都包含: ```yaml # cordis.patch.yml 中 dsh-auth-gateway 行的 config config: listenHost: 0.0.0.0 # 对外监听所有网卡(nginx 转发 / 直连都可达) listenPort: 8080 upstreamHost: 127.0.0.1 upstreamPort: 8081 ``` 启动命令(对外端口 8080): ```bash dsh web --port 8080 --no-open ``` > **`--public-url` 与 `--trusted-host` 在这里的角色不同(dsh 0.2.1-alpha.1 引入)**: > > - **`--trusted-host` 用不上**:它放宽的是内部 `/api` 的 Host/Origin 栅栏,而网关已在服务端把 Host/Origin 改写为回环,栅栏职责由网关的 Cookie 门禁接管(见 [SECURITY.md](SECURITY.md))。装了网关时不要依赖它。 > - **`--public-url` 受支持,建议按需使用**:网关的 bundle patch 会把该值原样传给内部 `web-runtime` 行。它只决定四个**展示面**——启动打印行、默认浏览器 handoff、智能体运行时上下文(dsh 0.2.1-alpha.3 起新增 `Current DeepSeek Harness Web GUI URL` 行)与 `DSH_WEB_URL`;dsh 官方 help 明确它 **grants no trust**:不参与路由、不改监听地址、不放宽 Host 栅栏、也不决定 cookie `Secure`。不传时这四个面显示内部回环地址 `http://127.0.0.1:`,对远程用户是误导——带 `basePath` 前缀的部署尤其应传,否则智能体看到的是无前缀的回环地址。 > > 对外地址、前缀剥离、cookie `Secure` 始终由网关(及其前面的 nginx)负责;`--public-url` 只是让 dsh 把这些展示面说对,不会替你做这些事。 > > **dsh 0.2.1-alpha.2 新增的 `--tls-cert` / `--tls-key` 同理用不上**:它们让 dsh 内部 webserver 直接跑原生 HTTPS,而网关的 bundle patch 对 `webserver` 行是整对象替换且**故意不含 `tls`**——内部服务恒为回环明文 HTTP,网关以明文转发该回环一跳。对外的 HTTPS 由网关前面的 nginx 终结(见拓扑 B/C),网关据 `X-Forwarded-Proto` 判定并给 cookie 打 `Secure`。所以装了网关时传 `--tls-cert/--tls-key` 不会产生对外 HTTPS,请勿使用。 --- ## 拓扑 A:裸金属直连(无 nginx) **适用**:内网 / 可信网络,不需要域名和 HTTPS。 网关监听 `0.0.0.0:8080`,浏览器直接访问: ``` http://203.0.113.10:8080 ``` - `basePath` 保持默认 `/`(根路径部署),无需任何额外配置; - 认证、OTP、WebSocket 全部由网关自行处理; - 若公网可达,务必配置防火墙只放行必要来源(网关本身有密码 + OTP 保护,但端口暴露面越小越好)。 --- ## 拓扑 B:子域名部署(推荐,零冲突) **适用**:DSH 与其他应用(如博客)共存于同一台服务器。用独立子域名(如 `dsh.example.com`)部署,DSH 根路径部署(`basePath=/`),与主站完全隔离——无需处理根路径资源冲突、无需维护 nginx 白名单、无需 `basePath` 配置。 ``` 浏览器 ──> nginx (443, TLS 终结) ──> http://127.0.0.1:8080 ──> 网关 dsh.example.com ``` 网关配置:`basePath` 保持默认 `/`。nginx server 块: ```nginx # 关键:WebSocket 握手透传,同时普通请求保持 keep-alive。 # 必须放在 http {} 块里(和 server 同级,不能写在 server 内)。 # 不要用固定的 Connection "upgrade"——那会给每个普通请求都贴上 upgrade。 map $http_upgrade $connection_upgrade { default upgrade; '' keep-alive; # 普通请求保持 keep-alive(close 会触发间歇性空 400,见 TROUBLESHOOTING.md 第 8 节) } server { listen 443 ssl; server_name dsh.example.com; ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; # 通配 location:所有路径(含 WebSocket)都从这里代理。 location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; 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; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } } # HTTP → HTTPS 跳转 server { listen 80; server_name dsh.example.com; return 301 https://$host$request_uri; } ``` 要点: - **`Upgrade` / `Connection` 必须在「兜底 location」上转发,不要按路径挑**。DSH 的 WebSocket 端点是 `/api/remote.mux`(API Gateway 独占的 Remote 流多路复用通道);路径名随 dsh 版本变动过(旧版曾叫 `/api/events.mux`、`/sidebar/ws/*`,这些在当前 dsh 里**已不存在**)。如果只为某些路径设置 Upgrade 头、其余落到没有该头的 `location /`,那个 WebSocket 会握手失败——nginx 会把 `Upgrade` 当逐跳头丢掉,握手被降级成普通 GET,浏览器只报 `WebSocket connection to 'wss://.../api/remote.mux' failed`,网关日志才有明确提示; - **map 的普通请求分支必须取 `keep-alive`,不要用 `close`**:网关转发响应时会剥离上游逐跳头(`connection`/`keep-alive` 等),连接语义只由客户端自己的 `Connection` 头决定。若反代给普通请求注入 `close`,与上游响应的 keep-alive 语义互相矛盾,高并发(页面首屏几十个并行请求)下会产生间歇性空 `400 Bad Request`(详见 [TROUBLESHOOTING.md 第 8 节](TROUBLESHOOTING.md)); - `proxy_read_timeout` / `proxy_send_timeout` 设长——SSE 事件流(`/plugins/events`)是长连接,默认 60s 会被掐断; - 网关默认没有 HTTPS(`Secure` Cookie 未启用),由 nginx 终结 TLS 即可。 --- ## 拓扑 C:子路径部署(如 `/dsh/`,有维护成本) **适用**:同一域名下还跑着其他 Web 应用,且**无法添加子域名**。dsh 需要挂在子路径(如 `https://example.com/dsh/`)。 > **0.1.7 起**:dsh 页面通过 `` 和文档相对路由(`api/...`、`plugins/...`)保留挂载前缀。nginx 只需把 `/dsh/` 前缀剥离后转发到网关,不再维护 `/api/`、`/plugins/`、`/assets/` 等根路径白名单。 > > **0.1.7 以下(0.1.5-rc.2 / 0.1.6)**:页面经 `` 按根路径解析 URL,需自行把根路径前缀(`/api/`、`/plugins/`、`/assets/` 等)转发到网关(网关的 `basePath` 逻辑会正确处理这些请求)。 ### 网关侧:配置 `basePath` 插件**默认 `basePath: /`(根路径)**,不随插件分发任何子路径配置。需要子路径部署时,在**部署方自己的 profile patch**(`~/.dsh/profiles/web/cordis.patch.yml`)中覆盖——用户补丁层优先级高于插件 bundle 层。 > **⚠️ 注意**:Cordis 的 `config:` 是**整个对象替换**,不是字段级合并。必须保留 bundle patch 里的所有字段(`listenHost`、`listenPort`、`upstreamHost`、`upstreamPort`),只追加 `basePath`。漏写任何字段会导致该字段回退到 config.js 默认值——例如漏写 `listenPort` 会导致 `--port 8080` 失效(端口变成默认 3080)。 ```yaml # ~/.dsh/profiles/web/cordis.patch.yml # 必须保留 bundle patch 里的所有字段,不能只写 basePath - id: dsh-auth-gateway config: listenHost: 0.0.0.0 listenPort: !!js ctx.webStartup.port ?? 3080 # 保留动态表达式,跟随 --port 参数 upstreamHost: 127.0.0.1 upstreamPort: !!js (ctx.webStartup.port ?? 3080) + 1 basePath: /dsh # ← 子路径前缀 ``` > **`!!js` 表达式**:Cordis 自定义 YAML 标签,用于在启动时动态计算值。`ctx.webStartup.port` 取自 `dsh web --port `,默认 3080。Profile patch 支持同样的 `!!js` 语法。 ### nginx 侧:按前缀分流 ```nginx # 逐跳头透传:放在 http {} 作用域(conf.d/*.conf 即 http 作用域)。 # 每个 nginx 实例只需定义一次。 map $http_upgrade $connection_upgrade { default upgrade; '' keep-alive; # 普通请求保持 keep-alive(close 会触发间歇性空 400,见 TROUBLESHOOTING.md 第 8 节) } # 其他 Web 应用(示例:博客站点) server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; # 你的其他应用 location / { # ... 其他应用自己的处理 ... } } # dsh 专用 server 块(也可并入上面的 server,按 location 分流) server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; # dsh 主入口及全部文档相对资源:/dsh/ → 网关根路径(去掉前缀) location /dsh/ { proxy_pass http://127.0.0.1:8080/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; # WebSocket 必需 proxy_set_header Connection $connection_upgrade; # 见文件顶部的 map 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; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } # /dsh 无斜杠 → 301 补斜杠 location = /dsh { return 301 /dsh/; } } ``` > dsh 0.1.7 起页面资源和 API 都相对当前文档目录解析;`location /dsh/` 已覆盖入口、插件 bundle、API、WebSocket 和静态资源,不需要额外根路径转发。 ### WebSocket 与 SSE 子路径 若同一 nginx 上还有其他应用占用 `/api/` 等路径,可将 dsh 的 WebSocket 端点显式分流——但**必须覆盖 dsh 实际使用的全部 WebSocket 路径**,当前是 `/api/remote.mux`(不要把旧的 `/api/events.mux`、`/sidebar/ws/*` 当成白名单,它们已不存在): ```nginx # 需要 http {} 块里的 map(见拓扑 B) # dsh Remote 流多路复用 WebSocket location = /api/remote.mux { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } ``` > 更稳的做法:不要把 WebSocket 路径写进白名单,而是让**兜底 location 统一转发** `Upgrade`/`Connection`(拓扑 B 的写法)。白名单一旦漏掉某个端点,症状是「页面能打开、登录成功,但功能提示连接失败」,且只在这里的 502/404 或网关日志里能看出原因。 --- ## 拓扑 D:Docker 内 nginx 容器 + 宿主机网关 **适用**:nginx 跑在 Docker 容器里,dsh 网关直接跑在宿主机(或另一个容器)。nginx 容器需要一种方式访问宿主机端口。可配合拓扑 B(子域名)或拓扑 C(子路径)使用——**推荐子域名**。 ### 让 nginx 容器访问宿主机 Docker Compose 中给 nginx 服务加 `extra_hosts`: ```yaml services: nginx: image: nginx:1.27-alpine ports: - "80:80" - "443:443" extra_hosts: - "host.docker.internal:host-gateway" # 容器内 host.docker.internal → 宿主机 volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro ``` > `host-gateway` 需要 Docker 20.10+ 与 Compose v2。旧版本可改用宿主机在 Docker 桥接网络上的网关 IP(如 `172.17.0.1`)。 ### nginx 配置(容器内视角,推荐子域名根路径部署) ```nginx # 容器内 nginx,访问宿主机网关用 host.docker.internal # 子域名 dsh.example.com 根路径部署,与主站零冲突 # map 放在 http {} 块里(和 server 同级) map $http_upgrade $connection_upgrade { default upgrade; '' keep-alive; # 普通请求保持 keep-alive(close 会触发间歇性空 400,见 TROUBLESHOOTING.md 第 8 节) } server { listen 443 ssl; server_name dsh.example.com; ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; location / { proxy_pass http://host.docker.internal:8080; proxy_http_version 1.1; 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; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } } ``` > **⚠️ 最常见的坑**:不要为「已知的 WebSocket 路径」单独写 `location` 白名单,而把兜底 `location /` 留成不带 `Upgrade` 的样子。一旦 dsh 换了 WebSocket 路径(`/api/remote.mux` 之前的名字就变过),握手就会静默失败:HTTP 全部正常、登录也正常,只有页面提示连接失败。**把 `Upgrade`/`Connection` 放在兜底 location 上,是唯一不会随版本漂移的写法。** 子路径部署(`/dsh/`)时,把拓扑 C 的 location 写法搬过来,`proxy_pass` 目标换成 `http://host.docker.internal:8080`(或 `/` 形式去前缀)即可——但**强烈建议用子域名替代子路径**,避免根路径资源冲突。 ### 若网关也在容器里 网关容器与 nginx 容器同网络时,`proxy_pass` 直接用服务名: ```yaml # docker-compose.yml services: nginx: ... depends_on: - dsh-gateway dsh-gateway: build: ./dsh-auth-gateway ports: - "8080:8080" ``` ```nginx location / { proxy_pass http://dsh-gateway:8080; # ... 其余头同上 ... } ``` --- ## 安全建议(所有拓扑通用) 1. **对外只暴露必要的端口**:理想情况下公网只开 443(nginx),网关端口(8080)仅允许 nginx / 内网访问; 2. **TLS 由 nginx 终结**:网关保持明文 HTTP 即可,不要在网关端口上重复加 TLS; 3. **WebSocket/SSE 长连接**:`Upgrade`/`Connection` 头与 `proxy_read_timeout`/`proxy_send_timeout` 三件套必须配齐,且 `Upgrade`/`Connection` 要设在**兜底 location** 上(用 `map $http_upgrade $connection_upgrade`,不要写死 `"upgrade"`、也不要按路径挑白名单),否则事件流 / 终端会被 60s 掐断,或 WebSocket 握手直接降级失败(浏览器表现为 `ERR_INCOMPLETE_CHUNKED_ENCODING` 或 `WebSocket connection to '.../api/remote.mux' failed`);map 的普通请求分支必须取 `keep-alive`(`close` 会引发间歇性空 400,见 [TROUBLESHOOTING.md 第 8 节](TROUBLESHOOTING.md)); 4. **直连与代理不要混用**:浏览器端要么全部走域名 + 代理,要么全部直连网关端口。混用时(如页面从 `https://dsh.example.com` 打开、却直连 `http://203.0.113.10:8080`)会因跨 scheme / 跨端口被 dsh 插件的同源校验拒绝(403 `origin-rejected`)——这是预期保护,不是故障。 5. **账号登录只在本机回环入口可用**:上游要求浏览器 origin 是带显式非零端口的回环 http(`http://127.0.0.1:<端口>` / `localhost`),因此域名、路径反代、LAN IP 等非回环入口下,点「登录 DeepSeek 账号」会**立即**失败(`account: protocol`)——这与 `publicPaths` 无关(那只管回调那一段)。需要账号登录时用 SSH 本地转发(`ssh -L 8080:127.0.0.1:8080 <主机>`)后经回环入口访问;纯域名部署改用 API Key 通道,详见 [TROUBLESHOOTING.md 第 10 节](TROUBLESHOOTING.md)。 --- ## 相关文档 - [INSTALL.md](INSTALL.md) — 安装、更新、卸载 - [DEPLOYMENT.md](DEPLOYMENT.md)(中文)| [English](../en/DEPLOYMENT.md) — 端口推导、LAN 部署、故障排查 - [SECURITY.md](SECURITY.md) — 威胁模型与 OTP 安全设计