Docker-08 数据卷与存储:持久化、权限、备份和磁盘治理
1. 容器里的文件到底存在哪里
容器的根文件系统通常由多个只读镜像层和一个可写层组成:
镜像层:/usr、/bin、应用依赖(只读)
容器层:运行时新建或修改的文件(可写)
↓ overlayfs 统一视图
容器内的 /
容器删除时,可写层通常也会被删除。因此日志、上传文件、数据库数据写在容器层里都是高风险做法。容器适合无状态,状态应该明确放到 volume、宿主目录或外部存储。
2. 三种常用挂载
| 类型 | 数据位置 | 适合场景 | 主要风险 |
|---|---|---|---|
| named volume | Docker 管理的目录 | 数据库、应用持久数据 | 备份和迁移需要通过容器/宿主路径处理 |
| 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 的原则:
- 规划新磁盘和挂载点,确认文件系统支持所需存储驱动。
- 停止容器和 Docker daemon,复制并校验数据。
- 修改
/etc/docker/daemon.json的data-root。 - 启动 daemon,检查
docker info、容器、镜像和 volume。 - 保留旧目录一段时间,确认业务稳定后再清理。
不要在 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 还涉及挂载传播:private、rslave、shared 决定容器内新挂载是否传播到宿主或其他 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 或外部存储。再回答“如何备份、恢复、迁移、限制权限和控制空间”,这样容器销毁和重建才不会变成数据事故。
| 上一篇 | 下一篇 |
|---|---|
| 07-网络原理与排障 | 09-Compose 多容器编排 |