前言

你有没有遇到过这种情况:

辛辛苦苦往 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 管理公司内部镜像 🚀


📌 觉得有帮助点个赞!有问题欢迎评论区交流 👇

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