4、容器数据持久化:数据卷(Volume)与挂载(bind mount)使用场景

容器默认是“用完即扔”的——你删掉容器,里面的数据也跟着没了。那数据库、日志、配置文件怎么办?总不能让它们每次重建都从头开始吧。

我最早踩过的坑就是:跑了个 MySQL 容器,插入一堆测试数据,然后 docker rm 手一抖……全没了。嗯,从那以后,我再也不敢不挂载数据卷。

说白了,持久化就是让容器里的数据活到容器外面,活到宿主机磁盘上。Docker 给了咱们两条路:数据卷(Volume) 和 绑定挂载(Bind Mount)。今天咱们把它们掰开揉碎讲清楚。

4.1 为什么需要持久化?

容器的文件系统层是临时的,删除容器后 docker build 产生的层、docker commit 保存的层都会被清理(除非你特意保留)。但业务数据、配置、日志这些,你肯定不想丢掉。

举个例子:

  • 数据库数据文件:删容器 = 删数据库,你受得了?
  • Nginx 访问日志:调试线上问题全靠它。
  • 应用配置文件:动态更新,不改镜像。
  • 文件上传目录:用户传的文件不能丢。

需求很明确:把宿主机上的目录或一块托管空间挂进容器,让容器进程能读写。

4.2 数据卷(Volume)

Volume 是 Docker 官方推荐的方式。它由 Docker 守护进程管理,存在宿主机上的特定目录(比如 Linux 下是 /var/lib/docker/volumes/)。你用 docker volume 命令操作它,不需要关心具体路径。

4.2.1 创建和使用 Volume

# 创建一个名为 mydata 的卷
docker volume create mydata

# 查看所有卷
docker volume ls

# 检查卷详情
docker volume inspect mydata

# 挂载到容器
docker run -d --name web -v mydata:/app/data nginx

# 或者用 --mount 语法(更显式)
docker run -d --name web --mount source=mydata,target=/app/data nginx

注意:如果卷不存在,-v 会自动创建,而 --mount 不会。我习惯用 --mount,因为它参数明确,不容易写错。

4.2.2 卷的生命周期

  • 创建:docker volume create 或 Dockerfile 中 VOLUME 指令。
  • 使用:容器跑的时候读写。
  • 删除:docker volume rm,但前提是没有容器在用。
  • 清理:docker volume prune 删除所有未被使用的卷。

你在构建镜像时可以用 VOLUME /data 声明一个挂载点,这样别人运行容器时就会自动创建一个匿名卷。匿名卷的好处是不用记名字,但缺点是清理时容易误删。我个人更推荐显式命名卷,方便维护。

4.2.3 Volume 的典型场景

场景例子
数据库数据MySQL 的 /var/lib/mysql
缓存数据集Redis 的持久化 RDB/AOF 文件
共享配置多个容器共用一个卷里的配置文件
备份和迁移卷可以导出为 tar,跨主机恢复
小技巧:想备份一个卷?docker run --rm -v mydata:/source -v $(pwd):/backup alpine tar czf /backup/mydata.tar.gz -C /source .

4.3 绑定挂载(Bind Mount)

Bind mount 直接把宿主机上的一个已有目录或文件挂进容器。路径写死,Docker 不帮你管理。适合开发环境、日志收集、配置文件注入等场景。

4.3.1 基本用法

# 把当前目录挂载到容器的 /app
docker run -d --name dev -v $(pwd):/app node:18 npm start

# 或者用 --mount
docker run -d --name dev --mount type=bind,source=$(pwd),target=/app node:18 npm start

注意:source 必须是绝对路径。我喜欢在项目里建一个 data 文件夹,然后把宿主机上的 ./data 挂进去,这样宿主机和容器都能读写,调试特别方便。

4.3.2 核心特点

  • 宿主机路径必须存在,否则报错(-v 模式下如果宿主机路径不存在,Docker 会创建一个目录,但 --mount 会直接失败)。
  • 文件权限直接沿用宿主机权限。容器用户需要和宿主机用户 uid/gid 匹配,否则会报 Permission denied。
  • 文件更改即时反映到容器内,适合热更新(比如开发时修改源码)。
  • 不适合生产环境,因为路径依赖宿主机目录结构,不利于迁移。
