数据卷与挂载
这一课你将能做到
- 亲眼复现「删容器丢数据」的事故,再用 named volume 修复,让 MySQL 数据在容器删除重建后完好如初
- 用 bind mount 把 MySQL 配置文件、Spring Boot 日志目录挂到宿主机,实现「容器外可查可改」
- 掌握数据备份与恢复的通用命令,并建立「什么数据用什么挂载方式」的决策习惯
为什么要学
这是整个 Docker 学习曲线里最经典的事故现场:数据库容器跑得好好的,某天升级版本 docker rm 掉旧容器再起新的——用户数据全没了。第 4 课已经从原理上解释过(可写层随容器销毁),本课给出工程解法:数据卷(Volume)。
对 Spring Boot 后端来说,需要持久化的东西不止 MySQL:Redis 的 AOF/RDB 文件、应用日志、上传的文件、MySQL 自定义配置——它们分别适合 named volume、bind mount 等不同方式。选错方式不会马上报错,但会在某天变成丢数据事故或权限地狱。
先建立图像
三种挂载方式,一条 -v 参数三种长相:
-v mysql-data:/var/lib/mysql ← ① named volume:Docker 统一管理(推荐存数据)
-v H:/work/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro
← ② bind mount:宿主机路径直挂(推荐放配置)
-v /dev/shm:/tmpfs ← ③ tmpfs:内存挂载(敏感临时数据,停即没)
宿主机
┌─────────────────────────────────────┐
│ Docker 管理区 你的目录 │
│ /var/lib/docker/volumes/ H:/work │
│ ▲ ▲ │
│ │ named volume │ bind │
│ ┌────┴──────────────────────┴───┐ │
│ │ MySQL 容器 │ │
│ │ /var/lib/mysql /etc/mysql │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
关键差异(高频面试点):
| named volume | bind mount |
|---|
| 路径来源 | Docker 管理(Linux 在 /var/lib/docker/volumes/) | 宿主机任意路径 |
| 首次挂载 | 会把镜像内已有内容拷入 volume | 直接遮盖容器内目录(空目录也会遮) |
| 可移植性 | 高(与宿主机路径解耦) | 依赖宿主机路径存在 |
| 典型用途 | MySQL/Redis 数据 | 配置文件、开发时代码热挂载 |
跟着做
步骤 1:先复现一次「删容器丢数据」
docker run -d --name bad-mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0
# 等 30 秒左右让 MySQL 初始化完成,然后写入数据
docker exec bad-mysql mysql -uroot -p123456 -e "CREATE DATABASE demo; USE demo; CREATE TABLE t(id INT); INSERT INTO t VALUES (1);"
docker rm -f bad-mysql
# 重新起一个同名容器
docker run -d --name bad-mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0
docker exec bad-mysql mysql -uroot -p123456 -e "SELECT * FROM demo.t;"
# 报错:Table 'demo.t' doesn't exist —— 数据没了
步骤 2:用 named volume 修复
docker volume create mysql-data
docker run -d --name good-mysql \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:8.0
# 初始化完成后写入数据
docker exec good-mysql mysql -uroot -p123456 -e "CREATE DATABASE demo; USE demo; CREATE TABLE t(id INT); INSERT INTO t VALUES (42);"
# 删除容器,卷不动
docker rm -f good-mysql
# 新容器挂同一个卷
docker run -d --name good-mysql2 \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:8.0
docker exec good-mysql2 mysql -uroot -p123456 -e "SELECT * FROM demo.t;"
# 输出 42 —— 数据完好
步骤 3:熟悉 volume 命令族
docker volume ls # 列出所有卷
docker volume inspect mysql-data # 查看卷详情(Mountpoint 是宿主机实际位置)
docker volume rm mysql-data # 删除卷(有容器使用会拒绝)
docker volume prune # 清理未被使用的匿名卷(慎用,先 ls 确认)
步骤 4:bind mount 挂 MySQL 自定义配置
新建 H:/work/mysql-conf/my.cnf:
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
max_connections = 500
slow_query_log = ON
long_query_time = 1
挂载启动(Windows PowerShell 下路径用正斜杠最稳):
docker run -d --name conf-mysql \
-v mysql-data:/var/lib/mysql \
-v H:/work/mysql-conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:8.0
docker exec conf-mysql mysql -uroot -p123456 -e "SHOW VARIABLES LIKE 'max_connections';"
# 输出 500 —— 配置生效,且文件带 :ro 只读保护
改配置 = 改宿主机文件 + docker restart conf-mysql,完全不用动镜像。
步骤 5:把 Spring Boot 日志挂出来
假设你的应用写 ./logs/app.log(logback 配置 LOG_PATH 环境变量可控制),镜像里日志在 /app/logs:
docker run -d --name demo-log \
-v app-logs:/app/logs \
-e LOG_PATH=/app/logs/ \
demo-app:1.0
# 查看卷在宿主机的实际位置(Linux 服务器)
docker volume inspect app-logs --format '{{.Mountpoint}}'
# 直接 tail 那个目录下的日志文件
开发环境也可以用 bind mount 直挂到项目目录,方便 IDE 插件实时查看。
步骤 6:数据备份与恢复
named volume 的备份是通用套路:起一个临时容器,同时挂卷和宿主机目录,用 tar 打包。
# 备份:把 mysql-data 卷打包到当前目录(PowerShell 用 ${PWD})
docker run --rm \
-v mysql-data:/source:ro \
-v ${PWD}:/backup \
alpine tar czf /backup/mysql-data-backup.tar.gz -C /source .
# 恢复:反向解包到一个新卷
docker volume create mysql-data-restored
docker run --rm \
-v mysql-data-restored:/target \
-v ${PWD}:/backup \
alpine tar xzf /backup/mysql-data-backup.tar.gz -C /target
# 用恢复的卷起容器验证
docker run -d --name restored-mysql \
-v mysql-data-restored:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 mysql:8.0
生产更常用的逻辑备份是 mysqldump(跨版本更稳):docker exec good-mysql mysqldump -uroot -p123456 --all-databases > backup.sql。
讲清楚要点
- 可写层与卷的本质区别:可写层属于单个容器、随容器销毁;卷是 Docker 独立管理的对象,生命周期独立于容器。有状态数据一律进卷。
- named volume 首次挂载会「填充」:如果挂载点在镜像里已有文件(如
/var/lib/mysql 初始结构),Docker 会把它们拷进空卷——这保证了全新 MySQL 容器能正常初始化。bind mount 没有这个行为,是直接遮盖。
- bind mount 的遮盖陷阱:把宿主机空目录挂到
/var/lib/mysql,MySQL 容器会以为数据目录被清空,可能报错或重新初始化。原则:数据用 volume,配置单文件用 bind mount。
:ro 只读挂载是低成本高收益的安全习惯:配置文件容器只该读不该写;误操作写坏配置文件的事故因此绝迹。
- Windows bind mount 的两个坑:路径分隔符用
/(H:/work/conf)而不是反斜杠;跨文件系统(NTFS ↔ WSL2 ext4)IO 性能明显低于原生——高频读写的目录优先放 volume,而不是挂载 NTFS 目录。
- 备份三选一:tar 卷(物理备份,快但依赖同版本);
mysqldump(逻辑备份,跨版本迁移首选);文件系统/云盘快照(整卷定期快照)。选哪种取决于 RTO/RPO 要求。
你可能会懵的地方
-
named volume 的数据到底在宿主机哪里?
Linux:/var/lib/docker/volumes/<卷名>/_data。Docker Desktop(Win/Mac):在 WSL2/虚拟机内部,不直接暴露到你的文件系统——这正是推荐用 tar 方式备份的原因。
-
-v 后面到底是卷名还是路径,怎么区分?
看第一段:不含 / 就是卷名(mysql-data:...),含 / 就是 bind mount(H:/work/... 或 /opt/data:...)。新语法 --mount type=volume,src=mysql-data,dst=/var/lib/mysql 更显式,不易歧义。
-
MySQL 容器反复重启/立刻退出,怀疑是卷的问题?
先 docker logs 看报错。两个高频原因:① 卷里残留了旧版本的数据目录,与新版 MySQL 不兼容(清卷重试);② 卷目录属主不对(Linux 上 MySQL 容器内进程是 uid 999):docker run --rm -v mysql-data:/data alpine chown -R 999:999 /data。
-
匿名卷是什么?
docker run -v /var/lib/mysql 不给卷名,Docker 自动生成一串哈希命名。容器删除时匿名卷容易沦为「无人认领的垃圾」,所以规范做法永远写显式卷名。
-
docker compose down 会删卷吗?
默认不会;但 down -v 会删掉 compose 文件里声明的卷——生产环境敲这个命令前请三读。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 数据只存容器可写层 | rm 容器数据即丢 | -v 卷名:/var/lib/mysql |
| bind mount 挂数据库数据目录 | 依赖宿主机路径 + 权限难管 + 遮盖陷阱 | 数据用 named volume |
配置挂载忘了 :ro | 容器可能写坏配置文件 | -v H:/work/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro |
| Windows 上 bind mount 用反斜杠路径 | 解析失败或挂错位置 | 用正斜杠 H:/work/conf |
docker volume prune 乱敲 | 误删未挂载但有价值的卷 | 先 volume ls 逐个确认 |
| 备份只拷贝 volume 目录 | Desktop 场景拿不到路径 | 临时容器 + tar 打包,或 mysqldump |
卡点急救
- 挂载后容器内看不到宿主机文件:检查路径是否存在、大小写是否一致、Windows 是否用了正斜杠;Docker Desktop → Settings → Resources → File Sharing 确认盘符已共享。
- MySQL 起不来报权限错误(Linux):卷目录属主与容器内用户(uid 999)不符。临时容器
chown -R 999:999 修复。
- 换了新容器数据「看起来丢了」:确认新容器挂的是不是同一个卷名(
docker inspect 新容器 看 Mounts)。常见错误是新容器忘了写 -v。
- 恢复备份后 MySQL 版本不兼容:物理备份跨大版本恢复会报错。改用 mysqldump 逻辑备份,或先用同版本容器恢复再升级。
- 磁盘被 volume 吃满:
docker system df -v 查看卷占用;确认无用的卷 volume rm,应用侧检查日志是否无限增长。
面试常问
-
volume 和 bind mount 的区别?(必考)
volume 由 Docker 管理与定位(可移植、支持卷驱动、首次挂载填充镜像内容),适合数据库等纯数据;bind mount 直接映射宿主机路径(依赖路径、可被容器写宿主机),适合配置文件与开发热挂载。敏感临时数据用 tmpfs。
-
容器化的 MySQL 如何保证数据不丢?
三层:named volume 持久化数据目录;定期备份(mysqldump 逻辑备份 + 卷快照);升级/迁移时先备份再操作,回滚依赖备份卷。
-
为什么数据库容器的数据不能放在容器内?
容器可写层随容器销毁,且可写层 IO 性能不如卷、无法被其他容器共享、备份困难。有状态服务容器化的前提就是存储与计算分离。
-
怎么备份一个 Docker volume?
标准套路:docker run --rm -v 数据卷:/source:ro -v 宿主机目录:/backup alpine tar czf /backup/x.tar.gz -C /source .。原理是借助临时容器把卷内容写到宿主机文件系统。
自测三问
-v mysql-data:/var/lib/mysql 和 -v H:/data/mysql:/var/lib/mysql 分别是哪种挂载?首次挂载行为有什么不同?
- 写出备份 mysql-data 卷的完整命令,并解释每个
-v 的作用。
- MySQL 容器起来就退出,日志报权限错误,你会按什么顺序排查?
小结一句话
卷把数据的生命周期从容器中解放出来——数据进 named volume、配置走只读 bind mount、备份用临时容器打包,这三条习惯守住「删容器不丢数据」的底线。
延伸