Github开源生信云平台 DEMO
Searched for regex ExposedPort\s*[:=]|ExposedPort[^a-zA-Z] (**/gobrave/internal/**/*.go), 9 results
ExposedPort\s*[:=]|ExposedPort[^a-zA-Z]
**/gobrave/internal/**/*.go
Read , lines 455 to 510
Searched for regex Port\s+int|PortHost|portConflict|IsPortAvailable|net.Listen (**/gobrave/internal/manager/*.go), 3 results
Port\s+int|PortHost|portConflict|IsPortAvailable|net.Listen
**/gobrave/internal/manager/*.go
Searched for regex Port\b (**/gobrave/internal/types/container.go), 14 results
Port\b
**/gobrave/internal/types/container.go
Searched for regex BackendPort|GatewayRoute\{ (**/gobrave/internal/**/*.go), 12 results
BackendPort|GatewayRoute\{
Read , lines 20 to 60
Searched for regex func |127.0.0.1|localhost|Target|Host (**/gobrave/internal/route/gateway.go), 23 results
func |127.0.0.1|localhost|Target|Host
**/gobrave/internal/route/gateway.go
Searched for regex Host: *\w+\.?IPAddress|Backend: *\w*[Rr]oute.*Host|ExposeService = true (**/gobrave/internal/**/*.go), no results
Host: *\w+\.?IPAddress|Backend: *\w*[Rr]oute.*Host|ExposeService = true
Searched for regex ExposeService|UpsertRoute\( (**/gobrave/internal/**/*.go), 19 results
ExposeService|UpsertRoute\(
Read , lines 505 to 545
Searched for regex Inspect\(ctx|IPAddress|Registration\{|route\.Backend|gateway\.Upsert (**/gobrave/internal/manager/*.go), 10 results
Inspect\(ctx|IPAddress|Registration\{|route\.Backend|gateway\.Upsert
Read , lines 1300 to 1350
Searched for regex Backend.Host|Host: *[a-z]|\.IPAddress (**/gobrave/internal/route/*.go), 25 results
Backend.Host|Host: *[a-z]|\.IPAddress
**/gobrave/internal/route/*.go
Searched for regex route\.Backend\.Host|Backend: *route\.\|backendHost (**/gobrave/internal/**/*.go), 8 results
route\.Backend\.Host|Backend: *route\.\|backendHost
Read , lines 40 to 100
Searched for regex type (Backend|Registration) struct (**/gobrave/internal/route/registry.go), 2 results
type (Backend|Registration) struct
**/gobrave/internal/route/registry.go
Searched for regex Host: *[^ ,]*(127\.0\.0\.1|localhost) (**/gobrave/internal/**/*.go), 2 results
Host: *[^ ,]*(127\.0\.0\.1|localhost)
Read , lines 1 to 30
Searched for regex ProxyConfig|ContainerProxy|backup|BackupFirst (**/gobrave/internal/**/*.go), 32 results
ProxyConfig|ContainerProxy|backup|BackupFirst
Read , lines 20 to 75
Searched for regex Container|HostPort|Exposed|Port (**/gobrave/internal/container_runtime/provision/**), 40 results
Container|HostPort|Exposed|Port
**/gobrave/internal/container_runtime/provision/**
Read
Searched for regex ResolveByPath|PathRouteResolver|http.RoundTripper|DialContext (**/gobrave/internal/container_runtime/**/*.go), no results
ResolveByPath|PathRouteResolver|http.RoundTripper|DialContext
**/gobrave/internal/container_runtime/**/*.go
Searched for regex ResolveByPath (**/gobrave/internal/**/*.go), 5 results
ResolveByPath
Read , lines 300 to 412
Read , lines 92 to 175
Searched for regex container:|proxy: (**/gobrave/config*.yml), 15 results
container:|proxy:
**/gobrave/config*.yml
Read , lines 70 to 80
Read , lines 1 to 44
Read , lines 110 to 175
Searched for regex func \(t \*ContainerTemplate\) ToExport|func \(t \*ContainerTemplate\) ToSpec|Port: (**/gobrave/internal/types/container.go), 2 results
func \(t \*ContainerTemplate\) ToExport|func \(t \*ContainerTemplate\) ToSpec|Port:
Read , lines 230 to 300
是的,会冲突。现在的写法有必然冲突和潜在冲突两类问题。
toPortBindings 给的是固定宿主机端口:
toPortBindings
HostPort: strconv.Itoa(port) // 8787 → 8787
而 toHostConfig 的注释写的是"只需发布到宿主机",但代码实际上每次都把 spec.ExposedPort 发布到宿主机同一端口。由于 ContainerTemplateSpec.Port 的默认值就是 8787(gorm:"not null;default:8787"),只要用户开第二个 RStudio 会话,dockerd 就会报:
toHostConfig
spec.ExposedPort
ContainerTemplateSpec.Port
8787
gorm:"not null;default:8787"
dockerd
Bind for 0.0.0.0:8787 failed: port is already allocated
这不是猜测——ContainerCreate 失败后代码会尝试 pull 再 create 一次,失败两次才返回错误,用户看到的是 create container after pull: ... port is already allocated,非常难排查。
ContainerCreate
create container after pull: ... port is already allocated
这是我上一轮改动破坏了原有设计的地方。原来的路径是:
浏览器 → AppSessionProxy → http://<容器IP>:8787 ← 用容器 IP + 容器端口
Inspect()
172.17.0.x
inst.IPAddress
route/handler.go:79
Host: inst.IPAddress, Port: tpl.Port
AppSessionProxy
http://172.17.0.x:8787
容器 IP 的 8787 是容器内部的端口,不需要 -p 发布就能访问。也就是说:在 Linux 上、或 daemon 与 gobrave 同主机时,本来就完全不需要 PortBindings——gobrave 自己就是"用户",它走 docker 网络直连。
-p
所以 toPortBindings 只有在一个场景下才有意义:gobrave 进程无法路由到容器 IP(Windows + WSL2 NAT 就是这种)。而它一旦加上,就变成了"宿主机固定端口",既解决不了多实例问题,还引入了端口冲突。
(a) ExposedPort 不区分形态:buildInstanceSpec 对所有形态都填了 ExposedPort: tpl.Port,虽然 ExposeService 只在 web 形态为 true,但 toHostConfig 只看 spec.ExposedPort,所以 terminal 容器(keep-alive)也会被发布 8787 —— 一个模板的终端会话就会占住 8787。
ExposedPort
buildInstanceSpec
ExposedPort: tpl.Port
ExposeService
(b) host.docker.internal 场景反而通不了:docker-desktop(Windows)+ npipe 模式下容器 IP 可直连,加 -p 纯属多余且会冲突。
host.docker.internal
docker-desktop
(c) RStudio 本身的问题:RStudio Server 对反向代理有额外要求(/rstudio/ 前缀、WebSocket),这跟端口发布无关,是另一码事。
/rstudio/
我倾向把端口发布收窄成显式开关,并且让端口可协商,而不是默认对 spec.ExposedPort 无条件发布。具体三种方案:
spec.PublishPort bool
HostPort: ""
HostPort: "0"
networkingMode=mirrored
127.0.0.1:8787
如果选 A 或 B,还要配套改 route/handler.go:Windows+WSL 时 Backend.Host 用 127.0.0.1(或 distroIP)、Backend.Port 用读回的实际宿主机端口。
route/handler.go
Backend.Host
127.0.0.1
distroIP
Backend.Port
要我先按方案 B 落地(改动最小、可立即验证),还是直接上 A(语义最清晰)?另外确认一下:你的部署里 gobrave 只跑在 Windows + WSL2,还是也需要兼顾 Linux/Docker Desktop?