Chapter 06

数据卷与持久化

掌握三种挂载方式,让容器化应用的数据安全持久地保存

为什么需要持久化

容器的设计哲学是无状态(Stateless)的:容器可以被随时创建、销毁、替换。当一个容器被删除时,它的可写层(Container Layer)也随之消失,其中写入的所有文件都会永久丢失。


# 演示数据丢失
docker run --name test-db mysql:8.0 -e MYSQL_ROOT_PASSWORD=root
# ... 写入数据 ...
docker rm -f test-db   # 删除容器,数据消失!

解决方案是将数据存储在容器之外的持久化存储中。Docker 提供三种挂载方式。

三种挂载方式对比

类型语法示例存储位置适用场景
Volume(命名卷) -v mydata:/var/lib/mysql Docker 管理(/var/lib/docker/volumes/ 数据库数据、需要备份的持久数据
Bind Mount(绑定挂载) -v /host/path:/container/path 宿主机任意路径 开发时代码热重载、挂载配置文件
tmpfs Mount --tmpfs /run 宿主机内存 敏感临时数据、高性能临时文件

存储位置对比图

宿主机文件系统
/var/lib/docker/volumes/(Volume 存储区域)
宿主机任意路径(Bind Mount 来源)
容器可写层(临时,随容器销毁)

Volume(命名卷)

Volume 是 Docker 推荐的持久化方式。由 Docker 完全管理,与宿主机的具体目录结构解耦。

Volume 操作命令


# 创建命名卷
docker volume create mysql-data

# 列出所有卷
docker volume ls

# 查看卷详情(存储路径、挂载点)
docker volume inspect mysql-data

# 删除指定卷
docker volume rm mysql-data

# 清理所有未使用的卷(危险!会丢失数据)
docker volume prune

使用 Volume 挂载(-v 语法)


# 命名卷:卷名:容器路径
docker run -d \
  --name my-mysql \
  -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  mysql:8.0

# 如果 mysql-data 卷不存在,Docker 自动创建

使用 --mount 语法(推荐,更清晰)


docker run -d \
  --name my-mysql \
  --mount type=volume,source=mysql-data,target=/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  mysql:8.0
💡

-v vs --mount-v 语法简洁但容易混淆(命名卷 vs 绑定挂载取决于路径是否以 / 开头)。--mount 语法更明确,推荐在脚本和 Compose 中使用。

Bind Mount(绑定挂载)

将宿主机上的具体文件或目录挂载到容器内。主要用于开发场景,让容器实时看到宿主机上的代码修改。


# 将当前目录挂载到容器(开发热重载)
docker run -d \
  --name dev-server \
  -v $(pwd):/app \
  -p 3000:3000 \
  node:18-alpine \
  sh -c "npm install && npm run dev"

# 挂载单个配置文件
docker run -d \
  -v /host/nginx.conf:/etc/nginx/nginx.conf:ro \  # :ro = 只读
  nginx:1.25

只读挂载


# -v 语法:在路径后加 :ro
docker run -v /config:/etc/app:ro myapp

# --mount 语法:添加 readonly 选项
docker run --mount type=bind,source=/config,target=/etc/app,readonly myapp
⚠️

Bind Mount 的注意事项

tmpfs Mount

数据存储在宿主机内存中,容器停止时自动清除。适合存储敏感临时数据(如密钥、Session Token)。


# 挂载 tmpfs 到 /run 目录
docker run --tmpfs /run \
  --tmpfs /tmp:rw,size=64m \
  myapp

# --mount 语法
docker run --mount type=tmpfs,target=/tmp,tmpfs-size=64m myapp

数据备份与恢复

Volume 备份的常用技巧:使用一个临时容器同时挂载数据卷和宿主机备份目录,执行 tar 打包:


# 备份:将 mysql-data 卷打包到当前目录
docker run --rm \
  -v mysql-data:/source:ro \
  -v $(pwd):/backup \
  busybox \
  tar czf /backup/mysql-backup-$(date +%Y%m%d).tar.gz -C /source .

# 恢复:从备份文件还原到新卷
docker volume create mysql-data-restored
docker run --rm \
  -v mysql-data-restored:/target \
  -v $(pwd):/backup \
  busybox \
  tar xzf /backup/mysql-backup-20241201.tar.gz -C /target

实战:MySQL 容器数据持久化

演示删除容器再重新创建后数据仍然存在:


# 1. 创建命名卷
docker volume create mysql-persistent

# 2. 启动 MySQL 并挂载卷
docker run -d \
  --name mysql-demo \
  -v mysql-persistent:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=mypassword \
  -e MYSQL_DATABASE=testdb \
  -p 3306:3306 \
  mysql:8.0

# 3. 等待 MySQL 启动,写入数据
docker exec -it mysql-demo mysql -uroot -pmypassword testdb

# 在 MySQL 控制台内执行:
# CREATE TABLE users (id INT, name VARCHAR(50));
# INSERT INTO users VALUES (1, 'Alice');
# exit

# 4. 删除容器
docker rm -f mysql-demo

# 5. 重新创建容器,挂载同一个卷
docker run -d \
  --name mysql-demo-new \
  -v mysql-persistent:/var/lib/mysql \  # 同一个卷!
  -e MYSQL_ROOT_PASSWORD=mypassword \
  -p 3306:3306 \
  mysql:8.0

# 6. 数据还在!
docker exec -it mysql-demo-new mysql -uroot -pmypassword testdb
# SELECT * FROM users;  → 返回 1, Alice

Volume 驱动与远程存储

默认 Volume 存储在本地磁盘,但 Docker 支持通过 Volume 驱动插件将数据存储在远程系统。

# 创建 NFS 类型的 Volume
docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100,rw \
  --opt device=:/data/shared \
  nfs-data

Volume 的底层原理

当你挂载一个 Volume 到容器时,实际发生了什么?

# Volume 挂载的工作流程:

1. Docker 创建 Volume 目录(如果不存在)
   /var/lib/docker/volumes/mysql-data/_data/

2. 容器启动时,Docker 通过 bind mount 将该目录
   挂载到容器内的指定路径 /var/lib/mysql

3. 与普通 bind mount 不同,Volume 由 Docker 管理:
   - 内容不随容器删除而消失
   - 可以被多个容器同时挂载(读写锁由应用自己保证)
   - 可以通过 docker volume 命令管理
⚠️

多容器同时写入同一 Volume 的危险 — Docker 允许多个容器同时挂载同一个 Volume 进行读写,但不提供任何并发控制。如果两个数据库容器同时写入同一目录,数据将损坏。同一 Volume 只应被一个可写容器使用,其他需要读取的容器应以只读方式挂载(:ro)。

开发实战:代码热重载与 node_modules 陷阱

在 Node.js 开发中,Bind Mount + Volume 的组合解决了 node_modules 的路径冲突问题:

# 问题:-v $(pwd):/app 会把宿主机的 node_modules 覆盖容器内的
# 解决方案:用匿名 Volume 保护容器内的 node_modules
docker run -d \
  -v $(pwd):/app \              # bind mount 代码(覆盖 /app)
  -v /app/node_modules \        # 匿名 Volume 保护容器的 node_modules
  -p 3000:3000 \
  node:18-alpine \
  npm run dev

# 原理:Volume 比 bind mount 优先级更高
# /app 被 bind mount(宿主机代码)覆盖
# /app/node_modules 被 Volume 覆盖(保留容器内安装的模块)

常见边界情况

⚠️

Volume 挂载目标已有文件 — 当你将一个空 Volume 挂载到容器内一个已有文件的目录时(首次),Docker 会将容器内的文件复制到 Volume 中(Seeding)。但如果 Volume 已有内容,则直接使用 Volume 中的内容,覆盖容器内原有文件。这是数据库镜像初始化工作的原理,也是很多人遇到"数据没有初始化"问题的原因。

⚠️

docker volume prune 会删除所有未使用的卷 — "未使用"指当前没有容器挂载该卷,即使卷中有重要数据。运行 docker compose down(不加 -v)后,卷虽然未挂载但仍然存在;但如果紧接着运行 docker volume prune,数据就会消失。养成显式命名卷(而非匿名卷)的习惯,便于识别和保护重要数据。

本章小结 — Volume 是持久化生产数据的最佳选择;Bind Mount 适合开发时代码同步;tmpfs 适合敏感临时数据。Volume 的底层是 bind mount 到 Docker 管理的目录。使用命名 Volume 而非匿名 Volume,定期备份,避免 prune 误删。