不会写 docker-compose.yml?没关系。会用 volumes 就赢了一半。
你好,我是静远。
前几天帮一个朋友看项目,他的 docker-compose.yml 长这样:
services:
mysql:
volumes:
- ./mysql_data:/var/lib/mysql
他拍胸脯说:"这样 MySQL 数据就在本地了,删了容器数据还在。"
我问他:"你试过 docker compose down 再 up 吗?"
他试了。然后给我发了个哭脸。
MySQL 起来了,但是所有表都没了,用户名密码也初始化回了默认。
他做错了什么?
错在把数据库这种「容器自己管理」的数据,用了宿主机的空目录去挂。
这篇文章就围绕这件事讲清楚:Docker Compose 的 volumes 到底在干嘛,三种挂载方式怎么选,5 个新手必踩的坑,以及一张决策树让你以后不用再猜。
读完你应该能:
-
看到任何一段 volumes:配置,立刻知道它在干什么 -
知道什么场景该用 bind mount、什么场景必须用命名卷 -
避开 90% 的 Docker 数据丢失和权限问题
一、先建立心智模型
每次写 volume 之前,问自己三个问题:
| # | 问题 | 说明 |
|---|---|---|
| 1 | 挂载什么? | 宿主机路径?命名卷?匿名卷? |
| 2 | 挂载到哪? | 容器内的哪个绝对路径 |
| 3 | 谁盖住谁? | 挂载源的内容会覆盖容器原本的内容 |
第三条最重要。
我把它翻译成人话:「你挂上去的东西,会把容器里那个位置原本的内容盖住」。哪怕挂的是个空目录,也是"用空盖住原内容"。
记住这个,后面所有坑都跟它有关。
二、三种挂载方式
2.1 Bind Mount(绑定挂载)
类比:你给容器开了个"传送门",容器里的某个目录直接通向你的宿主机文件夹。你在宿主机改文件,容器里马上看到;容器里写的文件,宿主机也能看到。
语法: ./data:/app/data ← 宿主机具体路径
或 ~/config:/etc/config
或 /absolute/path:/path
行为: 宿主机路径的内容 → 容器
双向同步,即时生效
宿主机内容强制覆盖容器
特点:
-
宿主机路径必须存在,否则 Docker 会创建空目录(不是空文件!这是大坑) -
挂载后,容器里该路径原有内容被遮蔽 -
适合:配置文件、代码热更新、日志查看
注意:
-
目录挂载 → 整个目录被替换,原容器目录的所有其他文件"消失" -
文件挂载 → 只替换那个文件,同目录其他文件正常
2.2 Named Volume(命名卷)
类比:你跟 Docker 说,"给我准备一个带名字的保险箱"。Docker 帮你存好,下次问它要它就给你。数据归 Docker 管,物理上放在 /var/lib/docker/volumes/。
语法: mysql_data:/var/lib/mysql
(需在顶层声明 volumes:)
行为: Docker 管理的命名存储
物理上存放于 /var/lib/docker/volumes/
跨容器重建保留
关键特征:初始化行为:
-
卷为空时 → 容器启动时,Docker 把镜像内该路径的原有内容复制到卷里 -
卷已有内容 → 直接用卷的内容,镜像内容被遮蔽 -
所以 MySQL、Redis 等容器能"记住"数据:数据在卷里,容器删了重建还在
适合:数据库数据、中间件持久化、需要跨越容器生命周期保留的数据。
2.3 Anonymous Volume(匿名卷)
类比:你啥也没说,Docker 偷偷给你准备了一个没名字的临时抽屉。用完就扔。
语法: - /app/node_modules ← 无冒号、无名前缀
行为: 无名字,Docker 分配 UUID 作为卷名
容器删除后通常会被清理
(docker compose down 默认不删匿名卷)
典型用途:防止 bind mount 覆盖某个子目录。
volumes:
- ./:/app # bind mount 项目代码
- /app/node_modules # 匿名卷"占位",保留容器装的 node_modules
原理:挂载规则按"最长路径匹配",/app/node_modules 比 /app 更具体。
三、覆盖规则:目录 vs 文件
这一节把"覆盖"讲透。看图:
目录挂载:整个目录被替换
宿主机 ./html/ 容器 /usr/share/nginx/html/
├── index.html ├── index.html ← 被遮蔽
└── about.html ├── 50x.html ← 被遮蔽
└── style.css ← 被遮蔽
挂载 ./html:/usr/share/nginx/html 后,容器里只有 index.html + about.html
其他文件全部"消失"(仍在镜像层,被挂载层遮蔽)
文件挂载:只替换一个文件
宿主机 nginx.conf 容器 /etc/nginx/
├── conf.d/ ← 正常
├── mime.types ← 正常
└── nginx.conf ← 被替换
挂载 ./nginx.conf:/etc/nginx/nginx.conf 后,只有 nginx.conf 被替换
同目录其他文件不受影响
一句话总结
| 场景 | 目录挂载 | 文件挂载 |
|---|---|---|
| 影响范围 | 整个目录被替换 | 仅单个文件被替换 |
| 同目录下其他文件 | 全部被遮蔽 | 不受影响 |
| 容器进程视角 | 看到宿主机的目录内容 | 看到原目录(只有该文件被替换) |
四、高频踩坑与解法
坑 1:配置文件挂载时宿主机文件不存在
❌ 错误写法
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
如果 ./nginx.conf 不存在,Docker 会在宿主机创建空目录(不是空文件!)
然后把它当目录挂到容器 → Nginx 报错找不到配置文件
✅ 正确做法:先准备好文件
# 从镜像里拷出默认配置
docker run --rm nginx:latest cat /etc/nginx/nginx.conf > ./nginx.conf
# 或先启动不挂载的容器,拷出来
docker run -d --name tmp nginx:latest
docker cp tmp:/etc/nginx/nginx.conf ./nginx.conf
docker rm -f tmp
为什么会这样:Docker 不知道你要的是文件还是目录,它看 ./nginx.conf 不存在就直接建一个空目录。这是我见过最多人栽进去的坑。
坑 2:数据库/中间件数据用 bind mount
❌ 不推荐:MySQL 数据用 bind mount
services:
mysql:
volumes:
- ./mysql_data:/var/lib/mysql
问题:
· 宿主机空目录 → 没有初始化行为,MySQL 当作全新目录初始化
· 权限问题:MySQL 进程 UID 999 vs 宿主机 UID 1000
· 跨机器迁移路径耦合
✅ 正确:用命名卷
services:
mysql:
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
口诀:数据库、中间件的数据 → 永远用命名卷。
坑 3:代码挂载导致 node_modules / vendor 消失
❌ 前端/后端开发常见错误
volumes:
- ./:/app
宿主机项目目录没有 node_modules(或版本不对)
挂载后覆盖了容器里 npm install / composer install 装的依赖
✅ 正确:用匿名卷保护子目录
volumes:
- ./:/app # 代码同步
- /app/node_modules # 匿名卷"占位",保留容器内的依赖
原理: Docker 按"最长路径匹配"决定哪个挂载生效
/app/node_modules 比 /app 更具体,优先级更高
坑 4:权限问题(Linux 尤其常见)
现象:
· 宿主机文件属主是你(UID 1000),容器进程是 root 或其他用户
· 比如 MySQL 进程 UID 999 → 无权读/写 bind mount 的文件
· 容器内创建的文件在宿主机属主为 root,你删不掉
解法(按推荐度排序):
-
用命名卷 → Docker 自动处理权限 -
宿主机改权限 → chown -R 999:999 ./mysql_data -
指定容器运行用户:
services:
app:
user: "1000:1000" # 用你的 UID/GID 跑进程
-
Dockerfile 里创建同 UID 用户
坑 5:忘记声明只读
❌ 配置文件、证书应该只读,防止容器意外改写
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
✅ 加 :ro(read only)
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/ssl/certs:ro
原则:只读的数据(配置、证书、静态资源)→ 加 :ro。能避免容器里某个 bug 把你的配置文件改坏。
五、决策树
下次写 volume 之前,沿着这棵树走一遍:
数据需要跨容器重建保留?
/ \
否 是
/ \
需要宿主机直接编辑? 数据由容器程序管理?
/ \ / \
是 否 是 否
| | | |
Bind Mount Anonymous Named Bind Mount
(配置/代码/ (临时/占位 Volume (用户上传/
日志) 保护) (DB/中间件 导出数据)
数据)
三条核心口诀
-
Bind mount = 宿主机内容强制覆盖容器内容(空目录也覆盖!) -
Named volume = 卷为空时从容器初始化,已有内容则用卷的 -
挂载按最长路径匹配,子路径的挂载优先级更高
六、调试命令
记住这 5 个命令,你就是 volume 调试高手:
# 查看容器的所有挂载点
docker inspect <container> --format '{{json .Mounts}}' | jq
# 查看命名卷详细信息(包括宿主机路径)
docker volume inspect mysql_data
# 查看命名卷的物理文件(Linux)
sudo ls -la /var/lib/docker/volumes/mysql_data/_data
# 列出 / 清理无用卷
docker volume ls
docker volume prune
# 进容器查看实际挂载状态
docker exec -it <container> ls -la /挂载路径
docker exec -it <container> mount | grep 挂载路径
第一个 docker inspect 是最常用的:容器启动后行为不对,先看 .Mounts 字段确认挂载关系。
七、可直接照抄的完整模板
这是我们做项目时最常用的范式,覆盖了 bind mount、命名卷、匿名卷、只读四种用法:
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # 单文件 + 只读
- ./nginx/conf.d:/etc/nginx/conf.d:ro # 目录 + 只读
- nginx_logs:/var/log/nginx # 日志用命名卷
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: secret
volumes:
- mysql_data:/var/lib/mysql # 数据用命名卷
- ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
app:
build: ./app
volumes:
- ./app/src:/workspace/src # 代码热更新
- /workspace/node_modules # 匿名卷保护依赖
- uploads:/workspace/uploads # 用户上传用命名卷
- ./app/logs:/workspace/logs # 日志用 bind mount
volumes:
mysql_data:
nginx_logs:
uploads:
你只需要改两个地方:
-
密码( MYSQL_ROOT_PASSWORD) -
镜像版本(按需固定到具体版本号,别用 latest)
剩下的直接抄走就能跑。
写在最后
回头看开头那个朋友的 MySQL:他用了 bind mount + 宿主机空目录,等于告诉 MySQL "你是个全新数据库,请重新初始化"。于是之前所有数据都被新初始化的表结构覆盖了。
改成命名卷一行的事:
volumes:
- mysql_data:/var/lib/mysql
下次 docker compose down && up,数据还在。
Volumes 是个看起来简单、用起来全是坑的东西。但你只要记住"覆盖规则" + "决策树" + "数据库用命名卷"这三件事,90% 的场景都不会出问题。
剩下 10%?欢迎留言你遇到过的奇葩场景,咱们一起补到决策树里。
静远,一个在AI时代坚持手把手带你上车的普通人。

