数据持久化与备份
容器适合随时删除和重建,业务数据却不能随之消失。本篇先验证容器可写层的生命周期,再使用命名卷和 bind mount 保存数据。
完成后,你将能够:
- 解释为什么重要数据不能只写在容器可写层;
- 在命名卷和 bind mount 之间做选择;
- 查看挂载信息和卷内容;
- 对普通文件卷执行备份与恢复;
- 避免误删仍有价值的数据。
1. 容器删除后,数据发生什么
容器在镜像上增加一层可写层。容器停止时该层仍存在,删除容器时它也会被删除。
# 创建容器,在其可写层中写入一个文件后退出
docker run --name writable-demo \
alpine:3.20 \
sh -c 'echo "important data" > /message.txt'
# 从已停止容器读取文件,证明停止不会立刻删除数据
docker start writable-demo
docker cp writable-demo:/message.txt ./message-from-container.txt
# 删除容器,可写层随之删除
docker rm writable-demo
# 同镜像创建的新容器中不存在刚才的文件
docker run --rm alpine:3.20 \
sh -c 'test ! -f /message.txt && echo "message.txt does not exist"'
这不是数据丢失故障,而是容器的正常模型。需要保留的数据必须写入容器生命周期之外的存储。
2. 三种存储方式
| 方式 | 数据位置 | 适合场景 | 特点 |
|---|---|---|---|
| 容器可写层 | Docker 内部 | 临时文件、缓存 | 删除容器后消失 |
| 命名卷 | Docker 管理 | 数据库、上传文件、应用状态 | 易迁移、与宿主路径解耦 |
| bind mount | 明确的宿主机路径 | 开发源码、配置文件、文件交换 | 路径和权限依赖宿主机 |
新手可以先遵循一个简单规则:持久化业务数据优先使用命名卷;需要直接编辑或交换宿主机文件时使用 bind mount。
3. 命名卷持久化实验
3.1 创建并写入卷
# 创建容易识别的命名卷
docker volume create beginner-data
# 列出卷,确认 beginner-data 已存在
docker volume ls --filter name=beginner-data
# 把卷挂载到 /data,并写入文件;容器退出后自动删除
docker run --rm \
--mount type=volume,src=beginner-data,dst=/data \
alpine:3.20 \
sh -c 'echo "saved in volume" > /data/message.txt'
这里使用更清晰的 --mount 语法。等价的短语法是 -v beginner-data:/data,但参数较多时 --mount 更容易读懂,也会在源路径不存在时更早报错。
3.2 用另一个容器读取
# 创建全新的临时容器,并以只读方式挂载同一个卷
docker run --rm \
--mount type=volume,src=beginner-data,dst=/data,readonly \
alpine:3.20 \
cat /data/message.txt
应输出 saved in volume。写入和读取使用的是两个不同容器,说明数据属于卷而不是容器。
3.3 查看挂载信息
# 查看卷的驱动、标签和 Docker 管理的宿主机挂载点
docker volume inspect beginner-data
# 创建一个保留的容器,便于查看 Mounts 配置
docker create --name volume-inspect \
--mount type=volume,src=beginner-data,dst=/data \
alpine:3.20
# 查看容器内目标路径、读写模式和源卷名称
docker inspect volume-inspect \
--format '{{json .Mounts}}'
# 删除仅用于检查的容器;卷不会一起删除
docker rm volume-inspect
不要直接编辑 /var/lib/docker/volumes 中的文件。该路径是 Docker 的内部实现,Docker Desktop 中还位于 Linux 虚拟机内。应通过挂载卷的容器访问数据。
4. bind mount 实验
bind mount 把一个明确的宿主机路径映射到容器。下面让 Nginx 直接读取本地网页。
# 创建实验目录和首页
mkdir -p ~/docker-beginner/bind-site
cd ~/docker-beginner/bind-site
# 使用 heredoc 创建网页;HTML 注释说明该文件来自宿主机
cat > index.html <<'HTML'
<!-- 这个文件通过 bind mount 提供给 Nginx -->
<!doctype html>
<html lang="zh-CN">
<body>
<h1>Bind mount 生效</h1>
</body>
</html>
HTML
运行容器:
# 将当前宿主机目录只读挂载到 Nginx 网站目录
docker run --detach \
--name bind-web \
--publish 127.0.0.1:8080:80 \
--mount type=bind,src="$PWD",dst=/usr/share/nginx/html,readonly \
nginx:1.27-alpine
# 请求页面,应看到刚创建的标题
curl http://127.0.0.1:8080
现在直接修改宿主机上的 index.html,刷新浏览器即可看到变化,不必重建镜像。开发时很方便,但生产部署应确保宿主机路径、权限和内容都由可靠流程管理。
把宿主机目录挂载到 /usr/share/nginx/html 后,镜像原本在该目录中的文件会被挂载内容遮住。卸载后原文件仍在镜像中。挂错目标路径可能让应用找不到原配置或文件。
只挂载一个配置文件
# 先验证本地配置语法;容器只读访问该文件
docker run --rm \
--mount type=bind,src="$PWD/nginx.conf",dst=/etc/nginx/nginx.conf,readonly \
nginx:1.27-alpine \
nginx -t
这条命令要求当前目录确实存在 nginx.conf。--mount 在源文件不存在时会直接报错,能避免短语法自动创建同名目录导致的困惑。
Linux 权限与 SELinux
- 容器进程仍受宿主机文件权限约束;需要按容器 UID/GID 授权;
- 启用 SELinux 的发行版可能还需要合适的标签;
- 不要为了省事使用
chmod 777;先确认应用实际所需的最小权限; - Docker Desktop 需要允许共享对应宿主机目录。
5. 匿名卷为什么难管理
只写容器目标路径会创建随机名称的匿名卷:
# -v 只给出容器路径,因此 Docker 创建匿名卷
docker create --name anonymous-demo \
--volume /data \
alpine:3.20
# 查看随机生成的卷名称
docker inspect anonymous-demo \
--format '{{range .Mounts}}{{.Name}}{{end}}'
# 删除容器并同时删除它关联的匿名卷
docker rm --volumes anonymous-demo
匿名卷难以识别数据归属。教程和生产环境应显式命名卷,或交给 Compose 使用项目化名称管理。
6. 备份普通文件卷
下面的方法适合普通文件。对于 PostgreSQL、MySQL 等数据库,应优先使用 pg_dump、mysqldump 或一致性快照;直接打包正在写入的数据目录可能得到不可恢复的备份。
先创建备份目录:
# 在当前实验目录创建宿主机备份目录
mkdir -p backup
# 确认待备份卷中的文件内容
docker run --rm \
--mount type=volume,src=beginner-data,dst=/data,readonly \
alpine:3.20 \
ls -la /data
执行备份:
# 源卷只读挂载到 /data,宿主机 backup 目录挂载到 /backup
docker run --rm \
--mount type=volume,src=beginner-data,dst=/data,readonly \
--mount type=bind,src="$PWD/backup",dst=/backup \
alpine:3.20 \
tar czf /backup/beginner-data.tgz -C /data .
# 在宿主机确认归档文件已创建
ls -lh ./backup/beginner-data.tgz
7. 恢复到新卷并验证
恢复时不要直接覆盖正在使用的原卷。先创建新卷,恢复并验证,再切换应用。
# 创建空的目标卷
docker volume create beginner-data-restored
# 将备份文件只读挂载,把内容解压到新卷
docker run --rm \
--mount type=volume,src=beginner-data-restored,dst=/data \
--mount type=bind,src="$PWD/backup",dst=/backup,readonly \
alpine:3.20 \
tar xzf /backup/beginner-data.tgz -C /data
# 用另一个临时容器读取恢复结果
docker run --rm \
--mount type=volume,src=beginner-data-restored,dst=/data,readonly \
alpine:3.20 \
cat /data/message.txt
输出应为 saved in volume。一次完整备份流程必须包含恢复验证。实际策略还需要定义加密、异地副本、保留周期、访问权限和定期演练。
8. 删除卷前必须确认
Docker 不允许删除仍被容器引用的卷,即使该容器已经停止:
# 查看全部容器及其挂载,排查谁还在引用卷
docker ps -a \
--format 'table {{.Names}}\t{{.Status}}\t{{.Mounts}}'
# 查看两个实验卷的详细信息
docker volume inspect beginner-data beginner-data-restored
确认备份和数据归属后,再清理本篇实验:
# 删除 bind mount 实验容器;宿主机文件不会被删除
docker rm --force bind-web
# 删除明确指定的实验卷;卷内数据会永久删除
docker volume rm beginner-data beginner-data-restored
# 确认实验卷已经不存在
docker volume ls --filter name=beginner-data
docker volume prune 会删除所有未被容器引用的卷。“未引用”不等于“没有价值”,例如已下线服务的卷可能仍需保留。不了解数据归属时,不要使用 docker system prune --volumes。
9. 本篇练习
- 创建一个命名卷,用容器 A 写入两个文件,再用容器 B 只读挂载并查看;
- 删除容器 A 和 B,确认卷及文件仍然存在;
- 将卷备份到宿主机,并恢复到另一个新卷;
- 比较
docker inspect 容器中 volume 和 bind mount 的Type、Source、Destination。
下一篇将用 Compose 把服务、网络、卷和健康检查写进一个可重复执行的 YAML 文件。