在生产环境中,将 Docker 容器的根文件系统设置为只读,可以减少配置被意外修改、恶意文件落盘以及运行时污染等风险。不过,许多应用启动时需要创建 PID 文件、缓存、临时文件或日志,一旦没有提前规划可写目录,就可能出现“Read-only file system”“Permission denied”等错误。本文从配置方法到故障定位,整理一套可直接执行的排查流程。🔍
一、只读文件系统的作用
Docker 镜像层本身是只读的,容器启动后通常会在其上增加一个可写层。启用只读模式后,应用不能再修改容器根文件系统,但挂载的 volume、bind mount 和可写 tmpfs 通常仍可写入。这样既能约束应用的写入范围,也便于发现依赖本地文件的隐性行为。
需要注意,只读根文件系统不是独立的安全解决方案。它应与非 root 用户、最小权限、镜像漏洞扫描、敏感信息管理和网络隔离配合使用。只读模式解决的是“写到哪里”的问题,并不能自动阻止所有攻击。
二、Docker 中如何启用只读模式
1. 使用 docker run
启动容器时加入 --read-only 参数:
docker run -d --name myapp --read-only myimage:latest
如果应用需要向临时目录写入,可增加 tmpfs:
docker run -d --name myapp --read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m myimage:latest
tmpfs 适合缓存、套接字和短期运行数据,容器停止后内容不会保留。Docker 官方说明指出,tmpfs 数据主要保存在宿主机内存中,但在特定系统配置下可能进入交换空间,因此不应把它简单理解为绝对不会触及磁盘。更多限制和参数可参考 Docker tmpfs 官方文档。
2. 使用 Docker Compose
在服务配置中设置 read_only: true,并声明必要的临时目录或持久化目录。配置思路如下:
services:
app:
image: myimage:latest
read_only: true
tmpfs:
- /tmp
- /run
volumes:
- app-data:/var/lib/myapp
其中,临时数据放入 tmpfs,需要跨容器重建保留的数据放入命名卷。不要为了让应用启动成功,直接把整个根目录重新设为可写,否则会抵消只读配置的意义。
三、应用为什么会写入失败
- 临时目录不可写:应用或运行时尝试在 /tmp、/var/tmp 中创建临时文件。
- 运行文件无处存放:进程需要向 /run、/var/run 写入 PID、锁文件或 Unix Socket。
- 日志仍写本地文件:日志配置指向 /var/log 或应用安装目录,而不是标准输出。
- 缓存目录未挂载:Web 服务、包管理器或框架需要写入缓存与编译产物。
- 启动脚本修改配置:入口脚本使用 sed、envsubst 等工具替换镜像内的配置文件。
- 非 root 用户权限不足:目录虽已通过 volume 或 tmpfs 挂载,但属主、组或权限与容器用户不匹配。
- 挂载本身是只读的:bind mount 或 volume 被显式指定为 ro,应用自然无法写入。
四、推荐的故障排查顺序
- 查看容器日志:执行 docker logs 容器名,记录报错中的完整路径,不要只关注“Permission denied”或“Read-only file system”字样。
- 检查只读状态:执行 docker inspect 容器名,重点查看 HostConfig.ReadonlyRootfs、Mounts 和 Tmpfs 配置,确认实际运行参数与预期一致。
- 检查挂载信息:进入容器后执行 mount 或 cat /proc/mounts,判断目标路径属于只读根文件系统、tmpfs、命名卷还是绑定挂载。
- 验证运行身份:执行 id,并使用 ls -ld 目标目录检查 UID、GID 和权限。目录可写不代表当前用户有写权限。
- 进行最小写入测试:在报错目录执行 touch 测试文件。如果返回只读错误,应补充可写挂载;如果返回权限错误,应调整属主或运行用户。
- 临时关闭只读模式对比:仅在隔离测试环境中移除 --read-only。若应用立即恢复,说明仍有未识别的写入路径,但这不是最终修复方案。
五、针对不同数据选择修复方案
临时数据:优先使用 tmpfs,并通过 size 参数限制容量,避免异常写入大量占用内存。对于不需要执行程序的目录,可以结合 noexec、nosuid 等选项缩小风险面。⚙️
持久化数据:数据库文件、上传内容、索引和业务状态应使用命名卷或绑定挂载。部署前需确认宿主机目录存在,并确保容器运行用户对其拥有所需权限。
日志数据:容器化应用优先把日志写到标准输出和标准错误,再由 Docker 日志驱动或日志采集系统处理。必须写文件时,应仅为指定日志目录挂载独立存储,并配置轮转与容量限制。
动态配置:如果入口脚本必须生成配置,可将模板保留在只读镜像中,把生成结果写入专用 tmpfs 或 volume,再让应用读取生成后的文件。更理想的方式是让应用直接从环境变量或外部配置源读取参数。
六、常见误区与改进建议
- 不要看到写入失败就使用 chmod 777;只读挂载无法通过 chmod 变成可写,而且过度授权会扩大风险。
- 不要把所有目录都挂载为可写,应先通过日志、审计或测试确定最小写入路径。
- 不要忽略容器用户的 UID 和 GID;宿主机目录的用户名可能不同,但内核实际依据数字 ID 判断权限。
- 不要把 tmpfs 用作数据库存储;容器停止或重建后,临时数据会消失。
- 不要在生产环境直接调试和修改容器;应在测试环境复现问题,并把修复固化到镜像或部署配置中。
总结
Docker 只读根文件系统的核心原则,是让镜像内容保持不可变,只向经过明确授权的目录开放写入。排查应用写入失败时,应先从日志提取目标路径,再依次检查只读状态、挂载类型、运行用户和目录权限,最后根据数据生命周期选择 tmpfs、volume、bind mount 或标准输出。✅ 通过“默认只读、按需开放、最小授权”的方式,既能保证应用正常运行,也能让容器部署更加稳定、清晰和安全。