编程语言研究书库

数据卷与配置 — 让朝生暮死的容器留下东西

数据卷与配置 — 让朝生暮死的容器留下东西

这一章回答一个矛盾: 容器生来朝生暮死(第 03 章原话:ephemeral,设计上就要能被随手 停掉重建),可应用偏偏要留下数据、接受配置。解法是三件东西——卷、绑定挂载、配置注入, 外加一套"谁该管备份"的责任划分。主线例子:一个叫 sample 的卷,从 Alpine 写进数据、 容器删掉,再从 CentOS 里原样读出来。

1. 问题的模样:写进容器层的东西,随容器一起死

先做一个书里的小实验建立痛感。跑一个 Alpine 容器,往里写个文件:

  $ docker container run --name demo alpine \
        /bin/sh -c 'echo "This is a test" > sample.txt'
  $ docker container diff demo
  → A /sample.txt        ← A=added,C=changed,D=deleted[^1]

diff 的输出证明:文件落在可写容器层——镜像层不可变,改动只能去那里。 容器层在容器被删除时一起被删,改动不可逆地消失1。 要让数据活过容器,就得把它写到容器层之外。这就是卷。

2. 顶层全景:三种持久化 + 两套配置

  数据要活过容器 ──┬─ 卷(volume):Docker 管理的命名存储,默认存在宿主机
  │                  │   /var/lib/docker/volumes/ 下;生命周期独立于容器
  │                  ├─ 绑定挂载(bind mount):宿主机任意目录直通容器;
  │                  │   开发期"改代码立刻生效"的主力
  │                  └─ 镜像声明 VOLUME:镜像作者(如 MongoDB)替用户
  │                      预定持久化点,容器启动时自动建卷
  │
  配置要进容器 ──┬─ 运行期:--env / --env-file(启动时注入);
  │                │   Dockerfile 里 ENV 定默认值,可被覆盖
  │                └─ 构建期:ARG / --build-arg(构建镜像时参数化)

  图说:左边管数据的生命,右边管应用的行为。两套配置机制常被混淆,3.8 节分开讲。

3. 核心原理

3.1 卷:生命周期超过容器的存储

卷(volume)让容器能消费、产生和修改状态;容器死了,卷还活着——这就是数据耐久的 全部秘密2。创建只要一条命令,默认驱动是 local(存在宿主机文件系统):

  $ docker volume create sample        → 输出:sample
  $ docker volume inspect sample
  → Mountpoint 字段: /var/lib/docker/volumes/sample/_data[^4]

inspect 的 Mountpoint 是在宿主机上的真实落点(注意:macOS/Windows 上 Docker 跑在 隐藏虚拟机里,这个路径在虚拟机内部,3.4 节讲怎么摸到)。除 local 外还有一堆第三方驱动 (以插件形式存在):nfs、cifs、rclone(挂 Google Drive/Dropbox)、rexray/ebs(亚马逊 EBS)、 flocker、portworx(企业级,带复制/快照/加密)、glusterfs、azurefile、gcepd 等, 把卷背后换成网络存储或云存储3。一个有趣的观察:AWS 没有官方 Docker 卷驱动, 但 ECS(EC2 launch type)里可以用 local 或 REX-Ray/Portworx 等第三方驱动3。

3.2 主线例子:一个卷,两个容器,一死一生

  $ docker container run --name test -it \
        -v sample:/data alpine /bin/sh
  /data # echo "Some data" > data.txt
  /data # echo "Some more data" > data2.txt
  /data # (Ctrl+D 退出)
  $ docker container rm test                ← 容器删了

  $ docker container run --name test2 -it --rm \
        -v sample:/app/data centos:7 /bin/bash
  → cd /app/data && ls
  → data.txt  data2.txt                      ← 原样都在![^6]

