容器适合被替换。把重要数据只写在容器可写层中,会让“删除旧容器、启动新容器”变成一次数据事故。数据卷把数据生命周期从容器生命周期中分离出来。

命名卷最适合 Redis 数据,因为应用只关心容器内的 /data,不需要知道数据在存储层的具体路径。
推荐使用 --mount,因为参数含义比 -v 更清楚:
--mount type=volume,source=course-task-data,target=/data容器可写层的身份属于某个容器。停止再启动同一个容器时它还存在,但删除并重建后,新容器得到的是新的可写层。数据库如果把唯一数据写在这里,就会把“替换应用实例”与“删除业务数据”绑在一起。
可写层还使用镜像存储驱动的 copy-on-write 机制,适合容器运行产生的小量临时变化,不是为高写入数据库提供的最佳持久化接口。卷把 I/O 放到独立挂载中,生命周期和容器分开,也更容易备份、迁移或交给专门驱动管理。
无论 volume、bind 还是 tmpfs,应用看到的都是某个普通路径。差异在路径背后的所有权与生命周期:
选择依据应是数据语义,不是命令哪个更短。需要长期保留的数据库数据用 volume;需要实时编辑的源码用 bind;明确不该落盘的短期数据才考虑 tmpfs。
如果镜像在 /data 中本来有文件,把卷挂到 /data 后,进程看到的是卷内容,原目录内容会被挂载遮住。删除挂载并不是删除镜像里的原文件,它们只是暂时不可见。这个行为能解释很多“镜像里明明有文件,挂载后却不见了”的问题。
$ docker volume create course-task-data
course-task-data
$ docker volume inspect course-task-data \
--format 'name={{.Name}} driver={{.Driver}}'
name=course-task-data driver=local卷已经存在,但还没有被任何容器使用。local 是卷驱动名,不代表课程要依赖某个固定目录。
docker volume inspect 可能显示 Mountpoint,但不应让业务代码直接依赖这个内部路径。在 Docker Desktop 中,引擎存储还位于它管理的 Linux 环境中,外部文件管理器看到的路径并不等同于普通宿主目录。正确访问方式是把卷挂载给容器。
先用一个一次性容器写入文件:
$ docker run --rm \
--mount type=volume,source=course-task-data,target=/data \
alpine:3.22 sh -c 'echo "第一条任务" > /data/tasks.txt'这个容器已经被 --rm 删除。现在用另一个新容器读取:
$ docker run --rm \
--mount type=volume,source=course-task-data,target=/data,readonly \
alpine:3.22 cat /data/tasks.txt
第一条任务写入容器和读取容器不是同一个对象,但文件仍然存在,证明数据属于卷。第二次挂载增加 readonly,读取容器无法修改内容。
这是数据持久化的优点,也是清理时必须注意的地方。查看哪些容器引用某个卷:
$ docker ps -a --filter volume=course-task-data确认不再需要数据后,才能执行 docker volume rm course-task-data。本章后面还要用它演示备份,所以现在只检查引用,不删除。这个顺序也对应真实操作:先确认使用者和备份,再做不可逆删除。
如果卷仍被容器使用,Docker 会拒绝删除。应先确认并删除相关容器,再处理卷,而不是用强制选项绕过引用关系。
docker compose down 默认保留命名卷;docker compose down --volumes 会删除该 Compose 项目的卷和其中的数据。生产数据删除前必须有备份和明确确认。
如果只声明目标路径而没有 source,Docker 会创建随机名称的匿名卷。它也能独立于容器存在,但难以从名字判断用途,长期使用容易留下孤立卷。课程显式命名为 task-data 或由 Compose 项目命名,是为了让归属、备份和清理都可追踪。
--rm 对匿名卷和命名卷的处理也不能混为一谈。课程采用的原则更简单:重要数据只用明确命名的卷,删除动作始终写出卷名并先确认内容。
卷能让数据跨容器重建保留,但如果卷本身损坏、误删或所在设备故障,数据仍会丢失。备份必须把数据复制到独立位置,并且能验证恢复过程。
对数据库而言,优先使用数据库自身的一致性备份工具;简单文件或学习数据可以在服务停止或保证一致性的前提下,用临时工具容器打包。下面只演示卷的文件级备份机制。
先确保上一段的 course-task-data 仍存在并包含 tasks.txt。创建备份目录,然后用 Alpine 同时挂载数据卷和当前目录:
$ mkdir -p backup
$ docker run --rm \
--mount type=volume,source=course-task-data,target=/data,readonly \
--mount type=bind,source="$PWD/backup",target=/backup \
alpine:3.22 tar -czf /backup/course-task-data.tar.gz -C /data .
$ ls -lh backup/course-task-data.tar.gz知识点在参数里得到落实:源卷以只读方式挂载,工具容器不能误改原数据;备份目录用 bind mount 映射,所以容器退出后压缩包仍在项目目录。
创建一个新卷并恢复:
$ docker volume create course-task-data-restored
$ docker run --rm \
--mount type=volume,source=course-task-data-restored,target=/data \
--mount type=bind,source="$PWD/backup",target=/backup,readonly \
alpine:3.22 tar -xzf /backup/course-task-data.tar.gz -C /data
$ docker run --rm \
--mount type=volume,source=course-task-data-restored,target=/data,readonly \
alpine:3.22 cat /data/tasks.txt
第一条任务恢复到新卷而不是直接覆盖原卷,能先验证备份是否可读。看到相同内容只证明这个简单文件恢复成功;真实数据库还要执行逻辑校验、版本兼容检查和业务抽样。
验证后清理恢复副本和原始练习卷:
$ docker volume rm course-task-data-restored
course-task-data-restored
$ docker volume rm course-task-data
course-task-data如果要把当前目录的配置文件只读挂进容器,可以这样写:
$ docker run --rm \
--mount type=bind,source="$PWD/config",target=/app/config,readonly \
alpine:3.22 ls -la /app/config绑定挂载依赖源路径实际存在,也会把设备目录结构暴露给容器。开发时挂载源码很方便;发布镜像时,应优先把应用文件构建进镜像,让交付物保持完整。
第一是路径耦合。同一份命令换到不同目录或另一台设备,source 可能不存在。使用 --mount 时源路径不存在会明确报错,这比 -v 在部分场景自动创建目录更容易发现拼写问题。
第二是权限映射。容器进程仍以自己的 UID/GID 访问挂载文件;“容器里用户名叫 app”不意味着外部文件系统认识这个名字。遇到 permission denied 时,应比较数字 UID/GID、文件所有者和挂载只读属性,而不是直接改成 root。
为下面四种数据选择位置,并解释生命周期:
task-api 二进制:应进入镜像,因为它属于可重建交付物。能用生命周期解释选择,比记住“数据库用 volume”更重要。换一种数据类型时,这套判断仍然有效。