在 docker-compose.yml 里加了 volumes 挂载,docker compose up 一启动,容器里原本有的文件全没了——Nginx 的默认页面不见了,直接 403。
这个坑几乎所有用 Docker Compose 的人都踩过。文件其实没丢,是被「遮蔽」了。这篇文章用一个可复现的实验讲清楚原理,再给 3 个相关坑的避坑方法。所有命令在 Docker 29.8 + Compose v5.5 上实测过。
复现:30 秒看到「文件消失」
准备一个空的 html 目录,写一份 compose 文件:
services:
web:
image: nginx:alpine
ports:
- "18080:80"
volumes:
- ./html:/usr/share/nginx/html
启动后看容器里的挂载点:
docker compose up -d
docker compose exec web ls -la /usr/share/nginx/html
实测输出——空的:
total 8
drwxr-xr-x 2 root root 4096 ...
drwxr-xr-x 1 root root 4096 ...
而镜像本身明明有默认文件,不挂载直接跑就能看见:
docker run --rm nginx:alpine ls /usr/share/nginx/html
# 50x.html index.html
访问 http://localhost:18080 直接 403 Forbidden——目录里连 index.html 都没有。
原理:bind mount 是「遮蔽」,不是「合并」
./html:/usr/share/nginx/html 这种写法叫 bind mount(绑定挂载)。它的行为是:把宿主机目录直接盖在容器路径上,原来路径下的内容被完全遮住。
注意三个关键点:
- 镜像里的文件没有被删除,只是看不见了——不挂载启动容器,文件还在
- 遮蔽是整目录替换,不做任何文件级合并,哪怕宿主机目录是空的
- 你往
./html里放文件,容器里立刻可见——因为容器看到的就是这个宿主机目录
所以正确用法是:把需要的内容先放进宿主机目录再挂载,而不是指望镜像内容和挂载目录「合并」。
对照实验:named volume 的行为完全不同
把 bind mount 换成 named volume:
services:
web:
image: nginx:alpine
volumes:
- webdata:/usr/share/nginx/html
volumes:
webdata:
实测:容器里 50x.html 和 index.html 都在。
差别在于初始化时机:named volume 首次创建时,Docker 会把镜像挂载点下的内容复制进 volume;之后 volume 里已有的内容保留,不再同步。而 bind mount 永远不做这个复制——宿主机给什么就是什么。
一句话记忆:
| 挂载方式 | 首次启动时 | 适合场景 |
|---|---|---|
bind mount(./dir:/path) |
宿主机内容直接遮蔽镜像 | 配置文件、代码目录(内容本来就在宿主机) |
named volume(name:/path) |
镜像内容先复制进 volume | 数据库数据、上传文件(想要镜像初始内容做底) |
坑 2:宿主机路径不存在,Docker 会静默创建空目录
写错路径没有报错:
volumes:
- ./htm:/usr/share/nginx/html # 手滑,少了个 l
docker compose up 正常启动,Docker 默默在宿主机创建了 ./htm 空目录,然后把容器内容遮成空。你盯着配置文件看半天也想不到是路径打错了。
排查方法:
docker compose exec web ls /usr/share/nginx/html # 看容器里
docker inspect <容器名> --format '{{json .Mounts}}' | jq # 看实际挂载源
Mounts 里的 Source 字段是宿主机绝对路径,一对照就能发现路径写错了。
坑 3:挂载单个文件,文件不存在会变成目录
想只挂一个配置文件:
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
如果宿主机上 ./nginx.conf 不存在,Docker 会把它创建成目录,容器里 cat /etc/nginx/nginx.conf 报 Is a directory。
规避:挂载单文件前,先确认宿主机上这个文件真实存在:
# 没有就先建一个,哪怕空文件
touch nginx.conf
相对路径 ./xxx 是相对 compose 文件所在目录解析的,不是你执行命令的目录——在别处敲 docker compose -f /path/to/docker-compose.yml up 时尤其容易搞错。
坑 4:Linux 上的权限问题
bind mount 进容器的目录,属主是宿主机的 UID。容器进程如果以非 root 运行(比如 node、postgres 镜像),可能写不进去,报 Permission denied。
两种解法:
# 方法一:指定容器进程 UID(先看宿主机目录属主 id -u)
services:
app:
user: "1000:1000"
# 方法二:给宿主机目录放权
chmod -R 777 ./data # 粗暴,仅本地开发用
Windows 的 Docker Desktop 走 WSL2 虚拟机做中转,权限问题基本被抹平了——这也是 Windows 上跑 Docker 反而少踩一类坑的罕见例子。
一张排查清单
遇到「挂载后文件没了 / 内容不对」,按这个顺序查:
# 1. 看容器里实际挂成了什么
docker inspect <容器> --format '{{json .Mounts}}' | jq
# 2. 确认宿主机路径内容是不是你想的
ls -la ./html
# 3. 确认容器里看到的内容
docker compose exec web ls -la /usr/share/nginx/html
# 4. 对照镜像原始内容(文件其实没丢)
docker run --rm nginx:alpine ls /usr/share/nginx/html
第 4 步最有说服力——镜像内容还在,就证明只是被遮蔽,问题一定出在挂载配置上。
更多 Docker 命令参数看 Docker 命令速查 和 Docker Compose 速查。