Docker存储与数据持久化
本文是Docker专栏的第六篇文章,将全面深入地讲解Docker存储体系、存储驱动、数据卷、绑定挂载、tmpfs挂载、数据卷进阶操作、Dockerfile存储配置、数据库持久化方案、存储安全与备份策略以及故障排查等全部知识。全文超过3万字,包含大量实战案例与代码示例,适合有一定Docker基础的开发者深入学习和参考。
前言
在Docker的世界里,容器的生命周期通常是短暂的。一个容器可以被创建、启动、停止、删除,而当容器被删除时,容器内部写入的所有数据也会随之消失。这种"临时性"是容器设计的核心特征之一,它使得容器可以随时被重建和替换,但也带来了一个严峻的挑战——如何持久化保存数据?
想象这样一个场景:你运行了一个MySQL容器,业务系统持续写入数据。某天因为配置变更需要删除并重建容器,结果所有数据库数据全部丢失。又或者,你在开发环境中修改了代码,但因为容器的隔离性,修改无法反映到容器内运行的应用中。这些问题都指向同一个核心概念——Docker存储与数据持久化。
Docker提供了多种存储机制来解决这些问题,包括Volume(数据卷)、Bind Mount(绑定挂载)和tmpfs Mount(临时文件系统挂载)。同时,Docker底层还依赖存储驱动(Storage Driver)来管理镜像层和容器可写层。理解这些机制的原理和使用方法,是每一位Docker使用者的必修课。
本文将从底层原理出发,全面讲解Docker存储的方方面面,帮助你在生产环境中正确、高效地管理数据。
第一章 Docker存储体系概览
1.1 容器存储的挑战
1.1.1 容器文件系统的临时性
Docker容器基于镜像运行,而镜像是只读的。当容器启动时,Docker会在镜像的最顶层添加一个可写层(Writable Layer),也称为容器层(Container Layer)。所有的写操作——创建文件、修改文件、删除文件——都发生在这个可写层中。
这个设计带来了一个根本性的问题:当容器被删除时,可写层也会被删除,所有写入的数据都会丢失。
让我们通过一个简单的实验来验证这一点:
# 启动一个Ubuntu容器,在容器内创建一个文件
docker run -d –name test-container ubuntu:22.04 sleep 3600
# 进入容器,创建一个测试文件
docker exec test-container bash -c 'echo "Hello Docker Storage" > /tmp/test.txt'
# 查看文件内容
docker exec test-container cat /tmp/test.txt
# 输出: Hello Docker Storage
# 删除容器
docker rm -f test-container
# 重新启动一个同名容器,尝试查看文件
docker run -d –name test-container ubuntu:22.04 sleep 3600
docker exec test-container cat /tmp/test.txt
# 输出: cat: /tmp/test.txt: No such file or directory
如上所示,文件已经丢失。这是因为新容器拥有全新的可写层,与之前的容器没有任何关联。
1.1.2 数据丢失带来的问题
容器存储的临时性在实际应用中会导致多种问题:
1.1.3 为什么不能直接在容器中存储数据
除了数据丢失的问题外,直接在容器可写层中存储数据还有以下缺点:
- 性能开销:存储驱动使用写时复制(Copy-on-Write)机制,对可写层的写操作有额外的性能开销。
- 存储效率低:可写层与镜像层分离,可能导致存储空间浪费。
- 不可移植:容器内的数据与容器绑定,无法轻松迁移到其他主机或其他容器。
- 难以备份:容器可写层的数据难以单独备份,通常需要借助docker commit生成新镜像,但这不是数据持久化的正确方式。
1.2 Docker存储三大方式: Volume、Bind Mount、tmpfs
为了解决容器存储的临时性问题,Docker提供了三种数据持久化机制:
1.2.1 Volume(数据卷)
Volume是Docker管理的存储方式,由Docker守护进程创建和管理,存储在宿主机的特定目录下(默认为/var/lib/docker/volumes/)。数据卷独立于容器的生命周期,即使容器被删除,数据卷中的数据依然存在。
主要特点:
- 由Docker管理,存储位置固定且可预测
- 支持命名数据卷和匿名数据卷
- 可以在多个容器之间共享
- 支持数据卷驱动,可以扩展到远程存储(NFS、云存储等)
- 不受容器可写层性能影响,读写性能接近宿主机原生文件系统
1.2.2 Bind Mount(绑定挂载)
Bind Mount将宿主机上的一个绝对路径直接挂载到容器中。容器可以直接读写宿主机上的文件,适合开发环境中的代码热重载等场景。
主要特点:
- 依赖宿主机的目录结构,需要指定绝对路径
- 容器可以直接修改宿主机上的文件,反之亦然
- 适合开发环境,如代码挂载、配置文件挂载
- 无法通过docker volume命令管理
- 在不同环境中的行为可能不一致(路径依赖)
1.2.3 tmpfs Mount(临时文件系统挂载)
tmpfs挂载将数据存储在宿主机的内存中,不会写入磁盘。适合存储敏感信息(如密钥)或临时缓存数据。
主要特点:
- 数据存储在内存中,读写速度极快
- 容器停止后数据消失,不会持久化到磁盘
- 适合存储敏感信息,避免敏感数据落盘
- 只能在Linux上使用
- 受可用内存大小限制
1.2.4 三种存储方式对比
| 存储位置 | Docker管理区域(/var/lib/docker/volumes/) | 宿主机任意路径 | 宿主机内存 |
| 管理方式 | docker volume命令 | 直接使用宿主机路径 | –tmpfs或–mount |
| 持久性 | 持久(独立于容器生命周期) | 持久(取决于宿主机文件) | 临时(容器停止即消失) |
| 共享性 | 多容器共享 | 多容器共享 | 不可共享(单容器) |
| 性能 | 接近原生 | 接近原生 | 极快(内存) |
| 可移植性 | 高 | 低(路径依赖) | 不适用 |
| 安全性 | 较高 | 中(直接操作宿主机文件) | 高(不落盘) |
| 适用场景 | 生产环境数据持久化 | 开发环境代码挂载 | 敏感信息、临时缓存 |
| 跨平台 | 支持 | 支持(路径不同) | 仅Linux |
| 备份恢复 | 方便(docker volume命令) | 直接操作宿主机文件 | 不需要(临时数据) |
1.3 存储驱动(Storage Driver)与数据卷的区别
许多Docker初学者容易混淆"存储驱动"和"数据卷"这两个概念。它们虽然都与存储相关,但在功能和作用层面完全不同。
1.3.1 存储驱动
存储驱动(Storage Driver)负责管理镜像层(Image Layers)和容器可写层(Container Writable Layer)。它处理的是Docker镜像的分层存储、写时复制(Copy-on-Write)等底层机制。
- 作用对象:镜像层和容器可写层
- 关注点:如何高效地存储和叠加镜像层,如何处理容器的读写操作
- 与数据持久化的关系:存储驱动管理的数据是临时的(容器删除即丢失)
- 配置方式:在/etc/docker/daemon.json中通过storage-driver参数配置
- 常见驱动:overlay2、aufs、devicemapper、btrfs、zfs、vfs
1.3.2 数据卷
数据卷(Volume)是Docker提供的数据持久化机制,用于将数据存储在容器可写层之外。
- 作用对象:需要持久化的数据
- 关注点:如何在容器生命周期之外保存数据
- 与存储驱动的关系:数据卷绕过存储驱动,直接写入宿主机文件系统
- 配置方式:在docker run时通过-v或–mount参数指定
- 管理命令:docker volume create/ls/rm/inspect/prune
1.3.3 一图理解两者关系
┌─────────────────────────────────────────────────────┐
│ Docker 容器 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 容器可写层 (存储驱动管理) │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ 镜像层N (只读) │ │ │
│ │ ├───────────────────────────────────────────┤ │ │
│ │ │ 镜像层… (只读) │ │ │
│ │ ├───────────────────────────────────────────┤ │ │
│ │ │ 镜像层1 (只读) │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 数据卷挂载点 (绕过存储驱动,直接写入宿主机) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 宿主机 │
│ /var/lib/docker/volumes/ ← 数据卷存储位置 │
│ /var/lib/docker/overlay2/ ← 存储驱动管理区域 │
└─────────────────────────────────────────────────────┘
简单来说:存储驱动管理"容器的临时数据",数据卷管理"需要持久化的数据"。数据卷绕过了存储驱动,直接在宿主机文件系统上进行读写,因此性能更好且数据更安全。
1.4 Docker存储架构总览
Docker的存储架构可以从多个层面来理解:
1.4.1 整体架构
┌──────────────────────────────────────────────────────────┐
│ Docker 守护进程 (dockerd) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 镜像管理模块 │ │ 容器管理模块 │ │ 数据卷管理模块 │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐│
│ │ 存储驱动 (Storage Driver) ││
│ │ overlay2 / aufs / devicemapper / btrfs / zfs ││
│ └──────────────────────────────────────────────────────┘│
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ /var/lib/ │ │ /var/lib/ │ │ /var/lib/ │ │
│ │ docker/ │ │ docker/ │ │ docker/ │ │
│ │ overlay2/ │ │ containers/ │ │ volumes/ │ │
│ │ (镜像层+可写层)│ │ (容器配置日志) │ │ (数据卷) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐│
│ │ 数据卷驱动 (Volume Driver) ││
│ │ local / nfs / block / 第三方插件 ││
│ └──────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 宿主机文件系统 │
│ ext4 / xfs / btrfs / zfs / nfs │
└──────────────────────────────────────────────────────────┘
1.4.2 关键目录说明
Docker在宿主机上的默认存储目录为/var/lib/docker/,其中包含以下重要子目录:
| /var/lib/docker/overlay2/ | overlay2存储驱动管理的镜像层和容器可写层 |
| /var/lib/docker/volumes/ | 数据卷的存储位置 |
| /var/lib/docker/containers/ | 容器的配置文件和日志文件 |
| /var/lib/docker/image/ | 镜像相关的元数据 |
| /var/lib/docker/plugins/ | 存储插件相关文件 |
| /var/lib/docker/network/ | 网络相关文件 |
| /var/lib/docker/tmp/ | Docker临时文件 |
| /var/lib/docker/swarm/ | Docker Swarm相关文件 |
可以通过以下命令查看Docker的根目录:
# 查看Docker信息中的根目录
docker info | grep "Docker Root Dir"
# 输出: Docker Root Dir: /var/lib/docker
# 查看各目录的磁盘使用情况
sudo du -sh /var/lib/docker/*
1.4.3 数据流向
当容器需要进行文件读写时,数据的流向取决于文件所在的层级:
1.5 容器可写层与数据持久化层的关系
1.5.1 容器可写层的工作机制
每个容器启动时,Docker会为其创建一个可写层。这个可写层叠加在镜像的所有只读层之上,形成一个统一的文件系统视图(通过联合文件系统UnionFS实现)。
# 查看容器的可写层信息(以overlay2为例)
docker inspect <container_id> | grep -A 10 "GraphDriver"
# 输出示例:
# "GraphDriver": {
# "Data": {
# "LowerDir": "/var/lib/docker/overlay2/…/diff",
# "MergedDir": "/var/lib/docker/overlay2/…/merged",
# "UpperDir": "/var/lib/docker/overlay2/…/diff",
# "WorkDir": "/var/lib/docker/overlay2/…/work"
# },
# "Name": "overlay2"
# }
其中:
- LowerDir:镜像层(只读),可能有多个
- UpperDir:容器可写层
- MergedDir:合并后的统一视图
- WorkDir:overlay内部使用的工作目录
1.5.2 写时复制(Copy-on-Write)机制
写时复制是容器存储的核心机制。当容器需要修改一个存在于镜像层中的文件时,Docker不会直接修改镜像层(因为镜像是只读的),而是:
这个机制确保了:
- 镜像层保持不变,多个容器可以共享同一个镜像
- 每个容器有自己的修改副本,互不影响
- 只有被修改的文件才会被复制,节省存储空间
# 演示写时复制
# 启动容器
docker run -d –name cow-test ubuntu:22.04 sleep 3600
# 查看可写层的大小(初始为很小)
sudo du -sh $(docker inspect cow-test –format '{{.GraphDriver.Data.UpperDir}}')
# 输出: 12K /var/lib/docker/overlay2/xxx/diff
# 在容器中修改一个文件
docker exec cow-test bash -c 'echo "modified" >> /etc/hostname'
# 再次查看可写层大小(增大了)
sudo du -sh $(docker inspect cow-test –format '{{.GraphDriver.Data.UpperDir}}')
# 输出: 16K /var/lib/docker/overlay2/xxx/diff
# 清理
docker rm -f cow-test
1.5.3 数据持久化层的角色
数据持久化层(数据卷、绑定挂载)与容器可写层是完全独立的:
# 演示数据卷挂载点覆盖
# 使用nginx镜像,其默认网站根目录为/usr/share/nginx/html
docker run -d –name nginx-test -v nginx-html:/usr/share/nginx/html nginx:latest
# 查看数据卷中的内容(会包含nginx默认的欢迎页面,因为首次挂载时从镜像复制)
docker exec nginx-test ls /usr/share/nginx/html
# 输出: 50x.html index.html
# 修改数据卷中的内容
docker exec nginx-test bash -c 'echo "<h1>Custom Content</h1>" > /usr/share/nginx/html/index.html'
# 删除容器
docker rm -f nginx-test
# 重新创建容器并挂载同一个数据卷
docker run -d –name nginx-test2 -v nginx-html:/usr/share/nginx/html nginx:latest
# 验证数据仍然存在
docker exec nginx-test2 cat /usr/share/nginx/html/index.html
# 输出: <h1>Custom Content</h1>
# 清理
docker rm -f nginx-test2
docker volume rm nginx-html
这个例子清楚地展示了数据卷如何绕过容器可写层,实现数据的持久化存储。
第二章 存储驱动详解
2.1 存储驱动的作用(管理镜像层和容器可写层)
存储驱动是Docker存储架构的核心组件,负责管理镜像层(Image Layers)和容器可写层(Container Writable Layer)。它的主要职责包括:
2.1.1 镜像层管理
Docker镜像由多个只读层(Layer)组成,每一层对应Dockerfile中的一条指令。存储驱动负责:
- 层叠加:将多个只读层叠加成一个统一的文件系统视图
- 层共享:多个镜像可以共享相同的底层,节省存储空间
- 层缓存:构建镜像时利用缓存层加速构建过程
# 每条指令都会创建一个新的镜像层
FROM ubuntu:22.04 # 基础层(来自ubuntu镜像)
RUN apt-get update # 新层
RUN apt-get install -y nginx # 新层
COPY index.html /var/www/html/ # 新层
CMD ["nginx", "-g", "daemon off;"] # 新层(元数据层)
# 查看镜像的分层结构
docker history nginx:latest
# 输出示例:
# IMAGE CREATED CREATED BY SIZE
# 605c77e624dd 2 weeks ago /bin/sh -c #(nop) CMD ["nginx" "g" "daemon… 0B
# 8140c0a4wr2e 2 weeks ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B
# 41a1a8e923e3 2 weeks ago /bin/sh -c #(nop) EXPOSE 80 0B
# 12ad5138f5b1 2 weeks ago /bin/sh -c #(nop) ENTRYPOINT ["/docker-entr… 0B
# …
2.1.2 容器可写层管理
当容器启动时,存储驱动会在镜像层之上添加一个可写层:
- 写操作处理:所有写操作都记录在可写层中
- 写时复制:修改镜像层文件时,先复制到可写层再修改
- 删除操作处理:在可写层中标记文件为已删除(whiteout文件)
2.1.3 存储驱动必须实现的核心功能
一个完整的存储驱动需要实现以下核心功能:
| 层叠加 | 将多个只读层与一个可写层叠加为统一视图 |
| 写时复制 | 修改文件时按需从只读层复制到可写层 |
| 层共享 | 不同镜像/容器共享相同的只读层 |
| 层去重 | 相同内容的层只存储一份 |
| 快速创建 | 快速创建容器(不需要复制整个镜像) |
| 快速删除 | 快速删除容器和镜像层 |
2.2 overlay2驱动详解
2.2.1 overlay2简介
overlay2是Docker当前推荐的存储驱动,它是OverlayFS的用户空间实现。OverlayFS是Linux内核3.18版本引入的联合文件系统(Union Filesystem),overlay2是其第二代实现,支持多层lowerdir叠加(最多128层),比第一代overlay驱动更高效。
overlay2的优势:
- 内核原生支持,性能优异
- 支持多层叠加(最多128层),无需为每层创建单独的挂载
- 页缓存共享,内存利用率高
- 稳定性好,适合生产环境
- Docker官方推荐
系统要求:
- Linux内核4.0或更高版本(推荐4.0+)
- 文件系统支持:ext4、xfs(需要ftype=1)、btrfs等
2.2.2 overlay2工作原理
overlay2将文件系统分为三层:
此外还有一个WorkDir(工作目录),用于overlayfs内部操作(如原子性操作)。
容器视角(MergedDir)
┌───────────────────────┐
│ /usr/share/nginx/ │
│ /etc/nginx/ │
│ /var/log/nginx/ │
│ … │
└───────────┬───────────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ UpperDir │ │ LowerDir │ │ LowerDir │
│ (可写层) │ │ (镜像层3) │ │ (镜像层2) │
│ 容器修改 │ │ COPY指令 │ │ RUN指令 │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ LowerDir │
│ (镜像层1) │
│ FROM指令 │
└─────────────┘
2.2.3 overlay2的文件操作机制
读取文件:
修改文件(写时复制):
删除文件:
创建目录:
2.2.4 查看overlay2的实际结构
# 启动一个容器
docker run -d –name overlay-test nginx:latest
# 查看容器的存储驱动信息
docker inspect overlay-test –format '{{json .GraphDriver.Data}}' | python3 -m json.tool
# 输出示例:
# {
# "LowerDir": "/var/lib/docker/overlay2/abc…/diff:/var/lib/docker/overlay2/def…/diff:…",
# "MergedDir": "/var/lib/docker/overlay2/xyz…/merged",
# "UpperDir": "/var/lib/docker/overlay2/xyz…/diff",
# "WorkDir": "/var/lib/docker/overlay2/xyz…/work"
# }
# 查看合并后的文件系统视图
sudo ls $(docker inspect overlay-test –format '{{.GraphDriver.Data.MergedDir}}')/
# 查看可写层(初始为空或很小)
sudo ls -la $(docker inspect overlay-test –format '{{.GraphDriver.Data.UpperDir}}')/
# 在容器中修改文件
docker exec overlay-test bash -c 'echo "test" > /tmp/overlay-test.txt'
# 再次查看可写层,可以看到新创建的文件
sudo ls -la $(docker inspect overlay-test –format '{{.GraphDriver.Data.UpperDir}}')/tmp/
# 清理
docker rm -f overlay-test
2.2.5 overlay2的whiteout文件
# 演示whiteout机制
docker run -d –name whiteout-test ubuntu:22.04 sleep 3600
# 获取UpperDir
UPPER=$(docker inspect whiteout-test –format '{{.GraphDriver.Data.UpperDir}}')
# 删除容器中的一个文件(该文件来自镜像层)
docker exec whiteout-test rm /etc/hostname
# 查看UpperDir中的whiteout文件
sudo ls -la $UPPER/etc/
# 可以看到一个名为 hostname 的字符设备文件(whiteout)
# crwxr-xr-x 1 root root 0, 0 … hostname
# 清理
docker rm -f whiteout-test
2.3 aufs驱动详解
2.3.1 aufs简介
aufs(Advanced Multi-Layered Unification Filesystem)是Docker早期使用的存储驱动之一,也是最早被Docker支持的联合文件系统。aufs功能强大,支持多层叠加,但由于未能被Linux内核主线接受,目前使用较少。
aufs的特点:
- 支持多层叠加(最多127层)
- 功能成熟稳定
- 未被Linux内核主线接受,需要额外安装
- 在Ubuntu/Debian上较常见,在其他发行版上可能不可用
- Docker已不再推荐使用aufs
2.3.2 aufs工作原理
aufs的工作原理与overlay2类似,都是通过联合挂载实现多层叠加:
容器视角
┌──────────┐
│ /usr/ │
│ /etc/ │
│ /var/ │
└────┬─────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ 可写层 │ │ 只读层3 │ … │ 只读层1 │
│(container)│ │(image) │ │(base) │
└────────┘ └────────┘ └────────┘
与overlay2不同的是:
- aufs使用aufs挂载类型,而非overlay
- aufs的whiteout实现方式不同(使用.wh.前缀文件)
- aufs支持更多的分支策略(round-robin等)
2.3.3 aufs的目录结构
# aufs驱动下,容器的存储目录
# /var/lib/docker/aufs/diff/<container_id>/ → 可写层
# /var/lib/docker/aufs/diff/<layer_id>/ → 镜像层
# /var/lib/docker/aufs/mnt/<container_id>/ → 合并视图
# /var/lib/docker/aufs/layers/<container_id> → 层依赖关系
2.3.4 为什么aufs逐渐被淘汰
2.4 devicemapper驱动详解
2.4.1 devicemapper简介
devicemapper是Linux内核提供的设备映射框架,Docker利用它实现了基于块设备的存储驱动。与overlay2和aufs基于文件的联合文件系统不同,devicemapper工作在块设备层面。
devicemapper的特点:
- 基于块设备,不依赖文件系统
- 使用精简配置(Thin Provisioning)机制
- 在CentOS/RHEL上曾是默认驱动
- 性能不如overlay2
- 配置复杂,容易出问题
- Docker已不再推荐
2.4.2 devicemapper的两种模式
2.4.3 devicemapper工作原理
┌─────────────────────────────────┐
│ ThinPool(精简池) │
│ ┌───────────┐ ┌───────────┐ │
│ │ Data Device│ │Metadata Dev│ │
│ │ (数据块设备) │ │(元数据设备) │ │
│ └───────────┘ └───────────┘ │
└─────────────────────────────────┘
│
┌─────────┼─────────┐
│ │ │
▼ ▼ ▼
┌────────┐┌────────┐┌────────┐
│容器可写层││镜像层3 ││镜像层1 │
│(Thin Vol)││(Snap) ││(Base) │
└────────┘└────────┘└────────┘
devicemapper使用精简配置(Thin Provisioning)来管理存储空间:
- 基础设备(Base Device):一个精简池(ThinPool),所有镜像层和容器层都从中分配空间
- 镜像层:作为精简池中的基础卷(Base Volume)
- 容器可写层:作为镜像层的快照(Snapshot)
- 写时复制:在块设备层面实现,修改时复制对应的块
2.4.4 配置direct-lvm模式
// /etc/docker/daemon.json
{
"storage-driver": "devicemapper",
"storage-opts": [
"dm.directlvm_device=/dev/sdb",
"dm.thinp_percent=95",
"dm.thinp_metapercent=1",
"dm.thinp_autoextend_threshold=80",
"dm.thinp_autoextend_percent=20",
"dm.directlvm_device_force=false"
]
}
# 配置完成后重启Docker
sudo systemctl restart docker
# 验证配置
docker info | grep -A 10 "Storage Driver"
# 输出应显示:
# Storage Driver: devicemapper
# Pool Name: docker-thinpool
# Pool Blocksize: 65.54kB
# …
2.5 btrfs驱动详解
2.5.1 btrfs简介
btrfs(B-tree file system,通常发音为"Butter FS"或"B-tree FS")是现代的写时复制文件系统,Docker可以利用btrfs的子卷(Subvolume)和快照(Snapshot)功能来实现存储驱动。
btrfs的特点:
- 基于子卷和快照,原生支持写时复制
- 支持透明压缩,节省存储空间
- 支持子卷配额,可以限制容器存储大小
- 适合大量小文件场景
- 稳定性曾受质疑,但在不断改善
- 需要将/var/lib/docker格式化为btrfs文件系统
2.5.2 btrfs工作原理
┌─────────────────────────────────┐
│ btrfs 文件系统 │
│ │
│ ┌───────────────────────────┐ │
│ │ /var/lib/docker/btrfs/ │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │ 子卷:容器 │ │ 子卷:镜像 │ │ │
│ │ │ (可写) │ │ (只读) │ │ │
│ │ │ (快照) │ │ │ │ │
│ │ └─────────┘ └─────────┘ │ │
│ │ │ │
│ └───────────────────────────┘ │
│ │
│ 透明压缩 │ 快照 │ 配额 │ 去重 │
└─────────────────────────────────┘
- 镜像层:btrfs子卷(只读)
- 容器可写层:镜像层子卷的快照(可读写)
- 写时复制:btrfs原生支持,在文件系统层面实现
2.5.3 配置btrfs驱动
# 1. 格式化设备为btrfs
sudo mkfs.btrfs /dev/sdb
# 2. 挂载到/var/lib/docker
sudo mkdir -p /var/lib/docker
sudo mount /dev/sdb /var/lib/docker
# 3. 配置Docker使用btrfs
# /etc/docker/daemon.json
# {
# "storage-driver": "btrfs"
# }
# 4. 重启Docker
sudo systemctl restart docker
# 5. 验证
docker info | grep "Storage Driver"
# 输出: Storage Driver: btrfs
2.5.4 btrfs的配额管理
# 启用配额
sudo btrfs quota enable /var/lib/docker
# 设置子卷配额(限制为10GB)
sudo btrfs qgroup limit 10G /var/lib/docker/btrfs/subvolumes/<container_id>
# 查看配额使用情况
sudo btrfs qgroup show /var/lib/docker
2.6 zfs驱动详解
2.6.1 zfs简介
ZFS(Zettabyte File System)是由Sun Microsystems开发的高级文件系统,以其数据完整性、快照功能和海量存储支持而闻名。Docker可以利用ZFS的数据集(Dataset)和快照(Snapshot)功能实现存储驱动。
zfs的特点:
- 高级文件系统,支持数据完整性校验
- 原生快照和克隆功能
- 内置压缩和去重
- ARC(Adaptive Replacement Cache)缓存,性能优异
- 内存占用较高
- 许可证(CDDL)与Linux内核(GPL)兼容性存在争议
2.6.2 zfs工作原理
┌─────────────────────────────────┐
│ zfs 存储池 (zpool) │
│ │
│ ┌───────────────────────────┐ │
│ │ docker/zfs (文件系统) │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │数据集:容器│ │数据集:镜像│ │ │
│ │ │ (快照) │ │ (基础) │ │ │
│ │ └─────────┘ └─────────┘ │ │
│ │ │ │
│ └───────────────────────────┘ │
│ │
│ 数据完整性 │ 快照 │ 压缩 │ 缓存 │
└─────────────────────────────────┘
- 镜像层:ZFS数据集(只读快照)
- 容器可写层:镜像层的克隆(Clone)
- 写时复制:ZFS原生支持
2.6.3 配置zfs驱动
# 1. 安装ZFS
sudo apt-get install zfsutils-linux # Ubuntu/Debian
# 或
sudo yum install zfs # CentOS/RHEL
# 2. 创建zpool
sudo zpool create -f zpool-docker /dev/sdb
# 3. 创建docker数据集
sudo zfs create zpool-docker/docker
# 4. 设置/var/lib/docker挂载点
sudo zfs set mountpoint=/var/lib/docker zpool-docker/docker
# 5. 配置Docker使用zfs
# /etc/docker/daemon.json
# {
# "storage-driver": "zfs"
# }
# 6. 重启Docker
sudo systemctl restart docker
# 7. 验证
docker info | grep "Storage Driver"
# 输出: Storage Driver: zfs
2.6.4 zfs配额和压缩
# 设置容器数据集配额
sudo zfs set quota=10G zpool-docker/docker/<container_id>
# 启用压缩
sudo zfs set compression=lz4 zpool-docker/docker
# 查看压缩效果
sudo zfs get compressratio zpool-docker/docker
2.7 vfs驱动详解
2.7.1 vfs简介
vfs(Virtual File System)是Docker最简单的存储驱动,它不使用任何联合文件系统技术,而是通过简单的文件复制来实现层的叠加。
vfs的特点:
- 实现简单,兼容性最好
- 没有任何写时复制优化,每个层都是完整的文件复制
- 磁盘空间占用极大
- 性能最差
- 不推荐在生产环境使用
- 适合测试环境或不支持其他驱动的系统
2.7.2 vfs工作原理
容器视角
┌──────────┐
│ /usr/ │
│ /etc/ │
│ /var/ │
└────┬─────┘
│
┌────┴────┐
│ 可写层 │ ← 完整复制镜像层的所有文件
└────┬────┘
┌────┴────┐
│ 镜像层3 │ ← 完整复制前一层的所有文件
└────┬────┘
┌────┴────┐
│ 镜像层2 │ ← 完整复制前一层的所有文件
└────┬────┘
┌────┴────┐
│ 镜像层1 │ ← 基础层
└─────────┘
在vfs驱动中,每一层都是完整复制前一层的所有文件,然后在其基础上进行修改。这意味着:
- 创建容器时会复制整个镜像的所有文件
- 磁盘空间占用是叠加的(不是共享的)
- 没有层共享机制
2.7.3 何时使用vfs
# vfs驱动通常在不支持其他驱动的环境中使用
# 例如某些NAS环境或受限的容器运行时
# 配置vfs驱动
# /etc/docker/daemon.json
# {
# "storage-driver": "vfs"
# }
# 重启Docker
sudo systemctl restart docker
2.8 存储驱动对比表
| 推荐度 | 推荐(首选) | 不推荐 | 不推荐 | 可选 | 可选 | 仅测试 |
| 底层技术 | OverlayFS | 联合文件系统 | 块设备映射 | Btrfs文件系统 | ZFS文件系统 | 文件复制 |
| 内核要求 | 4.0+ | 需补丁 | 2.6.9+ | 3.16+ | 需安装 | 无特殊要求 |
| 最大层数 | 128 | 127 | 127 | 无限制 | 无限制 | 无限制 |
| 性能 | 优秀 | 良好 | 一般 | 良好 | 良好 | 差 |
| 写时复制 | 文件级 | 文件级 | 块级 | 文件级 | 块级 | 无(复制) |
| 页缓存共享 | 是 | 是 | 否 | 是 | 是 | 否 |
| 磁盘空间效率 | 高 | 高 | 中 | 高 | 高 | 低 |
| 内存占用 | 低 | 低 | 中 | 中 | 高 | 低 |
| 配额支持 | 否 | 否 | 是 | 是 | 是 | 否 |
| 压缩支持 | 否 | 否 | 否 | 是 | 是 | 否 |
| 稳定性 | 高 | 高 | 中 | 中 | 高 | 高 |
| 适用场景 | 通用生产 | 旧系统 | 旧RHEL系统 | 需要配额 | 需要数据完整性 | 测试/兼容 |
| 跨平台 | Linux | Linux | Linux | Linux | Linux | Linux |
2.9 如何选择和切换存储驱动
2.9.1 选择存储驱动的原则
- ext4/xfs:使用overlay2
- btrfs:可以使用overlay2或btrfs驱动
- zfs:可以使用overlay2或zfs驱动(但不要在同一zfs文件系统上使用overlay2)
2.9.2 查看当前存储驱动
# 查看Docker信息
docker info | grep -i "storage"
# 输出示例:
# Storage Driver: overlay2
# Backing Filesystem: extfs
# Supports d_type: true
# Native Overlay Diff: true
# userxattr: false
2.9.3 切换存储驱动
重要警告:切换存储驱动会导致已有的镜像和容器无法使用!切换前请备份所有重要数据!
# 1. 停止Docker服务
sudo systemctl stop docker
# 2. 备份现有数据(可选但强烈推荐)
sudo cp -au /var/lib/docker /var/lib/docker.backup
# 3. 编辑Docker配置
sudo tee /etc/docker/daemon.json <<'EOF'
{
"storage-driver": "overlay2"
}
EOF
# 4. 删除旧数据(谨慎操作!)
# 如果是全新切换,可以删除旧数据
# sudo rm -rf /var/lib/docker
# 5. 启动Docker服务
sudo systemctl start docker
# 6. 验证新的存储驱动
docker info | grep "Storage Driver"
# 7. 重新拉取镜像(因为旧镜像在新驱动下不可用)
docker pull nginx:latest
2.9.4 各发行版默认存储驱动
| Ubuntu 20.04+ | overlay2 | overlay2 |
| Debian 10+ | overlay2 | overlay2 |
| CentOS 8+ | overlay2 | overlay2 |
| RHEL 8+ | overlay2 | overlay2 |
| Alpine | overlay2 | overlay2 |
| SUSE | btrfs(如在btrfs上) | overlay2 |
2.10 存储驱动配置
2.10.1 daemon.json中的存储驱动配置
// /etc/docker/daemon.json 完整配置示例
{
"storage-driver": "overlay2",
"storage-opts": [
"overlay2.override_kernel_check=true"
],
"data-root": "/var/lib/docker",
"max-concurrent-downloads": 10,
"max-concurrent-uploads": 5,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
2.10.2 验证存储驱动支持
# 检查内核是否支持overlay
grep overlay /proc/filesystems
# 输出: nodev overlay
# 检查文件系统是否支持d_type(overlay2需要)
sudo xfs_info / | grep ftype
# 输出: ftype=1 (需要为1)
# 检查内核版本
uname -r
# 输出应 >= 4.0
2.10.3 修改数据根目录
在某些情况下,系统盘空间不足,需要将Docker数据目录迁移到其他磁盘:
# 1. 停止Docker
sudo systemctl stop docker
# 2. 创建新目录
sudo mkdir -p /data/docker
# 3. 修改配置
sudo tee /etc/docker/daemon.json <<'EOF'
{
"data-root": "/data/docker",
"storage-driver": "overlay2"
}
EOF
# 4. 迁移现有数据
sudo rsync -aP /var/lib/docker/ /data/docker/
# 5. 启动Docker
sudo systemctl start docker
# 6. 验证
docker info | grep "Docker Root Dir"
# 输出: Docker Root Dir: /data/docker
# 7. 确认无误后删除旧目录
sudo rm -rf /var/lib/docker
2.10.4 存储驱动相关的故障排查
# 查看Docker守护进程日志
sudo journalctl -u docker.service –no-pager | tail -50
# 常见存储驱动错误及解决方法:
# 错误1: "overlay2 not supported"
# 原因: 内核版本过低或文件系统不支持
# 解决: 升级内核(>=4.0)或更换文件系统(使用ext4/xfs并设置ftype=1)
# 错误2: "Failed to start Docker Container Engine"
# 原因: 存储驱动配置错误或数据损坏
# 解决: 检查daemon.json配置,必要时清除/var/lib/docker重新初始化
# 错误3: "no space left on device"
# 原因: 磁盘空间不足
# 解决: 清理无用镜像和容器(docker system prune),或迁移数据根目录
第三章 Docker Volume(数据卷)
3.1 数据卷的概念与特点
3.1.1 什么是数据卷
数据卷(Volume)是Docker原生提供的数据持久化机制。它是一个由Docker守护进程管理的特殊目录,存在于宿主机上,可以被一个或多个容器挂载使用。数据卷完全独立于容器的生命周期——即使所有使用它的容器都被删除,数据卷及其数据依然存在。
3.1.2 数据卷的核心特点
3.1.3 数据卷的存储位置
数据卷默认存储在宿主机的/var/lib/docker/volumes/目录下:
/var/lib/docker/volumes/
├── my-volume/ # 命名数据卷
│ ├── _data/ # 实际数据存储目录
│ │ ├── file1.txt
│ │ └── subdir/
│ │ └── file2.txt
│ └── metadata.json # 数据卷元数据? (实际不存在此文件)
├── f3b4…45e/ # 匿名数据卷(使用哈希值命名)
│ └── _data/
│ └── …
└── backingFsBlockDev # 系统文件
在不同平台上,数据卷的存储位置有所不同:
| Linux | /var/lib/docker/volumes/ |
| Docker Desktop (macOS) | 在Linux VM内部 |
| Docker Desktop (Windows) | 在WSL2 VM内部 |
| Windows Containers | C:\\ProgramData\\docker\\volumes\\ |
3.2 docker volume create创建数据卷
3.2.1 基本创建命令
# 创建一个命名数据卷
docker volume create my-data-volume
# 输出: my-data-volume
创建后,Docker会在/var/lib/docker/volumes/my-data-volume/_data/目录下创建实际的数据存储目录。
3.2.2 创建时指定选项
# 创建数据卷时指定标签(label)
docker volume create –label environment=production –label app=web my-labeled-volume
# 创建数据卷时指定驱动(默认为local)
docker volume create –driver local my-local-volume
# 创建数据卷时指定驱动选项
# 例如:创建一个NFS数据卷
docker volume create \\
–driver local \\
–opt type=nfs \\
–opt o=addr=192.168.1.100,rw \\
–opt device=:/path/to/nfs/share \\
my-nfs-volume
# 创建一个tmpfs类型的数据卷(存储在内存中)
docker volume create \\
–driver local \\
–opt type=tmpfs \\
–opt device=tmpfs \\
–opt o=size=100m,mode=0755 \\
my-tmpfs-volume
# 创建一个btrfs子卷
docker volume create \\
–driver local \\
–opt type=btrfs \\
–opt device=/dev/sdb \\
–opt o=subvol=/docker-volumes/my-subvol \\
my-btrfs-volume
3.2.3 查看创建结果
# 查看数据卷详细信息
docker volume inspect my-data-volume
# 输出:
# [
# {
# "CreatedAt": "2024-01-15T10:30:00Z",
# "Driver": "local",
# "Labels": {},
# "Mountpoint": "/var/lib/docker/volumes/my-data-volume/_data",
# "Name": "my-data-volume",
# "Options": {},
# "Scope": "local"
# }
# ]
# 在宿主机上查看数据卷目录
sudo ls -la /var/lib/docker/volumes/my-data-volume/
# 输出:
# total 4
# drwxr-xr-x 3 root root 4096 Jan 15 10:30 .
# drwxr-xr-x 3 root root 4096 Jan 15 10:30 ..
# drwxr-xr-x 2 root root 40 Jan 15 10:30 _data
3.3 docker volume ls/rm/inspect/prune管理数据卷
3.3.1 docker volume ls – 列出数据卷
# 列出所有数据卷
docker volume ls
# 输出示例:
# DRIVER VOLUME NAME
# local my-data-volume
# local my-labeled-volume
# local nginx-html
# local f3b4a2c5e8d1… # 匿名数据卷(哈希命名)
# 按名称过滤
docker volume ls –filter name=my
# 输出:
# DRIVER VOLUME NAME
# local my-data-volume
# local my-labeled-volume
# 按标签过滤
docker volume ls –filter label=environment=production
# 输出:
# DRIVER VOLUME NAME
# local my-labeled-volume
# 只显示数据卷名称(不显示DRIVER列)
docker volume ls -q
# 输出:
# my-data-volume
# my-labeled-volume
3.3.2 docker volume inspect – 查看详情
# 查看单个数据卷详情
docker volume inspect my-data-volume
# 查看多个数据卷详情
docker volume inspect my-data-volume my-labeled-volume
# 只查看挂载点路径
docker volume inspect my-data-volume –format '{{.Mountpoint}}'
# 输出: /var/lib/docker/volumes/my-data-volume/_data
# 只查看驱动名称
docker volume inspect my-data-volume –format '{{.Driver}}'
# 输出: local
# 查看标签
docker volume inspect my-labeled-volume –format '{{.Labels}}'
# 输出: map[app:web environment:production]
3.3.3 docker volume rm – 删除数据卷
# 删除单个数据卷
docker volume rm my-data-volume
# 删除多个数据卷
docker volume rm volume1 volume2 volume3
# 注意:正在被容器使用的数据卷无法删除
docker volume rm my-data-volume
# 报错: Error response from daemon: unable to remove volume:
# remove my-data-volume: volume is in use
# 强制删除使用中的数据卷(需要先删除使用它的容器)
docker rm -f container_using_volume
docker volume rm my-data-volume
3.3.4 docker volume prune – 清理无用数据卷
# 删除所有未被容器使用的数据卷
docker volume prune
# 输出:
# WARNING! This will remove all local volumes not used by at least one container.
# Are you sure you want to continue? [y/N] y
# Deleted Volumes:
# unused-volume-1
# unused-volume-2
# Total reclaimed space: 1.2GB
# 跳过确认提示
docker volume prune -f
# 按标签过滤删除
docker volume prune –filter label!=keep=true
# 删除超过24小时未使用的数据卷
docker volume prune –filter "until=24h"
3.3.5 数据卷管理最佳实践
# 1. 为数据卷添加描述性标签
docker volume create –label app=mysql –label env=prod –label backup=daily mysql-data
# 2. 使用有意义的名称
docker volume create mysql_prod_data # 好的命名
# docker volume create vol1 # 不好的命名
# 3. 定期检查无用数据卷
docker volume ls -f dangling=true
# 4. 定期清理
docker volume prune -f # 谨慎使用,确保没有正在使用的匿名卷
# 5. 备份重要的数据卷(后续章节详细讲解)
3.4 在docker run中使用数据卷
3.4.1 基本用法(-v参数)
# 使用命名数据卷
docker run -d \\
–name web-server \\
-v web-data:/usr/share/nginx/html \\
nginx:latest
# 参数说明:
# -v web-data:/usr/share/nginx/html
# web-data: 数据卷名称(Docker会自动创建如果不存在)
# /usr/share/nginx/html: 容器内的挂载路径
3.4.2 使用–mount参数(推荐)
# 使用–mount参数(语法更明确,推荐使用)
docker run -d \\
–name web-server \\
–mount type=volume,src=web-data,dst=/usr/share/nginx/html \\
nginx:latest
# 参数说明:
# –mount type=volume: 指定挂载类型为数据卷
# src=web-data: 源数据卷名称(也可以用source=)
# dst=/usr/share/nginx/html: 目标路径(也可以用destination=或target=)
# 等价于:
# docker run -d –name web-server -v web-data:/usr/share/nginx/html nginx:latest
3.4.3 -v与–mount的对比
# -v 参数: 简洁但不够明确
# 格式: -v <volume_name>:<container_path>[:<options>]
docker run -d -v web-data:/data:ro nginx:latest
# –mount 参数: 语法明确,功能更丰富
# 格式: –mount type=volume,src=<name>,dst=<path>[,<options>]
docker run -d \\
–mount type=volume,src=web-data,dst=/data,readonly \\
nginx:latest
# –mount 的优势:
# 1. 语法更明确,每个参数都有键名
# 2. 更容易阅读和理解
# 3. 支持更多选项(如volume-nocopy)
# 4. 适用于Swarm服务
3.4.4 挂载多个数据卷
# 一个容器可以挂载多个数据卷
docker run -d \\
–name app-server \\
-v app-data:/app/data \\
-v app-logs:/app/logs \\
-v app-config:/app/config \\
my-app:latest
# 使用–mount语法
docker run -d \\
–name app-server \\
–mount type=volume,src=app-data,dst=/app/data \\
–mount type=volume,src=app-logs,dst=/app/logs \\
–mount type=volume,src=app-config,dst=/app/config \\
my-app:latest
3.4.5 volume-nocopy选项
默认情况下,当命名数据卷首次挂载到一个非空目录时,Docker会将容器镜像中该目录的内容复制到数据卷中。可以使用volume-nocopy选项禁用此行为:
# 默认行为:复制镜像内容到数据卷
docker run -d \\
–name web1 \\
–mount type=volume,src=web-data,dst=/usr/share/nginx/html \\
nginx:latest
# web-data会包含nginx默认的index.html和50x.html
# 禁止复制:不复制镜像内容
docker run -d \\
–name web2 \\
–mount type=volume,src=web-data2,dst=/usr/share/nginx/html,volume-nocopy \\
nginx:latest
# web-data2为空,不会包含nginx默认文件
3.5 数据卷的自动创建行为
3.5.1 自动创建命名数据卷
# 如果指定的命名数据卷不存在,Docker会自动创建
docker run -d \\
–name auto-create-test \\
-v non-existent-volume:/data \\
alpine:latest sleep 3600
# 查看是否自动创建了数据卷
docker volume ls | grep non-existent-volume
# 输出: local non-existent-volume
# 自动创建的数据卷没有任何特殊标签
docker volume inspect non-existent-volume
3.5.2 自动创建匿名数据卷
# 如果只指定容器路径而不指定数据卷名称,Docker会创建匿名数据卷
docker run -d \\
–name anonymous-volume-test \\
-v /data \\
alpine:latest sleep 3600
# 查看创建的匿名数据卷(名称为长哈希值)
docker volume ls
# 输出包含:
# DRIVER VOLUME NAME
# local f3b4a2c5e8d1234567890abcdef1234567890abcdef1234567890abcdef1234
# 查看匿名数据卷的详细信息
docker inspect anonymous-volume-test –format '{{json .Mounts}}' | python3 -m json.tool
# 输出:
# [
# {
# "Type": "volume",
# "Name": "f3b4a2c5…",
# "Source": "/var/lib/docker/volumes/f3b4a2c5…/_data",
# "Destination": "/data",
# "Driver": "local",
# "Mode": "",
# "RW": true,
# "Propagation": ""
# }
# ]
3.5.3 自动创建的风险与注意事项
匿名数据卷的自动创建可能导致"数据卷泄漏"问题——容器删除后,匿名数据卷不会被自动删除,长期积累会占用大量磁盘空间。
# 演示数据卷泄漏
for i in $(seq 1 10); do
docker run –rm -v /tmp/data alpine echo "creating anonymous volume $i"
done
# 查看
docker volume ls
# 会发现有10个匿名数据卷残留
# 清理
docker volume prune -f
最佳实践:始终使用命名数据卷,避免匿名数据卷。
# 不推荐:匿名数据卷
docker run -v /data alpine sleep 3600
# 推荐:命名数据卷
docker run -v my-data:/data alpine sleep 3600
3.6 数据卷在宿主机上的存储位置
3.6.1 默认存储位置
# 数据卷默认存储在
# /var/lib/docker/volumes/<volume_name>/_data/
# 查看数据卷的存储路径
docker volume inspect my-data-volume –format '{{.Mountpoint}}'
# 输出: /var/lib/docker/volumes/my-data-volume/_data
# 直接在宿主机上操作数据卷内容
sudo ls -la /var/lib/docker/volumes/my-data-volume/_data/
# 在宿主机上创建文件(会反映到容器中)
sudo echo "Hello from host" > /var/lib/docker/volumes/my-data-volume/_data/host-created.txt
# 在容器中验证
docker run –rm -v my-data-volume:/data alpine cat /data/host-created.txt
# 输出: Hello from host
3.6.2 修改数据卷存储位置
// /etc/docker/daemon.json
{
"data-root": "/data/docker"
}
修改后数据卷将存储在/data/docker/volumes/目录下。
3.6.3 数据卷的文件系统权限
# 数据卷在宿主机上默认由root拥有
sudo ls -la /var/lib/docker/volumes/
# drwxr-xr-x root root …
# drwxr-xr-x root root …
# 如果容器内以非root用户运行,可能遇到权限问题
# 解决方法1:在Dockerfile中调整权限
# Dockerfile:
# RUN mkdir -p /data && chown 1000:1000 /data
# USER 1000
# 解决方法2:在宿主机上修改数据卷权限
sudo chown -R 1000:1000 /var/lib/docker/volumes/my-data-volume/_data/
# 解决方法3:使用初始化容器设置权限
docker run –rm -v my-data-volume:/data alpine chown -R 1000:1000 /data
3.7 只读数据卷
3.7.1 创建只读数据卷
# 使用-v参数创建只读数据卷
docker run -d \\
–name readonly-test \\
-v readonly-data:/data:ro \\
alpine:latest sleep 3600
# 使用–mount参数创建只读数据卷
docker run -d \\
–name readonly-test2 \\
–mount type=volume,src=readonly-data,dst=/data,readonly \\
alpine:latest sleep 3600
# :ro 或 readonly 表示只读
3.7.2 验证只读属性
# 读取数据卷中的文件(正常)
docker exec readonly-test cat /data/somefile.txt
# 写入数据卷(会失败)
docker exec readonly-test sh -c 'echo "test" > /data/newfile.txt'
# 报错: sh: can't create /data/newfile.txt: Read-only file system
# 创建子目录(也会失败)
docker exec readonly-test mkdir /data/newdir
# 报错: mkdir: can't create directory '/data/newdir': Read-only file system
3.7.3 只读数据卷的应用场景
# 场景1:配置文件只读挂载(防止应用意外修改配置)
docker run -d \\
–name config-server \\
-v app-config:/etc/app/config:ro \\
my-app:latest
# 场景2:静态资源只读挂载
docker run -d \\
–name static-server \\
-v static-assets:/var/www/static:ro \\
nginx:latest
# 场景3:只读数据 + 可写日志(一个只读,一个可写)
docker run -d \\
–name mixed-server \\
-v app-data:/app/data:ro \\
-v app-logs:/app/logs \\
my-app:latest
3.8 数据卷的权限与所有权
3.8.1 权限问题分析
当容器内以非root用户运行时,可能会遇到数据卷权限问题。这是因为数据卷在宿主机上由root创建,容器内的非root用户可能没有读写权限。
# 示例:以非root用户运行的容器遇到权限问题
docker run -d \\
–name permission-test \\
–user 1000:1000 \\
-v perm-data:/data \\
alpine:latest sleep 3600
# 尝试写入文件
docker exec permission-test sh -c 'echo "test" > /data/test.txt'
# 可能报错: sh: can't create /data/test.txt: Permission denied
3.8.2 解决权限问题的方法
方法1:在Dockerfile中预设权限
# Dockerfile
FROM alpine:latest
# 创建目录并设置权限
RUN mkdir -p /data && chown 1000:1000 /data
# 切换到非root用户
USER 1000:1000
CMD ["sleep", "3600"]
方法2:使用初始化容器
# 步骤1:使用root用户初始化数据卷权限
docker run –rm \\
-v perm-data:/data \\
alpine:latest chown -R 1000:1000 /data
# 步骤2:以非root用户运行应用
docker run -d \\
–name app \\
–user 1000:1000 \\
-v perm-data:/data \\
my-app:latest
方法3:在宿主机上直接修改权限
# 获取数据卷路径
VOLUME_PATH=$(docker volume inspect perm-data –format '{{.Mountpoint}}')
# 修改所有权
sudo chown -R 1000:1000 $VOLUME_PATH
# 修改权限
sudo chmod -R 755 $VOLUME_PATH
方法4:使用Docker Compose的init容器
# docker-compose.yml
version: '3.8'
services:
init-permissions:
image: alpine:latest
volumes:
– app–data:/data
command: chown –R 1000:1000 /data
user: root
app:
image: my–app:latest
depends_on:
– init–permissions
user: "1000:1000"
volumes:
– app–data:/data
volumes:
app-data:
3.9 命名数据卷 vs 匿名数据卷
3.9.1 命名数据卷(Named Volume)
# 命名数据卷:有明确的名称
docker run -d \\
–name app \\
-v my-app-data:/data \\
my-app:latest
# 特点:
# 1. 有明确的、可读的名称
# 2. 可以通过名称引用和管理
# 3. 适合生产环境
# 4. 便于备份、迁移和共享
3.9.2 匿名数据卷(Anonymous Volume)
# 匿名数据卷:只指定容器路径,不指定数据卷名称
docker run -d \\
–name app \\
-v /data \\
my-app:latest
# 特点:
# 1. 名称是随机生成的哈希值
# 2. 难以管理和识别
# 3. 容器删除后容易成为"孤儿"数据卷
# 4. 不推荐在生产环境使用
3.9.3 对比
| 名称 | 有明确名称 | 随机哈希值 |
| 可读性 | 好 | 差 |
| 管理难度 | 低 | 高 |
| 可复用性 | 高 | 低 |
| 适用场景 | 生产环境 | 临时测试 |
| 清理难度 | 低(按名称删除) | 高(需找出对应哈希) |
3.9.4 Dockerfile中的VOLUME指令产生的匿名卷
# Dockerfile中使用VOLUME指令会创建匿名数据卷
FROM nginx:latest
VOLUME /data
# 构建并运行
docker build -t my-volume-image .
docker run -d –name vol-test my-volume-image
# 查看挂载信息,会发现一个匿名数据卷
docker inspect vol-test –format '{{json .Mounts}}' | python3 -m json.tool
3.10 数据卷实战: MySQL数据持久化
3.10.1 需求分析
我们需要运行一个MySQL容器,要求:
3.10.2 完整实现
# 步骤1:创建数据卷
docker volume create mysql_data
docker volume create mysql_logs
docker volume create mysql_config
# 步骤2:创建配置文件
mkdir -p /opt/mysql/conf
cat > /opt/mysql/conf/my.cnf << 'EOF'
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
max_connections=500
innodb_buffer_pool_size=256M
slow_query_log=1
long_query_time=2
EOF
# 步骤3:创建初始化SQL脚本
mkdir -p /opt/mysql/init
cat > /opt/mysql/init/init.sql << 'EOF'
CREATE DATABASE IF NOT EXISTS appdb DEFAULT CHARACTER SET utf8mb4;
CREATE USER IF NOT EXISTS 'appuser'@'%' IDENTIFIED BY 'apppass123';
GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'%';
FLUSH PRIVILEGES;
EOF
# 步骤4:启动MySQL容器
docker run -d \\
–name mysql-prod \\
–restart unless-stopped \\
-p 3306:3306 \\
-e MYSQL_ROOT_PASSWORD=RootPass123! \\
-e MYSQL_DATABASE=appdb \\
-e MYSQL_USER=appuser \\
-e MYSQL_PASSWORD=apppass123 \\
-v mysql_data:/var/lib/mysql \\
-v mysql_logs:/var/log/mysql \\
-v /opt/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro \\
-v /opt/mysql/init:/docker-entrypoint-initdb.d:ro \\
mysql:8.0
# 步骤5:验证MySQL运行状态
docker exec mysql-prod mysql -uroot -pRootPass123! -e "SHOW DATABASES;"
# 输出:
# +——————–+
# | Database |
# +——————–+
# | appdb |
# | information_schema |
# | mysql |
# | performance_schema |
# | sys |
# +——————–+
# 步骤6:写入测试数据
docker exec mysql-prod mysql -uroot -pRootPass123! appdb -e "
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (name, email) VALUES ('张三', 'zhangsan@example.com');
INSERT INTO users (name, email) VALUES ('李四', 'lisi@example.com');
SELECT * FROM users;
"
# 步骤7:模拟容器故障 – 删除并重建容器
docker rm -f mysql-prod
# 重新启动容器(使用相同的数据卷)
docker run -d \\
–name mysql-prod \\
–restart unless-stopped \\
-p 3306:3306 \\
-e MYSQL_ROOT_PASSWORD=RootPass123! \\
-v mysql_data:/var/lib/mysql \\
-v mysql_logs:/var/log/mysql \\
-v /opt/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro \\
mysql:8.0
# 步骤8:验证数据依然存在
docker exec mysql-prod mysql -uroot -pRootPass123! appdb -e "SELECT * FROM users;"
# 输出:
# +—-+——–+———————-+———————+
# | id | name | email | created_at |
# +—-+——–+———————-+———————+
# | 1 | 张三 | zhangsan@example.com | 2024-01-15 10:30:00 |
# | 2 | 李四 | lisi@example.com | 2024-01-15 10:30:00 |
# +—-+——–+———————-+———————+
# 数据完好无损!这就是数据卷持久化的威力。
3.10.3 使用Docker Compose管理
# docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: mysql–prod
restart: unless–stopped
ports:
– "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: RootPass123!
MYSQL_DATABASE: appdb
MYSQL_USER: appuser
MYSQL_PASSWORD: apppass123
volumes:
– mysql_data:/var/lib/mysql
– mysql_logs:/var/log/mysql
– ./conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro
– ./init:/docker–entrypoint–initdb.d:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-pRootPass123!"]
interval: 10s
timeout: 5s
retries: 5
volumes:
mysql_data:
name: mysql_data
mysql_logs:
name: mysql_logs
# 启动
docker-compose up -d
# 查看状态
docker-compose ps
# 查看日志
docker-compose logs -f mysql
# 停止
docker-compose down
# 注意:down不会删除数据卷,但down -v会删除数据卷!
# docker-compose down -v # 危险!会删除所有数据!
第四章 Bind Mount(绑定挂载)
4.1 绑定挂载的概念与特点
4.1.1 什么是绑定挂载
绑定挂载(Bind Mount)是Docker提供的另一种数据持久化机制。与数据卷不同,绑定挂载将宿主机上的一个绝对路径直接挂载到容器中。容器对该路径下文件的读写操作直接作用于宿主机的文件系统,两者之间是"双向同步"的。
绑定挂载在Docker的早期版本中就已经存在,可以说是最古老的数据持久化方式之一。它的概念来源于Linux的mount –bind命令,即将一个目录树挂载到另一个目录树上。
4.1.2 绑定挂载的核心特点
4.1.3 绑定挂载的工作原理
┌─────────────────────────────────────┐
│ 宿主机 │
│ │
│ /home/user/project/ │
│ ├── src/ │
│ │ ├── app.py │
│ │ └── utils.py │
│ ├── config/ │
│ │ └── settings.json │
│ └── requirements.txt │
│ │
│ ┌─────────────────────────────────┐│
│ │ Docker 容器 ││
│ │ ││
│ │ /app/ ← 绑定挂载点 ││
│ │ ├── src/ ← 同步 ││
│ │ │ ├── app.py ← 同步 ││
│ │ │ └── utils.py ← 同步 ││
│ │ ├── config/ ← 同步 ││
│ │ │ └── settings.json ← 同步 ││
│ │ └── requirements.txt ← 同步 ││
│ │ ││
│ └─────────────────────────────────┘│
└─────────────────────────────────────┘
当绑定挂载生效时,容器内挂载点路径上的内容会被宿主机路径的内容"覆盖"。即使镜像中该路径下有文件,这些文件也会被隐藏。
4.2 在docker run中使用绑定挂载
4.2.1 基本用法(-v参数)
# 使用-v参数创建绑定挂载
docker run -d \\
–name dev-server \\
-v /home/user/project:/app \\
node:18-alpine
# 参数说明:
# -v /home/user/project:/app
# /home/user/project: 宿主机绝对路径
# /app: 容器内挂载路径
# 注意:宿主机路径以 / 开头表示绑定挂载(不是以/开头表示数据卷名称)
4.2.2 区分绑定挂载和数据卷
# 绑定挂载:宿主机路径以/开头(或./、~/等绝对/相对路径)
docker run -v /home/user/data:/data alpine sleep 3600 # 绑定挂载
docker run -v ./data:/data alpine sleep 3600 # 绑定挂载(相对路径)
docker run -v ~/data:/data alpine sleep 3600 # 绑定挂载(家目录)
# 数据卷:不以/开头的是数据卷名称
docker run -v my-volume:/data alpine sleep 3600 # 命名数据卷
docker run -v /data alpine sleep 3600 # 匿名数据卷(只有容器路径)
4.2.3 挂载单个文件
# 绑定挂载也可以挂载单个文件(而不仅仅是目录)
docker run -d \\
–name nginx-custom \\
-v /opt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \\
nginx:latest
# 注意:挂载单个文件时,宿主机上该文件必须存在
# 如果文件不存在,Docker会将其创建为一个目录!
# 演示文件挂载的错误情况
# 如果文件不存在,Docker会创建目录
docker run –rm -v /tmp/nonexist.conf:/etc/config.conf alpine ls -la /etc/config.conf
# /etc/config.conf 实际上是一个目录!
# 正确做法:先创建文件
touch /tmp/config.conf
docker run –rm -v /tmp/config.conf:/etc/config.conf alpine cat /etc/config.conf
# 正常工作
4.2.4 挂载多个绑定挂载
docker run -d \\
–name full-dev-env \\
-v /home/user/project/src:/app/src \\
-v /home/user/project/config:/app/config:ro \\
-v /home/user/project/logs:/app/logs \\
-v /home/user/.ssh:/root/.ssh:ro \\
node:18-alpine
4.3 使用–mount参数(更明确的语法)
4.3.1 –mount语法
# 使用–mount参数创建绑定挂载
docker run -d \\
–name dev-server \\
–mount type=bind,src=/home/user/project,dst=/app \\
node:18-alpine
# 参数说明:
# type=bind: 指定挂载类型为绑定挂载
# src=/home/user/project: 宿主机源路径(也可以用source=)
# dst=/app: 容器内目标路径(也可以用destination=或target=)
4.3.2 –mount的完整选项
docker run -d \\
–name dev-server \\
–mount \\
type=bind,\\
src=/home/user/project,\\
dst=/app,\\
readonly,\\
bind-propagation=rprivate \\
node:18-alpine
# 选项说明:
# type=bind: 绑定挂载类型
# src: 宿主机源路径
# dst: 容器目标路径
# readonly: 只读挂载
# bind-propagation: 绑定传播模式(rprivate/private/rslave/slave/rshared/shared)
4.3.3 绑定传播(Bind Propagation)
绑定传播控制当挂载点下又有其他挂载时,这些挂载如何传播:
# 演示绑定传播
# 准备目录结构
mkdir -p /tmp/propagation/source
mkdir -p /tmp/propagation/dest
echo "source content" > /tmp/propagation/source/file.txt
# rprivate(默认): 不会传播任何挂载
docker run –rm \\
–mount type=bind,src=/tmp/propagation/source,dst=/tmp/propagation/dest,bind-propagation=rprivate \\
alpine ls /tmp/propagation/dest
# shared: 挂载点中的挂载会传播到原挂载,反之亦然
# slave: 只接收原挂载的传播,不反向传播
# rshared: 递归shared
# rslave: 递归slave
# private: 不传播
# rprivate: 递归private(默认)
| rprivate | 递归私有(默认),不传播任何挂载 |
| private | 私有,不传播挂载 |
| rshared | 递归共享,双向传播所有挂载 |
| shared | 共享,双向传播直接挂载 |
| rslave | 递归从属,只接收原挂载的传播 |
| slave | 从属,只接收原挂载的直接传播 |
4.4 绑定挂载的绝对路径要求
4.4.1 绝对路径规则
# 绑定挂载的宿主机路径必须是绝对路径
# 正确:绝对路径
docker run -v /home/user/data:/data alpine sleep 3600
# 正确:使用$(pwd)展开为绝对路径
docker run -v $(pwd)/data:/data alpine sleep 3600
# 错误:相对路径(不以/开头会被当作数据卷名称)
docker run -v data:/data alpine sleep 3600
# Docker会创建一个名为"data"的数据卷,而不是绑定挂载!
# 正确:使用相对路径展开
docker run -v "$(pwd)/data":/data alpine sleep 3600
4.4.2 使用相对路径的正确方式
# 在项目目录中使用绑定挂载
cd /home/user/myproject
# 使用$(pwd)获取当前目录的绝对路径
docker run -d \\
–name dev \\
-v "$(pwd)":/app \\
node:18-alpine
# 或者使用–mount语法(同样需要绝对路径)
docker run -d \\
–name dev \\
–mount type=bind,src=$(pwd),dst=/app \\
node:18-alpine
# 在Windows上使用PowerShell
docker run -d `
–name dev `
-v "${PWD}:/app" `
node:18-alpine
# 在Windows上使用CMD
docker run -d ^
–name dev ^
-v "%cd%:/app" ^
node:18-alpine
4.4.3 路径不存在时的行为
# 如果宿主机路径不存在,Docker会自动创建一个目录
docker run –rm -v /tmp/nonexistent-dir:/data alpine ls -la /data
# /data目录会被创建(为空)
# 但如果挂载的是文件且文件不存在,Docker会创建一个目录(而非文件)
docker run –rm -v /tmp/nonexistent-file.txt:/config.txt alpine ls -la /config.txt
# /config.txt实际上是一个目录,而非文件!
# 这可能导致应用报错
# 解决方法:先在宿主机上创建文件
touch /tmp/config.txt
docker run –rm -v /tmp/config.txt:/config.txt alpine cat /config.txt
4.5 只读绑定挂载
4.5.1 创建只读绑定挂载
# 使用-v参数
docker run -d \\
–name config-server \\
-v /opt/app/config:/app/config:ro \\
my-app:latest
# 使用–mount参数
docker run -d \\
–name config-server \\
–mount type=bind,src=/opt/app/config,dst=/app/config,readonly \\
my-app:latest
4.5.2 只读绑定挂载的应用场景
# 场景1:只读配置文件
docker run -d \\
–name nginx-server \\
-v /opt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \\
-v /opt/nginx/conf.d:/etc/nginx/conf.d:ro \\
nginx:latest
# 场景2:只读静态资源
docker run -d \\
–name static-server \\
-v /opt/static:/usr/share/nginx/html:ro \\
nginx:latest
# 场景3:只读SSL证书
docker run -d \\
–name https-server \\
-v /opt/ssl/certs:/etc/ssl/certs:ro \\
-v /opt/ssl/private:/etc/ssl/private:ro \\
nginx:latest
# 场景4:可读写的代码目录 + 只读的配置目录
docker run -d \\
–name dev-server \\
-v /home/user/project/src:/app/src \\
-v /opt/app/config:/app/config:ro \\
node:18-alpine
4.6 绑定挂载与文件同步
4.6.1 实时双向同步
绑定挂载的最大优势之一是宿主机与容器之间的文件实时双向同步:
# 启动一个绑定挂载的容器
mkdir -p /tmp/sync-test
docker run -d \\
–name sync-test \\
-v /tmp/sync-test:/data \\
alpine:latest sleep 3600
# 在宿主机创建文件
echo "Hello from host" > /tmp/sync-test/host-file.txt
# 在容器中查看(立即可见)
docker exec sync-test cat /data/host-file.txt
# 输出: Hello from host
# 在容器中创建文件
docker exec sync-test sh -c 'echo "Hello from container" > /data/container-file.txt'
# 在宿主机查看(立即可见)
cat /tmp/sync-test/container-file.txt
# 输出: Hello from container
# 清理
docker rm -f sync-test
rm -rf /tmp/sync-test
4.6.2 inotify事件传播
绑定挂载支持文件系统事件(inotify)的传播,这使得文件监听工具(如nodemon、webpack-dev-server等)可以在容器内正常运行:
# 在开发环境中使用nodemon监听文件变化
docker run -d \\
–name node-dev \\
-v $(pwd):/app \\
-w /app \\
node:18-alpine \\
npx nodemon app.js
# 当你在宿主机上修改app.js时,nodemon会检测到变化并自动重启
4.7 绑定挂载在开发环境中的应用
4.7.1 代码热重载(Node.js)
# 项目结构
# /home/user/myapp/
# ├── package.json
# ├── src/
# │ ├── index.js
# │ └── routes.js
# ├── node_modules/
# └── Dockerfile
# Dockerfile
# FROM node:18-alpine
# WORKDIR /app
# COPY package.json .
# RUN npm install
# EXPOSE 3000
# CMD ["npm", "start"]
# 使用绑定挂载实现热重载
docker run -d \\
–name node-dev \\
-p 3000:3000 \\
-v /home/user/myapp:/app \\
-w /app \\
node:18-alpine \\
npx nodemon –watch src src/index.js
# 修改宿主机上的src/index.js,容器内的应用会自动重启
4.7.2 Python Flask热重载
# 使用Flask的debug模式实现热重载
docker run -d \\
–name flask-dev \\
-p 5000:5000 \\
-v $(pwd):/app \\
-w /app \\
python:3.11-slim \\
bash -c "pip install flask && flask –app app run –host=0.0.0.0 –debug –reload"
# 修改app.py,Flask会自动重载
4.7.3 避免node_modules覆盖问题
# 问题:绑定挂载会覆盖容器内的node_modules
# 解决方法:使用匿名数据卷保护node_modules
docker run -d \\
–name node-dev \\
-p 3000:3000 \\
-v $(pwd):/app \\
-v /app/node_modules \\
-w /app \\
node:18-alpine \\
npm start
# 说明:
# -v $(pwd):/app: 绑定挂载项目目录
# -v /app/node_modules: 匿名数据卷,保护容器内的node_modules不被宿主机覆盖
# Docker会先处理绑定挂载,再处理匿名卷,所以node_modules会被单独挂载
4.8 绑定挂载的权限问题
4.8.1 Linux上的权限问题
# 在Linux上,绑定挂载直接使用宿主机的UID/GID
# 如果容器内以非root用户运行,需要确保UID匹配
# 查看宿主机当前用户的UID
id -u # 例如:1000
# 方法1:Dockerfile中创建相同UID的用户
# Dockerfile:
# FROM node:18-alpine
# RUN addgroup -g 1000 appgroup && adduser -u 1000 -G appgroup -s /bin/sh -D appuser
# USER appuser
# 方法2:运行时指定用户
docker run -d \\
–name dev \\
–user $(id -u):$(id -g) \\
-v $(pwd):/app \\
node:18-alpine
4.8.2 macOS上的特殊处理
在macOS上使用Docker Desktop时,绑定挂载通过一个Linux虚拟机中转,存在以下特点:
# macOS上Docker Desktop使用VirtioFS或osxfs进行文件共享
# 1. 文件权限自动转换
# 2. 性能比Linux原生绑定挂载稍差
# 3. 可以通过Docker Desktop设置配置文件共享路径
# 在Docker Desktop中设置:
# Settings → Resources → File Sharing
# 确保绑定的路径在共享列表中
# 性能优化:使用缓存选项(仅macOS)
# 在docker-compose.yml中:
# volumes:
# – type: bind
# source: ./src
# target: /app/src
# consistency: cached # macOS专用,提高读取性能
# # consistency: delegated # 容器内的修改延迟同步到宿主机
# # consistency: consistent # 默认,完全一致
4.8.3 Windows上的特殊处理
# 在Windows上,路径格式不同
# 使用PowerShell:
docker run –d `
—name dev `
–v "${PWD}\\src:/app/src" `
node:18-alpine
# 或者使用正斜杠(推荐):
docker run –d `
—name dev `
–v "${PWD}/src:/app/src" `
node:18-alpine
# Windows路径的特殊处理:
# 1. 盘符路径(C:\\…)会被转换为Linux路径
# 2. 使用Docker Desktop的WSL2后端性能更好
# 3. 需要在Docker Desktop设置中开启文件共享
# 注意Windows上的换行符问题(CRLF vs LF)
# 建议:.gitattributes中设置 * text=auto eol=lf
4.9 Bind Mount vs Volume对比
| 创建方式 | 直接使用宿主机路径 | docker volume create或自动创建 |
| 存储位置 | 宿主机任意路径 | /var/lib/docker/volumes/ |
| 管理方式 | 直接操作宿主机文件 | docker volume命令族 |
| 生命周期 | 依赖宿主机文件 | 独立于容器 |
| 初始内容 | 不复制镜像内容(覆盖) | 命名卷会复制镜像内容 |
| 可移植性 | 差(路径依赖) | 好 |
| 备份恢复 | 直接操作文件 | 需要通过容器操作 |
| 权限控制 | 依赖宿主机权限 | Docker管理权限 |
| 跨平台 | 路径格式不同 | 统一管理 |
| 驱动支持 | 不支持 | 支持多种驱动 |
| 多容器共享 | 支持 | 支持 |
| 适合场景 | 开发环境、配置文件挂载 | 生产环境数据持久化 |
| 安全性 | 中(直接操作宿主机) | 高(隔离管理) |
| 性能 | 接近原生 | 接近原生 |
选择建议:
- 生产环境:优先使用数据卷(Volume),便于管理、备份和迁移。
- 开发环境:使用绑定挂载(Bind Mount),方便代码热重载。
- 配置文件:两种都可以,绑定挂载更直观。
- 数据库数据:必须使用数据卷,不要使用绑定挂载。
4.10 绑定挂载实战: 开发环境代码热更新
4.10.1 项目结构
my-webapp/
├── docker-compose.yml
├── Dockerfile
├── package.json
├── src/
│ ├── index.js
│ ├── routes/
│ │ ├── users.js
│ │ └── products.js
│ └── public/
│ ├── index.html
│ └── styles.css
└── config/
└── config.json
4.10.2 Dockerfile
# Dockerfile
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 复制package.json并安装依赖
COPY package.json package-lock.json ./
RUN npm ci –only=production
# 安装开发依赖
RUN npm install –only=dev
# 暴露端口
EXPOSE 3000
# 使用nodemon启动(支持热重载)
CMD ["npx", "nodemon", "–watch", "src", "src/index.js"]
4.10.3 docker-compose.yml
# docker-compose.yml
version: '3.8'
services:
webapp:
build: .
container_name: webapp–dev
ports:
– "3000:3000"
volumes:
# 绑定挂载:源代码目录(支持热重载)
– ./src:/app/src
# 绑定挂载:配置文件(只读)
– ./config:/app/config:ro
# 匿名数据卷:保护node_modules不被覆盖
– /app/node_modules
environment:
– NODE_ENV=development
– CHOKIDAR_USEPOLLING=true # 确保文件变化检测在所有平台上正常工作
restart: unless–stopped
4.10.4 运行与验证
# 启动开发环境
docker-compose up -d
# 查看日志
docker-compose logs -f webapp
# 修改src/index.js(在宿主机上)
echo 'app.get("/health", (req, res) => res.json({status: "ok"}));' >> src/index.js
# nodemon会自动检测到变化并重启应用
# 日志中会出现: [nodemon] restarting due to changes…
# 测试新添加的路由
curl http://localhost:3000/health
# 输出: {"status":"ok"}
# 修改配置文件
echo '{"debug": true}' > config/config.json
# 应用可以读取最新的配置文件内容(因为绑定挂载是实时的)
4.10.5 开发环境的最佳实践
# 1. 使用.dockerignore避免不必要的文件被COPY
# .dockerignore:
# node_modules
# .git
# *.md
# .env
# 2. 使用docker-compose.override.yml覆盖开发配置
# docker-compose.yml(基础配置)
# docker-compose.override.yml(开发覆盖配置,自动加载)
# 3. 使用环境变量管理配置
# .env文件:
# NODE_ENV=development
# PORT=3000
# 4. 开发工具集成
# VS Code的Dev Containers扩展可以直接在容器内开发
# 使用.devcontainer/devcontainer.json配置
// .devcontainer/devcontainer.json
{
"name": "Node.js Dev",
"build": {
"dockerfile": "../Dockerfile"
},
"forwardPorts": [3000],
"mounts": [
"source=${localWorkspaceFolder}/src,target=/app/src,type=bind,consistency=cached",
"source=${localWorkspaceFolder}/config,target=/app/config,type=bind"
],
"remoteEnv": {
"NODE_ENV": "development"
},
"postCreateCommand": "npm install"
}
第五章 tmpfs Mount(临时文件系统)
5.1 tmpfs挂载的概念与特点
5.1.1 什么是tmpfs挂载
tmpfs挂载是Docker提供的第三种存储方式。与数据卷和绑定挂载不同,tmpfs将数据存储在宿主机的**内存(RAM)**中,而不是磁盘上。这意味着tmpfs中的数据读写速度极快,但容器停止后数据就会消失,不会持久化到磁盘。
tmpfs是Linux内核提供的临时文件系统,它将一部分内存作为文件系统使用。Docker利用这一机制,允许容器将特定路径挂载为tmpfs,实现高速临时存储。
5.1.2 tmpfs挂载的核心特点
5.1.3 tmpfs vs Volume vs Bind Mount
┌────────────────────────────────────────────────────┐
│ 存储位置对比 │
│ │
│ Volume: 磁盘(/var/lib/docker/volumes/) │
│ Bind Mount: 磁盘(宿主机任意路径) │
│ tmpfs: 内存(RAM) │
│ │
│ 持久性: Volume > Bind Mount > tmpfs │
│ 性能: tmpfs > Volume ≈ Bind Mount │
│ 安全性: tmpfs > Volume > Bind Mount │
│ 可共享: Volume = Bind Mount > tmpfs(不可共享) │
└────────────────────────────────────────────────────┘
5.2 使用tmpfs挂载
5.2.1 使用–tmpfs参数
# 使用–tmpfs参数创建tmpfs挂载
docker run -d \\
–name tmpfs-test \\
–tmpfs /app/temp \\
alpine:latest sleep 3600
# 参数说明:
# –tmpfs /app/temp: 在容器内的/app/temp路径挂载tmpfs
# 验证tmpfs
docker exec tmpfs-test df -h /app/temp
# 输出:
# Filesystem Size Used Available Use% Mounted on
# tmpfs 64.0M 0 64.0M 0% /app/temp
# 写入数据
docker exec tmpfs-test sh -c 'echo "temp data" > /app/temp/test.txt'
docker exec tmpfs-test cat /app/temp/test.txt
# 输出: temp data
5.2.2 使用–mount参数
# 使用–mount参数创建tmpfs挂载(语法更明确)
docker run -d \\
–name tmpfs-test2 \\
–mount type=tmpfs,dst=/app/temp \\
alpine:latest sleep 3600
# 带选项的tmpfs挂载
docker run -d \\
–name tmpfs-test3 \\
–mount type=tmpfs,dst=/app/temp,tmpfs-size=100m,tmpfs-mode=1777 \\
alpine:latest sleep 3600
# 参数说明:
# type=tmpfs: 指定挂载类型为tmpfs
# dst=/app/temp: 容器内目标路径
# tmpfs-size=100m: tmpfs大小限制为100MB
# tmpfs-mode=1777: tmpfs的权限模式
5.2.3 –tmpfs与–mount的区别
| 语法 | 简洁 | 明确 |
| 大小限制 | 不支持单独设置 | 支持(tmpfs-size) |
| 权限模式 | 不支持单独设置 | 支持(tmpfs-mode) |
| Swarm支持 | 不支持 | 支持 |
| 推荐度 | 简单场景 | 复杂场景/生产环境 |
5.3 tmpfs挂载的选项
5.3.1 tmpfs-size(大小限制)
# 设置tmpfs大小为64MB
docker run -d \\
–name tmpfs-sized \\
–mount type=tmpfs,dst=/app/temp,tmpfs-size=64m \\
alpine:latest sleep 3600
# 验证大小限制
docker exec tmpfs-sized df -h /app/temp
# 输出:
# Filesystem Size Used Available Use% Mounted on
# tmpfs 64.0M 0 64.0M 0% /app/temp
# 测试写入超过限制的数据(会失败)
docker exec tmpfs-sized sh -c 'dd if=/dev/zero of=/app/temp/bigfile bs=1M count=100'
# 报错: dd: writing '/app/temp/bigfile': No space left on device
# 注意:如果不设置tmpfs-size,默认大小为宿主机内存的一半
5.3.2 tmpfs-mode(权限模式)
# 设置tmpfs权限为1777(所有用户可读写执行)
docker run -d \\
–name tmpfs-mode-test \\
–mount type=tmpfs,dst=/app/temp,tmpfs-mode=1777 \\
alpine:latest sleep 3600
# 验证权限
docker exec tmpfs-mode-test ls -ld /app/temp
# 输出: drwxrwxrwt 2 root root 40 Jan 15 10:30 /app/temp
# 设置权限为700(仅root可访问)
docker run -d \\
–name tmpfs-private \\
–mount type=tmpfs,dst=/app/temp,tmpfs-mode=1700 \\
alpine:latest sleep 3600
docker exec tmpfs-private ls -ld /app/temp
# 输出: drwx—— 2 root root 40 Jan 15 10:30 /app/temp
5.3.3 完整选项列表
| tmpfs-size | tmpfs最大大小(字节) | tmpfs-size=100m |
| tmpfs-mode | 文件权限模式(八进制) | tmpfs-mode=1777 |
| tmpfs-mode | 可以与其他tmpfs选项组合 | –tmpfs /app/temp:rw,size=100m,mode=1777 |
5.4 tmpfs的内存使用
5.4.1 内存消耗机制
tmpfs使用宿主机的内存来存储数据。需要注意的是:
# 查看宿主机内存使用情况
free -h
# 启动使用tmpfs的容器
docker run -d \\
–name mem-test \\
–mount type=tmpfs,dst=/app/temp,tmpfs-size=512m \\
alpine:latest sleep 3600
# 在容器中写入数据
docker exec mem-test sh -c 'dd if=/dev/zero of=/app/temp/bigfile bs=1M count=200'
# 查看宿主机内存变化
free -h
# 可以看到可用内存减少了约200MB
# 清理
docker rm -f mem-test
5.4.2 与容器内存限制的关系
# 如果容器设置了内存限制,tmpfs的内存使用也计入限制
docker run -d \\
–name mem-limited \\
–memory=256m \\
–mount type=tmpfs,dst=/app/temp,tmpfs-size=512m \\
alpine:latest sleep 3600
# 尝试写入超过内存限制的数据
docker exec mem-limited sh -c 'dd if=/dev/zero of=/app/temp/bigfile bs=1M count=200'
# 可能会触发OOM Killer,导致进程被杀
# 查看容器是否因OOM被杀
docker inspect mem-limited –format '{{.State.OOMKilled}}'
# 输出: true (如果被OOM杀掉)
5.5 tmpfs适用场景
5.5.1 存储敏感信息
# 场景:临时存储API密钥或令牌
docker run -d \\
–name secure-app \\
–mount type=tmpfs,dst=/app/secrets,tmpfs-mode=1700 \\
my-app:latest
# 应用将敏感信息写入/app/secrets,容器停止后信息消失
# 不会在磁盘上留下任何痕迹
5.5.2 高速临时缓存
# 场景:应用缓存目录
docker run -d \\
–name cache-app \\
–mount type=tmpfs,dst=/app/cache,tmpfs-size=256m \\
my-app:latest
# 缓存数据存储在内存中,读写速度极快
# 容器重启后缓存自动清空(从后端重新加载)
5.5.3 减少磁盘I/O
# 场景:临时文件处理
docker run -d \\
–name file-processor \\
–mount type=tmpfs,dst=/tmp/processing,tmpfs-size=512m \\
my-app:latest
# 临时文件在内存中处理,减少磁盘I/O
# 处理完成后,结果写入持久化存储(数据卷)
5.5.4 不适合tmpfs的场景
# 不适合:需要持久化的数据
# 数据库数据 → 使用Volume
# 应用日志 → 使用Volume或日志驱动
# 用户上传文件 → 使用Volume
# 不适合:大量数据
# tmpfs受内存限制,不适合存储大量数据
# 不适合:需要多容器共享的数据
# tmpfs是容器私有的,无法共享
5.6 tmpfs实战: 安全存储临时密钥
5.6.1 需求场景
在一个微服务应用中,需要临时存储数据库密码、API密钥等敏感信息。这些信息不应该持久化到磁盘,仅在应用运行期间存在于内存中。
5.6.2 完整实现
# 步骤1:创建应用镜像
mkdir -p /tmp/secure-app
cat > /tmp/secure-app/Dockerfile << 'EOF'
FROM python:3.11-slim
WORKDIR /app
RUN pip install flask
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
EOF
cat > /tmp/secure-app/app.py << 'PYEOF'
import os
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRETS_DIR = '/app/secrets'
@app.route('/secret', methods=['POST'])
def store_secret():
"""临时存储密钥(仅存在于内存中)"""
data = request.json
key = data.get('key')
value = data.get('value')
if not key or not value:
return jsonify({'error': 'key and value required'}), 400
# 写入tmpfs(内存中)
with open(f'{SECRETS_DIR}/{key}.txt', 'w') as f:
f.write(value)
return jsonify({'message': f'Secret "{key}" stored in memory'}), 200
@app.route('/secret/<key>', methods=['GET'])
def get_secret(key):
"""读取临时密钥"""
try:
with open(f'{SECRETS_DIR}/{key}.txt', 'r') as f:
value = f.read()
return jsonify({'key': key, 'value': value}), 200
except FileNotFoundError:
return jsonify({'error': 'Secret not found'}), 404
@app.route('/secrets', methods=['GET'])
def list_secrets():
"""列出所有临时密钥"""
files = os.listdir(SECRETS_DIR)
return jsonify({'secrets': files}), 200
@app.route('/health', methods=['GET'])
def health():
return jsonify({'status': 'healthy'}), 200
if __name__ == '__main__':
# 确保密钥目录存在
os.makedirs(SECRETS_DIR, exist_ok=True)
app.run(host='0.0.0.0', port=5000)
PYEOF
# 步骤2:构建镜像
docker build -t secure-app /tmp/secure-app/
# 步骤3:使用tmpfs启动容器
docker run -d \\
–name secure-service \\
-p 5000:5000 \\
–mount type=tmpfs,dst=/app/secrets,tmpfs-size=64m,tmpfs-mode=1700 \\
–memory=256m \\
secure-app
# 步骤4:存储密钥
curl -X POST http://localhost:5000/secret \\
-H "Content-Type: application/json" \\
-d '{"key": "db_password", "value": "SuperSecret123!"}'
# 输出: {"message":"Secret \\"db_password\\" stored in memory"}
curl -X POST http://localhost:5000/secret \\
-H "Content-Type: application/json" \\
-d '{"key": "api_key", "value": "ak_1234567890abcdef"}'
# 输出: {"message":"Secret \\"api_key\\" stored in memory"}
# 步骤5:读取密钥
curl http://localhost:5000/secret/db_password
# 输出: {"key":"db_password","value":"SuperSecret123!"}
# 步骤6:列出所有密钥
curl http://localhost:5000/secrets
# 输出: {"secrets":["db_password.txt","api_key.txt"]}
# 步骤7:验证密钥不在磁盘上
# 查看容器挂载信息
docker inspect secure-service –format '{{json .Mounts}}' | python3 -m json.tool
# 输出显示tmpfs挂载,没有磁盘路径
# 步骤8:重启容器,验证密钥消失
docker restart secure-service
sleep 3
curl http://localhost:5000/secrets
# 输出: {"secrets":[]}
# 密钥已消失!因为tmpfs数据在容器重启后清除
# 清理
docker rm -f secure-service
rm -rf /tmp/secure-app
5.6.3 使用Docker Compose
# docker-compose.yml
version: '3.8'
services:
secure-service:
build: .
container_name: secure–service
ports:
– "5000:5000"
tmpfs:
– /app/secrets:size=64m,mode=1700
mem_limit: 256m
restart: unless–stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 5s
retries: 3
# 启动
docker-compose up -d
# 查看tmpfs挂载
docker exec secure-service df -h /app/secrets
# 输出:
# Filesystem Size Used Available Use% Mounted on
# tmpfs 64.0M 0 64.0M 0% /app/secrets
5.6.4 tmpfs安全注意事项
# 1. tmpfs数据可能被交换到swap分区
# 如果需要确保数据绝不落盘,需要禁用swap
docker run -d \\
–name ultra-secure \\
–mount type=tmpfs,dst=/app/secrets \\
–memory=256m \\
–memory-swap=0 \\
secure-app
# –memory-swap=0: 禁止内存交换,确保tmpfs数据不被写入swap
# 2. 使用tmpfs时注意内存监控
# tmpfs占用的内存会影响容器和宿主机的可用内存
# 3. tmpfs不适合长期存储大量数据
# 仅适合临时、少量的敏感数据
第六章 数据卷进阶操作
6.1 数据卷驱动与远程数据卷
6.1.1 数据卷驱动概述
数据卷驱动(Volume Driver)是Docker数据卷系统的扩展机制。默认的数据卷驱动是local,它将数据存储在宿主机的本地文件系统上。通过安装第三方数据卷驱动插件,可以将数据卷存储在各种远程存储系统上,如NFS、Amazon EBS、Google Persistent Disk、Azure File Storage等。
数据卷驱动的作用:
- 抽象底层存储:为Docker提供统一的存储接口,屏蔽底层存储差异
- 支持远程存储:将数据存储在网络存储上,实现跨主机数据共享
- 支持云存储:与云厂商的块存储、文件存储服务集成
- 支持高级特性:如加密、压缩、快照等
6.1.2 内置数据卷驱动
Docker内置了local驱动,它支持几种存储类型:
# 1. 默认本地目录(最常见)
docker volume create my-local-volume
# 存储: /var/lib/docker/volumes/my-local-volume/_data/
# 2. NFS类型
docker volume create \\
–driver local \\
–opt type=nfs \\
–opt o=addr=192.168.1.100,nfsvers=4,rw \\
–opt device=:/path/to/nfs/share \\
my-nfs-volume
# 3. CIFS/SMB类型(Windows共享)
docker volume create \\
–driver local \\
–opt type=cifs \\
–opt o=addr=192.168.1.200,username=user,password=pass,rw \\
–opt device=/sharename \\
my-smb-volume
# 4. tmpfs类型
docker volume create \\
–driver local \\
–opt type=tmpfs \\
–opt device=tmpfs \\
–opt o=size=100m,mode=0755 \\
my-tmpfs-volume
# 5. btrfs子卷
docker volume create \\
–driver local \\
–opt type=btrfs \\
–opt device=/dev/sdb \\
–opt o=subvol=/docker-volumes/my-subvol \\
my-btrfs-volume
# 6. 指定已有的块设备
docker volume create \\
–driver local \\
–opt type=ext4 \\
–opt device=/dev/sdc1 \\
my-block-volume
6.1.3 第三方数据卷驱动插件
# 查看已安装的插件
docker plugin ls
# 安装插件示例(以Docker NFS插件为例)
docker plugin install leapbit/docker-nfs:latest
# 使用插件创建数据卷
docker volume create \\
–driver leapbit/docker-nfs:latest \\
–opt nfsserver=192.168.1.100 \\
–opt nfspath=/shared/data \\
–opt nfsoptions=nfsvers=4 \\
my-remote-volume
6.2 使用数据卷插件(local、nfs、block)
6.2.1 local驱动详解
local驱动是Docker默认的数据卷驱动,它支持多种存储后端:
# 查看local驱动支持的选项
docker volume create –driver local –help
# local驱动支持的–opt选项:
# type: 文件系统类型(nfs, cifs, tmpfs, ext4, btrfs等)
# device: 设备路径或远程共享路径
# o: 挂载选项(对应mount命令的-o参数)
# 示例:创建一个基于ext4文件系统的数据卷
# 前提:/dev/sdb1已经格式化为ext4
docker volume create \\
–driver local \\
–opt type=ext4 \\
–opt device=/dev/sdb1 \\
my-ext4-volume
# 使用该数据卷
docker run -d \\
–name db \\
-v my-ext4-volume:/var/lib/mysql \\
mysql:8.0
6.2.2 NFS数据卷插件
NFS(Network File System)是常用的网络文件系统协议,适合多主机共享数据:
# 方法1:使用local驱动创建NFS数据卷(推荐)
docker volume create \\
–driver local \\
–opt type=nfs \\
–opt o=addr=192.168.1.100,nfsvers=4,soft,timeo=60,retrans=2 \\
–opt device=:/data/shared \\
–name nfs-shared-data
# 参数说明:
# type=nfs: 使用NFS文件系统
# o=addr=192.168.1.100: NFS服务器地址
# nfsvers=4: NFS版本4
# soft: 软挂载(NFS不可用时返回错误而非挂起)
# timeo=60: 超时时间(60秒)
# retrans=2: 重试次数
# device=:/data/shared: NFS服务器上的共享路径
# 使用NFS数据卷
docker run -d \\
–name shared-app \\
-v nfs-shared-data:/app/data \\
my-app:latest
# 在多台主机上创建同名NFS数据卷,实现跨主机数据共享
# 主机A:
docker volume create –driver local –opt type=nfs –opt o=addr=nfs-server-ip,nfsvers=4 –opt device=:/data/shared nfs-shared-data
# 主机B:
docker volume create –driver local –opt type=nfs –opt o=addr=nfs-server-ip,nfsvers=4 –opt device=:/data/shared nfs-shared-data
# 两台主机上的容器挂载同一个NFS数据卷,数据实时共享
6.2.3 block设备数据卷
# 使用块设备创建数据卷
# 前提:块设备已格式化
sudo mkfs.ext4 /dev/sdb1
# 创建基于块设备的数据卷
docker volume create \\
–driver local \\
–opt type=ext4 \\
–opt device=/dev/sdb1 \\
block-data-volume
# 使用块设备数据卷(适合高性能数据库场景)
docker run -d \\
–name high-perf-db \\
-v block-data-volume:/var/lib/postgresql/data \\
postgres:15
# 查看数据卷信息
docker volume inspect block-data-volume
# 输出:
# [
# {
# "CreatedAt": "2024-01-15T10:30:00Z",
# "Driver": "local",
# "Labels": null,
# "Mountpoint": "/var/lib/docker/volumes/block-data-volume/_data",
# "Name": "block-data-volume",
# "Options": {
# "device": "/dev/sdb1",
# "type": "ext4"
# },
# "Scope": "local"
# }
# ]
6.3 创建NFS数据卷
6.3.1 完整NFS数据卷配置
# ============================================
# 步骤1:在NFS服务器上配置共享(假设IP:192.168.1.100)
# ============================================
# 安装NFS服务
sudo apt-get install nfs-kernel-server # Ubuntu/Debian
# 或
sudo yum install nfs-utils # CentOS/RHEL
# 创建共享目录
sudo mkdir -p /data/docker-shared
sudo chown -R nobody:nogroup /data/docker-shared
sudo chmod 777 /data/docker-shared
# 配置NFS共享
sudo tee -a /etc/exports << 'EOF'
/data/docker-shared 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)
EOF
# 启动NFS服务
sudo exportfs -ra
sudo systemctl restart nfs-kernel-server
sudo systemctl enable nfs-kernel-server
# 验证共享
showmount -e localhost
# 输出: /data/docker-shared 192.168.1.0/24
# ============================================
# 步骤2:在Docker主机上创建NFS数据卷
# ============================================
# 安装NFS客户端
sudo apt-get install nfs-common # Ubuntu/Debian
# 或
sudo yum install nfs-utils # CentOS/RHEL
# 创建NFS数据卷
docker volume create \\
–driver local \\
–opt type=nfs \\
–opt o=addr=192.168.1.100,nfsvers=4,soft,timeo=60,retrans=2 \\
–opt device=:/data/docker-shared \\
nfs-docker-data
# ============================================
# 步骤3:使用NFS数据卷运行容器
# ============================================
# 运行一个web应用,数据存储在NFS上
docker run -d \\
–name web-app-1 \\
-p 8080:80 \\
-v nfs-docker-data:/usr/share/nginx/html \\
nginx:latest
# 在容器中创建文件
docker exec web-app-1 sh -c 'echo "<h1>Hello from NFS</h1>" > /usr/share/nginx/html/index.html'
# 在另一台Docker主机上运行容器,挂载同一个NFS数据卷
# (需要在另一台主机上也创建同名的NFS数据卷)
docker run -d \\
–name web-app-2 \\
-p 8080:80 \\
-v nfs-docker-data:/usr/share/nginx/html \\
nginx:latest
# 验证:web-app-2也能看到web-app-1创建的文件
docker exec web-app-2 cat /usr/share/nginx/html/index.html
# 输出: <h1>Hello from NFS</h1>
6.3.2 NFS数据卷在Docker Compose中的使用
# docker-compose.yml
version: '3.8'
services:
web:
image: nginx:latest
ports:
– "8080:80"
volumes:
– nfs–data:/usr/share/nginx/html
deploy:
replicas: 3 # 在Swarm集群中运行3个副本,共享同一NFS数据
volumes:
nfs-data:
driver: local
driver_opts:
type: nfs
o: addr=192.168.1.100,nfsvers=4,soft,timeo=60,retrans=2
device: :/data/docker–shared
6.4 数据卷共享与多容器同时挂载
6.4.1 多容器共享同一个数据卷
# 创建共享数据卷
docker volume create shared-data
# 容器1:写入数据
docker run -d \\
–name writer \\
-v shared-data:/data \\
alpine:latest \\
sh -c 'while true; do echo "$(date): writer" >> /data/log.txt; sleep 5; done'
# 容器2:读取数据
docker run -d \\
–name reader \\
-v shared-data:/data:ro \\
alpine:latest \\
sh -c 'while true; do tail -1 /data/log.txt; sleep 5; done'
# 容器3:同时读写
docker run -d \\
–name processor \\
-v shared-data:/data \\
alpine:latest \\
sh -c 'while true; do echo "$(date): processor" >> /data/log.txt; sleep 10; done'
# 查看日志
docker logs -f reader
# 输出:
# Wed Jan 15 10:30:05 UTC 2024: writer
# Wed Jan 15 10:30:10 UTC 2024: writer
# Wed Jan 15 10:30:10 UTC 2024: processor
# …
6.4.2 并发写入的注意事项
# 多个容器同时写入同一个数据卷时,需要注意并发问题
# Docker不提供文件锁机制,需要应用层面处理并发
# 场景:多个容器写入同一个文件
# 问题:可能导致数据交错或丢失
# 解决方法1:每个容器写入不同的文件
docker run -d –name app1 -v shared-data:/data alpine sh -c 'while true; do echo "$(date)" >> /data/app1.log; sleep 5; done'
docker run -d –name app2 -v shared-data:/data alpine sh -c 'while true; do echo "$(date)" >> /data/app2.log; sleep 5; done'
# 解决方法2:使用文件锁(flock)
docker run -d –name app1 -v shared-data:/data alpine sh -c '
while true; do
(
flock -x 200
echo "$(date): app1" >> /data/shared.log
) 200>/data/.lock
sleep 5
done
'
# 解决方法3:使用消息队列或数据库处理并发
6.5 数据卷容器模式(–volumes-from)
6.5.1 数据卷容器概念
数据卷容器(Data Volume Container)是一种特殊的容器,它本身不运行应用服务,仅用于提供数据卷给其他容器挂载。其他容器通过–volumes-from参数来挂载数据卷容器中的所有数据卷。
虽然在新版本的Docker中,直接使用命名数据卷是更推荐的做法,但数据卷容器模式在某些场景下仍然有用。
6.5.2 创建和使用数据卷容器
# 步骤1:创建数据卷容器(不运行实际应用)
docker create \\
–name data-container \\
-v /app/data \\
-v /app/config \\
-v /app/logs \\
busybox:latest
# 注意:使用docker create而非docker run,容器创建但不启动
# 步骤2:其他容器通过–volumes-from挂载数据卷容器的卷
docker run -d \\
–name app1 \\
–volumes-from data-container \\
my-app:latest
docker run -d \\
–name app2 \\
–volumes-from data-container \\
my-app:latest
# app1和app2都挂载了/app/data、/app/config、/app/logs
# 它们共享这些路径的数据
# 步骤3:验证共享
docker exec app1 sh -c 'echo "data from app1" > /app/data/test.txt'
docker exec app2 cat /app/data/test.txt
# 输出: data from app1
6.5.3 –volumes-from的权限继承
# –volumes-from会继承数据卷的读写权限
# 如果数据卷容器中是只读挂载,继承的也是只读
# 创建只读数据卷容器
docker create \\
–name readonly-data-container \\
-v /app/config:ro \\
busybox:latest
# 继承的容器也是只读
docker run -d \\
–name app-readonly \\
–volumes-from readonly-data-container \\
my-app:latest
# app-readonly无法写入/app/config
docker exec app-readonly sh -c 'echo "test" > /app/config/test.txt'
# 报错: Read-only file system
6.5.4 链式继承
# –volumes-from支持链式继承
docker create –name base-data -v /data busybox:latest
docker run -d –name mid-data –volumes-from base-data -v /logs busybox:latest sleep 3600
docker run -d –name app –volumes-from mid-data my-app:latest
# app同时挂载了/data(来自base-data)和/logs(来自mid-data)
6.5.5 数据卷容器模式的优缺点
# 优点:
# 1. 集中管理数据卷定义
# 2. 多个容器共享同一组数据卷
# 3. 可以方便地添加新的共享容器
# 缺点:
# 1. 数据卷容器本身不运行,容易被遗忘和误删
# 2. 匿名数据卷难以管理
# 3. Docker官方更推荐使用命名数据卷
# 推荐的现代做法:直接使用命名数据卷
docker volume create app-data
docker volume create app-logs
docker run -d –name app1 -v app-data:/app/data -v app-logs:/app/logs my-app:latest
docker run -d –name app2 -v app-data:/app/data -v app-logs:/app/logs my-app:latest
6.6 数据卷的备份(docker run + tar)
6.6.1 备份原理
数据卷虽然存储在宿主机上,但Docker推荐通过容器来操作数据卷内容。备份数据卷的标准方法是:启动一个新容器,同时挂载要备份的数据卷和宿主机上的备份目录,然后使用tar命令将数据卷内容打包到备份目录中。
6.6.2 基本备份操作
# 假设有一个名为mysql-data的数据卷需要备份
# 方法1:备份到宿主机目录
mkdir -p /backup
docker run –rm \\
-v mysql-data:/data:ro \\
-v /backup:/backup \\
alpine:latest \\
tar czf /backup/mysql-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /data .
# 参数说明:
# –rm: 容器退出后自动删除
# -v mysql-data:/data:ro: 以只读方式挂载要备份的数据卷
# -v /backup:/backup: 挂载宿主机的备份目录
# tar czf: 创建gzip压缩的tar包
# -C /data .: 切换到/data目录并打包当前目录下的所有内容
# 查看备份文件
ls -lh /backup/
# 输出: -rw-r–r– 1 root root 50M Jan 15 10:30 mysql-data-20240115-103000.tar.gz
6.6.3 备份脚本
#!/bin/bash
# backup-volume.sh – 数据卷备份脚本
# 参数
VOLUME_NAME=$1
BACKUP_DIR=${2:-/backup}
RETENTION_DAYS=${3:-7}
# 检查参数
if [ -z "$VOLUME_NAME" ]; then
echo "Usage: $0 <volume_name> [backup_dir] [retention_days]"
exit 1
fi
# 创建备份目录
mkdir -p "$BACKUP_DIR"
# 生成备份文件名
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_FILE="$BACKUP_DIR/${VOLUME_NAME}–${TIMESTAMP}.tar.gz"
echo "[$(date)] Backing up volume: $VOLUME_NAME"
echo " Backup file: $BACKUP_FILE"
# 执行备份
docker run –rm \\
-v "$VOLUME_NAME":/data:ro \\
-v "$BACKUP_DIR":/backup \\
alpine:latest \\
tar czf "/backup/${VOLUME_NAME}–${TIMESTAMP}.tar.gz" -C /data .
# 检查备份结果
if [ $? -eq 0 ]; then
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo "[$(date)] Backup completed successfully"
echo " Size: $BACKUP_SIZE"
else
echo "[$(date)] Backup FAILED!"
exit 1
fi
# 清理过期备份
echo "[$(date)] Cleaning up backups older than $RETENTION_DAYS days…"
find "$BACKUP_DIR" -name "${VOLUME_NAME}-*.tar.gz" -mtime +$RETENTION_DAYS -delete
echo "[$(date)] Done."
# 使用备份脚本
chmod +x backup-volume.sh
./backup-volume.sh mysql-data /backup 7
6.7 数据卷的恢复
6.7.1 从备份恢复数据卷
# 方法1:恢复到新数据卷
# 步骤1:创建新数据卷
docker volume create mysql-data-restored
# 步骤2:从备份恢复
docker run –rm \\
-v mysql-data-restored:/data \\
-v /backup:/backup:ro \\
alpine:latest \\
sh -c 'tar xzf /backup/mysql-data-20240115-103000.tar.gz -C /data'
# 步骤3:验证恢复
docker run –rm -v mysql-data-restored:/data alpine ls -la /data
# 步骤4:使用恢复的数据卷启动容器
docker run -d \\
–name mysql-restored \\
-v mysql-data-restored:/var/lib/mysql \\
-e MYSQL_ROOT_PASSWORD=RootPass123! \\
mysql:8.0
6.7.2 恢复脚本
#!/bin/bash
# restore-volume.sh – 数据卷恢复脚本
VOLUME_NAME=$1
BACKUP_FILE=$2
if [ -z "$VOLUME_NAME" ] || [ -z "$BACKUP_FILE" ]; then
echo "Usage: $0 <volume_name> <backup_file>"
echo "Example: $0 mysql-data /backup/mysql-data-20240115-103000.tar.gz"
exit 1
fi
if [ ! -f "$BACKUP_FILE" ]; then
echo "Error: Backup file not found: $BACKUP_FILE"
exit 1
fi
BACKUP_DIR=$(dirname "$BACKUP_FILE")
BACKUP_FILENAME=$(basename "$BACKUP_FILE")
echo "[$(date)] Restoring volume: $VOLUME_NAME"
echo " From: $BACKUP_FILE"
# 检查数据卷是否存在,不存在则创建
if ! docker volume inspect "$VOLUME_NAME" > /dev/null 2>&1; then
echo "[$(date)] Creating new volume: $VOLUME_NAME"
docker volume create "$VOLUME_NAME"
fi
# 执行恢复
docker run –rm \\
-v "$VOLUME_NAME":/data \\
-v "$BACKUP_DIR":/backup:ro \\
alpine:latest \\
sh -c "tar xzf /backup/$BACKUP_FILENAME -C /data"
if [ $? -eq 0 ]; then
echo "[$(date)] Restore completed successfully"
else
echo "[$(date)] Restore FAILED!"
exit 1
fi
# 使用恢复脚本
chmod +x restore-volume.sh
./restore-volume.sh mysql-data-restored /backup/mysql-data-20240115-103000.tar.gz
6.8 数据卷迁移
6.8.1 同主机迁移
# 场景:将数据从旧数据卷迁移到新数据卷(可能使用不同的驱动)
# 步骤1:创建新数据卷(例如使用NFS驱动)
docker volume create \\
–driver local \\
–opt type=nfs \\
–opt o=addr=192.168.1.100,nfsvers=4 \\
–opt device=:/data/new-storage \\
new-data-volume
# 步骤2:迁移数据
docker run –rm \\
-v old-data-volume:/source:ro \\
-v new-data-volume:/target \\
alpine:latest \\
sh -c 'cp -a /source/. /target/'
# 步骤3:验证数据
docker run –rm -v new-data-volume:/data alpine ls -la /data
# 步骤4:更新容器使用新数据卷
docker rm -f my-app
docker run -d –name my-app -v new-data-volume:/app/data my-app:latest
# 步骤5:确认无误后删除旧数据卷
docker volume rm old-data-volume
6.8.2 跨主机迁移
# 场景:将数据卷从主机A迁移到主机B
# 主机A:导出数据卷
docker run –rm \\
-v source-volume:/data:ro \\
-v /tmp:/backup \\
alpine:latest \\
tar czf /backup/source-volume-backup.tar.gz -C /data .
# 将备份文件传输到主机B
scp /tmp/source-volume-backup.tar.gz user@hostB:/tmp/
# 主机B:创建新数据卷并恢复
docker volume create source-volume
docker run –rm \\
-v source-volume:/data \\
-v /tmp:/backup:ro \\
alpine:latest \\
sh -c 'tar xzf /backup/source-volume-backup.tar.gz -C /data'
# 验证
docker run –rm -v source-volume:/data alpine ls -la /data
6.8.3 使用rsync增量迁移
# 对于大数据卷,使用rsync进行增量迁移更高效
# 需要在两端都支持rsync
# 步骤1:在目标主机创建数据卷
docker volume create migrated-data
# 步骤2:使用rsync迁移数据
# 方法:在源主机上临时启动rsync服务端
docker run -d \\
–name rsync-server \\
-v source-volume:/data \\
-p 873:873 \\
alpine:latest \\
sh -c 'apk add rsync && rsync –daemon –no-detach'
# 在目标主机上拉取数据
docker run –rm \\
-v migrated-data:/data \\
alpine:latest \\
sh -c 'apk add rsync && rsync -avz rsync://source-host:/data/ /data/'
# 步骤3:清理rsync服务端
# 在源主机上
docker rm -f rsync-server
6.9 数据卷大小限制
6.9.1 使用配额限制数据卷大小
Docker本身不直接支持为数据卷设置大小限制,但可以通过以下方法实现:
# 方法1:使用btrfs配额(需要Docker使用btrfs存储驱动)
# 前提:/var/lib/docker在btrfs文件系统上
sudo btrfs quota enable /var/lib/docker
sudo btrfs qgroup limit 10G /var/lib/docker/volumes/my-volume/_data
# 方法2:使用LVM创建指定大小的逻辑卷
# 创建一个10GB的逻辑卷
sudo lvcreate -L 10G -n docker-volume-data vg0
sudo mkfs.ext4 /dev/vg0/docker-volume-data
# 创建基于该逻辑卷的数据卷
docker volume create \\
–driver local \\
–opt type=ext4 \\
–opt device=/dev/vg0/docker-volume-data \\
size-limited-volume
# 方法3:使用loop设备创建固定大小的文件系统
# 创建一个10GB的文件
sudo dd if=/dev/zero of=/tmp/volume-image.img bs=1M count=10240
sudo mkfs.ext4 /tmp/volume-image.img
sudo mount -o loop /tmp/volume-image.img /mnt/volume-mount
# 创建数据卷时指向挂载点
docker volume create \\
–driver local \\
–opt type=none \\
–opt device=/mnt/volume-mount \\
–opt o=bind \\
loop-volume
# 方法4:使用XFS配额
# 挂载XFS时启用配额
sudo mount -o prjquota /dev/sdb1 /var/lib/docker
# 设置项目配额
sudo xfs_quota -x -c 'project -s -p /var/lib/docker/volumes/my-volume/_data 1'
sudo xfs_quota -x -c 'limit -p bhard=10g 1'
6.9.2 监控数据卷使用量
# 方法1:使用docker system df
docker system df -v
# 会显示每个数据卷的大小
# 方法2:直接检查宿主机上的数据卷目录
sudo du -sh /var/lib/docker/volumes/*/
# 方法3:通过容器检查
docker run –rm -v my-volume:/data alpine du -sh /data
# 方法4:监控脚本
#!/bin/bash
# monitor-volumes.sh
for vol in $(docker volume ls -q); do
SIZE=$(docker run –rm -v "$vol":/data alpine du -sh /data 2>/dev/null | cut -f1)
echo "$vol: $SIZE"
done
6.10 数据卷生命周期管理策略
6.10.1 生命周期阶段
创建 → 使用(挂载到容器) → 备份 → 迁移(可选) → 归档 → 清理/删除
6.10.2 生命周期管理最佳实践
# 1. 使用标签管理数据卷
docker volume create \\
–label app=mysql \\
–label env=production \\
–label backup=daily \\
–label retention=30d \\
–label owner=dba-team \\
mysql-prod-data
# 2. 定期备份脚本(crontab)
# /etc/crontab
# 0 2 * * * root /opt/scripts/backup-all-volumes.sh >> /var/log/volume-backup.log 2>&1
#!/bin/bash
# backup-all-volumes.sh – 备份所有标记了backup=daily的数据卷
BACKUP_DIR="/backup/volumes/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
for vol in $(docker volume ls –filter label=backup=daily -q); do
echo "Backing up $vol…"
docker run –rm \\
-v "$vol":/data:ro \\
-v "$BACKUP_DIR":/backup \\
alpine:latest \\
tar czf "/backup/${vol}.tar.gz" -C /data .
done
# 清理30天前的备份
find /backup/volumes/ -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \\;
# 3. 清理无用数据卷
# 谨慎使用!先检查再清理
docker volume ls -f dangling=true
# 确认后清理
docker volume prune -f
# 4. 数据卷归档
# 将不再使用但需要保留的数据卷归档
docker run –rm \\
-v old-data-volume:/data:ro \\
-v /archive:/archive \\
alpine:latest \\
tar czf /archive/old-data-volume-$(date +%Y%m%d).tar.gz -C /data .
# 归档后删除数据卷
docker volume rm old-data-volume
# 5. 定期完整性检查
#!/bin/bash
# check-volumes.sh – 检查数据卷完整性
for vol in $(docker volume ls -q); do
echo "Checking $vol…"
docker run –rm -v "$vol":/data alpine sh -c '
if [ -d /data ]; then
FILE_COUNT=$(find /data -type f | wc -l)
SIZE=$(du -sh /data | cut -f1)
echo " Files: $FILE_COUNT, Size: $SIZE"
else
echo " ERROR: /data directory not found!"
fi
'
done
第七章 Dockerfile中的存储配置
7.1 VOLUME指令详解
7.1.1 VOLUME指令语法
VOLUME指令在Dockerfile中用于声明数据卷挂载点。当容器启动时,如果用户没有通过-v参数指定数据卷,Docker会自动为这些路径创建匿名数据卷。
# Dockerfile中使用VOLUME指令
# 语法1:JSON数组形式(推荐)
VOLUME ["/data", "/var/log/app"]
# 语法2:空格分隔的字符串形式
VOLUME /data /var/log/app
7.1.2 VOLUME指令的作用
# 示例Dockerfile
FROM ubuntu:22.04
# 创建目录
RUN mkdir -p /app/data /app/logs
# 声明数据卷
VOLUME ["/app/data", "/app/logs"]
# 设置默认命令
CMD ["sleep", "3600"]
# 构建镜像
docker build -t volume-test .
# 运行容器(不指定-v参数)
docker run -d –name auto-volume volume-test
# 查看自动创建的匿名数据卷
docker inspect auto-volume –format '{{json .Mounts}}' | python3 -m json.tool
# 输出:
# [
# {
# "Type": "volume",
# "Name": "abc123def456…", # 匿名数据卷(哈希命名)
# "Source": "/var/lib/docker/volumes/abc123def456…/_data",
# "Destination": "/app/data",
# "Driver": "local",
# "Mode": "",
# "RW": true,
# "Propagation": ""
# },
# {
# "Type": "volume",
# "Name": "xyz789ghi012…",
# "Source": "/var/lib/docker/volumes/xyz789ghi012…/_data",
# "Destination": "/app/logs",
# "Driver": "local",
# "Mode": "",
# "RW": true,
# "Propagation": ""
# }
# ]
7.2 VOLUME指令的注意事项
7.2.1 匿名数据卷问题
# Dockerfile
FROM nginx:latest
VOLUME /usr/share/nginx/html
# 每次运行容器都会创建一个匿名数据卷
docker run -d –name web1 volume-nginx # 创建匿名卷1
docker run -d –name web2 volume-nginx # 创建匿名卷2
docker run -d –name web3 volume-nginx # 创建匿名卷3
# 删除容器后,匿名数据卷不会被自动删除
docker rm -f web1 web2 web3
# 查看遗留的匿名数据卷
docker volume ls
# 会看到3个哈希命名的匿名数据卷
# 清理
docker volume prune -f
7.2.2 VOLUME指令后的RUN指令
# 重要:VOLUME指令后的RUN指令对VOLUME声明的路径无效!
# 因为VOLUME声明后,Docker知道该路径将使用外部存储,
# 所以不会将RUN指令对该路径的修改保存到镜像中
# 错误示例:
FROM ubuntu:22.04
VOLUME /app/data
RUN echo "initial data" > /app/data/initial.txt # 这行不会被保存到镜像!
# 原因:VOLUME声明后,/app/data被认为是外部挂载点,
# RUN指令中的修改只会影响临时容器的可写层,不会写入镜像层
# 正确示例:先写入数据,再声明VOLUME
FROM ubuntu:22.04
RUN mkdir -p /app/data && echo "initial data" > /app/data/initial.txt
VOLUME /app/data
# 这样initial.txt会被包含在镜像中,首次挂载时会被复制到数据卷
7.2.3 VOLUME指令与镜像层的关系
# VOLUME指令本身不创建新层,它只是在镜像元数据中添加声明
# 但VOLUME指令会影响后续指令的行为
FROM ubuntu:22.04
# 这一层会包含文件
RUN mkdir -p /app/data && echo "content" > /app/data/file.txt
# VOLUME声明(不影响已有的镜像层)
VOLUME /app/data
# 这一层不会影响/app/data(因为VOLUME已声明)
RUN echo "new content" > /app/data/newfile.txt
# newfile.txt不会被保存到镜像中!
# 构建并验证
# docker build -t test .
# docker run –rm test ls /app/data/
# 输出: file.txt (没有newfile.txt!)
7.3 VOLUME与docker run -v的交互
7.3.1 -v覆盖VOLUME声明的路径
# Dockerfile
FROM nginx:latest
VOLUME ["/usr/share/nginx/html"]
# 情况1:不指定-v,Docker创建匿名数据卷(包含镜像内容)
docker run -d –name web1 volume-nginx
docker exec web1 ls /usr/share/nginx/html
# 输出: 50x.html index.html (从镜像复制)
# 情况2:指定命名数据卷
docker run -d –name web2 -v my-html:/usr/share/nginx/html volume-nginx
docker exec web2 ls /usr/share/nginx/html
# 输出: 50x.html index.html (首次挂载时从镜像复制)
# 情况3:指定绑定挂载(不会复制镜像内容)
docker run -d –name web3 -v /tmp/empty:/usr/share/nginx/html volume-nginx
docker exec web3 ls /usr/share/nginx/html
# 输出: (空,绑定挂载不复制镜像内容)
# 情况4:挂载已有内容的数据卷(不复制镜像内容)
docker volume create pre-filled-volume
docker run –rm -v pre-filled-volume:/data alpine sh -c 'echo "existing" > /data/existing.txt'
docker run -d –name web4 -v pre-filled-volume:/usr/share/nginx/html volume-nginx
docker exec web4 ls /usr/share/nginx/html
# 输出: existing.txt (已有内容,不复制镜像内容)
7.3.2 行为总结
| VOLUME声明 + 不指定-v(创建匿名卷) | 是 |
| VOLUME声明 + 命名数据卷(首次创建) | 是 |
| VOLUME声明 + 命名数据卷(已有内容) | 否 |
| VOLUME声明 + 绑定挂载 | 否 |
| 无VOLUME声明 + 命名数据卷(首次创建) | 是 |
| 无VOLUME声明 + 绑定挂载 | 否 |
7.4 在Dockerfile中定义数据卷的最佳实践
7.4.1 最佳实践建议
# ============================================
# 最佳实践1:谨慎使用VOLUME指令
# ============================================
# 不推荐:在基础镜像中使用VOLUME
# 这会导致所有继承的镜像都被强制创建匿名数据卷
# FROM ubuntu:22.04
# VOLUME /data # 不推荐
# 推荐:让用户在docker run时通过-v指定
# 在文档中说明需要持久化的路径即可
# ============================================
# 最佳实践2:对于数据库镜像,声明VOLUME是合理的
# ============================================
# MySQL官方Dockerfile中声明了VOLUME
# FROM mysql:8.0
# VOLUME /var/lib/mysql
# 这样即使用户忘记指定-v,数据也会保存在匿名数据卷中
# 虽然匿名卷难以管理,但至少不会因容器删除而丢失
# ============================================
# 最佳实践3:在VOLUME之前准备初始数据
# ============================================
FROM ubuntu:22.04
# 先创建目录和初始数据
RUN mkdir -p /app/data && \\
echo '{"initialized": true}' > /app/data/config.json && \\
echo '#!/bin/sh' > /app/data/init.sh && \\
chmod +x /app/data/init.sh
# 然后声明VOLUME
VOLUME ["/app/data"]
# 这样config.json和init.sh会被包含在镜像中
# 首次挂载时会复制到数据卷
CMD ["sleep", "3600"]
7.4.2 避免在VOLUME路径下执行写操作
# 不推荐
FROM node:18-alpine
WORKDIR /app
VOLUME ["/app"] # 整个/app目录声明为VOLUME
COPY . . # COPY操作发生在VOLUME声明之后,可能有问题
RUN npm install # npm install的结果可能不被保存
# 推荐
FROM node:18-alpine
WORKDIR /app
COPY . . # 先COPY
RUN npm install # 先安装依赖
# 不声明VOLUME,让用户决定如何持久化
# 或者只声明需要持久化的子目录
VOLUME ["/app/data"] # 只持久化数据目录
7.5 多阶段构建中的数据卷处理
7.5.1 多阶段构建与VOLUME
# 多阶段构建中,VOLUME指令只在最终阶段生效
# 阶段1:构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 阶段2:运行阶段
FROM nginx:alpine
# 从构建阶段复制产物
COPY –from=builder /app/dist /usr/share/nginx/html
# 声明数据卷(仅在此阶段生效)
VOLUME ["/var/log/nginx"]
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
7.5.2 多阶段构建中的数据传递
# 在多阶段构建中,数据卷声明不会跨阶段传递
# 每个阶段是独立的
# 阶段1:数据准备阶段
FROM alpine AS data-prep
RUN mkdir -p /data && \\
echo "prepared data" > /data/ready.txt
# 阶段2:运行阶段
FROM alpine
# 从阶段1复制数据(这不是数据卷,是镜像层复制)
COPY –from=data-prep /data /app/data
# 声明数据卷
VOLUME ["/app/data"]
CMD ["cat", "/app/data/ready.txt"]
7.6 镜像中预填充数据卷内容
7.6.1 预填充机制
当命名数据卷首次挂载到一个非空目录时(目录中有镜像层的内容),Docker会将这些内容复制到数据卷中。这被称为"预填充"。
# Dockerfile – 预填充数据卷内容
FROM ubuntu:22.04
# 创建目录并填充初始数据
RUN mkdir -p /app/data /app/config /app/templates
# 填充初始数据文件
RUN echo '{"version": "1.0", "initialized": false}' > /app/data/default.json
# 填充配置文件
RUN echo 'server.port=8080' > /app/config/application.properties
# 填充模板文件
RUN echo '<html><body>Hello {{name}}</body></html>' > /app/templates/index.html
# 声明数据卷
VOLUME ["/app/data", "/app/config", "/app/templates"]
CMD ["sleep", "3600"]
# 构建镜像
docker build -t pre-filled-app .
# 创建命名数据卷(首次挂载会复制镜像内容)
docker volume create app-data
docker volume create app-config
# 运行容器
docker run -d –name app \\
-v app-data:/app/data \\
-v app-config:/app/config \\
pre-filled-app
# 验证数据卷中包含了镜像的初始内容
docker exec app cat /app/data/default.json
# 输出: {"version": "1.0", "initialized": false}
docker exec app cat /app/config/application.properties
# 输出: server.port=8080
# 用户可以修改数据卷中的内容
docker exec app sh -c 'echo "{\\"version\\": \\"1.0\\", \\"initialized\\": true}" > /app/data/default.json'
# 下次创建容器时,数据卷中是修改后的内容(不再复制镜像内容)
docker rm -f app
docker run -d –name app2 \\
-v app-data:/app/data \\
-v app-config:/app/config \\
pre-filled-app
docker exec app2 cat /app/data/default.json
# 输出: {"version": "1.0", "initialized": true}
7.6.2 初始化脚本模式
# Dockerfile – 使用初始化脚本填充数据卷
FROM ubuntu:22.04
# 复制初始化脚本
COPY init-data.sh /docker-entrypoint-initdb.d/
RUN chmod +x /docker-entrypoint-initdb.d/init-data.sh
# 声明数据卷
VOLUME ["/app/data"]
# 自定义entrypoint
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["sleep", "3600"]
#!/bin/bash
# entrypoint.sh – 自定义入口脚本
set -e
# 如果数据卷为空,执行初始化
if [ -z "$(ls -A /app/data 2>/dev/null)" ]; then
echo "Initializing data volume…"
/docker-entrypoint-initdb.d/init-data.sh
fi
# 执行主命令
exec "$@"
#!/bin/bash
# init-data.sh – 初始化脚本
mkdir -p /app/data
echo '{"initialized": true, "timestamp": "'$(date -Iseconds)'"}' > /app/data/init.json
echo "Data volume initialized at $(date)"
# 构建并运行
docker build -t init-app .
docker volume create app-fresh-data
docker run -d –name app -v app-fresh-data:/app/data init-app
# 验证初始化
docker exec app cat /app/data/init.json
# 输出: {"initialized": true, "timestamp": "2024-01-15T10:30:00+00:00"}
# 重启容器,不会重复初始化(因为数据卷不为空)
docker restart app
docker logs app | grep "Initializing"
# 只在首次启动时出现
第八章 常见数据库的存储方案
数据库是容器化部署中最需要数据持久化的应用类型。本章将详细讲解MySQL、PostgreSQL、MongoDB、Redis和Elasticsearch五种常见数据库在Docker中的完整持久化方案,包括数据卷配置、配置文件挂载、日志管理、备份恢复和性能优化。
8.1 MySQL数据持久化方案(完整配置)
8.1.1 需求分析
MySQL是使用最广泛的关系型数据库之一。在Docker中部署MySQL,需要考虑以下持久化需求:
- 数据文件(/var/lib/mysql):存储所有数据库和表数据
- 配置文件(/etc/mysql):存储MySQL服务器配置
- 日志文件:包括错误日志、慢查询日志、二进制日志
- 初始化脚本:首次启动时自动执行SQL脚本
8.1.2 完整Docker Compose配置
# docker-compose.yml – MySQL完整持久化方案
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: mysql–prod
restart: unless–stopped
ports:
– "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:–RootPass123!}
MYSQL_DATABASE: ${MYSQL_DATABASE:–appdb}
MYSQL_USER: ${MYSQL_USER:–appuser}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:–apppass123}
TZ: Asia/Shanghai
volumes:
# 数据持久化 – 核心数据卷
– mysql_data:/var/lib/mysql
# 配置文件挂载
– ./mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro
# 初始化SQL脚本(仅首次启动时执行)
– ./mysql/init:/docker–entrypoint–initdb.d:ro
# 日志持久化
– mysql_logs:/var/log/mysql
# 二进制日志(用于主从复制)
– mysql_binlog:/var/lib/mysql–bin
command: >
–default-authentication-plugin=mysql_native_password
–character-set-server=utf8mb4
–collation-server=utf8mb4_unicode_ci
–max_connections=500
–innodb_buffer_pool_size=512M
–slow_query_log=1
–long_query_time=2
–slow_query_log_file=/var/log/mysql/slow.log
–log_error=/var/log/mysql/error.log
–binlog_expire_logs_seconds=604800
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -p$${MYSQL_ROOT_PASSWORD} –silent"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
deploy:
resources:
limits:
memory: 2G
cpus: '2.0'
reservations:
memory: 1G
cpus: '0.5'
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# 备份服务(定时备份)
mysql-backup:
image: alpine:latest
container_name: mysql–backup
depends_on:
mysql:
condition: service_healthy
volumes:
– mysql_data:/var/lib/mysql:ro
– ./backup:/backup
– ./scripts/backup–mysql.sh:/backup.sh:ro
environment:
MYSQL_HOST: mysql
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:–RootPass123!}
BACKUP_RETENTION_DAYS: 7
entrypoint: ["/bin/sh", "-c"]
command: ["crond -f -l 2"]
# 在实际使用中,需要先安装crond或使用其他调度方式
restart: unless–stopped
volumes:
mysql_data:
name: mysql_data
labels:
app: mysql
env: production
mysql_logs:
name: mysql_logs
mysql_binlog:
name: mysql_binlog
8.1.3 MySQL配置文件
# mysql/conf/my.cnf – MySQL配置文件
[mysqld]
# 基本设置
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
default-storage-engine = InnoDB
skip-name-resolve
# 连接设置
max_connections = 500
max_connect_errors = 1000
wait_timeout = 28800
interactive_timeout = 28800
# InnoDB设置
innodb_buffer_pool_size = 512M
innodb_buffer_pool_instances = 4
innodb_log_file_size = 128M
innodb_log_buffer_size = 16M
innodb_flush_log_at_trx_commit = 1
innodb_file_per_table = 1
innodb_flush_method = O_DIRECT
# 日志设置
slow_query_log = 1
long_query_time = 2
slow_query_log_file = /var/log/mysql/slow.log
log_error = /var/log/mysql/error.log
# 二进制日志设置(用于主从复制和数据恢复)
log_bin = /var/lib/mysql-bin/mysql-bin
binlog_format = ROW
binlog_expire_logs_seconds = 604800
server_id = 1
# 安全设置
local_infile = 0
sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
8.1.4 初始化脚本
— mysql/init/01-create-databases.sql
— 创建应用数据库
CREATE DATABASE IF NOT EXISTS appdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS logdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
— 创建应用用户
CREATE USER IF NOT EXISTS 'appuser'@'%' IDENTIFIED WITH mysql_native_password BY 'apppass123';
GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'%';
GRANT INSERT, SELECT ON logdb.* TO 'appuser'@'%';
— 创建只读用户
CREATE USER IF NOT EXISTS 'readonly'@'%' IDENTIFIED WITH mysql_native_password BY 'readonly123';
GRANT SELECT ON appdb.* TO 'readonly'@'%';
— 刷新权限
FLUSH PRIVILEGES;
— mysql/init/02-create-tables.sql
USE appdb;
— 用户表
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
— 商品表
CREATE TABLE IF NOT EXISTS products (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
8.1.5 备份与恢复
#!/bin/bash
# scripts/backup-mysql.sh – MySQL备份脚本
BACKUP_DIR="/backup/mysql/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
# 1. 逻辑备份(mysqldump)
echo "[$(date)] Starting mysqldump backup…"
docker exec mysql-prod \\
mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \\
–all-databases \\
–single-transaction \\
–routines \\
–triggers \\
–events \\
–set-gtid-purged=OFF \\
> "$BACKUP_DIR/alldatabases-$(date +%H%M%S).sql"
# 2. 单独备份每个数据库
for db in appdb logdb; do
echo "[$(date)] Backing up database: $db"
docker exec mysql-prod \\
mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \\
–single-transaction \\
–routines \\
–triggers \\
"$db" > "$BACKUP_DIR/${db}–$(date +%H%M%S).sql"
done
# 3. 物理备份(备份数据卷)
echo "[$(date)] Backing up data volume…"
docker run –rm \\
-v mysql_data:/var/lib/mysql:ro \\
-v "$BACKUP_DIR":/backup \\
alpine:latest \\
tar czf /backup/mysql-data-$(date +%H%M%S).tar.gz -C /var/lib/mysql .
# 4. 压缩备份文件
echo "[$(date)] Compressing backups…"
gzip "$BACKUP_DIR"/*.sql
# 5. 清理过期备份
find /backup/mysql/ -maxdepth 1 -type d -mtime +${BACKUP_RETENTION_DAYS:-7} -exec rm -rf {} \\;
echo "[$(date)] Backup completed."
echo " Location: $BACKUP_DIR"
ls -lh "$BACKUP_DIR"/
#!/bin/bash
# scripts/restore-mysql.sh – MySQL恢复脚本
BACKUP_FILE=$1
if [ -z "$BACKUP_FILE" ]; then
echo "Usage: $0 <backup_sql_file>"
exit 1
fi
echo "[$(date)] Restoring MySQL from: $BACKUP_FILE"
# 如果是gzip压缩文件,先解压
if [[ "$BACKUP_FILE" == *.gz ]]; then
gunzip -c "$BACKUP_FILE" | docker exec -i mysql-prod \\
mysql -uroot -p"$MYSQL_ROOT_PASSWORD"
else
docker exec -i mysql-prod \\
mysql -uroot -p"$MYSQL_ROOT_PASSWORD" < "$BACKUP_FILE"
fi
echo "[$(date)] Restore completed."
8.2 PostgreSQL数据持久化方案(完整配置)
8.2.1 完整Docker Compose配置
# docker-compose.yml – PostgreSQL完整持久化方案
version: '3.8'
services:
postgres:
image: postgres:15–alpine
container_name: postgres–prod
restart: unless–stopped
ports:
– "5432:5432"
environment:
POSTGRES_DB: ${POSTGRES_DB:–appdb}
POSTGRES_USER: ${POSTGRES_USER:–appuser}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:–apppass123}
POSTGRES_INITDB_ARGS: "–encoding=UTF8 –locale=C"
PGDATA: /var/lib/postgresql/data/pgdata
TZ: Asia/Shanghai
volumes:
# 数据持久化
– postgres_data:/var/lib/postgresql/data
# 配置文件
– ./postgres/conf/postgresql.conf:/etc/postgresql/postgresql.conf:ro
– ./postgres/conf/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
# 初始化脚本
– ./postgres/init:/docker–entrypoint–initdb.d:ro
# WAL归档日志
– postgres_wal:/var/lib/postgresql/wal_archive
# 日志
– postgres_logs:/var/log/postgresql
command: >
postgres
-c config_file=/etc/postgresql/postgresql.conf
-c hba_file=/etc/postgresql/pg_hba.conf
-c max_connections=200
-c shared_buffers=256MB
-c effective_cache_size=768MB
-c work_mem=4MB
-c maintenance_work_mem=64MB
-c wal_level=replica
-c archive_mode=on
-c archive_command='test ! -f /var/lib/postgresql/wal_archive/%f && cp %p /var/lib/postgresql/wal_archive/%f'
-c logging_collector=on
-c log_directory=/var/log/postgresql
-c log_filename='postgresql-%Y-%m-%d.log'
-c log_rotation_age=1d
-c log_rotation_size=100MB
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
shm_size: '256m'
volumes:
postgres_data:
name: postgres_data
postgres_wal:
name: postgres_wal
postgres_logs:
name: postgres_logs
8.2.2 PostgreSQL配置文件
# postgres/conf/postgresql.conf – PostgreSQL配置文件
# 连接设置
listen_addresses = '*'
max_connections = 200
superuser_reserved_connections = 3
# 内存设置
shared_buffers = 256MB
effective_cache_size = 768MB
work_mem = 4MB
maintenance_work_mem = 64MB
dynamic_shared_memory_type = posix
# WAL设置
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /var/lib/postgresql/wal_archive/%f && cp %p /var/lib/postgresql/wal_archive/%f'
max_wal_size = 1GB
min_wal_size = 80MB
wal_buffers = 16MB
checkpoint_completion_target = 0.9
# 查询优化
random_page_cost = 1.1
effective_io_concurrency = 200
default_statistics_target = 100
# 日志设置
logging_collector = on
log_directory = '/var/log/postgresql'
log_filename = 'postgresql-%Y-%m-%d.log'
log_rotation_age = 1d
log_rotation_size = 100MB
log_min_duration_statement = 1000
log_checkpoints = on
log_connections = on
log_disconnections = on
log_line_prefix = '%t [%p] %u@%d '
# 自动清理
autovacuum = on
autovacuum_max_workers = 3
autovacuum_naptime = 1min
autovacuum_vacuum_threshold = 50
autovacuum_analyze_threshold = 50
# 时区
timezone = 'Asia/Shanghai'
log_timezone = 'Asia/Shanghai'
# 字符集
client_encoding = utf8
lc_messages = 'C'
lc_monetary = 'C'
lc_numeric = 'C'
lc_time = 'C'
default_text_search_config = 'pg_catalog.english'
# postgres/conf/pg_hba.conf – 客户端认证配置
# TYPE DATABASE USER ADDRESS METHOD
local all all trust
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
host all all 0.0.0.0/0 md5
host replication all 0.0.0.0/0 md5
8.2.3 初始化脚本
— postgres/init/01-setup.sql
— 创建额外数据库
CREATE DATABASE logdb;
CREATE DATABASE testdb;
— 创建只读用户
CREATE USER readonly WITH PASSWORD 'readonly123';
GRANT CONNECT ON DATABASE appdb TO readonly;
— 连接到appdb并设置权限
\\c appdb
GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly;
— 创建扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
— 创建表
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS products (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(200) NOT NULL,
price NUMERIC(10,2) NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
— 创建索引
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_products_name ON products(name);
8.2.4 备份与恢复
#!/bin/bash
# scripts/backup-postgres.sh – PostgreSQL备份脚本
BACKUP_DIR="/backup/postgres/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%H%M%S)
# 1. 逻辑备份(pg_dump)
echo "[$(date)] Starting pg_dump backup…"
# 备份所有数据库
docker exec postgres-prod \\
pg_dumpall -U appuser \\
> "$BACKUP_DIR/alldatabases-$TIMESTAMP.sql"
# 单独备份appdb(自定义格式,支持并行恢复)
docker exec postgres-prod \\
pg_dump -U appuser -Fc -d appdb \\
> "$BACKUP_DIR/appdb-$TIMESTAMP.dump"
# 单独备份appdb(纯文本格式)
docker exec postgres-prod \\
pg_dump -U appuser -d appdb \\
> "$BACKUP_DIR/appdb-$TIMESTAMP.sql"
# 2. 物理备份(备份数据卷)
echo "[$(date)] Backing up data volume…"
docker run –rm \\
-v postgres_data:/var/lib/postgresql/data:ro \\
-v "$BACKUP_DIR":/backup \\
alpine:latest \\
tar czf /backup/postgres-data-$TIMESTAMP.tar.gz -C /var/lib/postgresql/data .
# 3. WAL归档备份
docker run –rm \\
-v postgres_wal:/wal:ro \\
-v "$BACKUP_DIR":/backup \\
alpine:latest \\
tar czf /backup/postgres-wal-$TIMESTAMP.tar.gz -C /wal .
# 4. 压缩
gzip "$BACKUP_DIR"/*.sql
# 5. 清理过期备份
find /backup/postgres/ -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \\;
echo "[$(date)] Backup completed."
ls -lh "$BACKUP_DIR"/
#!/bin/bash
# scripts/restore-postgres.sh – PostgreSQL恢复脚本
BACKUP_FILE=$1
DB_NAME=${2:-appdb}
echo "[$(date)] Restoring PostgreSQL from: $BACKUP_FILE"
if [[ "$BACKUP_FILE" == *.dump ]]; then
# 自定义格式恢复
docker exec -i postgres-prod \\
pg_restore -U appuser -d "$DB_NAME" –clean –if-exists < "$BACKUP_FILE"
elif [[ "$BACKUP_FILE" == *.sql.gz ]]; then
# gzip压缩的SQL恢复
gunzip -c "$BACKUP_FILE" | docker exec -i postgres-prod \\
psql -U appuser -d "$DB_NAME"
elif [[ "$BACKUP_FILE" == *.sql ]]; then
# 纯文本SQL恢复
docker exec -i postgres-prod \\
psql -U appuser -d "$DB_NAME" < "$BACKUP_FILE"
fi
echo "[$(date)] Restore completed."
8.3 MongoDB数据持久化方案(完整配置)
8.3.1 完整Docker Compose配置
# docker-compose.yml – MongoDB完整持久化方案
version: '3.8'
services:
mongodb:
image: mongo:6.0
container_name: mongodb–prod
restart: unless–stopped
ports:
– "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER:–root}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:–RootPass123!}
MONGO_INITDB_DATABASE: ${MONGO_DB:–appdb}
TZ: Asia/Shanghai
volumes:
# 数据持久化
– mongo_data:/data/db
# 配置文件
– ./mongo/conf/mongod.conf:/etc/mongo/mongod.conf:ro
# 初始化脚本
– ./mongo/init:/docker–entrypoint–initdb.d:ro
# 日志
– mongo_logs:/var/log/mongodb
command: ["mongod", "–config", "/etc/mongo/mongod.conf"]
healthcheck:
test: ["CMD", "mongosh", "–eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
mongo_data:
name: mongo_data
mongo_logs:
name: mongo_logs
8.3.2 MongoDB配置文件
# mongo/conf/mongod.conf – MongoDB配置文件
# 存储设置
storage:
dbPath: /data/db
journal:
enabled: true
wiredTiger:
engineConfig:
cacheSizeGB: 1
journalCompressor: snappy
collectionConfig:
blockCompressor: snappy
indexConfig:
prefixCompression: true
# 网络设置
net:
port: 27017
bindIp: 0.0.0.0
maxIncomingConnections: 1000
# 日志设置
systemLog:
destination: file
path: /var/log/mongodb/mongod.log
logAppend: true
logRotate: reopen
verbosity: 0
# 进程管理
processManagement:
fork: false
pidFilePath: /var/run/mongod.pid
# 复制集(可选,用于高可用)
# replication:
# replSetName: rs0
# oplogSizeMB: 1024
# 安全设置
security:
authorization: enabled
# 性能分析
operationProfiling:
mode: slowOp
slowOpThresholdMs: 100
# 分片设置(如需要)
# sharding:
# clusterRole: shardsvr
8.3.3 初始化脚本
// mongo/init/01-init.js – MongoDB初始化脚本
// 切换到admin数据库
db = db.getSiblingDB('admin');
// 创建应用用户
db.createUser({
user: 'appuser',
pwd: 'apppass123',
roles: [
{ role: 'readWrite', db: 'appdb' }
]
});
// 创建只读用户
db.createUser({
user: 'readonly',
pwd: 'readonly123',
roles: [
{ role: 'read', db: 'appdb' }
]
});
// 切换到appdb
db = db.getSiblingDB('appdb');
// 创建集合
db.createCollection('users');
db.createCollection('products');
db.createCollection('orders');
// 创建索引
db.users.createIndex({ "email": 1 }, { unique: true });
db.users.createIndex({ "username": 1 }, { unique: true });
db.products.createIndex({ "name": "text" });
db.products.createIndex({ "category": 1, "price": 1 });
db.orders.createIndex({ "userId": 1, "createdAt": –1 });
// 插入初始数据
db.products.insertMany([
{ name: "笔记本电脑", price: 5999, stock: 100, category: "electronics" },
{ name: "机械键盘", price: 299, stock: 200, category: "accessories" },
{ name: "显示器", price: 1599, stock: 50, category: "electronics" }
]);
8.3.4 备份与恢复
#!/bin/bash
# scripts/backup-mongo.sh – MongoDB备份脚本
BACKUP_DIR="/backup/mongo/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%H%M%S)
# 1. 逻辑备份(mongodump)
echo "[$(date)] Starting mongodump backup…"
docker exec mongodb-prod \\
mongodump \\
–uri="mongodb://root:RootPass123!@localhost:27017" \\
–gzip \\
–archive > "$BACKUP_DIR/mongodb-$TIMESTAMP.gz"
# 备份单个数据库
docker exec mongodb-prod \\
mongodump \\
–uri="mongodb://root:RootPass123!@localhost:27017/appdb" \\
–gzip \\
–archive > "$BACKUP_DIR/appdb-$TIMESTAMP.gz"
# 2. 物理备份(备份数据卷)
echo "[$(date)] Backing up data volume…"
docker run –rm \\
-v mongo_data:/data/db:ro \\
-v "$BACKUP_DIR":/backup \\
alpine:latest \\
tar czf /backup/mongo-data-$TIMESTAMP.tar.gz -C /data/db .
# 3. 清理过期备份
find /backup/mongo/ -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \\;
echo "[$(date)] Backup completed."
ls -lh "$BACKUP_DIR"/
#!/bin/bash
# scripts/restore-mongo.sh – MongoDB恢复脚本
BACKUP_FILE=$1
echo "[$(date)] Restoring MongoDB from: $BACKUP_FILE"
# 逻辑恢复(mongorestore)
docker exec -i mongodb-prod \\
mongorestore \\
–uri="mongodb://root:RootPass123!@localhost:27017" \\
–gzip \\
–drop \\
–archive < "$BACKUP_FILE"
echo "[$(date)] Restore completed."
8.4 Redis数据持久化方案(RDB + AOF)
8.4.1 完整Docker Compose配置
# docker-compose.yml – Redis完整持久化方案
version: '3.8'
services:
redis:
image: redis:7–alpine
container_name: redis–prod
restart: unless–stopped
ports:
– "6379:6379"
environment:
TZ: Asia/Shanghai
volumes:
# 数据持久化
– redis_data:/data
# 配置文件
– ./redis/conf/redis.conf:/usr/local/etc/redis/redis.conf:ro
# 日志
– redis_logs:/var/log/redis
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
redis_data:
name: redis_data
redis_logs:
name: redis_logs
8.4.2 Redis配置文件
# redis/conf/redis.conf – Redis配置文件
# 网络设置
bind 0.0.0.0
protected-mode yes
port 6379
timeout 300
tcp-keepalive 60
# 安全设置
requirepass YourRedisPassword123!
# 通用设置
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile /var/log/redis/redis.log
databases 16
# 快照(RDB)持久化设置
save 900 1 # 900秒内至少1个key变化则保存
save 300 10 # 300秒内至少10个key变化则保存
save 60 10000 # 60秒内至少10000个key变化则保存
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /data
# AOF(Append Only File)持久化设置
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec # 每秒同步一次(兼顾性能和安全)
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
# 内存管理
maxmemory 512mb
maxmemory-policy allkeys-lru
# 慢查询日志
slowlog-log-slower-than 10000
slowlog-max-len 128
# 客户端缓冲区限制
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
# 惰性释放
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
8.4.3 备份与恢复
#!/bin/bash
# scripts/backup-redis.sh – Redis备份脚本
BACKUP_DIR="/backup/redis/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%H%M%S)
# 1. 触发RDB快照
echo "[$(date)] Triggering BGSAVE…"
docker exec redis-prod redis-cli -a YourRedisPassword123! BGSAVE
# 等待BGSAVE完成
while true; do
STATUS=$(docker exec redis-prod redis-cli -a YourRedisPassword123! INFO persistence | grep rdb_bgsave_in_progress | cut -d: -f2 | tr -d '\\r')
if [ "$STATUS" = "0" ]; then
break
fi
sleep 1
done
echo "[$(date)] BGSAVE completed."
# 2. 复制RDB文件
docker cp redis-prod:/data/dump.rdb "$BACKUP_DIR/dump-$TIMESTAMP.rdb"
# 3. 复制AOF文件
docker cp redis-prod:/data/appendonly.aof "$BACKUP_DIR/appendonly-$TIMESTAMP.aof" 2>/dev/null || true
# 4. 物理备份(备份数据卷)
docker run –rm \\
-v redis_data:/data:ro \\
-v "$BACKUP_DIR":/backup \\
alpine:latest \\
tar czf /backup/redis-data-$TIMESTAMP.tar.gz -C /data .
# 5. 清理过期备份
find /backup/redis/ -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \\;
echo "[$(date)] Backup completed."
ls -lh "$BACKUP_DIR"/
#!/bin/bash
# scripts/restore-redis.sh – Redis恢复脚本
BACKUP_FILE=$1
echo "[$(date)] Stopping Redis…"
docker-compose stop redis
echo "[$(date)] Restoring data…"
if [[ "$BACKUP_FILE" == *.rdb ]]; then
# 恢复RDB文件
docker run –rm \\
-v redis_data:/data \\
-v "$(dirname "$BACKUP_FILE")":/backup:ro \\
alpine:latest \\
sh -c "cp /backup/$(basename $BACKUP_FILE) /data/dump.rdb && rm -f /data/appendonly.aof"
fi
echo "[$(date)] Starting Redis…"
docker-compose start redis
echo "[$(date)] Restore completed."
8.5 Elasticsearch数据持久化方案
8.5.1 完整Docker Compose配置
# docker-compose.yml – Elasticsearch完整持久化方案
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
container_name: es–prod
restart: unless–stopped
ports:
– "9200:9200"
– "9300:9300"
environment:
– discovery.type=single–node
– xpack.security.enabled=false
– ES_JAVA_OPTS=–Xms512m –Xmx512m
– cluster.name=es–cluster
– node.name=es–node–1
– bootstrap.memory_lock=true
– TZ=Asia/Shanghai
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
volumes:
# 数据持久化
– es_data:/usr/share/elasticsearch/data
# 配置文件
– ./es/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro
# 日志
– es_logs:/usr/share/elasticsearch/logs
# 分析插件
– es_plugins:/usr/share/elasticsearch/plugins
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
interval: 15s
timeout: 10s
retries: 5
start_period: 60s
kibana:
image: docker.elastic.co/kibana/kibana:8.11.0
container_name: kibana–prod
restart: unless–stopped
ports:
– "5601:5601"
environment:
– ELASTICSEARCH_HOSTS=http://es–prod:9200
depends_on:
elasticsearch:
condition: service_healthy
volumes:
es_data:
name: es_data
es_logs:
name: es_logs
es_plugins:
name: es_plugins
8.5.2 Elasticsearch配置文件
# es/conf/elasticsearch.yml – Elasticsearch配置文件
cluster.name: es–cluster
node.name: es–node–1
# 数据和日志路径
path.data: /usr/share/elasticsearch/data
path.logs: /usr/share/elasticsearch/logs
# 网络设置
network.host: 0.0.0.0
http.port: 9200
transport.port: 9300
# 发现设置
discovery.type: single–node
# 内存设置
bootstrap.memory_lock: true
# 安全设置
xpack.security.enabled: false
xpack.security.enrollment.enabled: false
xpack.security.http.ssl.enabled: false
xpack.security.transport.ssl.enabled: false
# 索引设置
action.destructive_requires_name: true
# 慢查询日志
indices.queries.cache.size: 10%
indices.fielddata.cache.size: 40%
# 线程池
thread_pool.search.size: 12
thread_pool.search.queue_size: 1000
8.5.3 备份与恢复
#!/bin/bash
# scripts/backup-es.sh – Elasticsearch备份脚本
BACKUP_DIR="/backup/es/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%H%M%S)
ES_HOST="http://localhost:9200"
# 1. 注册快照仓库(使用共享文件系统)
# 需要在elasticsearch.yml中设置 path.repo: ["/usr/share/elasticsearch/backup"]
docker exec es-prod curl -s -X PUT "$ES_HOST/_snapshot/backup_repo" \\
-H 'Content-Type: application/json' \\
-d '{
"type": "fs",
"settings": {
"location": "/usr/share/elasticsearch/backup",
"compress": true
}
}'
# 2. 创建快照
docker exec es-prod curl -s -X PUT "$ES_HOST/_snapshot/backup_repo/snapshot_$TIMESTAMP?wait_for_completion=true" \\
-H 'Content-Type: application/json' \\
-d '{
"indices": "*",
"ignore_unavailable": true,
"include_global_state": false
}'
# 3. 物理备份(备份数据卷)
echo "[$(date)] Backing up data volume…"
docker run –rm \\
-v es_data:/usr/share/elasticsearch/data:ro \\
-v "$BACKUP_DIR":/backup \\
alpine:latest \\
tar czf /backup/es-data-$TIMESTAMP.tar.gz -C /usr/share/elasticsearch/data .
# 4. 清理过期备份
find /backup/es/ -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \\;
echo "[$(date)] Backup completed."
ls -lh "$BACKUP_DIR"/
8.6 数据库存储通用最佳实践
8.6.1 通用配置建议
# 通用最佳实践(适用于所有数据库)
# 1. 始终使用命名数据卷,不要使用匿名数据卷
volumes:
– db_data:/var/lib/database # 命名数据卷(好)
# – /var/lib/database # 匿名数据卷(差)
# 2. 为数据卷添加标签
volumes:
db_data:
name: mysql_prod_data
labels:
app: mysql
env: production
backup: daily
retention: 30d
# 3. 设置合理的健康检查
healthcheck:
test: ["CMD-SHELL", "检查命令"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s # 给数据库足够的启动时间
# 4. 设置资源限制
deploy:
resources:
limits:
memory: 2G
cpus: '2.0'
reservations:
memory: 1G
# 5. 设置重启策略
restart: unless–stopped
# 6. 日志限制
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# 7. 使用环境变量管理密码(配合.env文件)
environment:
DB_PASSWORD: ${DB_PASSWORD}
8.6.2 数据卷分离策略
# 将不同类型的数据存储在不同的数据卷中,便于管理
volumes:
# 核心数据(最高优先级备份)
– db_data:/var/lib/mysql
# 二进制日志/WAL日志(用于复制和恢复)
– db_binlog:/var/lib/mysql–bin
# 慢查询日志(用于性能优化)
– db_slowlog:/var/log/mysql/slow
# 错误日志
– db_errorlog:/var/log/mysql/error
# 临时文件(tmpfs,不需要持久化)
# 通过tmpfs挂载
8.7 数据库存储性能优化
8.7.1 存储驱动优化
# 1. 使用overlay2存储驱动(性能最佳)
# /etc/docker/daemon.json
# {
# "storage-driver": "overlay2"
# }
# 2. 将数据目录放在高性能磁盘上
# 使用SSD存储数据库数据卷
# 修改Docker数据根目录
# /etc/docker/daemon.json
# {
# "data-root": "/mnt/ssd/docker"
# }
# 3. 使用块设备数据卷(绕过文件系统开销)
# 格式化SSD
sudo mkfs.ext4 /dev/nvme0n1
# 创建块设备数据卷
docker volume create \\
–driver local \\
–opt type=ext4 \\
–opt device=/dev/nvme0n1 \\
mysql-block-data
8.7.2 内存与缓存优化
# 数据库容器内存优化建议
services:
mysql:
# 设置足够的内存
deploy:
resources:
limits:
memory: 4G # 根据实际情况调整
# 调整InnoDB缓冲池(MySQL)
command: >
–innodb_buffer_pool_size=2G
–innodb_buffer_pool_instances=4
–innodb_flush_method=O_DIRECT
–innodb_io_capacity=2000
–innodb_io_capacity_max=4000
# 使用tmpfs存储临时表
tmpfs:
– /tmp:size=256m
– /var/tmp:size=256m
8.7.3 I/O调度优化
# 1. 为数据库磁盘选择合适的I/O调度器
# 查看当前调度器
cat /sys/block/sda/queue/scheduler
# 设置为deadline(适合数据库)
echo deadline > /sys/block/sda/queue/scheduler
# 对于NVMe SSD,使用none调度器
echo none > /sys/block/nvme0n1/queue/scheduler
# 2. 调整文件系统挂载选项(如果是绑定挂载或自定义数据根目录)
# /etc/fstab
# /dev/sdb1 /var/lib/docker ext4 defaults,noatime,nodiratime 0 0
# noatime: 不更新文件访问时间,减少I/O
# nodiratime: 不更新目录访问时间
# 3. 禁用透明大页(THP),对数据库性能有负面影响
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
8.7.4 数据库特定优化总结
| MySQL | innodb_buffer_pool_size | 物理内存的50-70% |
| MySQL | innodb_flush_log_at_trx_commit | 1(安全)/2(性能) |
| MySQL | innodb_flush_method | O_DIRECT |
| PostgreSQL | shared_buffers | 物理内存的25% |
| PostgreSQL | effective_cache_size | 物理内存的50-75% |
| PostgreSQL | wal_buffers | 16MB |
| MongoDB | wiredTiger.cacheSizeGB | 物理内存的50% |
| Redis | maxmemory | 根据需求设置 |
| Redis | appendfsync | everysec(兼顾安全和性能) |
| Elasticsearch | ES_JAVA_OPTS -Xms -Xmx | 相同值,物理内存的50% |
第九章 存储安全与备份策略
9.1 数据卷加密
9.1.1 为什么需要加密数据卷
在生产环境中,数据卷中可能存储着敏感信息,如用户数据、密码、密钥、商业机密等。如果宿主机被入侵或磁盘被盗,未加密的数据将直接暴露。加密数据卷可以确保即使物理存储介质泄露,数据也无法被读取。
9.1.2 使用LUKS加密(块设备级别)
# 步骤1:创建并格式化LUKS加密设备
sudo apt-get install cryptsetup # 安装cryptsetup
sudo cryptsetup luksFormat /dev/sdb1
# 输入加密密码(务必记住!)
# 步骤2:打开加密设备
sudo cryptsetup luksOpen /dev/sdb1 encrypted_docker
# 输入密码
# 步骤3:在加密设备上创建文件系统
sudo mkfs.ext4 /dev/mapper/encrypted_docker
# 步骤4:挂载加密设备
sudo mkdir -p /mnt/encrypted-docker
sudo mount /dev/mapper/encrypted_docker /mnt/encrypted-docker
# 步骤5:创建Docker数据卷指向加密设备
docker volume create \\
–driver local \\
–opt type=none \\
–opt device=/mnt/encrypted-docker \\
–opt o=bind \\
encrypted-data
# 步骤6:使用加密数据卷
docker run -d \\
–name secure-db \\
-v encrypted-data:/var/lib/mysql \\
-e MYSQL_ROOT_PASSWORD=RootPass123! \\
mysql:8.0
# 开机自动挂载加密设备
# /etc/crypttab:
# encrypted_docker /dev/sdb1 none luks
# /etc/fstab:
# /dev/mapper/encrypted_docker /mnt/encrypted-docker ext4 defaults 0 0
9.1.3 使用Docker加密插件
# 安装Docker加密卷插件(以vieux/sshfs为例,实际可使用专用加密插件)
# 常见的加密卷插件包括:
# – docker-volume-crypt(基于LUKS)
# – Rancher Longhorn(支持加密)
# – Portworx(企业级存储,支持加密)
# 示例:使用第三方加密插件
docker plugin install <encryption-plugin-name>
# 创建加密数据卷
docker volume create \\
–driver <encryption-plugin-name> \\
–opt encrypted=true \\
–opt key=<encryption-key-id> \\
encrypted-volume
9.1.4 应用层加密
# 应用层加密示例(Python)
# 在写入数据卷之前加密数据
from cryptography.fernet import Fernet
import os
# 生成或加载密钥
key_file = '/app/keys/fernet.key'
if os.path.exists(key_file):
with open(key_file, 'rb') as f:
key = f.read()
else:
key = Fernet.generate_key()
os.makedirs(os.path.dirname(key_file), exist_ok=True)
with open(key_file, 'wb') as f:
f.write(key)
cipher = Fernet(key)
# 加密数据并写入数据卷
def write_encrypted(filepath, data):
encrypted_data = cipher.encrypt(data.encode())
with open(filepath, 'wb') as f:
f.write(encrypted_data)
# 读取并解密数据
def read_encrypted(filepath):
with open(filepath, 'rb') as f:
encrypted_data = f.read()
return cipher.decrypt(encrypted_data).decode()
# 使用示例
write_encrypted('/app/data/secret.txt', '这是机密信息')
print(read_encrypted('/app/data/secret.txt'))
# 输出: 这是机密信息
9.2 数据卷访问控制
9.2.1 文件系统级别权限控制
# 1. 使用Linux用户和组控制访问
# 创建专用的Docker数据组
sudo groupadd docker-data
sudo usermod -aG docker-data $(whoami)
# 设置数据卷目录的组和权限
VOLUME_PATH=$(docker volume inspect my-volume –format '{{.Mountpoint}}')
sudo chgrp -R docker-data $VOLUME_PATH
sudo chmod -R 770 $VOLUME_PATH
# 2. 使用ACL(访问控制列表)进行更细粒度的控制
sudo setfacl -R -m u:appuser:rwx $VOLUME_PATH
sudo setfacl -R -m u:readonlyuser:rx $VOLUME_PATH
sudo setfacl -R -m o::— $VOLUME_PATH
# 查看ACL设置
sudo getfacl $VOLUME_PATH
9.2.2 容器级别权限控制
# 1. 使用–user参数限制容器内用户
docker run -d \\
–name app \\
–user 1000:1000 \\
-v my-data:/data \\
my-app:latest
# 2. 使用–read-only使整个根文件系统只读
docker run -d \\
–name secure-app \\
–read-only \\
-v my-data:/data \\
–tmpfs /tmp \\
my-app:latest
# 3. 使用–cap-drop移除不必要的Linux capabilities
docker run -d \\
–name secure-app \\
–cap-drop ALL \\
–cap-add CHOWN \\
–cap-add SETGID \\
–cap-add SETUID \\
-v my-data:/data \\
my-app:latest
# 4. 使用–security-opt增强安全性
docker run -d \\
–name secure-app \\
–security-opt no-new-privileges \\
–security-opt label=type:container_runtime_t \\
-v my-data:/data \\
my-app:latest
9.2.3 数据卷隔离策略
# docker-compose.yml – 多租户数据隔离
version: '3.8'
services:
# 租户A的服务
tenant-a-app:
image: my–app:latest
user: "1001:1001"
volumes:
– tenant_a_data:/data # 租户A专用数据卷
networks:
– tenant_a_network # 租户A专用网络
# 租户B的服务
tenant-b-app:
image: my–app:latest
user: "1002:1002"
volumes:
– tenant_b_data:/data # 租户B专用数据卷
networks:
– tenant_b_network # 租户B专用网络
volumes:
tenant_a_data:
name: tenant_a_data
labels:
tenant: "A"
security: "isolated"
tenant_b_data:
name: tenant_b_data
labels:
tenant: "B"
security: "isolated"
networks:
tenant_a_network:
driver: bridge
tenant_b_network:
driver: bridge
9.3 备份策略设计(全量/增量/差异备份)
9.3.1 备份类型详解
| 全量备份 | 备份所有数据 | 恢复简单,数据完整 | 耗时长,空间大 | 周期性基础备份 |
| 增量备份 | 只备份自上次备份以来变化的数据 | 速度快,空间小 | 恢复复杂,依赖链长 | 频繁备份 |
| 差异备份 | 只备份自上次全量备份以来变化的数据 | 恢复较简单 | 空间逐渐增大 | 中等频率备份 |
9.3.2 全量备份实现
#!/bin/bash
# full-backup.sh – 全量备份脚本
VOLUME_NAME=$1
BACKUP_DIR="/backup/full/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
echo "[$(date)] Starting full backup of $VOLUME_NAME…"
# 全量备份:打包整个数据卷
docker run –rm \\
-v "$VOLUME_NAME":/data:ro \\
-v "$BACKUP_DIR":/backup \\
alpine:latest \\
tar czf "/backup/${VOLUME_NAME}-full-$(date +%H%M%S).tar.gz" -C /data .
echo "[$(date)] Full backup completed."
ls -lh "$BACKUP_DIR"/
9.3.3 增量备份实现(使用rsync)
#!/bin/bash
# incremental-backup.sh – 增量备份脚本(基于rsync)
VOLUME_NAME=$1
BACKUP_BASE="/backup/incremental"
LATEST_LINK="$BACKUP_BASE/latest"
CURRENT_BACKUP="$BACKUP_BASE/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$CURRENT_BACKUP"
echo "[$(date)] Starting incremental backup of $VOLUME_NAME…"
# 使用rsync的–link-dest进行增量备份
# 原理:与上次备份比较,只传输变化的文件
# 未变化的文件通过硬链接引用,不占用额外空间
docker run –rm \\
-v "$VOLUME_NAME":/data:ro \\
-v "$BACKUP_BASE":/backup \\
alpine:latest \\
sh -c "
apk add –no-cache rsync
if [ -d /backup/latest ]; then
rsync -a –delete –link-dest=/backup/latest /data/ /backup/current/
else
rsync -a /data/ /backup/current/
fi
"
# 重命名current为带时间戳的目录
# (在实际实现中需要更完善的逻辑)
echo "[$(date)] Incremental backup completed."
9.3.4 差异备份实现
#!/bin/bash
# differential-backup.sh – 差异备份脚本
VOLUME_NAME=$1
BACKUP_BASE="/backup/differential"
LAST_FULL=$(ls -d "$BACKUP_BASE"/full-* 2>/dev/null | sort | tail -1)
CURRENT_BACKUP="$BACKUP_BASE/diff-$(date +%Y%m%d-%H%M%S)"
if [ -z "$LAST_FULL" ]; then
echo "Error: No full backup found. Please run full backup first."
exit 1
fi
mkdir -p "$CURRENT_BACKUP"
echo "[$(date)] Starting differential backup of $VOLUME_NAME…"
echo " Based on full backup: $LAST_FULL"
# 差异备份:与最近的全量备份比较
# 使用find命令找出变化的文件
docker run –rm \\
-v "$VOLUME_NAME":/data:ro \\
-v "$BACKUP_BASE":/backup \\
alpine:latest \\
sh -c "
REFERENCE_DIR=/backup/$(basename $LAST_FULL)
# 找出与全量备份不同的文件并打包
cd /data
find . -type f -newer "$REFERENCE_DIR" -print0 | \\
tar –null -czf /backup/$(basename $CURRENT_BACKUP).tar.gz -T –
"
echo "[$(date)] Differential backup completed."
9.3.5 综合备份策略
#!/bin/bash
# backup-strategy.sh – 综合备份策略
# 策略:每周一次全量备份,每天一次增量备份
# 保留:4个全量备份(4周),28个增量备份(28天)
VOLUME_NAME=$1
DAY_OF_WEEK=$(date +%u) # 1=Monday, 7=Sunday
BACKUP_DIR="/backup"
# 周日执行全量备份
if [ "$DAY_OF_WEEK" = "7" ]; then
echo "[$(date)] Running weekly full backup…"
/opt/scripts/full-backup.sh "$VOLUME_NAME"
# 清理超过4周的全量备份
find "$BACKUP_DIR/full/" -maxdepth 1 -type d -mtime +28 -exec rm -rf {} \\;
else
echo "[$(date)] Running daily incremental backup…"
/opt/scripts/incremental-backup.sh "$VOLUME_NAME"
fi
# 清理超过28天的增量备份
find "$BACKUP_DIR/incremental/" -maxdepth 1 -type d -mtime +28 -exec rm -rf {} \\;
echo "[$(date)] Backup strategy executed."
# 设置crontab定时执行
# 每天凌晨2点执行备份
# crontab -e
# 0 2 * * * /opt/scripts/backup-strategy.sh mysql_data >> /var/log/backup.log 2>&1
9.4 使用定时任务自动备份
9.4.1 宿主机crontab方案
#!/bin/bash
# /opt/scripts/auto-backup-all.sh – 自动备份所有数据卷
BACKUP_ROOT="/backup"
LOG_FILE="/var/log/docker-backup.log"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
log "=== Starting automatic backup ==="
# 获取所有标记了backup=yes的数据卷
VOLUMES=$(docker volume ls –filter label=backup=yes -q)
for vol in $VOLUMES; do
log "Backing up volume: $vol"
# 获取保留天数
RETENTION=$(docker volume inspect "$vol" –format '{{index .Labels "retention"}}' 2>/dev/null)
RETENTION=${RETENTION:-7}
BACKUP_DIR="$BACKUP_ROOT/$vol/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
# 执行备份
docker run –rm \\
-v "$vol":/data:ro \\
-v "$BACKUP_DIR":/backup \\
alpine:latest \\
tar czf "/backup/${vol}–${TIMESTAMP}.tar.gz" -C /data . 2>> "$LOG_FILE"
if [ $? -eq 0 ]; then
SIZE=$(du -sh "$BACKUP_DIR" | cut -f1)
log " Backup OK. Size: $SIZE"
# 清理过期备份
find "$BACKUP_ROOT/$vol/" -maxdepth 1 -type d -mtime +"$RETENTION" -exec rm -rf {} \\;
log " Cleaned backups older than $RETENTION days"
else
log " Backup FAILED for $vol!"
fi
done
log "=== Automatic backup completed ==="
# crontab配置
# 编辑crontab
sudo crontab -e
# 添加以下内容:
# 每天凌晨2:00执行备份
0 2 * * * /opt/scripts/auto-backup-all.sh
# 每周日凌晨1:00执行全量备份
0 1 * * 0 /opt/scripts/full-backup-all.sh
# 每小时执行增量备份(仅特定数据卷)
0 * * * * /opt/scripts/incremental-backup-all.sh
9.4.2 使用备份容器方案
# docker-compose.yml – 备份容器
version: '3.8'
services:
backup:
image: alpine:latest
container_name: docker–backup
restart: unless–stopped
volumes:
# 需要备份的数据卷(只读)
– mysql_data:/data/mysql:ro
– postgres_data:/data/postgres:ro
# 备份存储目录
– /backup:/backup
# 备份脚本
– ./scripts/auto–backup.sh:/backup.sh:ro
# crontab配置
– ./scripts/crontab:/etc/crontabs/root:ro
environment:
TZ: Asia/Shanghai
entrypoint: ["/bin/sh", "-c"]
command: ["apk add –no-cache docker-cli && crond -f -l 2"]
volumes:
mysql_data:
external: true
postgres_data:
external: true
# scripts/crontab – 备份容器的crontab配置
# 每天2:00备份MySQL
0 2 * * * /backup.sh mysql /data/mysql /backup/mysql
# 每天2:30备份PostgreSQL
30 2 * * * /backup.sh postgres /data/postgres /backup/postgres
# 每周日1:00清理旧备份
0 1 * * 0 find /backup -maxdepth 2 -type d -mtime +30 -exec rm -rf {} \\;
9.5 备份验证与恢复测试
9.5.1 备份验证
#!/bin/bash
# verify-backup.sh – 验证备份完整性
BACKUP_FILE=$1
if [ -z "$BACKUP_FILE" ]; then
echo "Usage: $0 <backup_file>"
exit 1
fi
echo "[$(date)] Verifying backup: $BACKUP_FILE"
# 1. 检查文件是否存在
if [ ! -f "$BACKUP_FILE" ]; then
echo "ERROR: Backup file not found!"
exit 1
fi
# 2. 检查文件大小
SIZE=$(stat -c%s "$BACKUP_FILE" 2>/dev/null || stat -f%z "$BACKUP_FILE")
if [ "$SIZE" -lt 1024 ]; then
echo "WARNING: Backup file is suspiciously small ($SIZE bytes)"
fi
# 3. 验证tar包完整性
echo "[$(date)] Checking tar integrity…"
tar tzf "$BACKUP_FILE" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "ERROR: Backup file is corrupted!"
exit 1
fi
# 4. 列出备份内容统计
FILE_COUNT=$(tar tzf "$BACKUP_FILE" | wc -l)
echo " File count: $FILE_COUNT"
echo " File size: $(du -h "$BACKUP_FILE" | cut -f1)"
# 5. 验证特定文件是否存在
echo "[$(date)] Checking key files…"
tar tzf "$BACKUP_FILE" | grep -q "ibdata1" && echo " MySQL data file: OK" || echo " MySQL data file: MISSING"
tar tzf "$BACKUP_FILE" | grep -q "mysql" && echo " MySQL directory: OK" || echo " MySQL directory: MISSING"
echo "[$(date)] Verification completed."
9.5.2 自动化恢复测试
#!/bin/bash
# restore-test.sh – 自动化恢复测试
# 在隔离环境中测试备份恢复
BACKUP_FILE=$1
TEST_VOLUME="test-restore-$(date +%s)"
echo "[$(date)] Starting restore test…"
echo " Backup file: $BACKUP_FILE"
echo " Test volume: $TEST_VOLUME"
# 1. 创建测试数据卷
docker volume create "$TEST_VOLUME"
# 2. 从备份恢复到测试数据卷
echo "[$(date)] Restoring backup to test volume…"
docker run –rm \\
-v "$TEST_VOLUME":/data \\
-v "$(dirname "$BACKUP_FILE")":/backup:ro \\
alpine:latest \\
tar xzf "/backup/$(basename "$BACKUP_FILE")" -C /data
# 3. 验证恢复的数据
echo "[$(date)] Verifying restored data…"
FILE_COUNT=$(docker run –rm -v "$TEST_VOLUME":/data alpine find /data -type f | wc -l)
echo " Restored file count: $FILE_COUNT"
# 4. 启动测试容器验证数据
echo "[$(date)] Starting test container…"
docker run -d \\
–name restore-test \\
-v "$TEST_VOLUME":/var/lib/mysql \\
-e MYSQL_ROOT_PASSWORD=testpass \\
mysql:8.0
# 等待MySQL启动
sleep 30
# 5. 验证数据库可用性
echo "[$(date)] Testing database connectivity…"
docker exec restore-test mysql -uroot -ptestpass -e "SHOW DATABASES;" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo " Database is accessible!"
docker exec restore-test mysql -uroot -ptestpass -e "SHOW DATABASES;"
else
echo " ERROR: Database is not accessible!"
fi
# 6. 清理测试环境
echo "[$(date)] Cleaning up…"
docker rm -f restore-test
docker volume rm "$TEST_VOLUME"
echo "[$(date)] Restore test completed."
9.6 灾难恢复方案
9.6.1 灾难恢复计划
灾难恢复计划(DR Plan)
1. 灾难场景
– 宿主机硬件故障
– 磁盘损坏
– 数据中心故障
– 误操作导致数据丢失
– 勒索软件攻击
2. 恢复目标
– RTO(恢复时间目标): < 4小时
– RPO(恢复点目标): < 24小时
3. 恢复步骤
Step 1: 评估损失 (15分钟)
Step 2: 准备恢复环境 (30分钟)
Step 3: 恢复数据卷 (1-2小时)
Step 4: 启动服务 (30分钟)
Step 5: 验证数据完整性 (30分钟)
Step 6: 切换流量 (15分钟)
9.6.2 异地备份方案
#!/bin/bash
# offsite-backup.sh – 异地备份脚本
LOCAL_BACKUP_DIR="/backup"
REMOTE_HOST="backup-server.example.com"
REMOTE_DIR="/mnt/backup/docker"
REMOTE_USER="backup"
echo "[$(date)] Starting offsite backup sync…"
# 使用rsync同步备份到远程服务器
rsync -avz –progress \\
–delete \\
–bwlimit=10000 \\
-e "ssh -p 22 -i /home/backup/.ssh/id_rsa" \\
"$LOCAL_BACKUP_DIR/" \\
"$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
if [ $? -eq 0 ]; then
echo "[$(date)] Offsite backup completed successfully."
else
echo "[$(date)] Offsite backup FAILED!"
# 发送告警
echo "Docker backup offsite sync failed!" | mail -s "Backup Alert" admin@example.com
fi
# 验证远程备份
echo "[$(date)] Verifying remote backup…"
ssh "$REMOTE_USER@$REMOTE_HOST" \\
"ls -lh $REMOTE_DIR/$(date +%Y%m%d)/"
9.6.3 完整灾难恢复流程
#!/bin/bash
# disaster-recovery.sh – 灾难恢复脚本
REMOTE_HOST="backup-server.example.com"
REMOTE_DIR="/mnt/backup/docker"
REMOTE_USER="backup"
echo "=========================================="
echo " Docker Disaster Recovery Script"
echo " Started at: $(date)"
echo "=========================================="
# Step 1: 评估环境
echo "[Step 1] Checking environment…"
docker version > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "ERROR: Docker is not running!"
exit 1
fi
# Step 2: 从远程下载最新备份
echo "[Step 2] Downloading latest backup…"
LATEST_BACKUP=$(ssh "$REMOTE_USER@$REMOTE_HOST" \\
"ls -d $REMOTE_DIR/*/ | sort | tail -1")
echo " Latest backup: $LATEST_BACKUP"
mkdir -p /tmp/restore
scp -r "$REMOTE_USER@$REMOTE_HOST:$LATEST_BACKUP" /tmp/restore/
# Step 3: 恢复数据卷
echo "[Step 3] Restoring volumes…"
for backup_file in /tmp/restore/$(basename $LATEST_BACKUP)/*.tar.gz; do
vol_name=$(basename "$backup_file" .tar.gz)
vol_name=${vol_name%%-*} # 提取卷名
echo " Restoring volume: $vol_name"
docker volume create "$vol_name" 2>/dev/null || true
docker run –rm \\
-v "$vol_name":/data \\
-v /tmp/restore:/backup:ro \\
alpine:latest \\
tar xzf "/backup/$(basename $LATEST_BACKUP)/$(basename $backup_file)" -C /data
done
# Step 4: 启动服务
echo "[Step 4] Starting services…"
cd /opt/docker-compose
docker-compose up -d
# Step 5: 等待服务就绪
echo "[Step 5] Waiting for services…"
sleep 60
# Step 6: 验证
echo "[Step 6] Verifying services…"
docker-compose ps
docker exec mysql-prod mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW DATABASES;"
echo "=========================================="
echo " Disaster recovery completed at: $(date)"
echo "=========================================="
9.7 存储安全审计
9.7.1 审计脚本
#!/bin/bash
# storage-audit.sh – Docker存储安全审计
echo "=========================================="
echo " Docker Storage Security Audit"
echo " Date: $(date)"
echo "=========================================="
echo ""
echo "=== 1. Docker Configuration ==="
echo "Storage Driver:"
docker info 2>/dev/null | grep "Storage Driver"
echo "Data Root:"
docker info 2>/dev/null | grep "Docker Root Dir"
echo ""
echo "=== 2. Volume Security ==="
echo "Total volumes: $(docker volume ls -q | wc -l)"
echo "Dangling volumes: $(docker volume ls -f dangling=true -q | wc -l)"
echo ""
echo "Checking volume permissions…"
for vol in $(docker volume ls -q); do
MOUNTPOINT=$(docker volume inspect "$vol" –format '{{.Mountpoint}}')
PERMS=$(stat -c "%a %U:%G" "$MOUNTPOINT" 2>/dev/null)
echo " $vol: $PERMS"
done
echo ""
echo "=== 3. Container Security ==="
echo "Running containers with root user:"
docker ps -q | while read cid; do
USER=$(docker inspect "$cid" –format '{{.Config.User}}')
NAME=$(docker inspect "$cid" –format '{{.Name}}')
if [ -z "$USER" ] || [ "$USER" = "root" ] || [ "$USER" = "0:0" ]; then
echo " WARNING: $NAME is running as root"
fi
done
echo ""
echo "=== 4. Bind Mount Security ==="
echo "Containers with bind mounts to sensitive paths:"
docker ps -q | while read cid; do
NAME=$(docker inspect "$cid" –format '{{.Name}}')
MOUNTS=$(docker inspect "$cid" –format '{{json .Mounts}}')
if echo "$MOUNTS" | grep -q '"Type":"bind"'; then
echo " $NAME has bind mounts"
echo "$MOUNTS" | python3 -m json.tool 2>/dev/null | grep -E '"Source"|"Destination"'
fi
done
echo ""
echo "=== 5. Backup Status ==="
echo "Recent backups:"
ls -lt /backup/*/*.tar.gz 2>/dev/null | head -5
echo ""
echo "=== 6. Disk Usage ==="
docker system df
echo ""
echo "=========================================="
echo " Audit completed."
echo "=========================================="
9.8 合规性要求与数据保留策略
9.8.1 数据保留策略
# 数据保留策略配置示例
# 数据保留级别
retention_policy:
# 关键业务数据 – 长期保留
critical:
description: "核心业务数据(用户数据、交易数据)"
daily_backup: true
weekly_full_backup: true
retention_days: 90 # 每日备份保留90天
retention_months: 12 # 每月备份保留12个月
retention_years: 7 # 每年备份保留7年
encryption: true
offsite_backup: true
# 重要业务数据 – 中期保留
important:
description: "重要业务数据(日志、配置)"
daily_backup: true
weekly_full_backup: true
retention_days: 30
retention_months: 6
encryption: true
offsite_backup: true
# 一般数据 – 短期保留
normal:
description: "一般数据(缓存、临时数据)"
daily_backup: false
weekly_full_backup: true
retention_days: 14
encryption: false
offsite_backup: false
# 临时数据 – 不保留
temporary:
description: "临时数据(临时文件、处理中间结果)"
backup: false
use_tmpfs: true
9.8.2 合规性检查脚本
#!/bin/bash
# compliance-check.sh – 合规性检查脚本
echo "=== Docker Storage Compliance Check ==="
echo "Date: $(date)"
echo ""
# 1. 检查数据卷是否有标签
echo "— Volume Labels —"
for vol in $(docker volume ls -q); do
LABELS=$(docker volume inspect "$vol" –format '{{json .Labels}}')
if [ "$LABELS" = "null" ] || [ "$LABELS" = "{}" ]; then
echo "WARNING: Volume '$vol' has no labels"
fi
done
# 2. 检查敏感数据卷是否加密
echo ""
echo "— Encryption Status —"
for vol in $(docker volume ls –filter label=sensitive=true -q); do
echo "Checking encryption for sensitive volume: $vol"
MOUNTPOINT=$(docker volume inspect "$vol" –format '{{.Mountpoint}}')
# 检查是否在加密文件系统上
FS_TYPE=$(df "$MOUNTPOINT" –output=fstype 2>/dev/null | tail -1)
echo " Filesystem: $FS_TYPE"
done
# 3. 检查备份状态
echo ""
echo "— Backup Compliance —"
for vol in $(docker volume ls –filter label=backup=daily -q); do
LATEST_BACKUP=$(find /backup/$vol/ -name "*.tar.gz" -mtime -1 2>/dev/null | head -1)
if [ -z "$LATEST_BACKUP" ]; then
echo "VIOLATION: Volume '$vol' has no backup in the last 24 hours!"
else
echo "OK: Volume '$vol' has recent backup"
fi
done
# 4. 检查数据保留合规性
echo ""
echo "— Retention Compliance —"
for vol in $(docker volume ls -q); do
RETENTION=$(docker volume inspect "$vol" –format '{{index .Labels "retention"}}' 2>/dev/null)
if [ -n "$RETENTION" ]; then
OLD_BACKUPS=$(find /backup/$vol/ -maxdepth 1 -type d -mtime +"${RETENTION%d}" 2>/dev/null | wc -l)
if [ "$OLD_BACKUPS" -gt 0 ]; then
echo "WARNING: Volume '$vol' has $OLD_BACKUPS backups exceeding retention policy ($RETENTION)"
fi
fi
done
echo ""
echo "=== Compliance check completed ==="
第十章 存储管理与故障排查
10.1 docker system df查看存储使用情况
10.1.1 基本用法
# 查看Docker整体磁盘使用情况
docker system df
# 输出示例:
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 15 5 5.2GB 2.1GB (40%)
# Containers 8 3 120MB 80MB (66%)
# Local Volumes 12 6 8.5GB 3.2GB (37%)
# Build Cache 0 0 0B 0B
# 列说明:
# TYPE: 资源类型(镜像/容器/数据卷/构建缓存)
# TOTAL: 总数量
# ACTIVE: 正在使用的数量
# SIZE: 总大小
# RECLAIMABLE: 可回收的大小(未使用的资源)
10.1.2 详细视图
# 查看详细的使用情况(-v参数)
docker system df -v
# 输出示例(镜像部分):
# Images space usage:
# REPOSITORY TAG IMAGE ID CREATED SIZE SHARED SIZE UNIQUE SIZE CONTAINERS
# nginx latest 605c77e624dd 2 weeks ago 141MB 80MB 61MB 2
# mysql 8.0 3f01e6e1e1e1 3 weeks ago 521MB 80MB 441MB 1
# …
# 输出示例(数据卷部分):
# VOLUME NAME LINKS SIZE
# mysql_data 1 2.3GB
# postgres_data 1 1.5GB
# redis_data 1 120MB
# unused_volume_1 0 500MB ← 未使用的(可清理)
# unused_volume_2 0 300MB ← 未使用的(可清理)
10.1.3 监控脚本
#!/bin/bash
# monitor-docker-storage.sh – Docker存储监控脚本
THRESHOLD=80 # 磁盘使用率阈值(%)
echo "=== Docker Storage Monitor ==="
echo "Time: $(date)"
echo ""
# 1. 整体磁盘使用
echo "— Disk Usage —"
df -h /var/lib/docker
echo ""
# 2. Docker资源使用
echo "— Docker Resources —"
docker system df
echo ""
# 3. 磁盘使用率告警
USAGE=$(df /var/lib/docker –output=pcent | tail -1 | tr -d ' %')
if [ "$USAGE" -gt "$THRESHOLD" ]; then
echo "WARNING: Disk usage is ${USAGE}% (threshold: ${THRESHOLD}%)"
echo "Consider cleaning up unused resources:"
echo " docker system prune"
echo " docker volume prune"
fi
echo ""
# 4. 数据卷使用详情
echo "— Volume Details —"
for vol in $(docker volume ls -q); do
SIZE=$(docker run –rm -v "$vol":/data alpine du -sh /data 2>/dev/null | cut -f1)
CONTAINERS=$(docker ps -a –filter volume="$vol" -q | wc -l)
echo " $vol: Size=$SIZE, Containers=$CONTAINERS"
done
10.2 清理无用数据(docker system prune, volume prune)
10.2.1 docker system prune
# 清理所有未使用的资源(停止的容器、未使用的网络、悬空镜像、构建缓存)
docker system prune
# 输出:
# WARNING! This will remove:
# – all stopped containers
# – all networks not used by at least one container
# – all dangling images
# – all dangling build cache
# Are you sure you want to continue? [y/N] y
# 清理所有未使用的资源(包括未被容器引用的镜像)
docker system prune -a
# 同时清理未被使用的数据卷(危险!谨慎使用!)
docker system prune -a –volumes
# 跳过确认
docker system prune -f
# 带过滤条件的清理(清理24小时前创建的)
docker system prune –filter "until=24h"
10.2.2 分别清理各类资源
# 1. 清理停止的容器
docker container prune
# 或
docker container prune -f # 跳过确认
# 2. 清理悬空镜像(没有标签的镜像)
docker image prune
# 清理所有未使用的镜像(不只是悬空镜像)
docker image prune -a
# 3. 清理未使用的网络
docker network prune
# 4. 清理未使用的数据卷
docker volume prune
# 带过滤条件
docker volume prune –filter "until=24h"
# 按标签过滤
docker volume prune –filter "label!=keep=true"
# 5. 清理构建缓存
docker builder prune
docker builder prune -a # 清理所有构建缓存
10.2.3 安全清理脚本
#!/bin/bash
# safe-cleanup.sh – 安全清理脚本
echo "=== Docker Safe Cleanup ==="
echo "Time: $(date)"
# 1. 显示当前使用情况
echo "— Before cleanup —"
docker system df
echo ""
# 2. 清理停止的容器(安全)
echo "Cleaning stopped containers…"
docker container prune -f
# 3. 清理悬空镜像(安全)
echo "Cleaning dangling images…"
docker image prune -f
# 4. 清理未使用的网络(安全)
echo "Cleaning unused networks…"
docker network prune -f
# 5. 清理构建缓存(安全)
echo "Cleaning build cache…"
docker builder prune -f
# 6. 显示清理后的使用情况
echo ""
echo "— After cleanup —"
docker system df
# 7. 显示可进一步清理的资源
echo ""
echo "— Further cleanup options —"
echo "Unused images (not used by any container):"
docker images -f "dangling=false" –format "{{.Repository}}:{{.Tag}} {{.Size}}" | head -10
echo ""
echo "Unused volumes (not used by any container):"
docker volume ls -f dangling=true
echo ""
echo "=== Cleanup completed ==="
10.3 磁盘空间不足的处理
10.3.1 诊断磁盘空间
# 1. 查看磁盘整体使用情况
df -h
# 2. 查看Docker目录使用情况
sudo du -sh /var/lib/docker/
sudo du -sh /var/lib/docker/*/
# 3. 查找最大的文件和目录
sudo du -ah /var/lib/docker/ | sort -rh | head -20
# 4. 查看容器日志大小
sudo du -sh /var/lib/docker/containers/*/
# 5. 查看数据卷大小
sudo du -sh /var/lib/docker/volumes/*/
# 6. 查看镜像层大小
sudo du -sh /var/lib/docker/overlay2/*/
10.3.2 清理容器日志
# 容器日志可能占用大量空间
# 查看容器日志大小
sudo sh -c 'du -sh /var/lib/docker/containers/*/*-json.log'
# 清理单个容器的日志
sudo truncate -s 0 /var/lib/docker/containers/<container_id>/<container_id>-json.log
# 清理所有容器日志
sudo sh -c 'truncate -s 0 /var/lib/docker/containers/*/*-json.log'
# 配置日志轮转(防止日志过大)
# /etc/docker/daemon.json
# {
# "log-driver": "json-file",
# "log-opts": {
# "max-size": "10m",
# "max-file": "3"
# }
# }
10.3.3 迁移Docker数据目录
# 当系统盘空间不足时,将Docker数据迁移到更大的磁盘
# 1. 停止Docker
sudo systemctl stop docker
sudo systemctl stop docker.socket
# 2. 创建新目录
sudo mkdir -p /data/docker
# 3. 迁移数据(使用rsync保留权限)
sudo rsync -aP /var/lib/docker/ /data/docker/
# 4. 修改Docker配置
sudo tee /etc/docker/daemon.json << 'EOF'
{
"data-root": "/data/docker"
}
EOF
# 5. 启动Docker
sudo systemctl start docker
# 6. 验证
docker info | grep "Docker Root Dir"
# 输出: Docker Root Dir: /data/docker
# 7. 确认所有容器正常运行后,删除旧目录
sudo rm -rf /var/lib/docker
10.3.4 紧急空间释放
#!/bin/bash
# emergency-cleanup.sh – 紧急空间释放脚本
echo "=== Emergency Space Cleanup ==="
# 1. 清理所有停止的容器
echo "Removing stopped containers…"
docker rm -v $(docker ps -aq –filter status=exited) 2>/dev/null
# 2. 清理所有未使用的镜像
echo "Removing unused images…"
docker rmi $(docker images -q –filter "dangling=true") 2>/dev/null
docker image prune -a -f 2>/dev/null
# 3. 清理未使用的数据卷(谨慎!)
echo "Removing unused volumes…"
docker volume prune -f
# 4. 清理构建缓存
echo "Cleaning build cache…"
docker builder prune -a -f
# 5. 清理容器日志
echo "Truncating container logs…"
sudo sh -c 'truncate -s 0 /var/lib/docker/containers/*/*-json.log' 2>/dev/null
# 6. 显示结果
echo ""
echo "=== After cleanup ==="
docker system df
echo ""
df -h /var/lib/docker
10.4 数据卷损坏修复
10.4.1 诊断数据卷损坏
# 1. 检查数据卷是否可访问
docker run –rm -v my-volume:/data alpine ls -la /data
# 如果报错,可能数据卷已损坏
# 2. 检查宿主机上的数据卷目录
VOLUME_PATH=$(docker volume inspect my-volume –format '{{.Mountpoint}}')
sudo ls -la $VOLUME_PATH
# 3. 检查文件系统
sudo fsck.ext4 /dev/sda1 # 如果使用ext4
# 或
sudo xfs_repair /dev/sda1 # 如果使用xfs
# 4. 查看dmesg中的文件系统错误
dmesg | grep -i error
10.4.2 修复步骤
# 步骤1:停止使用该数据卷的所有容器
docker ps –filter volume=my-volume -q | xargs docker stop
# 步骤2:尝试从备份恢复
# 如果有备份,直接恢复
docker volume rm my-volume
docker volume create my-volume
docker run –rm \\
-v my-volume:/data \\
-v /backup:/backup:ro \\
alpine:latest \\
tar xzf /backup/my-volume-backup.tar.gz -C /data
# 步骤3:如果没有备份,尝试手动修复
# 使用fsck修复底层文件系统
sudo umount /var/lib/docker/volumes/my-volume/_data 2>/dev/null
sudo fsck -y /dev/sda1
sudo mount -a
# 步骤4:如果数据卷目录损坏,尝试恢复文件
sudo mkdir -p /tmp/recovery
sudo cp -a /var/lib/docker/volumes/my-volume/_data/ /tmp/recovery/ 2>/dev/null
# 检查恢复的文件
ls -la /tmp/recovery/
10.5 权限问题排查
10.5.1 常见权限问题
# 问题1:容器内无法写入数据卷
# 错误信息: Permission denied
# 诊断步骤
# 1. 检查容器内进程的用户
docker exec <container> id
# 输出: uid=0(root) gid=0(root)
# 2. 检查数据卷的所有者
VOLUME_PATH=$(docker volume inspect my-volume –format '{{.Mountpoint}}')
sudo ls -la $VOLUME_PATH
# 输出: drwxr-xr-x 2 root root 4096 …
# 3. 检查容器挂载信息
docker inspect <container> –format '{{json .Mounts}}' | python3 -m json.tool
10.5.2 解决权限问题
# 解决方案1:修改数据卷权限
VOLUME_PATH=$(docker volume inspect my-volume –format '{{.Mountpoint}}')
# 修改为容器内用户的UID
# 例如容器内用户UID为1000
sudo chown -R 1000:1000 $VOLUME_PATH
sudo chmod -R 755 $VOLUME_PATH
# 解决方案2:使用初始化容器设置权限
docker run –rm \\
-v my-volume:/data \\
alpine:latest \\
chown -R 1000:1000 /data
# 解决方案3:在Dockerfile中设置
# Dockerfile:
# FROM alpine
# RUN mkdir -p /data && chown 1000:1000 /data
# USER 1000:1000
# 解决方案4:运行时指定用户
docker run -d \\
–user 1000:1000 \\
-v my-volume:/data \\
my-app
# 解决方案5:使用entrypoint脚本动态设置权限
# entrypoint.sh:
# #!/bin/sh
# if [ "$(id -u)" = "0" ]; then
# chown -R appuser:appgroup /data
# exec su-exec appuser "$@"
# fi
# exec "$@"
10.5.3 SELinux/AppArmor权限问题
# SELinux导致的权限问题(在CentOS/RHEL上常见)
# 查看SELinux状态
getenforce
# 输出: Enforcing
# 查看文件SELinux标签
sudo ls -Z /var/lib/docker/volumes/my-volume/_data/
# 解决方案1:添加:z或:Z后缀(Docker自动设置SELinux标签)
docker run -v my-volume:/data:z my-app # 共享标签
docker run -v my-volume:/data:Z my-app # 私有标签
# 解决方案2:临时设置为permissive模式(不推荐生产使用)
sudo setenforce 0
# 解决方案3:正确设置SELinux策略
sudo semanage fcontext -a -t container_file_t "/var/lib/docker/volumes/my-volume/_data(/.*)?"
sudo restorecon -Rv /var/lib/docker/volumes/my-volume/_data/
10.6 存储性能监控与优化
10.6.1 使用docker stats监控
# 实时监控容器资源使用
docker stats
# 输出示例:
# CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
# a1b2c3d4 mysql 2.35% 512MiB / 2GiB 25.00% 1.2kB / 648B 25MB / 15MB 120
# e5f6g7h8 redis 0.50% 50MiB / 512MiB 9.77% 648B / 1.2kB 5MB / 2MB 15
# BLOCK I/O列显示磁盘读写量,可用于判断存储性能
# 监控特定容器
docker stats mysql-prod
# 只输出一次(不持续监控)
docker stats –no-stream
10.6.2 使用iostat监控磁盘I/O
# 安装sysstat包
sudo apt-get install sysstat # Ubuntu/Debian
# 或
sudo yum install sysstat # CentOS/RHEL
# 监控磁盘I/O
iostat -dx 1
# 输出示例:
# Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await %util
# sda 0.00 1.00 2.00 5.00 16.00 48.00 9.14 0.03 4.29 3.00
# nvme0n1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
# 关注指标:
# %util: 磁盘使用率(接近100%说明磁盘瓶颈)
# await: I/O等待时间(过高说明磁盘慢)
# r/s, w/s: 每秒读写次数
# rkB/s, wkB/s: 每秒读写数据量
10.6.3 性能优化建议
# 1. 使用SSD替换HDD
# SSD的随机读写性能远超HDD,对数据库等I/O密集型应用影响巨大
# 2. 使用块设备数据卷(绕过文件系统开销)
docker volume create \\
–driver local \\
–opt type=ext4 \\
–opt device=/dev/nvme0n1 \\
high-perf-volume
# 3. 调整文件系统挂载选项
# /etc/fstab
# /dev/sdb1 /var/lib/docker ext4 defaults,noatime,nodiratime,data=writeback 0 0
# noatime: 不更新访问时间
# data=writeback: 写回模式(性能优先,但安全性降低)
# 4. 使用I/O调度器
# 对于SSD,使用none调度器
echo none > /sys/block/nvme0n1/queue/scheduler
# 5. 调整内核I/O参数
sudo sysctl -w vm.dirty_ratio=10
sudo sysctl -w vm.dirty_background_ratio=5
sudo sysctl -w vm.swappiness=10
# 6. 使用内存缓存(tmpfs)减少磁盘I/O
docker run -d \\
–name my-app \\
–mount type=tmpfs,dst=/app/cache,tmpfs-size=512m \\
my-app:latest
10.7 生产环境存储架构设计
10.7.1 单机存储架构
┌─────────────────────────────────────────┐
│ 单机存储架构 │
│ │
│ ┌─────────────────────────────────────┐│
│ │ Docker 主机 ││
│ │ ││
│ │ ┌──────────┐ ┌──────────┐ ││
│ │ │ App容器 │ │ DB容器 │ ││
│ │ └────┬─────┘ └────┬─────┘ ││
│ │ │ │ ││
│ │ ┌────▼──────────────▼─────┐ ││
│ │ │ Docker Volumes │ ││
│ │ │ /var/lib/docker/volumes │ ││
│ │ └────────────┬────────────┘ ││
│ │ │ ││
│ │ ┌────────────▼────────────┐ ││
│ │ │ 本地磁盘(SSD) │ ││
│ │ │ ext4/xfs 文件系统 │ ││
│ │ └─────────────────────────┘ ││
│ └─────────────────────────────────────┘│
│ │
│ 适用场景:小型应用、开发测试环境 │
│ 优点:简单、性能好 │
│ 缺点:无高可用、无法扩展 │
└─────────────────────────────────────────┘
10.7.2 分布式存储架构
┌──────────────────────────────────────────────────────┐
│ 分布式存储架构 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Docker │ │ Docker │ │ Docker │ │
│ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
│ │ │ │ │ │ │ │
│ │ App容器 │ │ App容器 │ │ App容器 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └──────────┬───┴──────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ NFS / GlusterFS │ │
│ │ / Ceph / 集群存储 │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ 存储集群 │ │
│ │ ┌───┐ ┌───┐ │ │
│ │ │SSD│ │SSD│ │ │
│ │ └───┘ └───┘ │ │
│ │ ┌───┐ ┌───┐ │ │
│ │ │SSD│ │SSD│ │ │
│ │ └───┘ └───┘ │ │
│ └─────────────────┘ │
│ │
│ 适用场景:中大型应用、需要高可用 │
│ 优点:高可用、可扩展、数据共享 │
│ 缺点:复杂、网络延迟 │
└──────────────────────────────────────────────────────┘
10.7.3 生产环境Docker Compose完整配置
# docker-compose.prod.yml – 生产环境完整配置
version: '3.8'
services:
# 反向代理
nginx:
image: nginx:alpine
container_name: nginx–proxy
restart: always
ports:
– "80:80"
– "443:443"
volumes:
– nginx_conf:/etc/nginx/conf.d:ro
– nginx_certs:/etc/nginx/certs:ro
– nginx_logs:/var/log/nginx
networks:
– frontend
depends_on:
– webapp
# Web应用
webapp:
image: my–app:latest
container_name: webapp
restart: always
environment:
– DB_HOST=mysql
– DB_PASSWORD=${DB_PASSWORD}
– REDIS_HOST=redis
– REDIS_PASSWORD=${REDIS_PASSWORD}
volumes:
– app_uploads:/app/uploads
– app_logs:/app/logs
networks:
– frontend
– backend
deploy:
resources:
limits:
memory: 1G
cpus: '1.0'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
# MySQL数据库
mysql:
image: mysql:8.0
container_name: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: appdb
volumes:
– mysql_data:/var/lib/mysql
– mysql_conf:/etc/mysql/conf.d:ro
– mysql_logs:/var/log/mysql
networks:
– backend
deploy:
resources:
limits:
memory: 2G
cpus: '2.0'
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
# Redis缓存
redis:
image: redis:7–alpine
container_name: redis
restart: always
command: ["redis-server", "–requirepass", "${REDIS_PASSWORD}", "–appendonly", "yes"]
volumes:
– redis_data:/data
networks:
– backend
deploy:
resources:
limits:
memory: 512M
volumes:
nginx_conf:
nginx_certs:
nginx_logs:
app_uploads:
app_logs:
mysql_data:
labels:
backup: daily
retention: 30d
mysql_conf:
mysql_logs:
redis_data:
labels:
backup: daily
retention: 7d
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 后端网络不对外暴露
10.8 本章总结与下一期预告
10.8.1 全文总结
本文全面深入地讲解了Docker存储与数据持久化的全部知识,从底层原理到生产实践,涵盖了以下核心内容:
Docker存储体系概览:理解了容器存储的临时性挑战,Docker三大存储方式(Volume、Bind Mount、tmpfs)的区别,以及存储驱动与数据卷的关系。
存储驱动详解:深入学习了overlay2、aufs、devicemapper、btrfs、zfs和vfs六种存储驱动的工作原理、优缺点和选择方法。
Docker Volume:掌握了数据卷的创建、管理、使用,包括命名卷与匿名卷、只读卷、权限管理,以及MySQL数据持久化实战。
Bind Mount:学习了绑定挂载的使用方法、绝对路径要求、只读挂载、文件同步机制,以及开发环境代码热更新实战。
tmpfs Mount:了解了tmpfs的内存存储特性、大小和权限设置,以及安全存储临时密钥的实战方案。
数据卷进阶操作:掌握了数据卷驱动、NFS数据卷、多容器共享、数据卷容器模式、备份恢复和迁移等高级操作。
Dockerfile存储配置:学习了VOLUME指令的使用、注意事项、与docker run -v的交互,以及镜像预填充机制。
数据库存储方案:完整实现了MySQL、PostgreSQL、MongoDB、Redis和Elasticsearch五种数据库的持久化配置,包括配置文件、初始化脚本、备份恢复和性能优化。
存储安全与备份策略:学习了数据卷加密、访问控制、全量/增量/差异备份、定时自动备份、备份验证、灾难恢复和合规性管理。
存储管理与故障排查:掌握了docker system df、资源清理、磁盘空间处理、数据卷损坏修复、权限排查、性能监控和生产环境架构设计。
10.8.2 关键要点回顾
| 存储方式选择 | 生产用Volume,开发用Bind Mount,敏感数据用tmpfs |
| 存储驱动 | 优先使用overlay2,内核4.0+ |
| 数据卷管理 | 使用命名数据卷,添加标签,定期清理 |
| 数据库持久化 | 数据+配置+日志分离,设置健康检查和资源限制 |
| 备份策略 | 3-2-1原则(3份备份,2种介质,1份异地) |
| 安全 | 加密敏感数据,最小权限原则,定期审计 |
| 监控 | docker system df, docker stats, iostat |
| 故障排查 | 先诊断后处理,权限问题最常见 |
10.8.3 下一期预告
在下一篇文章中,我们将探讨Docker网络与容器间通信的主题,内容包括:
- Docker网络模型详解(bridge、host、overlay、macvlan、none)
- 容器间通信机制
- 端口映射与端口暴露
- Docker DNS与服务发现
- 自定义网络创建与管理
- Docker Swarm覆盖网络
- 网络安全与隔离
- 负载均衡与流量分发
- 生产环境网络架构设计
敬请期待!
结语
Docker存储与数据持久化是容器化部署中最关键的主题之一。正确理解和运用Docker的存储机制,不仅能确保数据的安全性和持久性,还能显著提升应用的性能和可靠性。
在实际生产环境中,存储方案的选择和配置需要根据具体的业务需求、数据类型、性能要求和安全要求来综合考虑。没有一种方案适用于所有场景,关键在于理解每种存储方式的原理和特点,然后在合适的场景使用合适的工具。
希望本文能够帮助你全面掌握Docker存储与数据持久化的知识,在容器化部署的道路上更加得心应手。如果你在实践中遇到任何问题,欢迎留言讨论。
网硕互联帮助中心


评论前必须登录!
注册