06-Docker存储与数据持久化

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)。所有的写操作------创建文件、修改文件、删除文件------都发生在这个可写层中。

这个设计带来了一个根本性的问题:当容器被删除时,可写层也会被删除,所有写入的数据都会丢失。

让我们通过一个简单的实验来验证这一点:

bash 复制代码
# 启动一个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. 数据库数据丢失:运行在容器中的数据库(如MySQL、PostgreSQL)在容器删除后数据全部丢失。
  2. 配置文件无法持久化:容器内修改的配置文件在重建后恢复为镜像默认值。
  3. 日志文件丢失:应用运行日志随容器删除而消失,无法进行事后分析。
  4. 上传文件丢失:用户上传的文件存储在容器内,容器重建后文件丢失。
  5. 多容器间无法共享数据:不同容器之间无法直接共享文件数据。
  6. 容器升级困难:升级容器需要删除旧容器、创建新容器,但旧容器中的数据无法保留。
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 三种存储方式对比
特性 Volume Bind Mount tmpfs
存储位置 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的根目录:

bash 复制代码
# 查看Docker信息中的根目录
docker info | grep "Docker Root Dir"
# 输出: Docker Root Dir: /var/lib/docker

# 查看各目录的磁盘使用情况
sudo du -sh /var/lib/docker/*
1.4.3 数据流向

当容器需要进行文件读写时,数据的流向取决于文件所在的层级:

  1. 读取镜像层中的文件:存储驱动负责从对应的镜像层读取文件内容。
  2. 修改镜像层中的文件:触发写时复制(Copy-on-Write),存储驱动将文件从镜像层复制到容器可写层,然后在可写层进行修改。
  3. 读写数据卷中的文件:直接通过挂载机制读写宿主机文件系统,不经过存储驱动。
  4. 读写tmpfs中的文件:直接在内存中进行,不经过存储驱动,不写入磁盘。

1.5 容器可写层与数据持久化层的关系

1.5.1 容器可写层的工作机制

每个容器启动时,Docker会为其创建一个可写层。这个可写层叠加在镜像的所有只读层之上,形成一个统一的文件系统视图(通过联合文件系统UnionFS实现)。

bash 复制代码
# 查看容器的可写层信息(以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不会直接修改镜像层(因为镜像是只读的),而是:

  1. 将文件从镜像层(LowerDir)复制到可写层(UpperDir)
  2. 在可写层中对文件进行修改
  3. 后续的读取操作会优先从可写层读取

这个机制确保了:

  • 镜像层保持不变,多个容器可以共享同一个镜像
  • 每个容器有自己的修改副本,互不影响
  • 只有被修改的文件才会被复制,节省存储空间
bash 复制代码
# 演示写时复制
# 启动容器
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 数据持久化层的角色

数据持久化层(数据卷、绑定挂载)与容器可写层是完全独立的:

  1. 挂载点覆盖:当数据卷挂载到容器的某个路径时,该路径上的可写层内容会被"遮盖",容器看到的是数据卷中的内容。
  2. 独立生命周期:数据卷的创建、删除与容器的创建、删除完全独立。删除容器不会删除数据卷。
  3. 直接I/O:数据卷的读写操作直接作用于宿主机文件系统,不经过存储驱动的写时复制机制,因此性能更好。
  4. 初始内容复制:对于命名数据卷,如果挂载点在镜像中有内容,首次挂载时Docker会将镜像中的内容复制到数据卷中(对于绑定挂载则不会)。
bash 复制代码
# 演示数据卷挂载点覆盖
# 使用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中的一条指令。存储驱动负责:

  • 层叠加:将多个只读层叠加成一个统一的文件系统视图
  • 层共享:多个镜像可以共享相同的底层,节省存储空间
  • 层缓存:构建镜像时利用缓存层加速构建过程
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;"]  # 新层(元数据层)
bash 复制代码
# 查看镜像的分层结构
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将文件系统分为三层:

  1. LowerDir(下层):只读的镜像层,可以有多个(最多128层)。这些层从下到上叠加,上层覆盖下层。
  2. UpperDir(上层):容器的可写层,所有写操作都发生在这里。
  3. MergedDir(合并层):将LowerDir和UpperDir合并后的统一视图,容器内看到的就是这个视图。

此外还有一个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的文件操作机制

读取文件:

  1. 先在UpperDir中查找文件
  2. 如果UpperDir中没有,则依次在LowerDir中从上到下查找
  3. 返回第一个找到的文件

修改文件(写时复制):

  1. 文件在UpperDir中:直接修改
  2. 文件在LowerDir中:将文件从LowerDir复制到UpperDir,然后在UpperDir中修改

删除文件:

  1. 如果文件在UpperDir中:直接删除
  2. 如果文件在LowerDir中:在UpperDir中创建一个whiteout文件(字符设备文件,主次设备号均为0),表示该文件已被删除

创建目录:

  1. 在UpperDir中创建目录
  2. 如果LowerDir中有同名目录,需要在UpperDir中设置opaque属性,表示该目录不与下层合并
2.2.4 查看overlay2的实际结构
bash 复制代码
# 启动一个容器
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文件
bash 复制代码
# 演示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的目录结构
bash 复制代码
# 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逐渐被淘汰
  1. 内核不支持:aufs未被Linux内核主线接受,需要打补丁
  2. 维护困难:需要为每个内核版本维护补丁
  3. overlay2更优:overlay2被内核原生支持,性能和功能都优于aufs
  4. Docker官方弃用:从Docker 17.06开始,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的两种模式
  1. loop-lvm模式(默认但不推荐):使用回环设备(loop device)模拟块设备,性能差,仅用于测试。
  2. direct-lvm模式(生产推荐):直接使用物理块设备,性能好,适合生产环境。
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模式
json 复制代码
// /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"
  ]
}
bash 复制代码
# 配置完成后重启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驱动
bash 复制代码
# 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的配额管理
bash 复制代码
# 启用配额
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驱动
bash 复制代码
# 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配额和压缩
bash 复制代码
# 设置容器数据集配额
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
bash 复制代码
# vfs驱动通常在不支持其他驱动的环境中使用
# 例如某些NAS环境或受限的容器运行时

# 配置vfs驱动
# /etc/docker/daemon.json
# {
#   "storage-driver": "vfs"
# }

# 重启Docker
sudo systemctl restart docker

2.8 存储驱动对比表

特性 overlay2 aufs devicemapper btrfs zfs vfs
推荐度 推荐(首选) 不推荐 不推荐 可选 可选 仅测试
底层技术 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 选择存储驱动的原则
  1. 优先选择overlay2:这是Docker官方推荐的首选驱动,在大多数Linux发行版上都可用,性能和稳定性都很好。
  2. 根据文件系统选择 :
    • ext4/xfs:使用overlay2
    • btrfs:可以使用overlay2或btrfs驱动
    • zfs:可以使用overlay2或zfs驱动(但不要在同一zfs文件系统上使用overlay2)
  3. 避免使用已弃用的驱动:aufs和devicemapper(loop-lvm模式)已不推荐使用。
  4. 生产环境避免vfs:vfs性能太差,仅用于测试。
2.9.2 查看当前存储驱动
bash 复制代码
# 查看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 切换存储驱动

重要警告:切换存储驱动会导致已有的镜像和容器无法使用!切换前请备份所有重要数据!

bash 复制代码
# 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中的存储驱动配置
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 验证存储驱动支持
bash 复制代码
# 检查内核是否支持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数据目录迁移到其他磁盘:

bash 复制代码
# 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 存储驱动相关的故障排查
bash 复制代码
# 查看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 数据卷的核心特点
  1. 独立的生命周期 :数据卷的创建、存在和删除与容器完全解耦。容器删除不会自动删除数据卷(除非使用-v标志删除容器)。
  2. Docker统一管理 :通过docker volume命令族进行管理,无需关心底层存储路径。
  3. 跨容器共享:一个数据卷可以被多个容器同时挂载,实现数据共享。
  4. 绕过存储驱动:数据卷的读写直接作用于宿主机文件系统,不经过存储驱动的写时复制机制,性能接近原生。
  5. 支持驱动扩展:通过数据卷驱动(Volume Driver),可以将数据存储在远程存储(NFS、云存储、块存储等)上。
  6. 初始内容复制:对于命名数据卷,首次挂载到一个有内容的目录时,Docker会将镜像中的内容复制到数据卷中。
  7. 平台无关:数据卷的使用方式在不同操作系统上基本一致(虽然底层存储路径不同)。
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 基本创建命令
bash 复制代码
# 创建一个命名数据卷
docker volume create my-data-volume

# 输出: my-data-volume

创建后,Docker会在/var/lib/docker/volumes/my-data-volume/_data/目录下创建实际的数据存储目录。

3.2.2 创建时指定选项
bash 复制代码
# 创建数据卷时指定标签(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 查看创建结果
bash 复制代码
# 查看数据卷详细信息
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 - 列出数据卷
bash 复制代码
# 列出所有数据卷
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 - 查看详情
bash 复制代码
# 查看单个数据卷详情
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 - 删除数据卷
bash 复制代码
# 删除单个数据卷
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 - 清理无用数据卷
bash 复制代码
# 删除所有未被容器使用的数据卷
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 数据卷管理最佳实践
bash 复制代码
# 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参数)
bash 复制代码
# 使用命名数据卷
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参数(推荐)
bash 复制代码
# 使用--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的对比
bash 复制代码
# -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 挂载多个数据卷
bash 复制代码
# 一个容器可以挂载多个数据卷
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选项禁用此行为:

bash 复制代码
# 默认行为:复制镜像内容到数据卷
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 自动创建命名数据卷
bash 复制代码
# 如果指定的命名数据卷不存在,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 自动创建匿名数据卷
bash 复制代码
# 如果只指定容器路径而不指定数据卷名称,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 自动创建的风险与注意事项

匿名数据卷的自动创建可能导致"数据卷泄漏"问题------容器删除后,匿名数据卷不会被自动删除,长期积累会占用大量磁盘空间。

bash 复制代码
# 演示数据卷泄漏
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

最佳实践:始终使用命名数据卷,避免匿名数据卷。

bash 复制代码
# 不推荐:匿名数据卷
docker run -v /data alpine sleep 3600

# 推荐:命名数据卷
docker run -v my-data:/data alpine sleep 3600

3.6 数据卷在宿主机上的存储位置

3.6.1 默认存储位置
bash 复制代码
# 数据卷默认存储在
# /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 修改数据卷存储位置
json 复制代码
// /etc/docker/daemon.json
{
  "data-root": "/data/docker"
}

修改后数据卷将存储在/data/docker/volumes/目录下。

3.6.3 数据卷的文件系统权限
bash 复制代码
# 数据卷在宿主机上默认由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 创建只读数据卷
bash 复制代码
# 使用-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 验证只读属性
bash 复制代码
# 读取数据卷中的文件(正常)
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 只读数据卷的应用场景
bash 复制代码
# 场景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用户可能没有读写权限。

bash 复制代码
# 示例:以非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 复制代码
# Dockerfile
FROM alpine:latest

# 创建目录并设置权限
RUN mkdir -p /data && chown 1000:1000 /data

# 切换到非root用户
USER 1000:1000

CMD ["sleep", "3600"]

方法2:使用初始化容器

bash 复制代码
# 步骤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:在宿主机上直接修改权限

bash 复制代码
# 获取数据卷路径
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容器

yaml 复制代码
# 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)
bash 复制代码
# 命名数据卷:有明确的名称
docker run -d \
  --name app \
  -v my-app-data:/data \
  my-app:latest

# 特点:
# 1. 有明确的、可读的名称
# 2. 可以通过名称引用和管理
# 3. 适合生产环境
# 4. 便于备份、迁移和共享
3.9.2 匿名数据卷(Anonymous Volume)
bash 复制代码
# 匿名数据卷:只指定容器路径,不指定数据卷名称
docker run -d \
  --name app \
  -v /data \
  my-app:latest

# 特点:
# 1. 名称是随机生成的哈希值
# 2. 难以管理和识别
# 3. 容器删除后容易成为"孤儿"数据卷
# 4. 不推荐在生产环境使用
3.9.3 对比
特性 命名数据卷 匿名数据卷
名称 有明确名称 随机哈希值
可读性 好 差
管理难度 低 高
可复用性 高 低
适用场景 生产环境 临时测试
清理难度 低(按名称删除) 高(需找出对应哈希)
3.9.4 Dockerfile中的VOLUME指令产生的匿名卷
dockerfile 复制代码
# Dockerfile中使用VOLUME指令会创建匿名数据卷
FROM nginx:latest
VOLUME /data
bash 复制代码
# 构建并运行
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容器,要求:

  1. 数据库数据持久化,容器删除后数据不丢失
  2. 配置文件外部管理,方便修改
  3. 日志文件持久化
  4. 初始化SQL脚本自动执行
3.10.2 完整实现
bash 复制代码
# 步骤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管理
yaml 复制代码
# 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
bash 复制代码
# 启动
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 绑定挂载的核心特点
  1. 依赖宿主机路径:必须指定宿主机上的绝对路径,路径不存在时Docker会自动创建。
  2. 双向同步:容器内修改文件会反映到宿主机,宿主机修改文件也会反映到容器内。
  3. 绕过存储驱动:与数据卷一样,绑定挂载直接操作宿主机文件系统,不经过存储驱动。
  4. 不自动复制内容:与命名数据卷不同,绑定挂载不会将镜像中的内容复制到宿主机路径。如果宿主机路径为空,容器内挂载点也是空的(会"遮盖"镜像中的原有内容)。
  5. 无法通过docker volume管理 :绑定挂载不是数据卷,不能用docker volume命令管理。
  6. 路径依赖性强:在不同主机上运行时,宿主机路径必须一致,可移植性较差。
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参数)
bash 复制代码
# 使用-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 区分绑定挂载和数据卷
bash 复制代码
# 绑定挂载:宿主机路径以/开头(或./、~/等绝对/相对路径)
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 挂载单个文件
bash 复制代码
# 绑定挂载也可以挂载单个文件(而不仅仅是目录)
docker run -d \
  --name nginx-custom \
  -v /opt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \
  nginx:latest

# 注意:挂载单个文件时,宿主机上该文件必须存在
# 如果文件不存在,Docker会将其创建为一个目录!
bash 复制代码
# 演示文件挂载的错误情况
# 如果文件不存在,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 挂载多个绑定挂载
bash 复制代码
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语法
bash 复制代码
# 使用--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的完整选项
bash 复制代码
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)

绑定传播控制当挂载点下又有其他挂载时,这些挂载如何传播:

bash 复制代码
# 演示绑定传播
# 准备目录结构
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 绝对路径规则
bash 复制代码
# 绑定挂载的宿主机路径必须是绝对路径
# 正确:绝对路径
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 使用相对路径的正确方式
bash 复制代码
# 在项目目录中使用绑定挂载
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 路径不存在时的行为
bash 复制代码
# 如果宿主机路径不存在,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 创建只读绑定挂载
bash 复制代码
# 使用-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 只读绑定挂载的应用场景
bash 复制代码
# 场景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 实时双向同步

绑定挂载的最大优势之一是宿主机与容器之间的文件实时双向同步:

bash 复制代码
# 启动一个绑定挂载的容器
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等)可以在容器内正常运行:

bash 复制代码
# 在开发环境中使用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)
bash 复制代码
# 项目结构
# /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热重载
bash 复制代码
# 使用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覆盖问题
bash 复制代码
# 问题:绑定挂载会覆盖容器内的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上的权限问题
bash 复制代码
# 在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虚拟机中转,存在以下特点:

bash 复制代码
# 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上的特殊处理
powershell 复制代码
# 在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对比

对比项 Bind Mount(绑定挂载) 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 复制代码
# 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
yaml 复制代码
# 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 运行与验证
bash 复制代码
# 启动开发环境
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 开发环境的最佳实践
bash 复制代码
# 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配置
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挂载的核心特点
  1. 存储在内存中:数据存储在宿主机RAM中,不写入磁盘。
  2. 极快的读写速度:内存读写速度远超磁盘,适合高频读写场景。
  3. 临时性:容器停止后数据消失,不会在磁盘上留下痕迹。
  4. 安全性高:数据不落盘,适合存储敏感信息(如密钥、令牌)。
  5. 受内存限制:可用空间受宿主机可用内存限制。
  6. 仅支持Linux:tmpfs是Linux特性,在Windows容器上不支持。
  7. 单容器使用:tmpfs挂载不能在多个容器之间共享。
  8. 无法通过docker volume管理 :tmpfs不是数据卷,不能用docker volume命令管理。
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参数
bash 复制代码
# 使用--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参数
bash 复制代码
# 使用--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 --mount type=tmpfs
语法 简洁 明确
大小限制 不支持单独设置 支持(tmpfs-size)
权限模式 不支持单独设置 支持(tmpfs-mode)
Swarm支持 不支持 支持
推荐度 简单场景 复杂场景/生产环境

5.3 tmpfs挂载的选项

5.3.1 tmpfs-size(大小限制)
bash 复制代码
# 设置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(权限模式)
bash 复制代码
# 设置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使用宿主机的内存来存储数据。需要注意的是:

  1. 动态分配:tmpfs不会预分配全部内存,而是按需分配。
  2. 可被交换:如果系统内存不足,tmpfs中的数据可能被交换到swap分区。
  3. 影响容器内存限制:tmpfs占用的内存计入容器的内存使用。
bash 复制代码
# 查看宿主机内存使用情况
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 与容器内存限制的关系
bash 复制代码
# 如果容器设置了内存限制,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 存储敏感信息
bash 复制代码
# 场景:临时存储API密钥或令牌
docker run -d \
  --name secure-app \
  --mount type=tmpfs,dst=/app/secrets,tmpfs-mode=1700 \
  my-app:latest

# 应用将敏感信息写入/app/secrets,容器停止后信息消失
# 不会在磁盘上留下任何痕迹
5.5.2 高速临时缓存
bash 复制代码
# 场景:应用缓存目录
docker run -d \
  --name cache-app \
  --mount type=tmpfs,dst=/app/cache,tmpfs-size=256m \
  my-app:latest

# 缓存数据存储在内存中,读写速度极快
# 容器重启后缓存自动清空(从后端重新加载)
5.5.3 减少磁盘I/O
bash 复制代码
# 场景:临时文件处理
docker run -d \
  --name file-processor \
  --mount type=tmpfs,dst=/tmp/processing,tmpfs-size=512m \
  my-app:latest

# 临时文件在内存中处理,减少磁盘I/O
# 处理完成后,结果写入持久化存储(数据卷)
5.5.4 不适合tmpfs的场景
bash 复制代码
# 不适合:需要持久化的数据
# 数据库数据 → 使用Volume
# 应用日志 → 使用Volume或日志驱动
# 用户上传文件 → 使用Volume

# 不适合:大量数据
# tmpfs受内存限制,不适合存储大量数据

# 不适合:需要多容器共享的数据
# tmpfs是容器私有的,无法共享

5.6 tmpfs实战: 安全存储临时密钥

5.6.1 需求场景

在一个微服务应用中,需要临时存储数据库密码、API密钥等敏感信息。这些信息不应该持久化到磁盘,仅在应用运行期间存在于内存中。

5.6.2 完整实现
bash 复制代码
# 步骤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
yaml 复制代码
# 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
bash 复制代码
# 启动
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安全注意事项
bash 复制代码
# 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驱动,它支持几种存储类型:

bash 复制代码
# 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 第三方数据卷驱动插件
bash 复制代码
# 查看已安装的插件
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默认的数据卷驱动,它支持多种存储后端:

bash 复制代码
# 查看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)是常用的网络文件系统协议,适合多主机共享数据:

bash 复制代码
# 方法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设备数据卷
bash 复制代码
# 使用块设备创建数据卷
# 前提:块设备已格式化
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数据卷配置
bash 复制代码
# ============================================
# 步骤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中的使用
yaml 复制代码
# 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 多容器共享同一个数据卷
bash 复制代码
# 创建共享数据卷
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 并发写入的注意事项
bash 复制代码
# 多个容器同时写入同一个数据卷时,需要注意并发问题
# 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 创建和使用数据卷容器
bash 复制代码
# 步骤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的权限继承
bash 复制代码
# --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 链式继承
bash 复制代码
# --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 数据卷容器模式的优缺点
bash 复制代码
# 优点:
# 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 基本备份操作
bash 复制代码
# 假设有一个名为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 备份脚本
bash 复制代码
#!/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."
bash 复制代码
# 使用备份脚本
chmod +x backup-volume.sh
./backup-volume.sh mysql-data /backup 7

6.7 数据卷的恢复

6.7.1 从备份恢复数据卷
bash 复制代码
# 方法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 恢复脚本
bash 复制代码
#!/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
bash 复制代码
# 使用恢复脚本
chmod +x restore-volume.sh
./restore-volume.sh mysql-data-restored /backup/mysql-data-20240115-103000.tar.gz

6.8 数据卷迁移

6.8.1 同主机迁移
bash 复制代码
# 场景:将数据从旧数据卷迁移到新数据卷(可能使用不同的驱动)

# 步骤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 跨主机迁移
bash 复制代码
# 场景:将数据卷从主机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增量迁移
bash 复制代码
# 对于大数据卷,使用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本身不直接支持为数据卷设置大小限制,但可以通过以下方法实现:

bash 复制代码
# 方法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 监控数据卷使用量
bash 复制代码
# 方法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 生命周期管理最佳实践
bash 复制代码
# 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 复制代码
# Dockerfile中使用VOLUME指令

# 语法1:JSON数组形式(推荐)
VOLUME ["/data", "/var/log/app"]

# 语法2:空格分隔的字符串形式
VOLUME /data /var/log/app
7.1.2 VOLUME指令的作用
dockerfile 复制代码
# 示例Dockerfile
FROM ubuntu:22.04

# 创建目录
RUN mkdir -p /app/data /app/logs

# 声明数据卷
VOLUME ["/app/data", "/app/logs"]

# 设置默认命令
CMD ["sleep", "3600"]
bash 复制代码
# 构建镜像
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 复制代码
# Dockerfile
FROM nginx:latest
VOLUME /usr/share/nginx/html
bash 复制代码
# 每次运行容器都会创建一个匿名数据卷
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指令
dockerfile 复制代码
# 重要: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指令与镜像层的关系
dockerfile 复制代码
# 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 复制代码
# Dockerfile
FROM nginx:latest
VOLUME ["/usr/share/nginx/html"]
bash 复制代码
# 情况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 最佳实践建议
dockerfile 复制代码
# ============================================
# 最佳实践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路径下执行写操作
dockerfile 复制代码
# 不推荐
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
dockerfile 复制代码
# 多阶段构建中,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 多阶段构建中的数据传递
dockerfile 复制代码
# 在多阶段构建中,数据卷声明不会跨阶段传递
# 每个阶段是独立的

# 阶段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 复制代码
# 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"]
bash 复制代码
# 构建镜像
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 复制代码
# 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"]
bash 复制代码
#!/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 "$@"
bash 复制代码
#!/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)"
bash 复制代码
# 构建并运行
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配置
yaml 复制代码
# 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配置文件
ini 复制代码
# 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 初始化脚本
sql 复制代码
-- 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;
sql 复制代码
-- 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 备份与恢复
bash 复制代码
#!/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"/
bash 复制代码
#!/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配置
yaml 复制代码
# 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配置文件
ini 复制代码
# 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 初始化脚本
sql 复制代码
-- 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 备份与恢复
bash 复制代码
#!/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"/
bash 复制代码
#!/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配置
yaml 复制代码
# 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配置文件
yaml 复制代码
# 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 初始化脚本
javascript 复制代码
// 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 备份与恢复
bash 复制代码
#!/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"/
bash 复制代码
#!/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配置
yaml 复制代码
# 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配置文件
ini 复制代码
# 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 备份与恢复
bash 复制代码
#!/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"/
bash 复制代码
#!/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配置
yaml 复制代码
# 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配置文件
yaml 复制代码
# 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 备份与恢复
bash 复制代码
#!/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 通用配置建议
yaml 复制代码
# 通用最佳实践(适用于所有数据库)

# 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 数据卷分离策略
yaml 复制代码
# 将不同类型的数据存储在不同的数据卷中,便于管理

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 存储驱动优化
bash 复制代码
# 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 内存与缓存优化
yaml 复制代码
# 数据库容器内存优化建议

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调度优化
bash 复制代码
# 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加密(块设备级别)
bash 复制代码
# 步骤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加密插件
bash 复制代码
# 安装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 复制代码
# 应用层加密示例(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 文件系统级别权限控制
bash 复制代码
# 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 容器级别权限控制
bash 复制代码
# 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 数据卷隔离策略
yaml 复制代码
# 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 全量备份实现
bash 复制代码
#!/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)
bash 复制代码
#!/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 差异备份实现
bash 复制代码
#!/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 综合备份策略
bash 复制代码
#!/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."
bash 复制代码
# 设置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方案
bash 复制代码
#!/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 ==="
bash 复制代码
# 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 使用备份容器方案
yaml 复制代码
# 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 备份验证
bash 复制代码
#!/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 自动化恢复测试
bash 复制代码
#!/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 异地备份方案
bash 复制代码
#!/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 完整灾难恢复流程
bash 复制代码
#!/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 审计脚本
bash 复制代码
#!/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 数据保留策略
yaml 复制代码
# 数据保留策略配置示例

# 数据保留级别
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 合规性检查脚本
bash 复制代码
#!/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 基本用法
bash 复制代码
# 查看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 详细视图
bash 复制代码
# 查看详细的使用情况(-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 监控脚本
bash 复制代码
#!/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
bash 复制代码
# 清理所有未使用的资源(停止的容器、未使用的网络、悬空镜像、构建缓存)
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 分别清理各类资源
bash 复制代码
# 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 安全清理脚本
bash 复制代码
#!/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 诊断磁盘空间
bash 复制代码
# 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 清理容器日志
bash 复制代码
# 容器日志可能占用大量空间
# 查看容器日志大小
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数据目录
bash 复制代码
# 当系统盘空间不足时,将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 紧急空间释放
bash 复制代码
#!/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 诊断数据卷损坏
bash 复制代码
# 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 修复步骤
bash 复制代码
# 步骤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 常见权限问题
bash 复制代码
# 问题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 解决权限问题
bash 复制代码
# 解决方案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权限问题
bash 复制代码
# 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监控
bash 复制代码
# 实时监控容器资源使用
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
bash 复制代码
# 安装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 性能优化建议
bash 复制代码
# 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完整配置
yaml 复制代码
# 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存储与数据持久化的全部知识,从底层原理到生产实践,涵盖了以下核心内容:

  1. Docker存储体系概览:理解了容器存储的临时性挑战,Docker三大存储方式(Volume、Bind Mount、tmpfs)的区别,以及存储驱动与数据卷的关系。

  2. 存储驱动详解:深入学习了overlay2、aufs、devicemapper、btrfs、zfs和vfs六种存储驱动的工作原理、优缺点和选择方法。

  3. Docker Volume:掌握了数据卷的创建、管理、使用,包括命名卷与匿名卷、只读卷、权限管理,以及MySQL数据持久化实战。

  4. Bind Mount:学习了绑定挂载的使用方法、绝对路径要求、只读挂载、文件同步机制,以及开发环境代码热更新实战。

  5. tmpfs Mount:了解了tmpfs的内存存储特性、大小和权限设置,以及安全存储临时密钥的实战方案。

  6. 数据卷进阶操作:掌握了数据卷驱动、NFS数据卷、多容器共享、数据卷容器模式、备份恢复和迁移等高级操作。

  7. Dockerfile存储配置:学习了VOLUME指令的使用、注意事项、与docker run -v的交互,以及镜像预填充机制。

  8. 数据库存储方案:完整实现了MySQL、PostgreSQL、MongoDB、Redis和Elasticsearch五种数据库的持久化配置,包括配置文件、初始化脚本、备份恢复和性能优化。

  9. 存储安全与备份策略:学习了数据卷加密、访问控制、全量/增量/差异备份、定时自动备份、备份验证、灾难恢复和合规性管理。

  10. 存储管理与故障排查:掌握了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存储与数据持久化的知识,在容器化部署的道路上更加得心应手。如果你在实践中遇到任何问题,欢迎留言讨论。

相关推荐
PC2005-cloud5 小时前
Mysql学习笔记:InnoDB Cluster 集群从零搭建实战,基于 Docker 实现高可用读写分离架构
笔记·学习·mysql
小陈phd5 小时前
深入理解agent学习笔记(七)——动态提示词与Skills
笔记·学习
我命由我123455 小时前
田野笔记极简理解
经验分享·笔记·学习·职场和发展·求职招聘·职场发展·学习方法
a5986525 小时前
Docker云手机端口不通,无法访问|六边云手机教你故障排查完整流程
docker·容器·性能优化·云手机
一木 之林5 小时前
微信小程序开发全攻略·文章版|00 总览:74集学习路径与知识地图
人工智能·学习·计算机视觉·微信小程序·小程序
小陈phd6 小时前
深入理解agent学习笔记(六)——提示工程-优化系统提示词
笔记·学习
霖霖7146 小时前
【ESP32笔记】RGB灯WS2812实验
笔记·单片机·mcu·物联网
Aurora_eye6 小时前
Git 笔记
笔记·git
yi0116 小时前
DAY28: LeetCode 111. 二叉树的最小深度
人工智能·笔记·python·算法·leetcode·二叉树·递归
诸神缄默不语7 小时前
小春学习日刊 2026年第1期:2026.10.13
学习