两个要点。第一,数据活过了容器的生死,还能被另一个发行版完全不同的容器复用—— 这是卷的立身之本4。第二,机制上:挂载点目录被排除在 Union filesystem 之外, 这个目录里的改动不写容器层,直接落到卷驱动的后端存储里;容器层反正会随容器删除, 卷目录天然免疫5。

3.3 删卷的规矩

docker volume rm 销毁数据,不可逆,所以 Docker 立了两条护栏: 正在被容器使用的卷拒绝删除;rm 容器时加 -v 可以顺带删掉它的匿名卷 (仅当该卷只被这个容器用)6。书里的纪律:删卷之前,确认要么有备份,要么数据不要了7。

3.4 Mac/Windows 上的卷:藏在虚拟机里

macOS 不是 Linux,Docker Desktop 的容器跑在一个隐藏 VM 里,所以宿主机上根本不存在 /var/lib/docker/volumes/ 这个路径。书里给了一个既暴力又优雅的解法——用一个 特权容器钻进宿主机的命名空间:

  $ docker container run -it --privileged --pid=host \
        debian nsenter -t 1 -m -u -n -i sh
    ↑ --pid=host:容器能看到宿主机的进程命名空间
    ↑ nsenter -t 1:加入 PID 1(=宿主机)的 mount/user/net/ipc 命名空间
  → 现在你在容器里,却站在宿主机的文件系统上
  / # ls /var/lib/docker/volumes/          ← 卷的真身在这[^10]

书里对这段的点评值得记住:"如果这一节只带走一个收获,那就是意识到容器用对了能有多强大"8。 (同时也请注意:这是调试技巧,不是日常操作——把宿主机的命名空间整个交给一个容器, 等于把大门钥匙递了出去。)

3.5 容器间共享与只读挂载

A 容器产出数据、B 容器消费,做法是同一个卷挂进两个容器。书里随即立了并发规矩: 多进程同时碰一份数据,必须防竞态条件(race condition:两个以上的执行部分同时读写 同一数据,结果取决于事件时序,可能产生错误或崩溃)—— 理想格局是唯一写者 + 多个读者9。

只读挂载是给读者加锁的手段,挂载参数末尾加 :ro:

  writer: -v shared-data:/data          ← 默认读写
  reader: -v shared-data:/app/data:ro   ← 只读
  → reader 里写文件会失败:bash: /app/data/data.txt: Read-only file system[^12]

3.6 绑定挂载:开发期的 edit-and-continue

第二类是绑定挂载(bind mount):把宿主机的任意目录挂进容器。语法和卷一样, 区别是冒号左边是宿主机路径(必须是绝对路径,所以常用 $(pwd))10。

书里的场景是改网页。不用挂载时,改一行业务流程是:改文件 → 停容器 → 重建镜像 → 重跑容器 → 刷新——"不可持续"11。挂载之后:

  $ docker container run -d --name my-site \
        -v $(pwd):/usr/share/nginx/html \
        -p 8080:80 my-website:1.0
  → 改 index.html,存盘,浏览器刷新,立刻看到新版[^15]

这就叫 edit-and-continue(改完即看)。两个细节:改动是双向的(容器里改, 宿主机上也变);挂载点原有的内容会被宿主机目录整个替换掉12。 这也是为什么绑定挂载被定位成开发工具——生产环境你要的是"容器内容固定", 这正是绑定挂载破坏的东西(第 13 章安全章会从另一个角度再讲一遍)。

3.7 镜像里声明卷:VOLUME 关键字

数据库类镜像的维护者没法控制用户怎么 run 容器,但他们能在 Dockerfile 里替用户预备好 持久化点:

VOLUME /app/data
VOLUME ["/app/data", "/app/profiles", "/app/config"]

容器启动时,Docker 为每个声明的路径自动创建一个卷(自动创建的卷以 SHA-256 作为 ID, 即"匿名卷")挂到对应目录;这些目录同样被排除在 Union filesystem 之外, 备份责任从此落到运维肩上13。

