Docker Volume:容器删了,数据为什么还在?
前言
你有没有遇到过这种情况:
辛辛苦苦往 MySQL 容器里写了一堆数据,结果一不小心 docker rm 删掉容器,数据全没了。
这是 Docker 初学者最常踩的坑之一。要搞清楚为什么会这样,以及怎么解决,得先从容器存储的底层原理说起。
一、先从生活类比说起
你买了一台新电脑,系统装在硬盘里。你在上面装软件、存文件,用得好好的。突然有一天电脑坏了,你换了一台新电脑——之前存的文件全没了,因为它们都在那块硬盘上,换了机器就访问不到了。
但如果你用的是 U 盘或者移动硬盘存文件,换台电脑插上去,数据还在。
Docker 的逻辑完全一样:
- 容器本身就像那台电脑,是临时的
- 容器内部的可写层就像内置硬盘,容器删了数据就没了
- Volume 就像移动硬盘,数据存在宿主机上,容器删了照样在,换个容器插上去继续用
二、为什么容器删了数据会消失?
要理解这个问题,需要先了解容器的存储结构。
Docker 镜像采用联合文件系统(UnionFS),由多个只读层叠加而成。每一条 Dockerfile 指令生成一层,这些层是共享的、只读的。
为什么要分层? 主要是为了复用和节省空间。比如你有十个基于 ubuntu:22.04 的镜像,这十个镜像共享同一份 ubuntu 底层,宿主机上只存一份,而不是存十份。哪个镜像需要用,直接引用就行。
当你运行一个容器时,Docker 会在这些只读层之上再加一层可写层(Container Layer)。这层可写层存的是容器运行期间产生的所有变化——你在容器里新建的文件、修改的配置、MySQL 写入的数据库记录,全部存在这里。可写层是每个容器独有的,和镜像层完全分开。
┌─────────────────────┐
│ 可写层(容器独有) │ ← docker rm 时这层消失,里面的数据全没了
├─────────────────────┤
│ 镜像层3(只读) │
├─────────────────────┤
│ 镜像层2(只读) │ ← 镜像层只读,容器无法修改它,所以删容器不影响镜像
├─────────────────────┤
│ 镜像层1(只读) │
└─────────────────────┘
所以当你 docker rm 删掉容器时,只有可写层消失,镜像层完全不受影响。这就是为什么删了容器还能再 docker run 跑起来——镜像还在,Docker 重新创建一个新的可写层叠在镜像上,就是一个全新干净的容器,就好像这个应用从来没被用过一样。
解决方案就是:把需要持久化的数据挂载到容器外部。 Docker 提供了三种挂载方式。
三、方式一 —— Volume 具名卷(生产环境推荐)
原理:Docker 托管的存储卷
Volume 是 Docker 官方推荐的持久化方案。Docker 在宿主机的固定目录(Linux 下是 /var/lib/docker/volumes/)统一管理所有卷,你不需要关心具体路径,只需要给卷起个名字。
bash
# 创建一个具名卷
docker volume create mysql-data
# 运行容器时挂载卷,各参数含义:
# -d 后台运行,不占终端
# -v mysql-data:/var/lib/mysql 把具名卷 mysql-data 挂载到容器内 MySQL 的数据目录
# -e MYSQL_ROOT_PASSWORD 设置 root 密码,MySQL 8.0 启动必须指定,否则容器直接退出
# --name mysql 给容器起名
docker run -d \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=your_password \
--name mysql \
mysql:8.0
# 查看所有卷
docker volume ls
# 查看卷详情
docker volume inspect mysql-data
执行 docker volume inspect mysql-data 你会看到:
json
{
"Mountpoint": "/var/lib/docker/volumes/mysql-data/_data",
"Name": "mysql-data"
}
这里有个容易迷惑的地方——Mountpoint 显示的路径和 -v 里写的路径不一样:
/var/lib/docker/volumes/mysql-data/_data是宿主机上卷的实际存储位置,由 Docker 自动管理/var/lib/mysql是容器内部的挂载路径,是 MySQL 存数据的地方
两者的关系是:Docker 把宿主机的那个目录挂载到容器内的 /var/lib/mysql,容器往 /var/lib/mysql 写数据,实际上写进了宿主机的 _data 目录。用 Volume 时你不需要关心宿主机路径,只需要记住卷名就够了。
bash
# 删除卷(注意:卷被容器使用时无法直接删除)
# 需要先停止并删除容器,再删卷
docker stop mysql
docker rm mysql
docker volume rm mysql-data
# 或者强制删除容器再删卷
docker rm -f mysql
docker volume rm mysql-data
# 删除所有未使用的卷(正在被容器使用的卷不会被删除,这是保护机制)
docker volume prune
验证数据持久化:
bash
# 第一步:进入容器,创建一张测试表并写入数据
docker exec -it mysql mysql -uroot -pyour_password
# 在 MySQL 里执行:
CREATE DATABASE testdb;
USE testdb;
CREATE TABLE users (id INT, name VARCHAR(50));
INSERT INTO users VALUES (1, 'docker-test');
exit
# 第二步:删掉容器
docker rm -f mysql
# 第三步:用同一个卷重新创建容器
docker run -d \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=your_password \
--name mysql-new \
mysql:8.0
# 等几秒容器启动完,进入查看数据是否还在
docker exec -it mysql-new mysql -uroot -pyour_password -e "SELECT * FROM testdb.users;"
# 能看到之前插入的数据,说明持久化成功
为什么生产环境推荐 Volume?
- Docker 统一管理,不容易误删:Volume 存在 Docker 自己管理的目录里,不会因为你手滑删了某个宿主机目录就导致数据丢失。而且
docker volume prune只删未被任何容器引用的卷,正在使用的卷有保护,不会被误删。 - 跨平台兼容性好:Windows/Mac/Linux 行为一致,不需要关心宿主机路径差异。
- 支持 Volume Driver 对接云存储:默认是
local驱动存本地,但可以换成 NFS、S3 等云存储驱动,容器写数据直接写到云上,适合分布式和云原生场景。 - 容器之间共享数据方便:多个容器可以挂载同一个卷互相共享数据。
结合项目理解两处 volumes 的区别:
docker-compose.yaml 里有两个地方出现 volumes,含义完全不同,位置不同作用也不同:
yaml
services:
mysql:
volumes:
- mysql_data:/var/lib/mysql # 服务级别的 volumes:声明这个容器要挂载哪个卷到哪个路径
prometheus:
volumes:
- prometheus_data:/prometheus
- ./deploy/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml # Bind Mount,挂载配置文件
# 文件最底部,顶级的 volumes:声明上面用到的具名卷,让 Docker 知道要创建这些卷
# 如果不在这里声明,docker compose up 时会报错找不到卷
volumes:
mysql_data:
prometheus_data:
简单说:服务里的 volumes 是"用哪个卷挂到哪里",底部的 volumes 是"创建这些卷"。
四、方式二 —— Bind Mount 绑定挂载(开发环境推荐)
原理:直接映射宿主机目录
Bind Mount 是把宿主机上的任意一个目录直接映射进容器。宿主机和容器看到的是同一份文件,改哪边另一边立刻生效。
bash
# Windows 路径写法,把宿主机目录挂载到容器的 /app
# 注意:node:22-alpine 没有默认启动命令,需要加 sleep infinity 保持容器运行
docker run -d \
-v E:\CSDN\测试Bind挂载:/app \
--name my-app \
node:22-alpine \
sleep infinity
# 进入容器验证
docker exec -it my-app sh
ls /app # 能看到宿主机目录里的文件
# 或者用 --mount 写法(参数更清晰,效果一样)
docker run -d \
--mount type=bind,source=E:\CSDN\测试Bind挂载,target=/app \
--name my-app \
node:22-alpine \
sleep infinity
验证双向同步:
进入容器后执行 ls /app,然后去宿主机 E:\CSDN\测试Bind挂载 新建一个文件,回到容器再执行 ls /app,能看到刚才新建的文件,说明双向同步生效。
为什么开发环境推荐 Bind Mount?
本地开发时,你修改了代码,容器里立刻能看到变化,不需要重新构建镜像。这就是为什么很多开发环境的 docker-compose.yaml 里会有这样的配置:
yaml
volumes:
- ./src:/app/src # 本地代码实时同步进容器
和 Volume 的区别:
Bind Mount 的路径你自己指定,Docker 不管理它,也不会出现在 docker volume ls 里。如果宿主机上的目录被误删,容器里也访问不到了,没有 Volume 那样的保护机制。
你的项目里 gateway 服务就用了 Bind Mount:
yaml
volumes:
- ./uploads:/app/uploads # 用户上传的文件直接映射到宿主机目录
五、方式三 —— tmpfs 内存挂载
原理:数据写入内存,不落盘
tmpfs 把数据写进宿主机内存,不写入磁盘。容器停止后数据立刻消失,就像断电后 RAM 里的数据一样。
bash
docker run -d \
--mount type=tmpfs,destination=/tmp \
--name my-app \
nginx
什么时候用?
- 存放临时的敏感数据(比如 token、密钥),不想落盘留痕
- 需要极高读写性能的临时数据(内存比磁盘快几个数量级)
- 容器重启后数据自动清空,省去手动清理
缺点:
- 数据不持久,容器停止即消失
- 占用宿主机内存,数据量大时要注意
六、三种挂载方式对比
| Volume | Bind Mount | tmpfs | |
|---|---|---|---|
| 数据位置 | Docker 管理目录 | 宿主机任意目录 | 宿主机内存 |
| 持久化 | ✅ | ✅ | ❌ |
| Docker 管理 | ✅ | ❌ | ❌ |
| 跨平台兼容 | 好 | 一般 | 好 |
| 适用场景 | 数据库、生产环境 | 开发热更新 | 敏感数据、高性能临时存储 |
| 性能 | 好 | 好 | 最好 |
七、结合 Docker Compose 看数据持久化
回顾我们项目里的 docker-compose.yaml,数据持久化的设计一目了然:
yaml
# MySQL 用具名卷——数据库数据绝对不能丢
mysql:
volumes:
- mysql_data:/var/lib/mysql
# Prometheus 用具名卷存监控数据,配置文件用 Bind Mount 方便修改
prometheus:
volumes:
- prometheus_data:/prometheus
- ./deploy/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
# gateway 用 Bind Mount——用户上传的文件映射到宿主机,方便直接访问
gateway:
volumes:
- ./uploads:/app/uploads
设计原则:
- 数据库、中间件的数据目录 → 用 Volume,Docker 统一管理,安全可靠
- 配置文件、代码目录 → 用 Bind Mount,方便修改和查看
- 临时敏感数据 → 用 tmpfs,不落盘
总结
| 概念 | 作用 |
|---|---|
| UnionFS 可写层 | 容器运行时的临时读写空间,删容器即消失 |
| Volume 具名卷 | Docker 托管的持久化存储,生产环境首选 |
| Bind Mount | 宿主机目录直接映射,开发热更新利器 |
| tmpfs | 内存挂载,不落盘,适合临时敏感数据 |
下一篇:私有镜像仓库搭建——用 Harbor 管理公司内部镜像 🚀
📌 觉得有帮助点个赞!有问题欢迎评论区交流 👇
更多推荐


所有评论(0)