你是否经历过这样的场景:半夜三点,生产环境的数据库容器重启了,然后数据——没了。或者更常见的是,你在开发环境用 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/setgid
  • size=128m:限制最大使用 128MB 内存

万一遇到 No space left on device 但内存明明还够?检查一下是不是 tmpfs 的 size 设得太小了,或者被 cgroup 限住了。

权限问题:90%的坑都在这

这是我最想骂人的部分。你配好了挂载,容器启动了,应用报 permission denied

核心原因很简单:容器里的进程(比如 nginx 用 www-data 用户跑)的 UID/GID,和你宿主机上的文件所有者对不上。

场景一:Bind Mount 下容器写不进文件

你本地文件所有者是 uid=1000(普通用户),容器里 nginx 以 www-datauid=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。这样容器随便漂移,数据跟着走。

常见错误速查表

报错信息

原因

解决办法

permission denied

容器内用户无权访问挂载目录

--user 对齐 UID,或修改目录权限

no such file or directory

宿主目录路径不存在

mkdir -p 创建,或用绝对路径

mount denied: ... does not exist

Docker Desktop 未共享该驱动器

去 Docker Desktop 设置 → Resources → File Sharing 添加路径

No space left on device(tmpfs)

tmpfs 容量限制打满

调大 size= 参数

cannot mount volume over existing file

卷挂载覆盖了容器内非空目录

先清空目标目录或用 volume-nocopy 选项

docker: Got permission denied(直接跑 docker 命令)

当前用户不在 docker 组

sudo usermod -aG docker $USER,然后退出重登

补充说明:“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 只留给代码编辑。

如果你有更好的组合或者遇到过更奇葩的坑,欢迎在评论区聊聊。毕竟这种细节问题,踩过的坑才记得最牢。

更多推荐