问题背景

Docker Compose 里经常会把单个配置文件 bind mount 到容器中:

1
2
3
4
5
services:
web:
image: my-app
volumes:
- ./config.json:/app/config.json

这样写很方便:宿主机改配置,容器里也能读到新内容,不需要重新构建镜像。但它有个隐蔽问题:某些编辑方式会让宿主机文件和容器内文件断开同步

现象

容器启动后,最开始一切正常。后来你用 vim、VSCode 或 sed -i 修改 config.json,可能会发现:

  • 宿主机上的 config.json 已经更新
  • 容器里的 /app/config.json 仍然是旧内容
  • 容器继续使用旧配置
  • 两边对这个文件的修改不再互相可见

这个问题看起来像同步失效,但本质上是挂载点还指向旧文件。

原因

单文件 bind mount 的关键点是:Docker 挂载的是当时那个文件对象,而不是永远跟随这个文件名。

1
./config.json  inode A  ->  /app/config.json

很多编辑器保存文件时,并不是直接在原文件上修改,而是走类似这样的流程:

1
2
写入 config.json.tmp
rename(config.json.tmp, config.json)

保存完成后,宿主机路径 config.json 指向了新的 inode:

1
2
宿主机: ./config.json     -> inode B
容器内: /app/config.json -> inode A

容器仍然挂着旧文件对象,于是宿主机和容器里的文件就“分离”了。

为什么隐蔽

它不是每次修改都会触发,而是取决于工具的保存方式:

操作 是否可能更换 inode
vim 保存 可能
VSCode 保存 可能
sed -i 可能
cp 覆盖 可能
echo >> file 通常不会

所以它经常表现成“之前都正常,某次修改后突然不生效”。

推荐做法

不要挂载单个文件:

1
2
volumes:
- ./config.json:/app/config.json

推荐把配置放进目录,挂载整个目录:

1
2
volumes:
- ./data:/app/data

项目结构可以这样放:

1
2
3
4
5
project/
docker-compose.yml
data/
config.json
logs/

程序读取:

1
/app/data/config.json

这样即使 data/config.json 被编辑器替换成新 inode,容器看到的仍然是同一个挂载目录下的新文件。

Compose 示例

1
2
3
4
5
6
services:
web:
image: my-app
volumes:
- ./data:/app/data
- ./logs:/app/logs

配置、日志、运行数据都按目录挂载,后续新增文件也不需要再改 compose。

总结

单文件 bind mount 的坑在于:文件名没变,不代表底层文件对象没变。编辑器或命令一旦用 rename 覆盖原文件,容器可能还挂着旧 inode,导致宿主机和容器内的文件不再同步。

实际项目里,优先挂载目录:

1
2
不推荐: ./config.json:/app/config.json
推荐: ./data:/app/data

单文件挂载只适合非常明确、不会被运行时频繁编辑替换的场景。