目录

Docker-06 数据卷与存储:持久化、权限、备份和磁盘治理

1. 容器里的文件到底存在哪里

容器的根文件系统通常由多个只读镜像层和一个可写层组成:

镜像层:/usr、/bin、应用依赖(只读)
容器层:运行时新建或修改的文件(可写)
                ↓ overlayfs 统一视图
          容器内的 /

容器删除时,可写层通常也会被删除。因此日志、上传文件、数据库数据写在容器层里都是高风险做法。容器适合无状态,状态应该明确放到 volume、宿主目录或外部存储。

2. 三种常用挂载

类型数据位置适合场景主要风险
named volumeDocker 管理的目录数据库、应用持久数据备份和迁移需要通过容器/宿主路径处理
bind mount你指定的宿主路径配置、源码、日志、开发热加载路径权限、误覆盖、宿主文件泄露
tmpfs内存临时文件、敏感短期数据重启/停止丢失,消耗内存

2.1 Named volume

docker volume create pgdata
docker volume ls
docker volume inspect pgdata
docker run -d --name pg \
  -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=devpass \
  postgres:16

volume 的实际路径由 Docker daemon 管理,常见 Linux 路径是 /var/lib/docker/volumes/<name>/_data,但不要在脚本中硬编码,rootless、Desktop 和其他存储驱动可能不同。

2.2 Bind mount

mkdir -p ./config ./logs
docker run --rm \
  --mount type=bind,src="$PWD/config",dst=/app/config,readonly \
  --mount type=bind,src="$PWD/logs",dst=/app/logs \
  example/api:dev

--mount-v 更明确,路径拼写错误时也更容易暴露问题。开发时可以挂载源码,生产时应避免随意把宿主机敏感目录暴露给容器。

2.3 tmpfs

docker run --rm \
  --tmpfs /tmp:rw,noexec,nosuid,nodev,size=128m \
  example/api:dev

tmpfs 适合缓存、临时解压目录和短期秘密;它不是持久化磁盘,且计入主机/容器内存约束。

3. 挂载会覆盖镜像文件

如果镜像中 /app/config 已经有默认文件,bind mount 一个空目录到同一路径后,镜像里的默认内容会被遮住。应用看到的是挂载内容,不是“合并目录”。

docker inspect api | jq '.[0].Mounts'
docker exec api mount
docker exec api df -h

启动数据库前要确认挂载目标路径和镜像的初始化逻辑;错误的空目录、属主和权限可能让数据库认为数据目录无效或重复初始化。

4. 容器内 UID/GID 和宿主权限

容器不会自动把用户名映射成宿主同名用户,内核最终只看数字 UID/GID。示例:

id
stat -c '%u:%g %a %n' ./data
docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD/data:/data" alpine sh -c 'touch /data/from-container'

生产镜像使用非 root 用户时,提前让挂载目录拥有正确的数字 UID/GID,或在入口脚本中以最小权限修正。不要用 chmod -R 777 解决权限问题;这会扩大任何能访问挂载的进程权限。

启用 SELinux 的主机还要考虑标签:

docker run -v "$PWD/data:/data:Z" example/api:dev

:Z/:z 的含义取决于共享范围和发行版策略,先查阅本机 SELinux 文档。

5. 数据库为什么必须用 volume

数据库不仅写数据文件,还依赖 fsync、文件锁、目录权限和一致性快照。不要把数据库数据写进容器可写层,也不要在数据库运行时直接复制底层文件目录当作“热备份”。

更稳妥的备份方式是数据库原生工具:

docker exec pg pg_dump -U postgres -Fc appdb > appdb.dump
docker exec -i pg pg_restore -U postgres -d appdb < appdb.dump

生产环境还要考虑 WAL/binlog、时间点恢复、异地副本、加密、备份校验和恢复演练。volume 解决的是数据放在哪里,不自动解决备份一致性和灾难恢复。

6. Volume 备份和迁移

对不需要应用级一致性的普通文件 volume,可以使用临时容器打包:

docker run --rm \
  -v pgdata:/from:ro \
  -v "$PWD/backup:/to" \
  alpine sh -c 'tar czf /to/pgdata.tgz -C /from .'

恢复:

docker volume create pgdata-restore
docker run --rm \
  -v pgdata-restore:/to \
  -v "$PWD/backup:/from:ro" \
  alpine sh -c 'tar xzf /from/pgdata.tgz -C /to'

恢复前要停止写入者,检查备份文件可读、属主正确、容量足够,并在隔离环境启动验证。备份命令成功不代表备份可恢复。

7. Docker data-root 和磁盘空间

查看空间:

docker system df -v
df -h
df -ih
du -xhd1 /var/lib/docker | sort -h

磁盘占用来源包括镜像 layer、容器可写层、BuildKit cache、volume、json-file 日志和 Desktop VM 磁盘。不要只看 docker images,也要看日志和构建缓存。

迁移 data-root 的原则:

  1. 规划新磁盘和挂载点,确认文件系统支持所需存储驱动。
  2. 停止容器和 Docker daemon,复制并校验数据。
  3. 修改 /etc/docker/daemon.jsondata-root
  4. 启动 daemon,检查 docker info、容器、镜像和 volume。
  5. 保留旧目录一段时间,确认业务稳定后再清理。

