容器里起不来 stdio:MCP 在云主机上的进程边界与出网路径
笔记本上装一个 MCP server,起不来的时候人容易往 server 本身想:是不是包坏了、版本不对、作者半年没维护。把这批 server 原样搬到云主机上,同一个失败会换一副面孔——报错落在 registry 的域名解析上,落在 $HOME 的写权限上,落在容器的退出码上。毛病没变,但它在云上的暴露面比在本机大得多。
五份分宿主文件里,14 个远程 http server 在五个宿主上的通过格数是 11/14、10/14、10/14、10/14、11/14;同一批宿主,本地 stdio 一栏是 98/147、97/147、72/96、29/147、21/96。前者挤在 71.4% 到 78.6% 的窄带里,后者从 75.0% 一路滑到 19.7%,差 3.8 倍。这个落差在笔记本上只是「有个宿主不好使」,上了云会变成一整套部署问题。
先把它当两类工作负载,不是一类软件
远程 http server 是「一个已经在跑的端点,宿主只当客户端」。为它准备的东西是一条能出去的连接,剩下的都在对面。
本地 stdio server 是「一段要在这台机器上被拉起来的代码」。为它准备的是四件套:一条能出去拉包的连接、一个能落缓存的目录、一个能活到握手完成的进程、一个会在它死掉时知道它死了的父进程。两份清单的长度差,就是通过率的差。
出网:一条 443 到端点,和一条 443 到 registry
http 的出网路径是一跳。安全组出方向放通 443,机器有 NAT 网关或自带公网 IP,请求发出去,状态码回来,握手建立。出问题时看目标端口和返回码就够。
stdio 的出网路径至少两跳。宿主按配置里的 command 去执行 npx -y <包名>,这条命令先解析 registry 域名、取 tarball、落到本地缓存,再执行入口脚本;如果这个 server 自己还要访问外部 API,那是第三跳。
第一跳不通的表现,和「server 装不上」一模一样:宿主那一格只记 install 失败。而云主机上最常见的第一跳故障恰恰不在应用层——安全组只放了入方向、实例没有公网出口、NAT 网关没绑、私有子网的路由表指向一个不存在的下一跳。
这里必须说清一条边界:分宿主文件只记「失败发生在哪个阶段」,checks.note 是通用文案,不含具体报错。所以从数据里你能确定 install 阶段挂了 23、47、46、74、116 格,但不能读出哪一格是 registry 不通。云上排查只能自己按环节走:解析一次 registry 域名、手动 npx 拉一次、看缓存目录里有没有东西。
常驻:PID 1、信号,和三秒半的启动预算
容器里 PID 1 是宿主进程,npx 拉起的 server 是它的子进程。这一层在云上有两个具体后果。
一是信号。宿主收到 SIGTERM 后如果不转发给子进程、也不回收,子进程会变成孤儿继续占着管道;滚动发布时旧容器卡在 terminating,日志里 server 比宿主活得久,新容器已经在接流量,旧的那份还在跑。
二是冷启动预算。claude-code 文件里的元数据:启动中位 3,532 ms,最小 224 ms,最大 48,750 ms,超过 5,000 ms 的有 35 个。本机上多等三秒没人在意;在云上它就是探针的 initialDelay、是就绪超时、是失败重启,而重启会把冷启动从头再来一遍。
npx -y 还有个隐藏成本。容器重建后,如果 $HOME 不在挂载卷上,缓存整个丢掉,第一次启动必然走一遍完整拉包。3.5 秒那个中位数是在有缓存的前提下成立的,空缓存上不成立——所以「本地能起、新副本起不来」总在扩容那一刻出现。
权限收敛:非 root、只读根,卡住的往往是一个目录
安全基线一般是三条:以非 root 运行、根文件系统只读、只挂一个可写的临时目录或数据盘。这三条对 http server 基本无感,它不落盘。对 stdio server 是硬约束,而且是叠加的。
$HOME 不可写,npx 的用户级缓存无处落;只读根文件系统,有些包安装时要往自身目录写,直接 EROFS;非 root,全局 node_modules 不可写,只能退回用户级缓存,又撞回第一条;PATH 里没有 npx 所在目录(非登录 shell 不读 profile),报 command not found。
四条里的任何一条,最后都落在同一格:install=fail。而 install 失败在五个宿主上是 23、47、46、74、116 格。同一个 server 在这台机器过、在那台不过,差别经常就是这一个目录的权限位——而不是 server 的代码。
真要在只读根的容器里跑,路径只有一条:把缓存目录用 NPM_CONFIG_CACHE 指到一个挂载点上,并保证它下次启动还在。这不是绕过基线,是把基线翻译成 stdio 需要的形态。
一张按负载分开的规划表
| 维度 | 远程 http | 本地 stdio |
|---|---|---|
| 出网 | 出方向 443 到端点 | 出方向 443 到 registry,必要时再到 server 依赖的外部 API |
| 基础镜像 | 任意,能跑宿主即可 | 需带 node 与 npx,版本与宿主匹配 |
| 文件系统 | 只读根即可 | 需要一个可写缓存目录,且能跨重建保留 |
| 进程模型 | 无本地子进程 | 需 PID 1 转发信号、回收子进程 |
| 就绪判定 | 端点返回 200、会话建立 | initialize 握手成功、tools/list 非空 |
| 超时预算 | 秒级 | 数十秒级(观测到最大 48,750 ms) |
| 扩副本 | 无状态,直接加 | 每副本一份缓存,冷启动被放大 |
用法很直接:同一个集群里,两类 server 不该共用一套安全组和一份探针配置。http 那一栏按无状态服务排,副本随便加;stdio 那一栏按「带本地缓存的短生命周期进程」排。一套配置打天下的结果,就是 703 个格子里 stdio 那一半集体掉队,远程那一半稳在 10 到 11 格。
总结
stdio 在云上的失败面不在协议层,在出网、可写目录、进程存活这三处;把远程 http 与本地 stdio 当两类负载分别规划,比给 server 换版本有用得多。想按宿主逐格看这段落差,MCP 宿主兼容性矩阵 里能直接翻到每个宿主的 stdio 与 http 两栏;要中文条目对照着配,看 DeepSeek Harness Hub 插件清单。
适合与不适合
适合:正在把 MCP server 从笔记本迁到 ECS 或自建容器平台的人;要给两类 server 写不同安全组与探针配置的运维;排查「本机能跑、云上 install 失败」的人;需要在发布流程里给 stdio 进程留启动宽限的人。
不适合:想让文章直接告出某个 server 在云上报什么错的人,采集只记阶段,checks.note 是通用文案;想拿通过率给五个宿主排优劣的人,分母 110 与 161 不同、五份文件也不同时生成;不肯改容器基线、只想找一条把 stdio 塞进只读根镜像的偏方的人,这篇给的是配置,不是绕过。
标签:MCP、DeepSeek Harness、云上部署、stdio
本文由 DeepSeek Harness Hub 自动整理,数据来源于插件详情页。