LabHub
学习 学习路径 课程

卷、网络与 Compose

数据得活得比容器长

在 LabHub 中继续学习

一句话总结

容器的可写层会随容器一起消失。需要长期保存的数据必须放在容器外部,使用绑定挂载

概念图: 卷 · 绑定挂载 · 大规模写入必须移到卷中 · 只读挂载

为什么需要它

重新部署容器后,所有上传文件都消失了——这样的故障报告至今仍很常见。原因不是缺陷,而是设计如此。可写层依附于容器对象,删除容器时,该层也会一并消失。

与此同时还有性能问题。修改下层中的文件时,必须把整个文件复制到上层(copy-up),对数据库文件这类大文件非常不利。因此,大规模写入必须移到卷中已成为一条常用准则。

工作原理

有三种方式,特性各不相同。

项目 绑定挂载 命名卷 tmpfs
位置 直接指定主机路径 由运行时管理 内存
持久性 永久保留在主机 永久保留在卷中 结束时消失
可移植性 低(依赖路径)
备份 自行管理 专用命令 不可备份
主要用途 配置文件、开发中的源代码 数据库数据、上传文件 临时文件、秘密

语法也有两种:简短形式 -v source:target:opts,以及显式形式 --mount type=...,source=...,target=...,readonly。后者会显示选项名称,因此在代码审查中不易产生误解。

只读挂载比想象中更重要。以 :ro 挂载配置文件后,无论是误操作还是入侵,容器都无法修改配置。应用程序尝试写入时会收到 Read-only file system 错误,而该错误正是“这个容器正在写入不该写的位置”的信号。

实际现场中的情况

备份模式已形成一种固定用法:启动一个临时容器,同时挂载数据卷和备份目录,并在其中创建归档。

docker run --rm -v dk-data:/source:ro -v /root/backup:/backup   alpine:3.20 tar czf /backup/dk-data.tgz -C /source .

关键是通过 -C 进入源目录后,使用相对路径归档。这样还原时路径才不会错位。

在 rootless 环境中,经常会遇到所有权问题。这是因为主机与容器的 UID 映射不同。此时不应通过 chmod 777 开放文件,而应理解并正确匹配映射关系。放开权限不仅会对该容器开放,也会对同一主机上的其他进程开放。

如何选择卷与 bind mount

两者名称相似,性质却不同。

命名卷 bind mount
管理者 Docker 人员
位置 /var/lib/docker/volumes/… 主机上的任意路径
备份 docker run --rm -v vol:/d … 导出 直接使用主机文件
权限 按镜像中的 UID 初始化 完全沿用主机权限
开发便利性 看不到源代码修改 立即反映

权限这一行是实践中的陷阱。bind mount 会原样继承主机文件的所有者和权限模式。因此,如果容器以 nonroot(UID 65532)运行,而主机文件是 root:root 0644,就会无法写入。相反,命名卷首次创建时会复制容器内目标路径的权限进行初始化,通常可以直接使用。

只在第一次复制

命名卷的初始化只会在卷为空时执行一次

빈 볼륨을 /app/config 에 붙이면
  → 이미지의 /app/config 내용이 볼륨으로 복사된다

이미 내용이 있는 볼륨을 붙이면
  → 이미지의 내용은 가려진다. 복사하지 않는다

因此,即使重新构建镜像并修改默认配置文件,使用旧卷的容器仍会看到旧文件。 这是“镜像明明修好了,却没有生效”的常见原因。最好不要把配置放在卷中,而应通过 ConfigMap 或环境变量注入。

在 Kubernetes 中的对应关系

Docker Kubernetes 使用场景
命名卷 PVC 数据库数据
bind mount hostPath 访问节点文件(应尽量避免)
tmpfs emptyDir: {medium: Memory} 秘密、临时内容
emptyDir: {} 容器间共享(生命周期与 Pod 相同)

emptyDir 会在Pod 被删除时消失,节点重启时也会消失。但在 Pod 重启(只有容器退出)的情况下仍会保留,因此测试时很容易产生误判。

权限问题可通过 fsGroup 解决。

spec:
  securityContext:
    fsGroup: 2000          # 볼륨의 그룹 소유자를 2000 으로 바꿔 준다
    runAsUser: 1000
    runAsNonRoot: true

fsGroup 会递归更改卷内所有文件的所有权,因此,文件数量非常多时,Pod 启动会变慢。 此时可使用 fsGroupChangePolicy: OnRootMismatch,只检查根目录。

下一项实验要做什么

创建并使用一个卷,确认删除容器后数据是否仍然存在;亲自触发只读挂载的错误消息;然后完整进行一次备份与还原。