用容器跑数据库,最怕的一件事就是数据没了。很多新手第一次踩的坑是:把一个 MySQL 容器删掉重建,发现库里的数据全不见了——因为容器的可写层是跟着容器生命周期走的,容器一删,写在里面的数据就一起消失。Docker 数据卷(volume)就是专门解决这个问题的:它把数据放到容器之外,让数据独立于容器的生灭。这篇文章讲清数据卷和 bind mount 的区别与选择、创建与管理命令、-v 和 --mount 两种写法的差异,然后重点给出备份与恢复的完整实操,最后说明 UID/GID 权限问题这个高频坑,并给出备份策略建议。
一、为什么需要数据持久化
先回到容器原理:镜像由只读层组成,容器启动时会加一个可写层,你所有的写操作都落在可写层。可写层的生命周期和容器绑定,容器删除,可写层一并销毁。所以:
# 这个容器里的改动不会保存,删了就没了
docker run -d --name db mysql:8.0
# ... 写入数据 ...
docker rm -f db # 数据随之消失
要持久化,必须把需要保存的目录挂到容器外部。Docker 提供两类挂载方式:volume(数据卷,由 Docker 管理)和 bind mount(绑定挂载,直接挂宿主目录或文件)。此外还有 tmpfs(内存文件系统,重启即失),用得少。
二、volume 与 bind mount 的区别与选择
| 维度 | volume(数据卷) | bind mount(绑定挂载) |
|---|---|---|
| 存放位置 | Docker 管理目录,Linux 默认 /var/lib/docker/volumes/ |
你自己指定的任意宿主路径 |
| 管理方式 | docker volume 命令管理 |
直接用文件系统管理 |
| 可移植性 | 好,跨平台一致 | 依赖宿主路径,跨平台不一致 |
| 备份 | 需借临时容器 | 直接打包宿主目录即可 |
| 权限 | Docker 处理,问题较少 | 受宿主目录权限、SELinux 影响 |
| 适用 | 数据库、需长期保存的数据 | 配置文件、代码、需直接查看编辑的内容 |
| 性能 | 与 bind 相当(都在本地盘) | 与 volume 相当 |
| 首次空目录 | 会复制镜像中的内容进去 | 直接覆盖,可能遮住镜像内容 |
选择的经验法则很清晰:数据(数据库文件、用户上传、日志归档)用 volume;配置文件和代码用 bind mount;需要频繁在宿主上查看和编辑的内容用 bind mount。原因在于 volume 的权限和可移植性更好,而配置类内容需要人直接在宿主上改。
需要特别注意 bind mount 的一个陷阱:把一个空的宿主目录挂到容器里有内容的位置,会"遮住"镜像里原有的文件。比如把空目录挂到 /etc/nginx,nginx 的配置就全看不见了。而用命名 volume 挂载时,Docker 会把镜像里该目录的内容复制到空卷里,行为更友好。
三、数据卷的创建与管理
# 创建命名卷
docker volume create db-data
# 列出所有卷
docker volume ls
# 查看卷详情,Location 是它在宿主上的实际路径
docker volume inspect db-data
# 删除未被使用的卷(只在没有任何容器使用时才能删)
docker volume rm db-data
# 删除所有未使用的卷(危险:会删除所有悬空卷里的数据,务必先确认)
docker volume prune
# 用完即删:容器删除时顺带删除匿名卷
docker run -d --rm -v db-data:/var/lib/mysql mysql:8.0
docker volume inspect 输出的 Mountpoint 字段,通常是 /var/lib/docker/volumes/db-data/_data,直接进这个目录也能看到卷里的文件——但请务必理解:直接操作这个目录是有风险的,卷由 Docker 管理,绕过 Docker 直接改文件要格外小心,尤其别在容器运行时改动数据库文件。
四、-v 与 --mount 两种写法
挂载有两种语法,老式的 -v(--volume)和新式的 --mount。
# -v 语法:宿主路径或卷名:容器路径[:选项]
docker run -d --name db1 -v db-data:/var/lib/mysql mysql:8.0
docker run -d --name web1 -v /data/html:/usr/share/nginx/html:ro nginx:alpine
# --mount 语法:更明确,键值对形式
docker run -d --name db2 \
--mount type=volume,source=db-data,target=/var/lib/mysql \
mysql:8.0
docker run -d --name web2 \
--mount type=bind,source=/data/html,target=/usr/share/nginx/html,readonly \
nginx:alpine
两者有重要差异:
| 差异点 | -v | --mount |
|---|---|---|
| 语法 | 位置参数,逗号分隔选项 | 键值对,更易读 |
| 宿主路径不存在时 | 自动创建空目录 | 报错,不会偷偷创建 |
| 类型识别 | 靠路径是否以 / 开头猜 |
显式 type= 指定,不歧义 |
| 推荐度 | 简短命令方便 | 生产、Compose 及排错更推荐 |
关键的一条:-v 在宿主路径不存在时会静默创建一个空目录,这会把"路径写错"这种问题掩盖掉,等你发现文件没生效时已经排查半天。--mount 遇到不存在的宿主路径直接报错,帮你早发现错误。所以生产环境建议用 --mount。
五、备份:把卷打包成 tar
备份的基本思路是:起一个临时容器,把要备份的卷和一个宿主目录同时挂进去,在容器里用 tar 打包,然后把 tar 存到宿主上,再挪到异地。
# 备份命名卷 db-data 到宿主当前目录下的 backup 文件夹
# 思路:临时容器同时挂载 【数据卷】 和 【宿主备份目录】
docker run --rm \
-v db-data:/data:ro \
-v "$(pwd)/backup":/backup \
alpine \
tar czf /backup/db-data-$(date +%F).tar.gz -C /data .
# 查看备份文件
ls -lh backup/
这段命令的要点:用 --rm 让临时容器用完即删;把源卷以只读(:ro)挂进去,避免打包过程误改数据;-C /data . 表示进入 /data 后打包当前目录内容,避免 tar 里出现多余的路径前缀。这样得到的 tar 就是卷内容的完整快照。
对于数据库这类有状态服务,直接打包数据文件可能得到不一致的备份,因为备份过程中数据仍在变。更稳妥的做法是:先让数据库处于一致状态,或临时停写。以 MySQL 为例,正规做法是先 mysqldump 出逻辑备份,或者短暂停止数据库容器再打包文件。
# 方式一:逻辑备份(推荐,一致性最好)
docker exec db sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases' \
> backup/all-databases-$(date +%F).sql
# 方式二:物理备份前先停容器(短暂停写,适合不能容忍逻辑备份耗时的场景)
docker stop db
docker run --rm -v db-data:/data:ro -v "$(pwd)/backup":/backup \
alpine tar czf /backup/db-data-$(date +%F).tar.gz -C /data .
docker start db
另一种常见做法是用 --volumes-from 共享另一个容器的卷,适合容器里卷很多、想整体备份的场景:
# 借助一个正在运行的容器,把它的所有卷挂到临时容器里
docker run --rm \
--volumes-from db \
-v "$(pwd)/backup":/backup \
alpine tar czf /backup/db-full-$(date +%F).tar.gz -C / \
var/lib/mysql etc/mysql
--volumes-from db 表示继承名为 db 的容器所定义的所有挂载,临时容器因此能访问到同样的卷。
六、恢复:把 tar 解回去
恢复就是把备份 tar 解开到目标卷。目标卷可以是一个新建的卷,也可以是重建后的同名卷。
# 1) 准备一个目标卷(若沿用原卷,先确认它已存在且为空或可覆盖)
docker volume create db-data-restore
# 2) 解包到目标卷
docker run --rm \
-v db-data-restore:/data \
-v "$(pwd)/backup":/backup:ro \
alpine \
sh -c 'tar xzf /backup/db-data-2026-01-01.tar.gz -C /data'
# 3) 用恢复出来的卷启动服务
docker run -d --name db -v db-data-restore:/var/lib/mysql mysql:8.0
恢复数据库时要注意:目标目录必须是空的(或与原数据版本兼容),否则可能启动失败。恢复前务必确认目标卷没有正在写入,最好在服务停止状态下进行。恢复完成后,先验证数据可读、服务能正常启动,再切换生产流量。
恢复演练很重要。很多团队做了备份却从没恢复过,等到真出事才发现备份不完整或步骤走不通。建议定期(如每季度)在测试环境跑一遍恢复流程。
七、UID/GID 权限问题
这是容器数据卷上最容易被卡住的问题。现象是:容器以非 root 用户运行时,无法写入挂载的卷或宿主目录,报 Permission denied。
原因在于,容器内进程的用户 ID(UID)映射到宿主时,实际就是宿主的 UID。比如镜像里 nginx 用户是 UID 101,它写的文件在宿主上属主也是 UID 101;如果你用 bind mount 挂了一个属主是宿主 root、权限 755 的目录,nginx 就写不进去。volume 由 Docker 管理,权限通常处理得较好,但 bind mount 完全取决于宿主目录权限。
# 查容器内运行用户的 UID/GID
docker exec <容器名> id
# 查宿主目录属主
ls -ln /data/html
# 解决:把宿主目录属主改成容器用户的 UID(注意先确认 UID 再用)
sudo chown -R 101:101 /data/html
# 或在运行容器时用 --user 指定与宿主目录匹配的 UID
docker run -d --user 1000:1000 -v /data/html:/usr/share/nginx/html nginx:alpine
# SELinux 系统(RHEL/CentOS/Rocky)还要处理标签
docker run -d -v /data/html:/usr/share/nginx/html:z nginx:alpine
处理权限时有个原则:尽量让容器以非 root 运行(更安全),同时让宿主目录的属主匹配容器用户的 UID,而不是图省事把宿主目录设成 777——777 会削弱安全性,只在临时调试时用。
八、备份策略建议
数据卷只是"放在容器之外",不等于自动有备份。真正的备份要看这几条:
第一,备份要有多个副本,至少一份离开本机,理想情况是异地。放在同一台服务器上的备份,遇到整机故障会一起丢。
第二,区分逻辑备份和物理备份。数据库优先用逻辑备份(如 mysqldump、pg_dump),跨版本恢复更友好、一致性更好;文件类数据用 tar 打包即可。
第三,保留策略要明确:按日/周保留多少份,超过的自动清理。若无脑堆积,磁盘早晚会被备份撑满。
第四,定期做恢复演练。备份的价值只在它能恢复时成立。
第五,备份文件同样要注意权限和加密,数据库备份里往往包含敏感数据。
# 一个把备份同步到异地的示例(传输走密钥,目标目录受限)
rsync -avz -e "ssh -i ~/.ssh/backup_ed25519" \
"$(pwd)/backup/" backupuser@backup-host:/backup/$(hostname)/
# 清理本机 7 天前的备份(-delete 会真实删除,先 --dry-run 预演)
find "$(pwd)/backup" -name '*.tar.gz' -mtime +7 -print # 先只打印
# find "$(pwd)/backup" -name '*.tar.gz' -mtime +7 -delete # 确认后再删
上面 find ... -delete 会真实删除文件,执行前务必先只打印确认路径与范围。
常见问题
Q:volume 和 bind mount 到底怎么选?
数据用 volume(数据库文件、上传内容),配置和代码用 bind mount。volume 权限和可移植性更好,bind mount 便于宿主直接查看编辑。
Q:删容器会删卷吗?
不会。命名卷独立于容器存在,docker rm 容器不会删命名卷(匿名卷在 --rm 时会删)。但 docker volume prune 或 docker compose down -v 会删卷,执行前确认。
Q:为什么挂载后容器里原来的文件不见了?
bind mount 用空宿主目录挂到有内容的容器路径,会遮住原内容。改用命名卷,Docker 会把镜像内容复制进空卷;或者确保宿主目录里已有需要的文件。
Q:备份数据库时直接 tar 数据目录靠谱吗?
不完全靠谱。备份期间数据仍在写,可能得到不一致的快照。优先用逻辑备份(mysqldump/pg_dump),或短暂停库后再打包文件。
Q:容器用户写不进挂载目录怎么办?
这是 UID/GID 不匹配。用 docker exec <名> id 看容器用户 ID,把宿主目录属主改成该 ID,或用 --user 对齐。SELinux 系统再加 :z 标签。不要图省事直接 777。
总结
容器可写层随容器销毁,数据持久化必须挂到容器之外的 volume 或 bind mount 上;数据用 volume,配置代码用 bind mount,是最实用的分工。
挂载语法上,-v 简短但会静默创建不存在的宿主目录,--mount 更明确且会报错,生产环境推荐后者。
备份的核心思路是"临时容器同时挂载数据卷和宿主备份目录,用 tar 打包",数据库优先逻辑备份;恢复时把 tar 解回目标卷,并在测试环境定期演练。
UID/GID 权限问题源自容器用户与宿主目录属主不匹配,正确做法是让两者对齐或用 --user,避免使用 777 和长期以 root 运行。
卷不是备份。备份要异地、要多份、要有保留策略、要能恢复;只把数据放在 volume 上,只解决了"不随容器消失",没解决"机器坏了怎么办"。
企业QQ咨询




