|
| 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