你是否经历过这样的场景:半夜三点,生产环境的数据库容器重启了,然后数据——没了。或者更常见的是,你在开发环境用docker run -v .:/app跑得飞起,一上 CI,权限炸了。
读完本文,你将能:
- 搞清楚什么时候用 volume、什么时候用 bind mount、什么时候用 tmpfs
- 随手写出一条不会因为权限问题翻车的挂载命令
- 在 5 分钟内完成数据卷的备份和恢复
- 避开我踩过的 3 个最常见的坑
好了,直接上干货。
先搞懂这三个东西是干嘛的
先说结论:Docker 容器默认是无状态的。你往容器里写的数据,容器一删就没了。所以必须把数据存到容器外面。
Docker 给了我们三种方案:Volume、Bind Mount、tmpfs。
特性 | Volume(命名卷) | Bind Mount(绑定挂载) | tmpfs |
数据存哪 | Docker 托管(/var/lib/docker/volumes/) | 你指定的宿主机路径 | 内存 |
持久化 | ✅ 容器删除后还在 | ✅ 容器删除后还在 | ❌ 容器停止就没了 |
生产环境推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐(仅限特定场景) | ⭐⭐⭐(临时数据) |
可移植性 | 高,换个主机同名卷就挂上了 | 低,路径变了就挂不上 | N/A |
Linux 性能 | 原生 | 原生 | 极高(纯内存) |
Docker Desktop 性能 | 好 | 差(有文件系统转换开销) | N/A |
什么时候选 Volume?
说白了就是生产环境无脑上 Volume。Docker 官方也明确说了:卷是持久化 Docker 容器生成和使用的数据的首选机制。因为:
- 卷比绑定挂载更容易备份和迁移
- 可以跨容器安全共享
- 性能更好——直接写宿主机文件系统,不经过存储驱动那一层
举个典型场景:跑 MySQL、PostgreSQL、Redis(RDB/AOF 持久化模式)、MongoDB,数据必须用 Volume。
什么时候用 Bind Mount?
开发调试用。比如你改了前端代码,想让容器里实时刷新。绑定挂载直接把宿主机目录映射进去,两边同步。
一个经典误区:node_modules目录千万别用 bind mount!大量小文件频繁读写 + Docker Desktop 的文件系统转换开销,能让你等到怀疑人生。正确做法是用一个命名卷专门存node_modules。
什么时候用 tmpfs?
需要极快读写速度 + 数据不需要持久化 + 敏感数据不希望落盘的时候。比如:
- Session 缓存
- 图片处理中间文件
- 应用运行时的临时文件(
/tmp这种)
tmpfs 数据存在内存里,容器停了就没了。而且写入 tmpfs 不经过容器的可写层,性能直逼内存访问。
顺便提一嘴,tmpfs 只在 Linux 上能用。你在 Mac 上用 Docker Desktop 跑--tmpfs?它会给你一个静默的空挂载,数据还是走的 VM 磁盘。
实战:三种挂载方式怎么用
1. Volume(命名卷)
# 创建一个命名卷 docker volume create my_app_data # 启动容器并挂载 docker run -d --name my_app -v my_app_data:/app/data nginx # 查看卷的详细信息(可以看它在宿主机上的实际路径) docker volume inspect my_app_data # 列出所有卷 docker volume ls # 删除一个卷(需要先停掉所有在用这个卷的容器) docker volume rm my_app_data # 清理所有未使用的卷 docker volume prune预期效果:你可以反复停掉、删除、再重建这个容器,/app/data里的数据一直保留。
一条命令验证:
docker run --rm -v my_app_data:/data alpine ls /data每次跑这条命令,看到的文件应该一样。
2. Bind Mount
# 绝对路径是必须的,不要用 ~,用 $HOME 或完整的 /home/xxx docker run -d --name dev_app -v "$(pwd)"/src:/app/src nginx # 只读挂载(生产环境安全推荐) docker run -d -v /host/config:/app/config:ro nginx # 在 Docker Compose 里Compose 写法:
services: app: image: nginx volumes: - ./src:/app/src # bind mount - my_volume:/app/data # named volume volumes: my_volume: # 声明一下,Docker 自动创建注意:在 Docker Compose 中,如果volumes:块里没有声明对应的卷名,Docker 会自动创建一个名字很长的匿名卷,用docker volume ls可以看到,但不好管理。
3. tmpfs
# 基本用法 docker run -d --tmpfs /tmp:rw,noexec,nosuid,size=128m ubuntu sleep infinity # 或者用 --mount 语法(更明确,推荐) docker run -d --mount type=tmpfs,destination=/tmp,tmpfs-size=128m ubuntu sleep infinity参数说明:
rw:读写(默认就是 rw)noexec:禁止执行二进制文件nosuid:忽略 setuid/setgidsize=128m:限制最大使用 128MB 内存
万一遇到No space left on device但内存明明还够?检查一下是不是 tmpfs 的 size 设得太小了,或者被 cgroup 限住了。
权限问题:90%的坑都在这
这是我最想骂人的部分。你配好了挂载,容器启动了,应用报permission denied。
核心原因很简单:容器里的进程(比如 nginx 用www-data用户跑)的 UID/GID,和你宿主机上的文件所有者对不上。
场景一:Bind Mount 下容器写不进文件
你本地文件所有者是uid=1000(普通用户),容器里 nginx 以www-data(uid=33)运行。容器往/app/cache写文件?报错。
解法一:用--user让容器以你的 UID 跑。
docker run -v "$(pwd)"/app:/app --user $(id -u):$(id -g) my_image解法二:修改宿主目录权限(粗暴但有效)。
chmod -R 777 /path/to/mount # 别在生产环境这么搞 # 更推荐的做法: sudo chown -R 33:33 /path/to/mount # 把目录所有者改成 www-data 的 uid解法三(我最常用的):在 Dockerfile 里对齐用户 ID。
FROM nginx:alpine ARG USER_ID=1000 ARG GROUP_ID=1000 RUN addgroup -g $GROUP_ID appgroup && \ adduser -u $USER_ID -G appgroup -D appuser USER appuser然后在 run 的时候传入你的 UID/GID。
场景二:SELinux 搞事情
如果你用 CentOS/RHEL,挂载 bind mount 后容器读不了文件,大概率是 SELinux 在挡路。解决方案是在挂载参数后面加:Z或:z。
docker run -v /host/data:/container/data:Z my_image:z:多个容器共享这个卷:Z:私有卷,只有当前容器能用
这个参数在非 SELinux 系统上会被忽略,所以放心加。
场景三:容器里创建的文件在宿主机上删不了
反过来也一样。容器以www-data(uid=33)在 bind mount 目录下创建了文件,你在宿主机上用普通用户去删它,报 permission denied。
解法:找到那个 uid=33 的进程(或直接 sudo),或者提前用上面说的用户对齐方法让 UID 一致。
性能与备份:生产环境避坑要点
存储驱动选哪个?
Overlay2 是目前生产环境的第一选择。它是 Docker 在主流 Linux 发行版上的默认驱动。不要再用 aufs(已被内核移除)和 devicemapper(已废弃)了。
检查你当前的存储驱动:
docker info | grep "Storage Driver"输出应该是overlay2。如果不是,赶紧切。
顺带一提,在 NVMe 设备上,overlay2 的 4K 随机写入性能比 aufs 提升了约 37%。
备份 Volume 的标准姿势
我最常用的是docker run --rm -v+tar组合:
# 备份 docker run --rm -v my_volume:/source alpine tar czf - -C /source . > my_volume_backup.tar.gz # 恢复 docker run --rm -v my_volume:/target alpine sh -c "tar xzf - -C /target" < my_volume_backup.tar.gz为什么用tar而不是直接 cp?因为tar能完整保留文件属主、权限、时间戳。这对于数据库数据目录来说至关重要。
跨主机迁移 Volume
如果你的宿主机用 Linux 且用的是默认的 local 驱动,Volume 的数据实际在/var/lib/docker/volumes/<volume_name>/_data里。直接把整个目录 rsync 到另一台机器的相同位置就行。但更优雅的做法是用docker volume create --driver接外部存储(NFS、AWS EBS 等)。
生产环境我强烈推荐用存储插件接云盘或 NFS。这样容器随便漂移,数据跟着走。
常见错误速查表
报错信息 | 原因 | 解决办法 |
| 容器内用户无权访问挂载目录 | 用 |
| 宿主目录路径不存在 | 先 |
| Docker Desktop 未共享该驱动器 | 去 Docker Desktop 设置 → Resources → File Sharing 添加路径 |
| tmpfs 容量限制打满 | 调大 |
| 卷挂载覆盖了容器内非空目录 | 先清空目标目录或用 |
| 当前用户不在 docker 组 |
|
补充说明:“cannot mount volume over existing file”
如果你把一个非空的卷挂载到容器里一个已经存在文件的目录上,原有的文件会被“遮住”(不是删除,是暂时看不见)。Docker 没有直接的办法把它再露出来,只能重建一个不带这个挂载的容器。
我推荐的几套黄金组合
组合一:开发环境 + 代码热更新
- 源码 → Bind Mount
- 依赖目录(node_modules/vendor)→ 命名卷
- 临时文件 → tmpfs
组合二:生产环境 + 数据库
- 数据目录 → 命名卷
- 配置文件 → Bind Mount(只读)
- 日志目录 → 命名卷 + 定期轮转
组合三:CI/CD 流水线
- 构建缓存 → 命名卷
- 测试结果 → 临时目录或 Bind Mount
- 代码 → Bind Mount(runner 挂进去)
组合四:高 IOPS 临时处理
- 图片/视频处理中间文件 → tmpfs
- 最终产物 → Volume
最后说两句
存储卷这个东西,说起来不复杂,但一遇到权限和性能就各种花式翻车。我建议你从一开始就养成一个习惯:所有持久化数据都用命名卷,所有临时数据都用 tmpfs,bind mount 只留给代码编辑。
如果你有更好的组合或者遇到过更奇葩的坑,欢迎在评论区聊聊。毕竟这种细节问题,踩过的坑才记得最牢。