在生产环境中,容器默认拥有可写层,应用进程可以修改配置、写入脚本或生成临时文件。一旦程序存在漏洞或配置错误,可写根文件系统可能被用于植入文件、篡改运行内容,甚至留下持久化痕迹。🔒 将 Docker 容器的根文件系统设为只读,再为确有需要的目录单独提供临时写入空间,是一种简单、实用的最小权限方案。
一、只读文件系统解决什么问题
Docker 的只读模式会限制容器对根文件系统的写操作,但不会影响外部挂载的卷、绑定挂载或 tmpfs。这样可以把“允许写入”的范围从整个容器收缩到几个明确目录,既减少误写配置的风险,也更容易发现应用不合理的文件依赖。
需要注意,只读根文件系统并不等于容器绝对安全。它无法替代非 root 用户、能力限制、镜像漏洞扫描和网络隔离,但可以作为纵深防御中的重要一层。🛡️
二、先用最小命令验证
启动一个只读容器,可以使用 --read-only 参数:
docker run --rm --read-only alpine sh -c "touch /test.txt"
运行后会看到“Read-only file system”之类的错误,这说明根文件系统已经不能写入。如果应用只是读取配置并提供无状态服务,这种模式可能直接可用;但多数程序还需要使用 /tmp、/run、缓存目录或 PID 文件目录。
三、使用 tmpfs 提供临时写入空间
tmpfs 适合保存无需持久化的临时文件。它独立于容器可写层,容器停止后其中的数据会消失。Docker 官方说明,tmpfs 主要驻留在主机内存中,但在主机启用交换空间时,相关页面仍可能被换出,因此不应简单地把它理解为“敏感数据永远不会落盘”。具体机制可参考 Docker tmpfs 官方文档。
下面为只读容器开放 /tmp,并限制容量与权限:
docker run --rm --read-only --tmpfs /tmp:rw,nosuid,nodev,noexec,size=64m,mode=1777 alpine sh -c "echo ok > /tmp/result && cat /tmp/result"
- rw:允许在该挂载点写入。
- nosuid:不执行 setuid、setgid 权限位。
- nodev:不解释设备文件。
- noexec:禁止直接执行该目录中的程序。
- size=64m:限制临时文件系统的最大容量。
- mode=1777:提供类似常见 /tmp 的权限规则。
容量限制非常重要。tmpfs 会消耗主机内存资源,如果应用持续写入且没有边界,可能给主机带来内存压力。📦 应根据真实业务峰值设置上限,并配合内存监控,而不是无限制开放。
四、Docker Compose 实战配置
在 Compose 中,可通过 read_only 和 tmpfs 组合实现相同效果:
services:
app:
image: my-app:latest
read_only: true
tmpfs:
- /tmp:size=64m,mode=1777
- /run:size=16m,mode=755
volumes:
- ./config:/app/config:ro
这里的根文件系统保持只读,/tmp 和 /run 用于短期运行数据,配置目录则通过只读绑定挂载提供。若应用产生必须保留的数据,例如上传文件、数据库内容或审计记录,应使用命名卷或受控的持久化存储,不能放入 tmpfs。
临时数据与持久化数据要分开
- 临时缓存、套接字、PID 文件:优先使用 tmpfs。
- 业务数据、用户上传内容:使用命名卷或外部存储。
- 配置文件、证书和静态资源:采用只读挂载。
- 应用日志:优先输出到标准输出和标准错误,由日志系统采集。
五、排查应用到底要写哪些目录
不要一开始就给 /var、/app 等大目录整体开放写权限。更稳妥的方式是先在测试环境开启只读模式,启动应用并观察报错,再逐个识别必需的写入路径。常见错误包括无法创建缓存、PID 文件、Unix Socket、临时编译文件以及锁文件。
- 先执行应用的启动流程和健康检查。
- 覆盖上传、导出、定时任务等功能路径。
- 记录每一个只读文件系统错误对应的目录。
- 判断数据是否需要跨容器重启保留。
- 仅为必要目录配置 tmpfs 或持久化卷。
还可以查看容器的挂载和只读配置:
docker inspect 容器名
重点检查 ReadonlyRootfs 是否为 true,并确认目标目录对应的是 tmpfs、volume 还是 bind mount。✅
六、非 root 用户的权限陷阱
如果容器以非 root 用户运行,tmpfs 的默认所有者和权限可能不符合应用要求,最终仍会出现“Permission denied”。此时应根据镜像内用户的 UID、GID 设置挂载权限,或选择合适的 mode。不要为了快速通过测试直接使用 0777,更不要因此把应用改回 root 用户。
另外,tmpfs 挂载会遮挡目标目录中原有的内容。如果镜像已经在 /run/app 中预置文件,再把 tmpfs 挂载到同一路径,这些文件在容器运行期间将不可见。部署前应检查目录内容,必要时调整挂载点或在启动阶段初始化所需文件。
七、推荐的加固组合
只读文件系统最好与其他限制同时使用:
docker run --rm --read-only --user 10001:10001 --cap-drop ALL --security-opt no-new-privileges:true --tmpfs /tmp:rw,nosuid,nodev,noexec,size=64m,mode=1777 my-app:latest
这组参数分别限制根文件系统写入、避免 root 身份、移除 Linux capabilities、阻止进程获取新权限,并只开放受控的临时目录。正式上线前仍需完成兼容性测试,尤其要验证健康检查、优雅退出、证书加载和故障诊断流程。
总结
Docker 只读根文件系统的核心思路不是“禁止一切写入”,而是把写权限精确缩小到业务真正需要的位置。🚀 临时数据使用有容量限制的 tmpfs,持久化数据使用卷,配置和静态文件采用只读挂载,再配合非 root 用户与权限收敛,就能在不明显增加运维复杂度的前提下,提高容器运行环境的可控性与抗篡改能力。