注意:绑定挂载会暴露宿主机文件系统给容器,安全性稍差。如果容器被攻破,攻击者可以读写宿主机上的任何被挂载的目录。

4.3.3 避坑指南

我曾经在 CI 流水线上用绑定挂载,把 Jenkins workspace 挂进容器,结果同事改了个文件权限,容器里就跑不起来了。排查了半天,原来是 uid 不匹配……从那以后,我强制在 Dockerfile 里创建和宿主机同 uid 的用户,或者用 --user 参数指定。

# 运行时指定用户 uid(假设宿主机用户的 uid 是 1000)
docker run --user 1000:1000 -v $(pwd):/app node:18 npm run build

4.4 Volume vs Bind Mount 对比

维度VolumeBind Mount
管理方式由 Docker 管理,存在 /var/lib/docker/volumes/由用户管理,依赖宿主机路径
跨主机迁移容易(备份恢复卷)困难(依赖具体路径)
权限控制相对安全,容器内默认 root 可写权限继承宿主机,uid 冲突常见
性能略有优势(Docker 内部优化)直接文件系统,无额外开销
适用环境生产环境、数据库、持久化数据开发环境、配置文件注入、日志收集

4.5 数据持久化常见错误

  • 忘记挂载:容器删了数据就没了。解决方案:编写 docker-compose 或脚本,显式挂载。
  • 权限错误:容器用户无法写挂载目录。我曾经在 Jenkins 里跑 Docker 构建,目录权限是 755,容器运行用户是 www-data,死活写不了日志。后来加上 chmod 777 才解决——但这不安全,建议用 --user 匹配 uid。
  • 挂载目录冲突:两个容器同时写同一个卷/目录,导致数据损坏。对于数据库,永远不要多个进程写相同数据目录。
  • 匿名卷堆积:每次 docker run 不带卷名,Docker 会生成一串随机名字的卷,久而久之占满磁盘。定期 docker volume prune 能清掉未使用的。
警告:生产环境千万别用 -v $(pwd):/data 这种绑定挂载。你换个路径部署就完蛋。用 Volume 或者命名卷加 --mount,可维护性高得多。

4.6 一张图看懂持久化方案

下面这张 SVG 图帮你理清 Volume 和 Bind Mount 在宿主机和容器之间的层级关系:

Docker 数据持久化:Volume vs Bind Mount 结构图 宿主机文件系统 容器 Docker Volume 存储区 /var/lib/docker/volumes/ 数据由 Docker 守护进程管理 宿主机目录(Bind Mount 源) /home/user/project/data 用户自己管理的路径 容器文件系统 /app/data (挂载点) 临时层 + 镜像层 Volume Bind Mount 💡 建议 生产环境:用 Volume 开发调试:用 Bind Mount 日志/配置:Bind Mount 更灵活

简单说:Volume 走 Docker 托管的“黑盒”路径,Bind Mount 直接走你指定的真实路径。一个稳,一个方便。

4.7 实践建议

  • 数据库、消息队列、需要持久化存储的服务:用命名 Volume。
  • 开发环境、代码热更新、配置注入:用 Bind Mount,注意先解决问题。
  • 日志收集:Bind Mount 挂载宿主机日志目录,配合 logrotate 自动轮转。
  • 文件上传:可以用 Volume,也可以 Bind Mount 到宿主机的共享存储(如 NFS)。
  • 多容器共享数据:多个容器挂载同一个 Volume 是安全的(要避免写冲突),或使用 Bind Mount 到同一目录(注意文件锁)。
我的习惯:在 docker-compose.yml 里把所有业务数据都用 volumes: 定义成命名卷,开发环境再覆盖个别路径为 bind mount。这样生产部署时直接 docker-compose up 就用卷,本地开发自动用本地目录。

持久化是容器化应用的“地基”。地基打不稳,数据丢了,再好的架构也白搭。记住:容器可以重建,数据必须保留。下次写 Dockerfile 或者跑容器时,记得先想好:数据放哪里?怎么挂?