MongoDB 的实证:docker image inspect --format='{{json .Config.Volumes}}' mongo:8.0.8 显示官方 mongo 镜像声明了 /data/configdb 和 /data/db 两个卷; run 起来后再 docker inspect --format '{{json .Mounts}}' my-mongo, Source 字段给出宿主机落点——备份操作员该备份哪个目录,就是这么查出来的14。

3.8 配置:运行期与构建期,两套别搞混

运行期配置——环境变量。 先证明一件事:容器是沙箱,容器里 export 看到的变量和 宿主机完全不同15。注入方式按规模递进:

方式例子适用
-e/--env 单个--env LOG_DIR=/var/log/my-log一两个变量16
重复 -e--env MAX_LOG_FILES=5 --env MAX_LOG_SIZE=1G几个变量
--env-file 文件文件每行 KEY=VALUE,如 development.config变量多、命令行臃肿时17
Dockerfile ENVENV LOG_DIR=/var/log/my-log给镜像定默认值;run 时 -e 可覆盖18

构建期配置——ARG。 ARG BASE_IMAGE_VERSION=12.7-stretch 在 Dockerfile 里定义 构建期变量,FROM node:${BASE_IMAGE_VERSION} 可以引用它;构建时用 --build-arg BASE_IMAGE_VERSION=12.7-alpine 覆盖,产出一个换过基础镜像的测试版19。

一句话区分(书里原话的口径):--env/--env-file 管容器运行时,ARG/--build-arg 管镜像 构建时;前者配置容器里的应用,后者给构建过程参数化20。

3.9 持久化模式与最佳实践

章末把零散知识收拢成格局21:

  • Volume vs bind mount:volume 由 Docker 管理、存在 /var/lib/docker/volumes/, 可移植、有管理功能,是持久化的首选;bind mount 依赖宿主机的目录结构和 OS, 但开发期挂代码进去极其顺手——各归其位。
  • 模式:老式"数据卷容器"(一个容器专职背卷)已被命名卷取代;编排环境对应物是 Kubernetes 的 StatefulSet(保证 Pod 的顺序与唯一性,每个 Pod 配自己的持久卷, 第 15 章展开);卷插件把后端换成远程/云存储。
  • 最佳实践:持久化用卷;定期备份;监控存储用量(磁盘满了容器会挂); 卷里的敏感数据要设好权限、必要时加密;要弹性就上官配/第三方卷驱动22。

4. 作者的判断与证据

  • "volume 优于 bind mount"是有条件的推荐:作者给的理由是可移植性和管理功能, 同时承认 bind mount 在开发流程里的地位21。这不是教条,是场景分工。
  • **"唯一写者 + 多读者"**是并发控制的工程常识,书里作为纪律而非可选建议提出9。
  • 备份责任归运维是 VOLUME 声明的直接推论,书里讲 MongoDB 时顺手点破1314—— 镜像作者只能宣告"这里的数据重要",保不住数据的是没有备份流程的团队。

判断(我们的,不是书里的): 本章隐藏的一条主线是**"可变与不可变的边界划在哪"**。 容器层可变但短命(适合临时态),卷可变且长命(适合状态),镜像不可变(适合代码)。 几乎所有容器事故(数据丢失、配置漂移、安全隐患)都能归结为把东西放错了层—— 状态进了容器层,或代码塞进了挂载目录。 如果错,会错在: 有状态服务在编排层(Kubernetes)还有更细的所有权语义 (PV/PVC/StorageClass),Docker 单机卷的模型到那里会被重新表达; 但"分层放对东西"的原则在任何一层都成立。

