Featured image of post Docker Volume 深度实践:命名卷、bind mount 与跨主机挂载

Docker Volume 深度实践:命名卷、bind mount 与跨主机挂载

Docker 1.9+ 命名卷生命周期、Copy-on-Write 原理、NFS 与 volume driver 跨主机挂载、共享卷在容器间传文件、清理策略

容器的文件系统是临时的——docker rm 时容器层全部销毁。任何需要持久化的数据,都必须挂载到 Volume 或 bind mount。这一篇把 Volume 的生命周期、Copy-on-Write 机制、跨主机挂载、共享卷在容器间传文件、清理策略一次性收齐。

阅读对象:需要把数据库 / 上传文件 / 配置中心等状态放到容器里的开发者、运维同学 覆盖范围:volumes vs bind mounts vs tmpfs + 命名卷操作 + Copy-on-Write + docker-compose 卷写法 + NFS / SSHFS / vieux 跨主机 + 共享卷传文件 + 清理策略

一、为什么需要 Volume

容器分层结构:

  • 镜像层(Image Layer):只读、共享,多个容器可共用
  • 容器层(Container Layer):读写、临时,容器销毁就消失

如果数据写在容器层,docker rm 那一刻数据就没了。Volume 就是把数据"挂"到宿主机文件系统上,绕开容器层,让数据活过容器生命周期。

二、三种挂载方式

Docker 1.9+(2015-11 GA)正式引入了 named volume(命名卷),与 bind mount、tmpfs 并列为三种挂载类型:

