3_Docker数据卷实践
1. 核心概念
Docker 数据卷是独立于容器生命周期的数据存储机制,将”需要保留的数据”从”随时可能被销毁的容器”中分离出来:
- 数据持久化:容器天生是”用完即弃”的,没有数据卷,
docker rm一下数据库数据就全没了 - 解耦架构:数据和应用分离,升级容器镜像时不影响已有数据
- 多容器共享:多个容器可以同时访问同一个数据卷,实现数据协作
类比:你租了一间酒店房间(容器),退房后房间会被清扫一空。但酒店前台有个保险柜(数据卷)——把贵重物品存进去,无论换哪个房间,东西都还在。
1.1 全景架构图
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F', 'primaryBorderColor': '#2563EB', 'lineColor': '#60A5FA', 'secondaryColor': '#10B981', 'tertiaryColor': '#F59E0B'}}}%%
flowchart LR
subgraph Storage ["存储层"]
V["Named Volume<br/>/var/lib/docker/volumes/"]
BM["Bind Mount<br/>宿主机任意路径"]
TF["tmpfs<br/>内存 RAM"]
end
subgraph Containers ["容器层"]
C1["容器 A"]
C2["容器 B"]
end
V -->|"挂载"| C1
V -->|"共享"| C2
BM -->|"挂载"| C1
TF -->|"挂载"| C2
classDef primary fill:#3B82F6,stroke:#2563EB,color:#fff
classDef success fill:#10B981,stroke:#059669,color:#fff
classDef warning fill:#F59E0B,stroke:#D97706,color:#fff
class V,BM primary
class TF warning
class C1,C2 success1.2 工作原理
数据卷的生命周期可以拆分为四个阶段:
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F', 'primaryBorderColor': '#2563EB', 'lineColor': '#60A5FA', 'secondaryColor': '#10B981', 'tertiaryColor': '#F59E0B'}}}%%
flowchart LR
A(["创建 Volume"]) --> B(["挂载到容器"])
B --> C(["容器读写数据"])
C --> D(["容器销毁"])
D --> E{{"卷是否还需要?"}}
E -->|"是"| F(["挂载到新容器"])
E -->|"否"| G(["手动删除卷"])
F --> C
classDef primary fill:#3B82F6,stroke:#2563EB,color:#fff
classDef success fill:#10B981,stroke:#059669,color:#fff
classDef decision fill:#F59E0B,stroke:#D97706,color:#fff
classDef danger fill:#EF4444,stroke:#DC2626,color:#fff
class A,B primary
class C,F success
class E decision
class D,G danger- 创建:通过
docker volume create my-volume创建命名卷。Docker 在/var/lib/docker/volumes/下创建对应目录 - 挂载:启动容器时通过
-v或--mount将卷挂载到容器内指定路径 - 读写:容器启动时,Docker 通过内核的 bind mount 机制将卷目录挂载到容器的文件系统命名空间中,之后的读写操作由内核直接处理,容器进程对此无感知
- 解耦:容器停止或删除时,挂载关系解除,但卷的数据保持不变,可随时挂载到新容器
1 | # 完整示例 |
2. 三种挂载方式对比
最容易混淆的就是三种数据挂载方式。用一句话记住区别:命名卷是 Docker 管的”U 盘”,绑定挂载是主机的”共享文件夹”,tmpfs 是内存中的”临时文件夹”。
2.1 选型决策
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F', 'primaryBorderColor': '#2563EB', 'lineColor': '#60A5FA', 'secondaryColor': '#10B981', 'tertiaryColor': '#F59E0B'}}}%%
flowchart TD
START{{"需要持久化数据吗?"}}
START -->|"不需要"| TMPFS(["tmpfs Mount"])
START -->|"需要"| Q2{{"需要精确控制宿主机路径吗?"}}
Q2 -->|"是(开发/调试/手动管理)"| BIND(["Bind Mount"])
Q2 -->|"否(生产环境推荐)"| NAMED(["Named Volume"])
TMPFS --> U1["场景: 密钥、临时缓存"]
BIND --> U2["场景: 代码热更新、配置文件注入"]
NAMED --> U3["场景: 数据库、用户上传、应用数据"]
classDef primary fill:#3B82F6,stroke:#2563EB,color:#fff
classDef success fill:#10B981,stroke:#059669,color:#fff
classDef warning fill:#F59E0B,stroke:#D97706,color:#fff
classDef decision fill:#0EA5E9,stroke:#0284C7,color:#fff
class NAMED primary
class BIND success
class TMPFS warning
class START,Q2 decision
class U1,U2,U3 default2.2 详细对比
| 特性 | Named Volume | Bind Mount | tmpfs Mount |
|---|---|---|---|
| 一句话描述 | Docker 管理的”U 盘” | 主机上的”共享文件夹” | 内存中的”临时文件夹” |
| 管理方 | Docker 引擎 | 用户/宿主机 | 系统内存 |
| 存储位置 | /var/lib/docker/volumes/ | 宿主机任意路径 | 内存 RAM |
| 持久性 | ✅ 持久 | ✅ 持久 | ❌ 容器移除即丢失(stop 后 start 仍在) |
| 可移植性 | ⭐⭐⭐ 不依赖主机目录结构 | ⭐ 依赖宿主机路径 | 不适用 |
| 多容器共享 | ✅ 支持 | ✅ 支持 | ❌ 不支持(每个容器独立内存区域) |
| 自动填充 | ✅ 空卷首次挂载时复制容器目录内容 | ❌ 主机目录直接覆盖容器目录 | 不适用 |
| 性能 | Linux 原生与 Bind Mount 相当;Docker Desktop 上显著优于 Bind Mount | Linux 原生高,Docker Desktop 有损耗 | 最高(内存操作) |
| 典型场景 | 数据库文件、用户上传、应用配置 | 开发环境源码同步、Nginx 配置 | 密钥、缓存、临时会话 |
--mount 语法 | type=volume,source=my-vol,target=/app | type=bind,source=/path/on/host,target=/app | type=tmpfs,destination=/app |
2.3 语法区分
Docker 通过 : 前面的部分判断挂载类型——不是”绝对路径 vs 相对路径”,而是”路径 vs 名字”:
1 | # Named Volume — 只写名字,没有路径分隔符 |
判断规则:
- 以
/或./开头 → Bind Mount(这是一个路径) - 不以
/开头,没有路径分隔符 → Named Volume(这是一个名字)
./data是 Bind Mount(以./开头,是路径),mydata是 Named Volume(只是一个名字)。Compose 注意:在
docker-compose.yml中,data:/path是 Named Volume,./data:/path是 Bind Mount。不加./前缀的相对路径名会被当作卷名,这是最常见的混淆场景。
2.4 自动填充机制
当你将一个空的命名卷挂载到容器中一个已有内容的目录时,Docker 会自动将该目录内容复制到卷里。触发条件:
- 必须是 Named Volume 或匿名卷,Bind Mount 不支持(Bind Mount 是主机优先,直接覆盖容器目录)
- 卷必须是空的。非空卷会反过来覆盖容器内的目录
- 只在首次挂载时触发。一旦卷被填充过,后续挂载不再自动填充
3. 实践要点
3.1 最佳实践
生产环境优先使用 Named Volume:
1 | docker volume create mydata |
开发环境用 Bind Mount 实现热更新:
1 | docker run -v $(pwd)/src:/app/src node |
定期清理孤立数据卷:
1 | docker system df -v # 查看磁盘占用 |
推荐使用 --mount 替代 -v:
1 | # --mount 语法更明确,支持额外选项(如 readonly) |
利用 Volume Driver 对接云存储(如 AWS S3、Azure Files、NFS),构建跨主机高可用服务。
3.2 新手三大坑
1 | ❌ 错误:Bind Mount 时宿主机源路径不存在或为空目录 |
1 | ❌ 错误:删除容器时带了 -v 参数,把匿名卷一起删了 |
1 | ❌ 错误:多个容器同时写同一个数据卷,没有做并发控制 |
3.3 常见问题
权限问题(Permission Denied)
容器内应用通常以非 root 用户运行,其 UID/GID 与主机目录所有者不匹配时无法写入。正确做法是确保容器用户的 UID/GID 与主机目录权限匹配,或启动时指定 --user $(id -u):$(id -g)。
匿名卷的滥用
1 | # 匿名卷:名字是随机哈希,难以管理 |
docker run -v /data 这种只指定容器路径的写法会创建匿名卷,名字是一串随机哈希,极难管理。始终使用命名卷。
macOS/Windows 上找不到 Volume 目录
Docker Volume 默认存储在 /var/lib/docker/volumes/,目录归 root 所有。在 macOS 和 Windows 上,Docker 运行在 Linux 虚拟机里,宿主机上找不到这个路径——它藏在虚拟机的磁盘镜像里。
4. 动手验证
创建一个数据卷,验证数据在容器销毁后仍然存在:
1 | # 1. 创建命名卷 |
预期结果:第 3 步输出 hello volumes,证明数据在容器销毁后依然存在。
进阶练习:参考 PostgreSQL 官方镜像,用命名卷部署数据库,验证数据持久化:
1 | docker run -d --name pg \ |