5. 边界与局限

  • 第三方卷驱动只给了名单,没有使用细节(书里明说超出范围)3——真上 NFS/EBS 要查对应插件文档。
  • nsenter 技巧只在 Desktop 的 VM 结构下有必要;纯 Linux 主机直接 cd 进 /var/lib/docker/volumes 就行。
  • 绑定挂载的权限坑(容器内 UID 与宿主机用户对不上导致读写失败)本章没展开, 是实际开发中最高频的槽点之一。
  • 配置一章没提 secrets 管理(密钥怎么进容器)——那是第 13 章安全专章的内容, 本章只把"secrets 不要走环境变量明文"的规矩留了个尾巴(见 04 的 lift-and-shift)。

6. 可带走的

  1. 容器层随容器死,要持久就写到卷;卷的生命周期独立于容器12。
  2. docker volume create/inspect/rm;inspect 的 Mountpoint 是宿主机落点23。
  3. 挂载点被排除在 Union filesystem 外——卷目录的改动不进容器层5。
  4. 同卷跨容器共享,挂载可加 :ro 只读;并发规矩是唯一写者、其余只读24。
  5. 开发用 bind mount 实现 edit-and-continue;注意挂载点内容被宿主机目录替换、改动双向2512。
  6. 数据库镜像用 VOLUME 声明持久化点;docker inspect {{json .Mounts}} 查备份目录1314。
  7. 运行期配置 -e/--env-file/Dockerfile ENV;构建期参数 ARG/--build-arg——两套别混20。
  8. 持久化首选 volume(可移植),bind mount 归开发;定期备份、监控容量、敏感数据加密2122。

7. 原文地图

主题原书章原文位置
容器层实验、diff、A/C/DTechnical requirementstext/61-fm-technical-requirements.txt:36(搜「This is a test」) · text/61-fm-technical-requirements.txt:48(搜「A /sample.txt」) · text/61-fm-technical-requirements.txt:58(搜「irreversibly」)
卷的定义与生命周期Technical requirementstext/61-fm-technical-requirements.txt:23(搜「consume, produce」) · text/61-fm-technical-requirements.txt:24(搜「goes beyond」)
卷创建、inspect、MountpointTechnical requirementstext/61-fm-technical-requirements.txt:72(搜「volume create」) · text/61-fm-technical-requirements.txt:87(搜「volume inspect」) · text/61-fm-technical-requirements.txt:92(搜「Mountpoint」)
卷驱动表Technical requirementstext/61-fm-technical-requirements.txt:79(搜「local driver」) · text/61-fm-technical-requirements.txt:119(搜「Driver」) · text/61-fm-technical-requirements.txt:185(搜「ECS」)
挂载与跨容器复用Technical requirementstext/61-fm-technical-requirements.txt:197(搜「-v sample:/data」) · text/61-fm-technical-requirements.txt:251(搜「definitive proof」) · text/61-fm-technical-requirements.txt:253(搜「excluded」)
删卷规矩、rm -vTechnical requirementstext/61-fm-technical-requirements.txt:268(搜「irreversibly」) · text/61-fm-technical-requirements.txt:290(搜「-v or --volume」)
nsenter 技巧Technical requirementstext/61-fm-technical-requirements.txt:325(搜「com.docker.docker」) · text/61-fm-technical-requirements.txt:334(搜「--privileged」) · text/61-fm-technical-requirements.txt:345(搜「how powerful」)
共享与竞态、ro 挂载Sharing data between containerstext/62-fm-sharing-data-between-containers.txt:15(搜「race conditions」) · text/62-fm-sharing-data-between-containers.txt:30(搜「read-only」) · text/62-fm-sharing-data-between-containers.txt:68(搜「Read-only file system」)
bind mount 语法与开发用途Using host volumestext/63-fm-using-host-volumes.txt:16(搜「-v $(pwd)」) · text/63-fm-using-host-volumes.txt:21(搜「absolute paths」) · text/63-fm-using-host-volumes.txt:30(搜「latest changes」)
edit-and-continue 与双向替换Using host volumestext/64-fm-defining-volumes-in-images.txt:6(搜「edit-and-continue」) · text/64-fm-defining-volumes-in-images.txt:15(搜「bi-directionally」) · text/64-fm-defining-volumes-in-images.txt:19(搜「replaced」)
VOLUME 关键字与匿名卷Defining volumes in imagestext/64-fm-defining-volumes-in-images.txt:41(搜「VOLUME」) · text/64-fm-defining-volumes-in-images.txt:58(搜「SHA-256」) · text/64-fm-defining-volumes-in-images.txt:59(搜「excluded」)
MongoDB 卷实证Defining volumes in imagestext/64-fm-defining-volumes-in-images.txt:67(搜「mongo:8.0.8」) · text/64-fm-defining-volumes-in-images.txt:91(搜「/data/configdb」) · text/65-fm-configuring-containers.txt:3(搜「Source field」)
沙箱环境变量证明Configuring containerstext/65-fm-configuring-containers.txt:21(搜「shielded」) · text/65-fm-configuring-containers.txt:60(搜「different than」)
--env 与 --env-fileConfiguring containerstext/65-fm-configuring-containers.txt:70(搜「--env」) · text/65-fm-configuring-containers.txt:119(搜「env-file」) · text/65-fm-configuring-containers.txt:137(搜「per line」)
ENV 默认值与覆盖Configuring containerstext/65-fm-configuring-containers.txt:161(搜「default value」) · text/65-fm-configuring-containers.txt:200(搜「override」)
ARG 与 --build-argConfiguring containerstext/65-fm-configuring-containers.txt:224(搜「ARG BASE_IMAGE_VERSION」) · text/65-fm-configuring-containers.txt:236(搜「--build-arg」) · text/65-fm-configuring-containers.txt:247(搜「summarize」)
持久化模式与最佳实践Persistent storage and stateful container patternstext/66-fm-persistent-storage-and-stateful-container-patter.txt:12(搜「volumes and bind mounts」) · text/66-fm-persistent-storage-and-stateful-container-patter.txt:29(搜「Data volume containers」) · text/66-fm-persistent-storage-and-stateful-container-patter.txt:35(搜「StatefulSets」) · text/67-fm-summary.txt:1(搜「Monitor storage usage」)