类型路径持久化性能适用
VolumeDocker 管理(默认 /var/lib/docker/volumes/数据库、上传文件、共享数据
bind mount任意宿主机路径配置文件、特殊路径、debug
tmpfs宿主机内存否(重启丢)极优敏感数据、临时缓存

When to use

  • Volume(推荐):数据库、长期存储——Docker 帮你管理路径、迁移、备份
  • bind mount:配置文件、日志、需要直接编辑宿主机文件的场景
  • tmpfs:密码、token、不想落盘的临时缓存

三、Volume 操作

3.1 命名卷的生命周期

1
2
3
4
5
6
7
8
# 创建(指定名字)
docker volume create data_volume

# 列出
docker volume ls

# 看详情
docker volume inspect data_volume

inspect 输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[
  {
    "CreatedAt": "2022-07-28T11:02:14+08:00",
    "Driver": "local",
    "Labels": null,
    "Mountpoint": "/var/lib/docker/volumes/data_volume/_data",
    "Name": "data_volume",
    "Scope": "local"
  }
]

关键字段

  • Driver: local:默认驱动,只能用于当前 Docker 主机上的容器
  • Scope: local:本地范围(要跨主机挂载需要用其他 driver)
  • Mountpoint:数据在宿主机上的实际位置

3.2 隐式创建(run 时指定卷名)

如果不显式 docker volume createdocker run -v name:/path自动创建

1
docker run -it --rm --name nginx -p 8080:80 -v demo-earthly:/usr/share/nginx/html nginx

3.3 Dockerfile 中声明

1
2
3
4
FROM nginx:latest

RUN echo "<h1>Hello from Volume</h1>" > /usr/share/nginx/html/index.html
VOLUME /usr/share/nginx/html

构建并运行:

1
2
3
4
docker build -t demo-earthly .
docker run -p 8080:80 demo-earthly
docker volume ls
# 输出随机卷名:20879e3f0bfaf0eed63cb7f37c4b9545084a703f888a230b8aedc2082c836281

每次启动新容器都会创建新卷(匿名卷),内容是 /usr/share/nginx/html

3.4 删除

1
2
3
4
5
6
7
8
# 删指定卷
docker volume rm data_volume

# 删所有未挂载的卷
docker volume prune

# 删 dangling 镜像 + 未挂载卷(危险)
docker system prune -af --volumes

重要约束docker volume rm 不能删正在被容器或服务使用的卷。docker volume prune 默认不会删正在使用的卷,但加 -f 也不会绕过这个保护——保护在 daemon 层。

四、Copy-on-Write 机制

docker build 时每条指令构建一层,只读docker run 时构建一个读写容器层

修改容器内某个文件时:

  1. 如果该文件在镜像层(只读),Docker 会从镜像层"复制"一份到容器层
  2. 容器层是读写层,修改只在容器层生效
  3. 容器销毁,容器层消失,修改也没了——这就是为什么需要 Volume

典型场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 容器内
echo "new content" > /etc/nginx/nginx.conf

# 容器外看
docker exec -it <container> cat /etc/nginx/nginx.conf
# 输出 "new content"

# 删容器
docker rm -f <container>

# 启动新容器
docker run -d nginx

# 新容器内的 /etc/nginx/nginx.conf 是默认的
# 原因:刚才的"new content"在容器层里,随容器销毁而消失

重要结论容器层的修改不能跨容器共享。要让数据持久 + 跨容器共享,必须 Volume / bind mount。

五、bind mount vs Volume

5.1 语法对比

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Volume(短语法)
docker run -d -v mydata:/var/lib/mysql mysql:5.7

# bind mount(短语法)
docker run -d -v /host/path:/container/path nginx

# 长语法(--mount)— 更显式
docker run -d \
  --mount type=volume,source=mydata,target=/var/lib/mysql \
  mysql:5.7

docker run -d \
  --mount type=bind,source=/host/path,target=/container/path \
  nginx

5.2 长语法参数说明

参数含义
typevolume / bind / tmpfs
source / src卷名 / 宿主机路径
destination / dst / target容器内路径
readonly / ro是否只读挂载
volume-opt额外 mount 选项(可多次)

5.3 实战:初始化数据到 Volume

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 创建两个卷
docker volume create test_nginx_html
docker volume create test_nginx_conf

# 准备数据到宿主机
sudo cp -r /home/nginx/html/* /var/lib/docker/volumes/test_nginx_html/_data
sudo cp /home/nginx/conf/my_custom.conf /var/lib/docker/volumes/test_nginx_conf/_data

# 启动容器,volume 自动挂载
docker run -d \
  --restart=always \
  -p 8080:8080 \
  --name test_nginx \
  --mount src=test_nginx_html,dst=/app \
  --mount src=test_nginx_conf,dst=/opt/bitnami/nginx/conf/server_blocks \
  bitnami/nginx:latest

Why 直接 cp 到 _data:volume 在宿主机上的真实路径是 /var/lib/docker/volumes/<name>/_data容器启动前预先填数据是常见模式。也可以用 docker cp 复制到运行中的容器目录,但已挂载的 volume 路径会变

六、docker-compose 里的卷

6.1 bind mount 写法

1
2
3
4
5
6
7
8
version: "3.2"
services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    volumes:
      - ./target:/usr/share/nginx/html

6.2 命名卷 + 多容器共享

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
version: "3.2"
services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    volumes:
      - html_files:/usr/share/nginx/html
  web1:
    image: nginx:latest
    ports:
      - "8081:80"
    volumes:
      - html_files:/usr/share/nginx/html

volumes:
  html_files:

docker compose up自动创建 <project_name>_html_files 命名卷,html_fileswebweb1 之间共享

6.3 引用已存在的卷

1
2
3
volumes:
  html_files:
    external: true

要先用 docker volume create html_files 创建,compose 才不会报错说找不到。

七、跨主机挂载:NFS

默认的 local driver 只能在当前主机用。Docker 1.9+ 内置支持 NFS 挂载(用 local driver 即可),不需要装额外插件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 安装 NFS 客户端
apt install nfs-common    # Debian/Ubuntu
yum install -y nfs-utils # CentOS/RHEL

# 创建 NFS 卷
docker volume create --driver local \
  --opt type=nfs \
  --opt o=addr=10.0.0.10,rw \
  --opt device=:/path/to/dir \
  nfs_volume

挂载到容器:

1
2
3
4
docker run -d -it \
  --name myapp \
  --mount source=nfs_volume,target=/data \
  myapp:latest

docker-compose 写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
version: "3.2"
services:
  myapp:
    image: myapp:latest
    ports:
      - "8080:8080"
    volumes:
      - type: volume
        source: nfs_volume
        target: /data
        volume:
          nocopy: true

volumes:
  nfs_volume:
    driver_opts:
      type: "nfs"
      o: "addr=10.0.0.10,nolock,soft,rw"
      device: ":/path/to/dir"

Why 推荐 nolock,soft:网络抖动时 hard 会卡住进程、nolock 减少锁竞争、soft 失败会返回错误而不是 hang。

八、用 volume driver 把数据存到其他主机

Docker Volume Plugins 生态丰富:vieux/sshfs(已停更)、Azure File Storage、AWS EFS、S3 driver、GlusterFS、Ceph、IPFS…

重要vieux/sshfs plugin 已经停更,生产不要用——新的容器场景推荐自建 NFS 或云厂商文件存储。

历史 demo(演示用):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 安装 sshfs 插件
docker plugin install --grant-all-permissions vieux/sshfs

# 用 sshfs 创建卷
docker volume create --driver vieux/sshfs \
  -o sshcmd=user@10.32.2.134:/home/user/sshvolume \
  -o password=yourpassword \
  mysshvolume

# 挂载到容器
docker run -id \
  --name testcon \
  --mount type=volume,volume-driver=vieux/sshfs,source=mysshvolume,target=/world \
  ubuntu /bin/bash

九、共享卷在容器间复制文件

Volume 的天然优势:多容器可同时挂载。可以用这个特性在容器间传文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 1. 创建共享卷
docker volume create shared_data

# 2. 容器 A 写文件到卷
docker run -it --name=writer \
  --mount source=shared_data,destination=/data \
  ubuntu bash
# 在容器内
root@ded392c589ea:/# touch /data/demo.txt

# 3. 容器 B 读同一卷
docker run -it --name=reader \
  --mount source=shared_data,destination=/data \
  ubuntu bash
# 在容器内
root@feef37293ea5:/# ls /data
# 输出:demo.txt

典型场景:CI 流水线里"build 容器产出 artifacts → 测试容器拿同一份 artifacts 跑测试",共享卷是干净的解法。

十、清理无用的卷

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 只删 dangling 卷
docker volume ls -f "dangling=true" -q | xargs --no-run-if-empty docker volume rm

# 一键清(删所有未挂载的卷 + dangling 镜像 + 停止的容器)
docker system prune -af --volumes

# 找出最大的卷 / 目录,定位"占空间元凶"
du -sh * | sort -hr | head

# 清理 2 天前的文件
find ./ -mtime +1 | xargs rm -rf

十一、要点回顾

  1. Volume / bind mount / tmpfs 三选一,数据库用 Volume、配置用 bind mount、敏感数据用 tmpfs
  2. 容器层是临时的——任何持久化数据必须挂到卷或宿主机目录
  3. Copy-on-Write决定了"修改容器内文件 ≠ 改镜像"——重启后默认文件
  4. docker volume rm 不能删正在使用的卷——保护在 daemon 层
  5. NFS 跨主机用内置 local driver 即可,不需要装额外插件
  6. 共享卷传文件是多容器协作的"原始"模式
  7. vieux/sshfs 已停更——生产场景用云厂商文件存储或自建 NFS

十二、小结

Volume 是 Docker 持久化的"地基"。理解了:

  • 三种挂载类型——volume / bind mount / tmpfs 的取舍
  • Copy-on-Write——容器层为什么不能持久化
  • NFS 跨主机——单机 → 多机的进阶
  • 共享卷传文件——容器间协作的底层模式

剩下的就是"挑对的存储后端":本地 SSD 给数据库、NFS / EFS / Ceph 给共享文件、S3 兼容给对象存储。

下一步:理解了单机 Volume,下一步是 Kubernetes 的 PV / PVC / StorageClass——把"申请存储"变成声明式 API,配合 CSI 驱动对接各种后端(NFS / Ceph / 云盘)。K8s 的存储抽象在 1.21+ 进入 GA,是云原生存储的事实标准。

参考资料

使用 Hugo 构建
主题 StackJimmy 设计