DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

Docker 25 升级后 /dev/shm 变为只读

整个 /dev 的只读绑定在支持递归只读的环境会让子挂载也只读。核对 API 与内核,优先收窄设备映射,保留共享内存隔离。

Linux 宿主机升级到 Docker Engine 25 后,原先能运行的 Python 多进程程序开始在 /dev/shm 报 Read-only file system。创建参数里若有 -v /dev:/dev:ro,先检查这个整目录绑定:它会把宿主机 /dev 下的子挂载带进容器,而不是保留通常的容器私有 /dev/shm。

Docker Engine 25.0.0 在 Linux 5.12 及以上内核引入默认递归只读行为。原来父挂载只读、子挂载仍可写的组合,可能在升级后变成子挂载也只读。这里限定普通 Linux Docker Engine 的绑定挂载;不是所有共享内存错误都由这项变化引起。

先确认 /dev/shm 的来源和挂载标志

在连接正确 Engine 的终端记录服务端和客户端版本,并检查实际容器:

docker version
docker inspect --type container --format '{{json .Mounts}}' worker-app
docker inspect --type container --format '{{json .HostConfig.Binds}}' worker-app

worker-app 是容器名占位。查看是否把宿主机整个 /dev 绑定到容器 /dev,以及是否设置了只读。声明在配置文件里但没有应用到当前容器的参数,不能解释当前挂载状态。

容器提供 cat 时,可以只读查看其挂载信息:

docker exec worker-app cat /proc/self/mountinfo

找到 /dev 和 /dev/shm 的挂载记录,核对挂载选项中的 ro 或 rw。同时查看 daemon 所在 Linux 主机的内核版本;远程 Docker 的内核不是运行 CLI 的笔记本内核。只有父目录的 inspect 记录,不足以证明每个子挂载实际可写。

还要保留客户端 API 版本条件:25.0.4 为使用旧 API(低于 1.44)的客户端恢复了只读挂载默认非递归的兼容行为。因此不能仅根据“服务端是 25”预测所有创建入口的结果,应以实际挂载和协商版本为准。

增大 shm-size 不会替换这个宿主机挂载

用户显式挂载整个 /dev 时,Docker 会跳过其默认 /dev 子挂载,包括通常由容器获得的 /dev/shm。此时继承的是宿主机目录下的挂载,增加 --shm-size 不会把它自动变回独立的容器共享内存。

同样,Read-only file system 与共享内存空间用尽不同。更大的容量不能改变只读标志,chmod 也不能让一个只读挂载变成可写。不要因为 Python 报错出现在共享内存相关代码,就直接扩大所有容器的 shm 大小。

优先收窄整个 /dev 的暴露范围

如果整目录绑定只是为了访问一两个设备,先确认能否去掉 /dev:/dev:ro,通过明确的设备映射和应用配置访问所需设备,让 Docker 保留正常的容器设备与共享内存布局。

例如,应用确实只需要一个已确认的串口节点时,可以在原部署参数中保留该设备的定向映射:

--device=/dev/ttyUSB0:/dev/ttyUSB0:rw

这只是局部参数示例,不是完整启动命令。设备路径应换成已核实的节点,并保留业务用户、必要的组权限及其他部署参数。宿主机 udev 创建的设备别名、热插拔或动态设备发现可能需要另外设计;不能保证删除整目录挂载后原来的别名仍在。

不要把这一步替换为 --privileged 或把整个 /dev 改成读写。设备可能控制真实硬件,验证时只做业务允许的操作,不随意发送测试数据。

确实需要旧的子挂载策略时,显式表达

如果经过评估确实需要保留父挂载只读、子挂载可写,可以在支持该选项的 Docker 25 及以上创建入口,用长格式替换原来的短格式挂载:

--mount type=bind,src=/dev,dst=/dev,readonly,bind-recursive=writable

bind-recursive 只由 --mount 支持,不能直接拼到 -v /dev:/dev:ro 后面。这个选项会让包含的子挂载可写,不只针对 /dev/shm;它放宽了原有递归只读保护,还可能暴露宿主机其他子挂载的写入能力。只有已经核对全部子挂载、共享内存隔离及安全边界时,才采用这个兼容分支。

更合适的长期配置通常是缩小挂载范围。若实际要求所有子挂载严格只读,则应保留这一要求,并把程序需要写入的资源放到另行设计的容器私有位置;不能为了消除报错默认降低挂载保护。

bind-recursive=disabled 表示不包含子挂载,不是“让子挂载恢复可写”的同义选项。挂载传播的 rslave、rprivate 也负责不同的事情,不能代替读写策略。

重建目标容器后核对两种访问

保存原创建配置、镜像和需要保留的容器数据,再安排允许中断的窗口重建目标容器。单纯重启应用或执行 docker restart 不会把新的创建参数应用进去;不需要提交整个容器为新镜像,更不需要删除数据卷。

重建后再次核对 /dev/shm 的实际来源和读写标志,然后验证原先失败的多进程操作,以及仍需使用的设备访问。程序能启动不代表设备权限正确;共享内存能写,也不代表没有额外暴露宿主机资源。

回退时恢复保存的挂载配置并按部署流程重建。旧的子挂载可写行为本身并不是安全保证;若回退只是暂时恢复兼容,应保留这项例外的范围,不把它扩展到其他只读挂载。