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 success

1.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
  1. 创建:通过 docker volume create my-volume 创建命名卷。Docker 在 /var/lib/docker/volumes/ 下创建对应目录
  2. 挂载:启动容器时通过 -v--mount 将卷挂载到容器内指定路径
  3. 读写:容器启动时,Docker 通过内核的 bind mount 机制将卷目录挂载到容器的文件系统命名空间中,之后的读写操作由内核直接处理,容器进程对此无感知
  4. 解耦:容器停止或删除时,挂载关系解除,但卷的数据保持不变,可随时挂载到新容器
1
2
3
4
# 完整示例
docker volume create my-volume
docker run -d -v my-volume:/app/data my-image
# 容器内写 /app/data → 实际写入 /var/lib/docker/volumes/my-volume/_data

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 default

2.2 详细对比

特性Named VolumeBind Mounttmpfs Mount
一句话描述Docker 管理的”U 盘”主机上的”共享文件夹”内存中的”临时文件夹”
管理方Docker 引擎用户/宿主机系统内存
存储位置/var/lib/docker/volumes/宿主机任意路径内存 RAM
持久性✅ 持久✅ 持久❌ 容器移除即丢失(stop 后 start 仍在)
可移植性⭐⭐⭐ 不依赖主机目录结构⭐ 依赖宿主机路径不适用
多容器共享✅ 支持✅ 支持❌ 不支持(每个容器独立内存区域)
自动填充✅ 空卷首次挂载时复制容器目录内容❌ 主机目录直接覆盖容器目录不适用
性能Linux 原生与 Bind Mount 相当;Docker Desktop 上显著优于 Bind MountLinux 原生高,Docker Desktop 有损耗最高(内存操作)
典型场景数据库文件、用户上传、应用配置开发环境源码同步、Nginx 配置密钥、缓存、临时会话
--mount 语法type=volume,source=my-vol,target=/apptype=bind,source=/path/on/host,target=/apptype=tmpfs,destination=/app

2.3 语法区分

Docker 通过 : 前面的部分判断挂载类型——不是”绝对路径 vs 相对路径”,而是”路径 vs 名字”:

1
2
3
4
5
# Named Volume — 只写名字,没有路径分隔符
-v volume-name:/container/path

# Bind Mount — 写完整的宿主机路径
-v /host/path:/container/path

判断规则:

  • /./ 开头 → 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 会自动将该目录内容复制到卷里。触发条件:

  1. 必须是 Named Volume 或匿名卷,Bind Mount 不支持(Bind Mount 是主机优先,直接覆盖容器目录)
  2. 卷必须是空的。非空卷会反过来覆盖容器内的目录
  3. 只在首次挂载时触发。一旦卷被填充过,后续挂载不再自动填充

3. 实践要点

3.1 最佳实践

生产环境优先使用 Named Volume:

1
2
docker volume create mydata
docker run -v mydata:/app/data nginx

开发环境用 Bind Mount 实现热更新:

1
docker run -v $(pwd)/src:/app/src node

定期清理孤立数据卷:

1
2
docker system df -v        # 查看磁盘占用
docker volume prune # 清理未使用的卷

推荐使用 --mount 替代 -v

1
2
# --mount 语法更明确,支持额外选项(如 readonly)
docker run --mount type=volume,source=mydata,target=/app/data,readonly nginx

利用 Volume Driver 对接云存储(如 AWS S3、Azure Files、NFS),构建跨主机高可用服务。

3.2 新手三大坑

1
2
3
❌ 错误:Bind Mount 时宿主机源路径不存在或为空目录
💥 后果:Docker 自动创建空目录并挂载,容器内该路径下原有文件被覆盖,应用启动失败
✅ 正解:始终确保宿主机源路径存在且内容正确,挂载前先检查
1
2
3
❌ 错误:删除容器时带了 -v 参数,把匿名卷一起删了
💥 后果:docker rm -v 会删除该容器关联的匿名卷(命名卷不受影响),匿名卷中的数据无法恢复
✅ 正解:使用命名卷,删容器前确认是否需要保留数据
1
2
3
❌ 错误:多个容器同时写同一个数据卷,没有做并发控制
💥 后果:数据竞争导致文件损坏,尤其数据库文件
✅ 正解:数据库等有状态服务用独立卷,或通过应用层加锁

3.3 常见问题

权限问题(Permission Denied)

容器内应用通常以非 root 用户运行,其 UID/GID 与主机目录所有者不匹配时无法写入。正确做法是确保容器用户的 UID/GID 与主机目录权限匹配,或启动时指定 --user $(id -u):$(id -g)

匿名卷的滥用

1
2
3
4
# 匿名卷:名字是随机哈希,难以管理
docker run -v /app/data my-image
# 命名卷:有意义的名字,便于引用和维护
docker run -v my-app-data:/app/data my-image

docker run -v /data 这种只指定容器路径的写法会创建匿名卷,名字是一串随机哈希,极难管理。始终使用命名卷。

macOS/Windows 上找不到 Volume 目录

Docker Volume 默认存储在 /var/lib/docker/volumes/,目录归 root 所有。在 macOS 和 Windows 上,Docker 运行在 Linux 虚拟机里,宿主机上找不到这个路径——它藏在虚拟机的磁盘镜像里。

4. 动手验证

创建一个数据卷,验证数据在容器销毁后仍然存在:

1
2
3
4
5
6
7
8
9
10
# 1. 创建命名卷
docker volume create test-vol
# 2. 启动容器,写入数据
docker run --rm -v test-vol:/data alpine \
sh -c "echo 'hello volumes' > /data/test.txt"
# 3. 销毁容器后,用新容器读取数据
docker run --rm -v test-vol:/data alpine \
cat /data/test.txt
# 4. 清理
docker volume rm test-vol

预期结果:第 3 步输出 hello volumes,证明数据在容器销毁后依然存在。

进阶练习:参考 PostgreSQL 官方镜像,用命名卷部署数据库,验证数据持久化:

1
2
3
4
5
6
docker run -d --name pg \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16

# 写入数据后删除容器,再用同一个卷启动新容器,验证数据是否保留