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 对比
| 维度 | Volume | Bind 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 在宿主机和容器之间的层级关系:
简单说:Volume 走 Docker 托管的“黑盒”路径,Bind Mount 直接走你指定的真实路径。一个稳,一个方便。
4.7 实践建议
- 数据库、消息队列、需要持久化存储的服务:用命名 Volume。
- 开发环境、代码热更新、配置注入:用 Bind Mount,注意先解决问题。
- 日志收集:Bind Mount 挂载宿主机日志目录,配合 logrotate 自动轮转。
- 文件上传:可以用 Volume,也可以 Bind Mount 到宿主机的共享存储(如 NFS)。
- 多容器共享数据:多个容器挂载同一个 Volume 是安全的(要避免写冲突),或使用 Bind Mount 到同一目录(注意文件锁)。
volumes: 定义成命名卷,开发环境再覆盖个别路径为 bind mount。这样生产部署时直接 docker-compose up 就用卷,本地开发自动用本地目录。
持久化是容器化应用的“地基”。地基打不稳,数据丢了,再好的架构也白搭。记住:容器可以重建,数据必须保留。下次写 Dockerfile 或者跑容器时,记得先想好:数据放哪里?怎么挂?