问题背景:Docker 挂载单个文件

在使用 Docker 时,我们经常会把配置文件通过 bind mount 挂载到容器中。例如:

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

这样做的目的通常是:

  • 让容器读取宿主机的配置
  • 修改配置后无需重新构建镜像
  • 配置文件可以持久化

看起来一切正常,但实际上这种写法隐藏着一个非常常见、却不容易被发现的问题。

本文介绍这个问题产生的原因,以及为什么在大多数情况下 不应该挂载单个文件,而应该挂载目录


现象:配置文件突然不同步

假设你有如下结构:

1
2
3
4
project/
├ docker-compose.yml
├ config.json
└ logs/

docker-compose.yml

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

容器运行后:

  • 容器读取 /app/config.json
  • 宿主机修改 config.json
  • 配置同步到容器

一切正常。

但是某一天你使用 vim 编辑配置:

1
2
vim config.json
:w

突然发现:

  • 容器中的配置 没有变化
  • 容器继续使用旧配置
  • 宿主机 config.json 已经是新内容

更奇怪的是:

  • 容器如果修改 /app/config.json
  • 宿主机文件 也不会变化

两边似乎“分离”了。


根本原因:inode 被替换

问题的核心在于 Linux 文件系统的 inode 机制

Docker 的 bind mount 在挂载文件时:

1
宿主机 inode A  →  容器 /app/config.json

Docker 绑定的是 inode,而不是文件名。

而许多编辑器(例如:

  • vim
  • neovim
  • VSCode
  • sed -i

在保存文件时,并不是直接修改原文件,而是执行 原子写入

  1. 写入一个临时文件
1
config.json.tmp
  1. 删除原文件
1
rm config.json
  1. 将临时文件重命名
1
mv config.json.tmp config.json

在 Linux 中,rename 会创建 新的 inode

于是就发生了:

1
2
3
4
5
原来:
config.json → inode A

保存后:
config.json → inode B

但 Docker 仍然挂载的是 inode A

结果就变成:

1
2
3
4
5
宿主机:
config.json → inode B

容器:
/app/config.json → inode A

于是两边彻底分离。


为什么这个问题很隐蔽

这个问题往往只在某些操作下触发,例如:

操作 是否更换 inode
vim 保存
VSCode 保存
sed -i
cp 覆盖
echo >> 文件 不会

因此有时:

  • 改配置正常
  • 突然某次修改后不同步

问题就非常难排查。


Docker 官方的建议

Docker 文档其实更推荐:

挂载目录,而不是单个文件

原因正是避免 inode 替换带来的问题。


正确的做法:挂载目录

不要这样写:

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

此时即使编辑器替换 inode:

1
data/config.json

目录本身仍然是同一个挂载点,Docker 能正确看到新文件。


额外的好处

挂载目录还有很多优点:

1 未来新增配置无需修改 compose

例如新增:

1
2
3
4
data/
├ config.json
├ trigger.txt
└ users.json

无需修改 Docker 配置。


2 避免各种编辑器行为差异

不同编辑器:

  • vim
  • VSCode
  • JetBrains
  • sed

行为都不一样,目录挂载可以彻底规避这些问题。


3 更符合容器化设计

很多服务都采用类似结构:

1
2
3
4
/app
├ config
├ data
└ logs

然后只挂载:

1
./data → /app/data

推荐的 Docker Compose 写法

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

这样:

  • 配置
  • 日志
  • 运行数据

都可以持久化,同时不会遇到 inode 替换的问题。


总结

Docker 挂载单个文件看起来很方便,但隐藏着一个很容易踩到的坑:

许多编辑器在保存文件时会替换 inode,导致容器和宿主机文件“分离”。

因此在实际项目中,更推荐:

不要挂载单个文件,而是挂载目录。

1
2
3
4
5
不要这样:
./config.json:/app/config.json

推荐这样:
./data:/app/data

这样可以避免很多隐蔽问题,也更符合容器化的最佳实践。