Skip to content

Commit 4e1ef52

Browse files
committed
docs: add derper localhost binding design
1 parent 736a11f commit 4e1ef52

1 file changed

Lines changed: 322 additions & 0 deletions

File tree

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
# Start-Container Derper And Localhost Binding Design
2+
3+
## Summary
4+
5+
本设计为通用容器入口 `scripts/pwsh/devops/start-container.ps1` 补充两项能力:
6+
7+
-`derper` 作为新的通用 compose 服务接入 `config/dockerfiles/compose/docker-compose.yml`
8+
- 为所有使用 `ports:` 暴露端口的服务增加统一的 localhost 绑定能力,既支持命令行临时开启,也支持通过 `config/dockerfiles/compose/.env.local` 持久化配置
9+
10+
默认行为保持不变。只有显式开启 localhost 绑定时,脚本才会把宿主机端口限制到 `127.0.0.1`
11+
12+
## Context
13+
14+
当前 `start-container.ps1` 已经具备以下基础能力:
15+
16+
- 自动定位项目根目录与 `config/dockerfiles/compose/docker-compose.yml`
17+
- 按“服务默认值 -> 进程环境 -> .env -> .env.local -> CLI 环境变量”的优先级合并 compose 配置
18+
- 通过 `docker compose``docker-compose` 启动目标 profile
19+
- 在启动后输出服务访问信息
20+
21+
当前通用 compose 模板的端口暴露方式也比较统一:
22+
23+
- 大部分服务使用 `ports: ["5432:5432"]` 这类字符串写法
24+
- 少数服务使用多行列表写法,例如 RustDesk
25+
- 仓库中已经存在 `network_mode: host` 的服务块,例如 `beszel-agent`
26+
27+
这意味着我们有两个明确约束:
28+
29+
- localhost 绑定应围绕 `ports:` 服务设计,而不是试图统一改写 host 网络服务
30+
- 方案应尽量复用现有配置解析链路,而不是再引入一套独立配置系统
31+
32+
## Goals
33+
34+
- 在通用 compose 模板中新增 `derper` 服务,并让它能通过 `-ServiceName derper` 启动
35+
-`start-container.ps1` 增加 `-BindLocalhost` 参数
36+
- 支持通过 `config/dockerfiles/compose/.env.local` 中的 `BIND_LOCALHOST=true|false` 持久化控制 localhost 绑定
37+
- 明确定义 CLI、`.env.local` 与默认值之间的优先级
38+
-`Show-ServiceAccessInfo` 与实际绑定行为保持一致
39+
- 在测试中覆盖端口改写、配置优先级、非法配置与 host 网络拒绝逻辑
40+
41+
## Non-Goals
42+
43+
- 不把所有服务改为 `network_mode: host`
44+
- 不修改现有服务默认是否对外开放的行为
45+
- 不让原生命令 `docker compose -f config/dockerfiles/compose/docker-compose.yml ...` 自动继承 localhost 绑定能力
46+
- 不在本次改动中扩展更复杂的网络策略,例如按单个端口选择绑定地址、绑定到特定内网 IP 或 Tailscale IP
47+
- 不为 `network_mode: host` 服务实现半自动兼容逻辑
48+
49+
## Chosen Approach
50+
51+
采用“基础 compose 保持声明式配置 + `start-container.ps1` 在需要时生成临时 localhost override”的方案。
52+
53+
理由如下:
54+
55+
- 默认行为可以保持完全不变,不会让未开启 localhost 模式的用户受到影响
56+
- `-BindLocalhost``.env.local` 可以沿用现有配置解析优先级,入口一致
57+
- 不需要把整个基础 compose 模板重构为依赖 `host_ip` 插值的形式
58+
- 当目标服务不是 `ports:` 暴露而是 `network_mode: host` 时,可以明确拒绝执行,边界清晰
59+
60+
相比维护一份额外的 `docker-compose.localhost.yml`,临时 override 不会引入长期双份配置的同步成本。
61+
62+
## Configuration Contract
63+
64+
### CLI Parameter
65+
66+
`start-container.ps1` 新增 `-BindLocalhost` 开关,用于显式要求把目标服务的宿主机端口绑定到 `127.0.0.1`
67+
68+
该参数需要支持三种状态:
69+
70+
- 未传入:表示不覆盖配置文件,继续走配置解析结果
71+
- 显式为 `true`:强制开启 localhost 绑定
72+
- 显式为 `false`:例如 `-BindLocalhost:$false`,即使 `.env.local` 中写了 `BIND_LOCALHOST=true`,也要临时关闭 localhost 绑定
73+
74+
这意味着实现层不能只把它当作普通的“有没有传开关”来处理,而需要保留“显式传 false”这一覆盖语义。
75+
76+
### Persistent Env Setting
77+
78+
`config/dockerfiles/compose/.env.local` 增加:
79+
80+
```dotenv
81+
BIND_LOCALHOST=true
82+
```
83+
84+
该变量只影响 `start-container.ps1` 的启动路径,不承诺对用户直接运行原生命令时仍然生效。
85+
86+
### Precedence
87+
88+
最终优先级定义为:
89+
90+
1. CLI 显式 `-BindLocalhost`
91+
2. `config/dockerfiles/compose/.env.local` 中的 `BIND_LOCALHOST`
92+
3. 默认值 `false`
93+
94+
这保证了:
95+
96+
- 默认保持当前对外暴露行为
97+
- 用户可以通过 `.env.local` 为本机长期启用 localhost 模式
98+
- 用户仍然可以临时通过 CLI 覆盖 `.env.local`
99+
100+
### Boolean Parsing
101+
102+
布尔值解析采用严格模式,只接受常见明确值:
103+
104+
- 真值:`true``1``yes``on`
105+
- 假值:`false``0``no``off`
106+
107+
如果 `BIND_LOCALHOST` 出现其它值,脚本直接失败并给出明确错误,避免因为拼写错误导致端口意外暴露或意外关闭。
108+
109+
## Compose Override Design
110+
111+
### Base Principle
112+
113+
基础文件 `config/dockerfiles/compose/docker-compose.yml` 继续作为唯一长期维护的 compose 模板,不直接写入 localhost 绑定逻辑。
114+
115+
当最终配置判定需要启用 localhost 绑定时,`start-container.ps1` 额外生成一个临时 override compose 文件,并在执行时叠加:
116+
117+
```text
118+
docker compose -f docker-compose.yml -f /tmp/start-container.localhost.override.12345.yml ...
119+
```
120+
121+
### Override Scope
122+
123+
临时 override 只覆盖本次目标服务对应的 `ports:` 定义,不修改镜像、环境变量、volume 或 profile。
124+
125+
例如:
126+
127+
- `postgre` 只覆盖 `postgre.ports`
128+
- `rustdesk` 组合服务会同时覆盖 `rustdesk-hbbs``rustdesk-hbbr`
129+
- `derper` 会同时覆盖 `8443/tcp``3478/udp`
130+
131+
### Port Rewrite Rules
132+
133+
端口改写规则统一为:
134+
135+
- `5432:5432` -> `127.0.0.1:5432:5432`
136+
- `21116:21116/udp` -> `127.0.0.1:21116:21116/udp`
137+
- 已经带协议后缀的端口必须保留协议
138+
139+
解析器只支持当前仓库实际使用的两类 `ports:` 写法:
140+
141+
- 行内数组写法,例如 `ports: ["6379:6379"]`
142+
- 多行字符串列表写法,例如
143+
144+
```yaml
145+
ports:
146+
- "21117:21117"
147+
- "21119:21119"
148+
```
149+
150+
如果未来基础 compose 中出现更复杂的 `ports` map 语法,脚本应直接失败并提示当前 override 生成器不支持该格式,而不是静默跳过。
151+
152+
### Host Network Guard
153+
154+
当本次目标服务或其关联服务块声明了 `network_mode: host`,并且最终配置要求启用 localhost 绑定时,脚本直接报错。
155+
156+
原因是:
157+
158+
- host 网络模式下 `ports:` 映射不存在统一改写入口
159+
- “应用自身监听地址”与“Docker 发布端口地址”是两层不同语义
160+
- 模糊兼容会让用户误以为已经只绑定本机,实际却可能仍暴露在宿主机所有网卡上
161+
162+
错误信息应明确指出:
163+
164+
- 哪个服务使用了 `network_mode: host`
165+
- `-BindLocalhost` 只支持 `ports:` 服务
166+
167+
## Derper Service Design
168+
169+
`derper` 以普通 `ports:` 服务形式接入,而不是 `network_mode: host`。
170+
171+
建议的 compose 角色如下:
172+
173+
- 镜像:`fredliang/derper`
174+
- 重启策略:沿用 `${RESTART_POLICY:-unless-stopped}`
175+
- 环境变量:
176+
- `DERP_ADDR=:8443`
177+
- `DERP_STUN_PORT=3478`
178+
- `DERP_VERIFY_CLIENTS=false`
179+
- 端口:
180+
- `8443:8443`
181+
- `3478:3478/udp`
182+
- `profiles: ["derper"]`
183+
184+
之所以不采用 host 网络模式,是因为:
185+
186+
- 这样能无缝兼容本次新增的 localhost 绑定能力
187+
- 行为与仓库里大多数通用容器服务更一致
188+
- 对用户而言,是否对外开放由统一入口控制,而不是由单个服务另起一套网络模型
189+
190+
## Script Responsibilities
191+
192+
`start-container.ps1` 在本设计下新增以下职责:
193+
194+
1. 解析最终的 localhost 绑定偏好
195+
2. 在需要时生成并清理临时 override compose 文件
196+
3. 把 override 文件追加到所有相关 `docker compose` 调用中
197+
4. 在目标服务不支持 localhost 绑定时明确失败
198+
5. 让帮助信息、服务列表说明与实际能力保持一致
199+
200+
以下事情仍不应由脚本承担:
201+
202+
- 不去动态修改基础 compose 模板本身
203+
- 不让 `docker compose` 的原生命令与脚本自动共享额外能力
204+
- 不对 host 网络服务推断其内部监听地址
205+
206+
## Service Access Output
207+
208+
`Show-ServiceAccessInfo` 的显示需要与绑定行为一致。
209+
210+
调整原则如下:
211+
212+
- `127.0.0.1` 与 `::1` 都统一展示为 `localhost`
213+
- 当端口实际只绑定到 localhost 时,不再打印 `LAN` 地址
214+
- 只有在端口发布到 `0.0.0.0` / `::` 时,才继续打印局域网访问地址
215+
216+
这样用户看到的“Local / LAN”信息才能准确反映安全边界,而不会在 localhost 模式下误导性地暗示局域网可达。
217+
218+
## Error Handling
219+
220+
新增能力需要主动处理以下错误:
221+
222+
- `BIND_LOCALHOST` 不是合法布尔值
223+
- 目标服务不存在 `ports:`,但用户要求 localhost 绑定
224+
- 目标服务命中 `network_mode: host`
225+
- 目标服务的 `ports:` 使用了当前 override 生成器不支持的格式
226+
- 临时 override 文件生成失败
227+
228+
这些错误都应在执行 `docker compose` 前抛出,避免用户误以为容器已按预期启动。
229+
230+
## Testing Strategy
231+
232+
本次改动涉及 `scripts/pwsh/devops/start-container.ps1` 与 `tests/**/*.ps1`,因此实现阶段需要执行:
233+
234+
```powershell
235+
pnpm qa
236+
pnpm test:pwsh:all
237+
```
238+
239+
测试覆盖至少包含以下几类:
240+
241+
### Compose Static Tests
242+
243+
在 `tests/StartContainer.Tests.ps1` 中新增断言:
244+
245+
- `derper` 服务块存在
246+
- `derper` 端口同时包含 `8443:8443` 与 `3478:3478/udp`
247+
248+
### Configuration Resolution Tests
249+
250+
在 `tests/StartContainer.ConfigIsolation.Tests.ps1` 中新增断言:
251+
252+
- `.env.local` 中的 `BIND_LOCALHOST=true` 会被正确识别
253+
- CLI 显式关闭可以覆盖 `.env.local` 的开启状态
254+
- 非法布尔值会立即报错
255+
256+
### Localhost Override Tests
257+
258+
新增或扩展测试覆盖:
259+
260+
- 行内数组形式的 `ports` 会被改写为 `127.0.0.1:...`
261+
- 多行列表形式的 `ports` 会被改写为 `127.0.0.1:...`
262+
- UDP 端口后缀不会丢失
263+
- 关闭 localhost 绑定时不会生成 override
264+
- 命中 `network_mode: host` 时会明确拒绝执行
265+
266+
### Access Info Tests
267+
268+
如果对输出辅助函数增加可测试封装,应验证:
269+
270+
- localhost 绑定场景下仅显示 `Local: localhost:...`
271+
- 对外发布场景下仍显示 `LAN: ...`
272+
273+
## Risks And Trade-offs
274+
275+
### Text-Based Port Parsing
276+
277+
由于当前脚本与仓库没有现成的 YAML 结构化解析依赖,override 生成器大概率需要基于当前 compose 模板的受控文本格式工作。
278+
279+
这带来的权衡是:
280+
281+
- 优点:无需引入额外模块,能快速复用当前模板风格
282+
- 缺点:未来若 compose 写法变复杂,需要同步扩展解析器
283+
284+
因此设计上必须要求“不支持就失败”,而不是做静默兼容。
285+
286+
### Script-Only Capability
287+
288+
localhost 绑定能力只在 `start-container.ps1` 入口里生效,意味着:
289+
290+
- 优点:默认行为稳定,改动范围集中
291+
- 缺点:用户如果绕过脚本直接运行原生命令,不会自动获得该能力
292+
293+
这是一个有意保留的边界,因为本次目标是增强仓库统一入口,而不是重写所有原生命令使用方式。
294+
295+
## Documentation Changes
296+
297+
实现时需要同步更新以下文档或帮助内容:
298+
299+
- `scripts/pwsh/devops/start-container.ps1` 顶部帮助注释
300+
- 服务列表说明,补充 `derper`
301+
- `-BindLocalhost` 参数说明与示例
302+
- `config/dockerfiles/compose/.env.local` 中 `BIND_LOCALHOST` 的用法说明
303+
304+
如需补仓库文档,优先在现有 Docker localhost 速查或容器启动相关说明中补一段简短示例,而不是新增长篇独立教程。
305+
306+
## Validation Plan
307+
308+
实现完成后至少验证以下路径:
309+
310+
1. `./scripts/pwsh/devops/start-container.ps1 -ServiceName derper -DryRun` 能输出包含 `derper` profile 的 compose 命令
311+
2. `./scripts/pwsh/devops/start-container.ps1 -ServiceName postgre -BindLocalhost -DryRun` 会叠加临时 override,并体现 localhost 绑定语义
312+
3. `config/dockerfiles/compose/.env.local` 中写 `BIND_LOCALHOST=true` 后,不传 CLI 参数也会默认启用 localhost 绑定
313+
4. UDP 端口服务在 localhost 模式下仍保留正确协议
314+
5. 命中 `network_mode: host` 的目标服务在 localhost 模式下会被拒绝
315+
316+
## Deferred Work
317+
318+
如后续有需要,可在独立变更中继续考虑:
319+
320+
- 为 localhost 绑定补 `-BindAddress` 之类的更通用能力,支持指定内网 IP、Tailscale IP 等
321+
- 为原生命令提供辅助脚本,降低“脚本入口”和“直接 compose”之间的体验差异
322+
- 在不引入过重依赖的前提下,把受控文本解析升级为更稳健的 YAML 结构化处理

0 commit comments

Comments
 (0)