Docker 存储卷终极避坑指南:三种持久化方案怎么选?
你是否经历过这样的场景:半夜三点,生产环境的数据库容器重启了,然后数据——没了。或者更常见的是,你在开发环境用 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 只留给代码编辑。
如果你有更好的组合或者遇到过更奇葩的坑,欢迎在评论区聊聊。毕竟这种细节问题,踩过的坑才记得最牢。
更多推荐
所有评论(0)