不要在 daemon 运行时直接移动 /var/lib/docker,也不要把 Docker 的内部目录同时交给另一个 daemon 使用。

8. 日志不是数据卷的替代品

推荐应用把日志写到 stdout/stderr,让 Docker 日志驱动或外部采集器统一处理:

docker run --log-driver local \
  --log-opt max-size=50m --log-opt max-file=5 \
  example/api:dev

高吞吐或合规场景可以使用 journald、fluentd、syslog、云日志驱动或 sidecar。无论选哪种方式,都要限制保留周期、验证采集失败时的退化行为,并避免日志里写密码、token 和个人信息。

9. 常见存储故障排查

文件突然消失?       是否写在容器层,容器是否被重建
目录为空?           挂载是否覆盖了镜像默认文件
Permission denied?  容器 UID/GID、SELinux label、只读挂载
磁盘爆满?           layer、volume、BuildKit、日志分别统计
数据库启动失败?     数据目录属主、版本、锁文件、是否一致恢复
Desktop 空间不足?   Linux VM 磁盘镜像,而不是宿主目录表面大小

证据命令:

docker inspect api | jq '.[0].Mounts'
docker diff api
docker system df -v
docker volume inspect pgdata

10. volume 的初始化、nocopy 与驱动边界

Docker 首次把一个空 volume 挂载到镜像中已有的非空目标目录时,可能把目标目录的初始内容复制进 volume。这个行为方便初始化默认配置,但对大型目录、数据库目录和误判的挂载目标都可能带来意外开销。

需要明确禁止复制时:

docker run --rm \
  --mount type=volume,src=appdata,dst=/app/data,volume-nocopy \
  example/api:dev

volume-nocopy 不是数据清理工具;已有 volume 数据不会被删除。先 docker volume inspect、确认挂载点和初始化脚本,再决定是否使用。

Docker volume 也可以使用第三方 volume driver,把数据放到 NFS、云盘或其他后端:

docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=storage.example.com,rw,nfsvers=4 \
  --opt device=:/exports/app \
  shared-data

网络存储会改变锁、fsync、延迟、故障和一致性语义。数据库能否放在 NFS 上不能只看“容器能否挂载”,必须按照数据库官方支持矩阵和目标存储做故障、延迟、断网、恢复测试。

11. mount propagation 和宿主挂载变化

bind mount 还涉及挂载传播:privaterslaveshared 决定容器内新挂载是否传播到宿主或其他 namespace。Docker 默认会选择较安全的传播行为;需要让 CSI、设备插件或 systemd mount 变化进入容器时,才显式配置:

docker run --rm \
  --mount type=bind,src=/var/lib/plugin,dst=/plugin,bind-propagation=rslave \
  example/plugin:dev

这不是普通目录权限。即使目录是只读 bind mount,进程还可能通过其他路径、设备或 capability 影响底层对象。涉及宿主 mount 的容器要配合 CAP_SYS_ADMIN 风险评估、只读根和 LSM 策略。

12. 一致性备份的三种层级

方式一致性恢复速度适用范围
应用原生 dump通常最高,可做逻辑选择较慢PostgreSQL、MySQL、Redis 等
文件系统快照取决于冻结/快照协调支持快照的块存储/volume
直接 tar volume只有停止写入或应用配合时可靠中等普通文件、冷备、迁移

“备份命令返回 0”只证明读取过程完成,不证明恢复可用。恢复验收至少包括:

# 恢复到隔离 volume/临时实例
docker volume create restore-check
docker run --rm -v restore-check:/data -v "$PWD/backup:/backup:ro" \
  alpine sh -c 'tar xzf /backup/appdata.tgz -C /data'

# 启动临时实例并运行应用级校验
docker run -d --name restore-check-app \
  -v restore-check:/var/lib/app \
  example/api:1.0.0
docker logs --tail 100 restore-check-app

验证结束后精确删除临时容器和 volume,并记录恢复耗时、数据校验结果和缺失的外部依赖。生产备份还应加密、异地复制、限制恢复权限和定期演练。

13. 容器层写放大实验

docker run -d --name io-lab alpine:3.20 sh -c \
  'mkdir -p /work; while true; do dd if=/dev/zero of=/work/blob bs=1M count=32 conv=fsync; rm -f /work/blob; done'

docker stats --no-stream io-lab
docker diff io-lab
docker system df -v

即使文件反复删除,overlayfs upperdir 的元数据和写放大也可能影响磁盘与 I/O。生产高频临时文件、数据库 WAL、缓存和日志应按生命周期使用 tmpfs、专用 volume、外部存储或日志管道,而不是全部塞进容器层。

docker rm -f io-lab

14. 本篇小结

存储设计先回答“数据生命周期是什么”:临时缓存使用容器层或 tmpfs,配置和源码视场景使用只读 bind mount,数据库和业务数据使用 volume 或外部存储。再回答“如何备份、恢复、迁移、限制权限和控制空间”,这样容器销毁和重建才不会变成数据事故。