数据卷与配置 — 让朝生暮死的容器留下东西¶
这一章回答一个矛盾: 容器生来朝生暮死(第 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 的输出证明:文件落在可写容器层——镜像层不可变,改动只能去那里。 容器层在容器被删除时一起被删,改动不可逆地消失2。 要让数据活过容器,就得把它写到容器层之外。这就是卷。
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)让容器能消费、产生和修改状态;容器死了,卷还活着——这就是数据耐久的 全部秘密3。创建只要一条命令,默认驱动是 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 等, 把卷背后换成网络存储或云存储5。一个有趣的观察:AWS 没有官方 Docker 卷驱动, 但 ECS(EC2 launch type)里可以用 local 或 REX-Ray/Portworx 等第三方驱动5。
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]
两个要点。第一,数据活过了容器的生死,还能被另一个发行版完全不同的容器复用—— 这是卷的立身之本6。第二,机制上:挂载点目录被排除在 Union filesystem 之外, 这个目录里的改动不写容器层,直接落到卷驱动的后端存储里;容器层反正会随容器删除, 卷目录天然免疫7。
3.3 删卷的规矩¶
docker volume rm 销毁数据,不可逆,所以 Docker 立了两条护栏:
正在被容器使用的卷拒绝删除;rm 容器时加 -v 可以顺带删掉它的匿名卷
(仅当该卷只被这个容器用)8。书里的纪律:删卷之前,确认要么有备份,要么数据不要了9。
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]
书里对这段的点评值得记住:"如果这一节只带走一个收获,那就是意识到容器用对了能有多强大"10。 (同时也请注意:这是调试技巧,不是日常操作——把宿主机的命名空间整个交给一个容器, 等于把大门钥匙递了出去。)
3.5 容器间共享与只读挂载¶
A 容器产出数据、B 容器消费,做法是同一个卷挂进两个容器。书里随即立了并发规矩: 多进程同时碰一份数据,必须防竞态条件(race condition:两个以上的执行部分同时读写 同一数据,结果取决于事件时序,可能产生错误或崩溃)—— 理想格局是唯一写者 + 多个读者11。
只读挂载是给读者加锁的手段,挂载参数末尾加 :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))13。
书里的场景是改网页。不用挂载时,改一行业务流程是:改文件 → 停容器 → 重建镜像 → 重跑容器 → 刷新——"不可持续"14。挂载之后:
$ 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(改完即看)。两个细节:改动是双向的(容器里改, 宿主机上也变);挂载点原有的内容会被宿主机目录整个替换掉16。 这也是为什么绑定挂载被定位成开发工具——生产环境你要的是"容器内容固定", 这正是绑定挂载破坏的东西(第 13 章安全章会从另一个角度再讲一遍)。
3.7 镜像里声明卷:VOLUME 关键字¶
数据库类镜像的维护者没法控制用户怎么 run 容器,但他们能在 Dockerfile 里替用户预备好 持久化点:
容器启动时,Docker 为每个声明的路径自动创建一个卷(自动创建的卷以 SHA-256 作为 ID, 即"匿名卷")挂到对应目录;这些目录同样被排除在 Union filesystem 之外, 备份责任从此落到运维肩上17。
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 字段给出宿主机落点——备份操作员该备份哪个目录,就是这么查出来的18。
3.8 配置:运行期与构建期,两套别搞混¶
运行期配置——环境变量。 先证明一件事:容器是沙箱,容器里 export 看到的变量和
宿主机完全不同19。注入方式按规模递进:
| 方式 | 例子 | 适用 |
|---|---|---|
-e/--env 单个 |
--env LOG_DIR=/var/log/my-log |
一两个变量20 |
重复 -e |
--env MAX_LOG_FILES=5 --env MAX_LOG_SIZE=1G |
几个变量 |
--env-file 文件 |
文件每行 KEY=VALUE,如 development.config |
变量多、命令行臃肿时21 |
Dockerfile ENV |
ENV LOG_DIR=/var/log/my-log |
给镜像定默认值;run 时 -e 可覆盖22 |
构建期配置——ARG。 ARG BASE_IMAGE_VERSION=12.7-stretch 在 Dockerfile 里定义
构建期变量,FROM node:${BASE_IMAGE_VERSION} 可以引用它;构建时用
--build-arg BASE_IMAGE_VERSION=12.7-alpine 覆盖,产出一个换过基础镜像的测试版23。
一句话区分(书里原话的口径):--env/--env-file 管容器运行时,ARG/--build-arg 管镜像 构建时;前者配置容器里的应用,后者给构建过程参数化24。
3.9 持久化模式与最佳实践¶
章末把零散知识收拢成格局25:
- Volume vs bind mount:volume 由 Docker 管理、存在 /var/lib/docker/volumes/, 可移植、有管理功能,是持久化的首选;bind mount 依赖宿主机的目录结构和 OS, 但开发期挂代码进去极其顺手——各归其位。
- 模式:老式"数据卷容器"(一个容器专职背卷)已被命名卷取代;编排环境对应物是 Kubernetes 的 StatefulSet(保证 Pod 的顺序与唯一性,每个 Pod 配自己的持久卷, 第 15 章展开);卷插件把后端换成远程/云存储。
- 最佳实践:持久化用卷;定期备份;监控存储用量(磁盘满了容器会挂); 卷里的敏感数据要设好权限、必要时加密;要弹性就上官配/第三方卷驱动26。
4. 作者的判断与证据¶
- "volume 优于 bind mount"是有条件的推荐:作者给的理由是可移植性和管理功能, 同时承认 bind mount 在开发流程里的地位25。这不是教条,是场景分工。
- "唯一写者 + 多读者"是并发控制的工程常识,书里作为纪律而非可选建议提出11。
- 备份责任归运维是 VOLUME 声明的直接推论,书里讲 MongoDB 时顺手点破1718—— 镜像作者只能宣告"这里的数据重要",保不住数据的是没有备份流程的团队。
判断(我们的,不是书里的): 本章隐藏的一条主线是"可变与不可变的边界划在哪"。 容器层可变但短命(适合临时态),卷可变且长命(适合状态),镜像不可变(适合代码)。 几乎所有容器事故(数据丢失、配置漂移、安全隐患)都能归结为把东西放错了层—— 状态进了容器层,或代码塞进了挂载目录。 如果错,会错在: 有状态服务在编排层(Kubernetes)还有更细的所有权语义 (PV/PVC/StorageClass),Docker 单机卷的模型到那里会被重新表达; 但"分层放对东西"的原则在任何一层都成立。
5. 边界与局限¶
- 第三方卷驱动只给了名单,没有使用细节(书里明说超出范围)5——真上 NFS/EBS 要查对应插件文档。
- nsenter 技巧只在 Desktop 的 VM 结构下有必要;纯 Linux 主机直接 cd 进 /var/lib/docker/volumes 就行。
- 绑定挂载的权限坑(容器内 UID 与宿主机用户对不上导致读写失败)本章没展开, 是实际开发中最高频的槽点之一。
- 配置一章没提 secrets 管理(密钥怎么进容器)——那是第 13 章安全专章的内容, 本章只把"secrets 不要走环境变量明文"的规矩留了个尾巴(见 04 的 lift-and-shift)。
6. 可带走的¶
- 容器层随容器死,要持久就写到卷;卷的生命周期独立于容器23。
docker volume create/inspect/rm;inspect 的 Mountpoint 是宿主机落点4。- 挂载点被排除在 Union filesystem 外——卷目录的改动不进容器层7。
- 同卷跨容器共享,挂载可加
:ro只读;并发规矩是唯一写者、其余只读12。 - 开发用 bind mount 实现 edit-and-continue;注意挂载点内容被宿主机目录替换、改动双向1516。
- 数据库镜像用
VOLUME声明持久化点;docker inspect {{json .Mounts}}查备份目录1718。 - 运行期配置
-e/--env-file/DockerfileENV;构建期参数ARG/--build-arg——两套别混24。 - 持久化首选 volume(可移植),bind mount 归开发;定期备份、监控容量、敏感数据加密2526。
7. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 容器层实验、diff、A/C/D | Technical requirements | text/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 requirements | text/61-fm-technical-requirements.txt:23(搜「consume, produce」) · text/61-fm-technical-requirements.txt:24(搜「goes beyond」) |
| 卷创建、inspect、Mountpoint | Technical requirements | text/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 requirements | text/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 requirements | text/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 -v | Technical requirements | text/61-fm-technical-requirements.txt:268(搜「irreversibly」) · text/61-fm-technical-requirements.txt:290(搜「-v or --volume」) |
| nsenter 技巧 | Technical requirements | text/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 containers | text/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 volumes | text/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 volumes | text/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 images | text/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 images | text/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 containers | text/65-fm-configuring-containers.txt:21(搜「shielded」) · text/65-fm-configuring-containers.txt:60(搜「different than」) |
| --env 与 --env-file | Configuring containers | text/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 containers | text/65-fm-configuring-containers.txt:161(搜「default value」) · text/65-fm-configuring-containers.txt:200(搜「override」) |
| ARG 与 --build-arg | Configuring containers | text/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 patterns | text/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」) |
-
出处:「Technical requirements」第 27–55 段(
text/61-fm-technical-requirements.txt:36,搜「This is a test」;:44,搜「diff demo」;:48,搜「A /sample.txt」;:54,搜「marked with a C」)。 ↩ -
出处:「Technical requirements」第 56–59 段(
text/61-fm-technical-requirements.txt:58,搜「irreversibly deleted」)。 ↩↩ -
出处:「Technical requirements」第 19–26 段(
text/61-fm-technical-requirements.txt:23,搜「Volumes allow」;:24,搜「goes beyond」)。 ↩↩ -
出处:「Technical requirements」第 61–94 段(
text/61-fm-technical-requirements.txt:72,搜「volume create」;:79,搜「local driver」;:87,搜「volume inspect」;:92,搜「Mountpoint」)。 ↩ -
出处:「Technical requirements」第 108–188 段(
text/61-fm-technical-requirements.txt:108,搜「--driver」;:119,搜「Driver」;:184,搜「ECS」)。 ↩↩↩ -
出处:「Technical requirements」第 190–257 段(
text/61-fm-technical-requirements.txt:197,搜「-v sample:/data」;:214,搜「mounts the sample volume」;:251,搜「definitive proof」)。 ↩ -
出处:「Technical requirements」第 253–257 段(
text/61-fm-technical-requirements.txt:253,搜「excluded」;:255,搜「backing storage」)。 ↩↩ -
出处:「Technical requirements」第 264–292 段(
text/61-fm-technical-requirements.txt:269,搜「does not allow」;:288,搜「rm -v -f」;:290,搜「anonymous volume」)。 ↩ -
出处:「Technical requirements」第 265–271 段(
text/61-fm-technical-requirements.txt:270,搜「backup」)。 ↩ -
出处:「Technical requirements」第 297–431 段(
text/61-fm-technical-requirements.txt:325,搜「com.docker.docker」;:334,搜「--privileged」;:337,搜「--pid=host」;:345,搜「how powerful」)。 ↩ -
出处:「Sharing data between containers」第 1–26 段(
text/62-fm-sharing-data-between-containers.txt:15,搜「race conditions」;:16,搜「only one」)。 ↩↩ -
出处:「Sharing data between containers」第 27–77 段(
text/62-fm-sharing-data-between-containers.txt:48,搜「:ro」;:68,搜「Read-only file system」)。 ↩ -
出处:「Using host volumes」第 6–26 段(
text/63-fm-using-host-volumes.txt:16,搜「-v $(pwd)」;:21,搜「absolute paths」)。 ↩ -
出处:「Using host volumes」第 27–98 段(
text/63-fm-using-host-volumes.txt:30,搜「latest changes」;:96,搜「not sustainable」)。 ↩ -
出处:「Using host volumes」第 99–107 段(
text/63-fm-using-host-volumes.txt:105,搜「$(pwd):/usr/share/nginx/html」)。 ↩ -
出处:「Defining volumes in images」第 1–20 段(该节开头因转码顺序位于本文件:
text/64-fm-defining-volumes-in-images.txt:6,搜「edit-and-continue」;:15,搜「bi-directionally」;:19,搜「replaced」)。 ↩↩ -
出处:「Defining volumes in images」第 23–62 段(
text/64-fm-defining-volumes-in-images.txt:41,搜「VOLUME」;:56,搜「SHA-256」;:59,搜「excluded」;:61,搜「backed up」)。 ↩↩↩ -
出处:「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」)。 ↩↩↩ -
出处:「Configuring containers」第 14–63 段(
text/65-fm-configuring-containers.txt:21,搜「shielded」;:60,搜「different than what we saw」)。 ↩ -
出处:「Configuring containers」第 66–111 段(
text/65-fm-configuring-containers.txt:70,搜「--env」;:86,搜「LOG_DIR='/var/log/my-log'」;:92,搜「repeat」)。 ↩ -
出处:「Configuring containers」第 114–155 段(
text/65-fm-configuring-containers.txt:119,搜「env-file」;:134,搜「LOG_DIR=/var/log/my-log」;:137,搜「per line」)。 ↩ -
出处:「Configuring containers」第 160–214 段(
text/65-fm-configuring-containers.txt:177,搜「ENV LOG_DIR」;:200,搜「override」)。 ↩ -
出处:「Configuring containers」第 218–245 段(
text/65-fm-configuring-containers.txt:224,搜「ARG BASE_IMAGE_VERSION」;:236,搜「--build-arg」;:242,搜「my-node-app-test」)。 ↩ -
出处:「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」)。 ↩↩ -
出处:「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」)。 ↩↩↩ -
出处:「Summary」第 1–8 段(
text/67-fm-summary.txt:1,搜「Monitor storage usage」;:3,搜「sensitive data」;:5,搜「volume drivers」)。 ↩↩