Footnotes

  1. 出处:「Technical requirements」第 56–59 段(text/61-fm-technical-requirements.txt:58,搜「irreversibly deleted」)。 ↩ ↩2

  2. 出处:「Technical requirements」第 19–26 段(text/61-fm-technical-requirements.txt:23,搜「Volumes allow」;:24,搜「goes beyond」)。 ↩ ↩2

  3. 出处:「Technical requirements」第 108–188 段(text/61-fm-technical-requirements.txt:108,搜「--driver」;:119,搜「Driver」;:184,搜「ECS」)。 ↩ ↩2 ↩3

  4. 出处:「Technical requirements」第 190–257 段(text/61-fm-technical-requirements.txt:197,搜「-v sample:/data」;:214,搜「mounts the sample volume」;:251,搜「definitive proof」)。 ↩

  5. 出处:「Technical requirements」第 253–257 段(text/61-fm-technical-requirements.txt:253,搜「excluded」;:255,搜「backing storage」)。 ↩ ↩2

  6. 出处:「Technical requirements」第 264–292 段(text/61-fm-technical-requirements.txt:269,搜「does not allow」;:288,搜「rm -v -f」;:290,搜「anonymous volume」)。 ↩

  7. 出处:「Technical requirements」第 265–271 段(text/61-fm-technical-requirements.txt:270,搜「backup」)。 ↩

  8. 出处:「Technical requirements」第 297–431 段(text/61-fm-technical-requirements.txt:325,搜「com.docker.docker」;:334,搜「--privileged」;:337,搜「--pid=host」;:345,搜「how powerful」)。 ↩

  9. 出处:「Sharing data between containers」第 1–26 段(text/62-fm-sharing-data-between-containers.txt:15,搜「race conditions」;:16,搜「only one」)。 ↩ ↩2

  10. 出处:「Using host volumes」第 6–26 段(text/63-fm-using-host-volumes.txt:16,搜「-v $(pwd)」;:21,搜「absolute paths」)。 ↩

  11. 出处:「Using host volumes」第 27–98 段(text/63-fm-using-host-volumes.txt:30,搜「latest changes」;:96,搜「not sustainable」)。 ↩

  12. 出处:「Defining volumes in images」第 1–20 段(该节开头因转码顺序位于本文件:text/64-fm-defining-volumes-in-images.txt:6,搜「edit-and-continue」;:15,搜「bi-directionally」;:19,搜「replaced」)。 ↩ ↩2

  13. 出处:「Defining volumes in images」第 23–62 段(text/64-fm-defining-volumes-in-images.txt:41,搜「VOLUME」;:56,搜「SHA-256」;:59,搜「excluded」;:61,搜「backed up」)。 ↩ ↩2 ↩3

  14. 出处:「Defining volumes in images」第 63–105 段与「Configuring containers」开头(text/64-fm-defining-volumes-in-images.txt:67,搜「mongo:8.0.8」;:91,搜「/data/configdb」;:102,搜「.Mounts」;text/65-fm-configuring-containers.txt:3,搜「Source field」)。 ↩ ↩2 ↩3

  15. 出处:「Configuring containers」第 14–63 段(text/65-fm-configuring-containers.txt:21,搜「shielded」;:60,搜「different than what we saw」)。 ↩

  16. 出处:「Configuring containers」第 66–111 段(text/65-fm-configuring-containers.txt:70,搜「--env」;:86,搜「LOG_DIR='/var/log/my-log'」;:92,搜「repeat」)。 ↩

  17. 出处:「Configuring containers」第 114–155 段(text/65-fm-configuring-containers.txt:119,搜「env-file」;:134,搜「LOG_DIR=/var/log/my-log」;:137,搜「per line」)。 ↩

  18. 出处:「Configuring containers」第 160–214 段(text/65-fm-configuring-containers.txt:177,搜「ENV LOG_DIR」;:200,搜「override」)。 ↩

  19. 出处:「Configuring containers」第 218–245 段(text/65-fm-configuring-containers.txt:224,搜「ARG BASE_IMAGE_VERSION」;:236,搜「--build-arg」;:242,搜「my-node-app-test」)。 ↩

  20. 出处:「Configuring containers」第 247–248 段与「Persistent storage and stateful container patterns」开头(text/65-fm-configuring-containers.txt:247,搜「summarize」;text/66-fm-persistent-storage-and-stateful-container-patter.txt:1,搜「build time」)。 ↩ ↩2

  21. 出处:「Persistent storage and stateful container patterns」第 6–42 段(text/66-fm-persistent-storage-and-stateful-container-patter.txt:12,搜「volumes and bind mounts」;:22,搜「recommended over bind mounts」;:29,搜「Data volume containers」;:32,搜「Named volumes」;:35,搜「StatefulSets」;:40,搜「plugins」)。 ↩ ↩2 ↩3

  22. 出处:「Summary」第 1–8 段(text/67-fm-summary.txt:1,搜「Monitor storage usage」;:3,搜「sensitive data」;:5,搜「volume drivers」)。 ↩ ↩2

  23. 出处:「Technical requirements」第 61–94 段(text/61-fm-technical-requirements.txt:72,搜「volume create」;:79,搜「local driver」;:87,搜「volume inspect」;:92,搜「Mountpoint」)。 ↩

  24. 出处:「Sharing data between containers」第 27–77 段(text/62-fm-sharing-data-between-containers.txt:48,搜「:ro」;:68,搜「Read-only file system」)。 ↩

  25. 出处:「Using host volumes」第 99–107 段(text/63-fm-using-host-volumes.txt:105,搜「$(pwd):/usr/share/nginx/html」)。 ↩

On this page