云计算百科
云计算领域专业知识百科平台

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

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

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

# 启动一个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 数据丢失带来的问题

容器存储的临时性在实际应用中会导致多种问题:

  • 数据库数据丢失:运行在容器中的数据库(如MySQL、PostgreSQL)在容器删除后数据全部丢失。
  • 配置文件无法持久化:容器内修改的配置文件在重建后恢复为镜像默认值。
  • 日志文件丢失:应用运行日志随容器删除而消失,无法进行事后分析。
  • 上传文件丢失:用户上传的文件存储在容器内,容器重建后文件丢失。
  • 多容器间无法共享数据:不同容器之间无法直接共享文件数据。
  • 容器升级困难:升级容器需要删除旧容器、创建新容器,但旧容器中的数据无法保留。
  • 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 三种存储方式对比
    特性VolumeBind Mounttmpfs
    存储位置 Docker管理区域(/var/lib/docker/volumes/) 宿主机任意路径 宿主机内存
    管理方式 docker volume命令 直接使用宿主机路径 –tmpfs或–mount
    持久性 持久(独立于容器生命周期) 持久(取决于宿主机文件) 临时(容器停止即消失)
    共享性 多容器共享 多容器共享 不可共享(单容器)
    性能 接近原生 接近原生 极快(内存)
    可移植性 低(路径依赖) 不适用
    安全性 较高 中(直接操作宿主机文件) 高(不落盘)
    适用场景 生产环境数据持久化 开发环境代码挂载 敏感信息、临时缓存
    跨平台 支持 支持(路径不同) 仅Linux
    备份恢复 方便(docker volume命令) 直接操作宿主机文件 不需要(临时数据)

    1.3 存储驱动(Storage Driver)与数据卷的区别

    许多Docker初学者容易混淆"存储驱动"和"数据卷"这两个概念。它们虽然都与存储相关,但在功能和作用层面完全不同。

    1.3.1 存储驱动

    存储驱动(Storage Driver)负责管理镜像层(Image Layers)和容器可写层(Container Writable Layer)。它处理的是Docker镜像的分层存储、写时复制(Copy-on-Write)等底层机制。

    • 作用对象:镜像层和容器可写层
    • 关注点:如何高效地存储和叠加镜像层,如何处理容器的读写操作
    • 与数据持久化的关系:存储驱动管理的数据是临时的(容器删除即丢失)
    • 配置方式:在/etc/docker/daemon.json中通过storage-driver参数配置
    • 常见驱动:overlay2、aufs、devicemapper、btrfs、zfs、vfs
    1.3.2 数据卷

    数据卷(Volume)是Docker提供的数据持久化机制,用于将数据存储在容器可写层之外。

    • 作用对象:需要持久化的数据
    • 关注点:如何在容器生命周期之外保存数据
    • 与存储驱动的关系:数据卷绕过存储驱动,直接写入宿主机文件系统
    • 配置方式:在docker run时通过-v或–mount参数指定
    • 管理命令:docker volume create/ls/rm/inspect/prune
    1.3.3 一图理解两者关系

    ┌─────────────────────────────────────────────────────┐
    │ Docker 容器 │
    │ ┌─────────────────────────────────────────────────┐ │
    │ │ 容器可写层 (存储驱动管理) │ │
    │ │ ┌───────────────────────────────────────────┐ │ │
    │ │ │ 镜像层N (只读) │ │ │
    │ │ ├───────────────────────────────────────────┤ │ │
    │ │ │ 镜像层… (只读) │ │ │
    │ │ ├───────────────────────────────────────────┤ │ │
    │ │ │ 镜像层1 (只读) │ │ │
    │ │ └───────────────────────────────────────────┘ │ │
    │ └─────────────────────────────────────────────────┘ │
    │ ┌─────────────────────────────────────────────────┐ │
    │ │ 数据卷挂载点 (绕过存储驱动,直接写入宿主机) │ │
    │ └─────────────────────────────────────────────────┘ │
    └─────────────────────────────────────────────────────┘


    ┌─────────────────────────────────────────────────────┐
    │ 宿主机 │
    │ /var/lib/docker/volumes/ ← 数据卷存储位置 │
    │ /var/lib/docker/overlay2/ ← 存储驱动管理区域 │
    └─────────────────────────────────────────────────────┘

    简单来说:存储驱动管理"容器的临时数据",数据卷管理"需要持久化的数据"。数据卷绕过了存储驱动,直接在宿主机文件系统上进行读写,因此性能更好且数据更安全。

    1.4 Docker存储架构总览

    Docker的存储架构可以从多个层面来理解:

    1.4.1 整体架构

    ┌──────────────────────────────────────────────────────────┐
    │ Docker 守护进程 (dockerd) │
    │ │
    │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
    │ │ 镜像管理模块 │ │ 容器管理模块 │ │ 数据卷管理模块 │ │
    │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
    │ │ │ │ │
    │ ▼ ▼ ▼ │
    │ ┌──────────────────────────────────────────────────────┐│
    │ │ 存储驱动 (Storage Driver) ││
    │ │ overlay2 / aufs / devicemapper / btrfs / zfs ││
    │ └──────────────────────────────────────────────────────┘│
    │ │ │ │ │
    │ ▼ ▼ ▼ │
    │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
    │ │ /var/lib/ │ │ /var/lib/ │ │ /var/lib/ │ │
    │ │ docker/ │ │ docker/ │ │ docker/ │ │
    │ │ overlay2/ │ │ containers/ │ │ volumes/ │ │
    │ │ (镜像层+可写层)│ │ (容器配置日志) │ │ (数据卷) │ │
    │ └──────────────┘ └──────────────┘ └──────────────┘ │
    │ │
    │ ┌──────────────────────────────────────────────────────┐│
    │ │ 数据卷驱动 (Volume Driver) ││
    │ │ local / nfs / block / 第三方插件 ││
    │ └──────────────────────────────────────────────────────┘│
    └──────────────────────────────────────────────────────────┘


    ┌──────────────────────────────────────────────────────────┐
    │ 宿主机文件系统 │
    │ ext4 / xfs / btrfs / zfs / nfs │
    └──────────────────────────────────────────────────────────┘

    1.4.2 关键目录说明

    Docker在宿主机上的默认存储目录为/var/lib/docker/,其中包含以下重要子目录:

    目录用途
    /var/lib/docker/overlay2/ overlay2存储驱动管理的镜像层和容器可写层
    /var/lib/docker/volumes/ 数据卷的存储位置
    /var/lib/docker/containers/ 容器的配置文件和日志文件
    /var/lib/docker/image/ 镜像相关的元数据
    /var/lib/docker/plugins/ 存储插件相关文件
    /var/lib/docker/network/ 网络相关文件
    /var/lib/docker/tmp/ Docker临时文件
    /var/lib/docker/swarm/ Docker Swarm相关文件

    可以通过以下命令查看Docker的根目录:

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

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

    1.4.3 数据流向

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

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

    1.5.1 容器可写层的工作机制

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

    # 查看容器的可写层信息(以overlay2为例)
    docker inspect <container_id> | grep -A 10 "GraphDriver"

    # 输出示例:
    # "GraphDriver": {
    # "Data": {
    # "LowerDir": "/var/lib/docker/overlay2/…/diff",
    # "MergedDir": "/var/lib/docker/overlay2/…/merged",
    # "UpperDir": "/var/lib/docker/overlay2/…/diff",
    # "WorkDir": "/var/lib/docker/overlay2/…/work"
    # },
    # "Name": "overlay2"
    # }

    其中:

    • LowerDir:镜像层(只读),可能有多个
    • UpperDir:容器可写层
    • MergedDir:合并后的统一视图
    • WorkDir:overlay内部使用的工作目录
    1.5.2 写时复制(Copy-on-Write)机制

    写时复制是容器存储的核心机制。当容器需要修改一个存在于镜像层中的文件时,Docker不会直接修改镜像层(因为镜像是只读的),而是:

  • 将文件从镜像层(LowerDir)复制到可写层(UpperDir)
  • 在可写层中对文件进行修改
  • 后续的读取操作会优先从可写层读取
  • 这个机制确保了:

    • 镜像层保持不变,多个容器可以共享同一个镜像
    • 每个容器有自己的修改副本,互不影响
    • 只有被修改的文件才会被复制,节省存储空间

    # 演示写时复制
    # 启动容器
    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 数据持久化层的角色

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

  • 挂载点覆盖:当数据卷挂载到容器的某个路径时,该路径上的可写层内容会被"遮盖",容器看到的是数据卷中的内容。
  • 独立生命周期:数据卷的创建、删除与容器的创建、删除完全独立。删除容器不会删除数据卷。
  • 直接I/O:数据卷的读写操作直接作用于宿主机文件系统,不经过存储驱动的写时复制机制,因此性能更好。
  • 初始内容复制:对于命名数据卷,如果挂载点在镜像中有内容,首次挂载时Docker会将镜像中的内容复制到数据卷中(对于绑定挂载则不会)。
  • # 演示数据卷挂载点覆盖
    # 使用nginx镜像,其默认网站根目录为/usr/share/nginx/html
    docker run -d –name nginx-test -v nginx-html:/usr/share/nginx/html nginx:latest

    # 查看数据卷中的内容(会包含nginx默认的欢迎页面,因为首次挂载时从镜像复制)
    docker exec nginx-test ls /usr/share/nginx/html
    # 输出: 50x.html index.html

    # 修改数据卷中的内容
    docker exec nginx-test bash -c 'echo "<h1>Custom Content</h1>" > /usr/share/nginx/html/index.html'

    # 删除容器
    docker rm -f nginx-test

    # 重新创建容器并挂载同一个数据卷
    docker run -d –name nginx-test2 -v nginx-html:/usr/share/nginx/html nginx:latest

    # 验证数据仍然存在
    docker exec nginx-test2 cat /usr/share/nginx/html/index.html
    # 输出: <h1>Custom Content</h1>

    # 清理
    docker rm -f nginx-test2
    docker volume rm nginx-html

    这个例子清楚地展示了数据卷如何绕过容器可写层,实现数据的持久化存储。


    第二章 存储驱动详解

    2.1 存储驱动的作用(管理镜像层和容器可写层)

    存储驱动是Docker存储架构的核心组件,负责管理镜像层(Image Layers)和容器可写层(Container Writable Layer)。它的主要职责包括:

    2.1.1 镜像层管理

    Docker镜像由多个只读层(Layer)组成,每一层对应Dockerfile中的一条指令。存储驱动负责:

    • 层叠加:将多个只读层叠加成一个统一的文件系统视图
    • 层共享:多个镜像可以共享相同的底层,节省存储空间
    • 层缓存:构建镜像时利用缓存层加速构建过程

    # 每条指令都会创建一个新的镜像层
    FROM ubuntu:22.04 # 基础层(来自ubuntu镜像)
    RUN apt-get update # 新层
    RUN apt-get install -y nginx # 新层
    COPY index.html /var/www/html/ # 新层
    CMD ["nginx", "-g", "daemon off;"] # 新层(元数据层)

    # 查看镜像的分层结构
    docker history nginx:latest

    # 输出示例:
    # IMAGE CREATED CREATED BY SIZE
    # 605c77e624dd 2 weeks ago /bin/sh -c #(nop) CMD ["nginx" "g" "daemon… 0B
    # 8140c0a4wr2e 2 weeks ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B
    # 41a1a8e923e3 2 weeks ago /bin/sh -c #(nop) EXPOSE 80 0B
    # 12ad5138f5b1 2 weeks ago /bin/sh -c #(nop) ENTRYPOINT ["/docker-entr… 0B
    # …

    2.1.2 容器可写层管理

    当容器启动时,存储驱动会在镜像层之上添加一个可写层:

    • 写操作处理:所有写操作都记录在可写层中
    • 写时复制:修改镜像层文件时,先复制到可写层再修改
    • 删除操作处理:在可写层中标记文件为已删除(whiteout文件)
    2.1.3 存储驱动必须实现的核心功能

    一个完整的存储驱动需要实现以下核心功能:

    功能说明
    层叠加 将多个只读层与一个可写层叠加为统一视图
    写时复制 修改文件时按需从只读层复制到可写层
    层共享 不同镜像/容器共享相同的只读层
    层去重 相同内容的层只存储一份
    快速创建 快速创建容器(不需要复制整个镜像)
    快速删除 快速删除容器和镜像层

    2.2 overlay2驱动详解

    2.2.1 overlay2简介

    overlay2是Docker当前推荐的存储驱动,它是OverlayFS的用户空间实现。OverlayFS是Linux内核3.18版本引入的联合文件系统(Union Filesystem),overlay2是其第二代实现,支持多层lowerdir叠加(最多128层),比第一代overlay驱动更高效。

    overlay2的优势:

    • 内核原生支持,性能优异
    • 支持多层叠加(最多128层),无需为每层创建单独的挂载
    • 页缓存共享,内存利用率高
    • 稳定性好,适合生产环境
    • Docker官方推荐

    系统要求:

    • Linux内核4.0或更高版本(推荐4.0+)
    • 文件系统支持:ext4、xfs(需要ftype=1)、btrfs等
    2.2.2 overlay2工作原理

    overlay2将文件系统分为三层:

  • LowerDir(下层):只读的镜像层,可以有多个(最多128层)。这些层从下到上叠加,上层覆盖下层。
  • UpperDir(上层):容器的可写层,所有写操作都发生在这里。
  • 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的文件操作机制

    读取文件:

  • 先在UpperDir中查找文件
  • 如果UpperDir中没有,则依次在LowerDir中从上到下查找
  • 返回第一个找到的文件
  • 修改文件(写时复制):

  • 文件在UpperDir中:直接修改
  • 文件在LowerDir中:将文件从LowerDir复制到UpperDir,然后在UpperDir中修改
  • 删除文件:

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

  • 在UpperDir中创建目录
  • 如果LowerDir中有同名目录,需要在UpperDir中设置opaque属性,表示该目录不与下层合并
  • 2.2.4 查看overlay2的实际结构

    # 启动一个容器
    docker run -d –name overlay-test nginx:latest

    # 查看容器的存储驱动信息
    docker inspect overlay-test –format '{{json .GraphDriver.Data}}' | python3 -m json.tool

    # 输出示例:
    # {
    # "LowerDir": "/var/lib/docker/overlay2/abc…/diff:/var/lib/docker/overlay2/def…/diff:…",
    # "MergedDir": "/var/lib/docker/overlay2/xyz…/merged",
    # "UpperDir": "/var/lib/docker/overlay2/xyz…/diff",
    # "WorkDir": "/var/lib/docker/overlay2/xyz…/work"
    # }

    # 查看合并后的文件系统视图
    sudo ls $(docker inspect overlay-test –format '{{.GraphDriver.Data.MergedDir}}')/

    # 查看可写层(初始为空或很小)
    sudo ls -la $(docker inspect overlay-test –format '{{.GraphDriver.Data.UpperDir}}')/

    # 在容器中修改文件
    docker exec overlay-test bash -c 'echo "test" > /tmp/overlay-test.txt'

    # 再次查看可写层,可以看到新创建的文件
    sudo ls -la $(docker inspect overlay-test –format '{{.GraphDriver.Data.UpperDir}}')/tmp/

    # 清理
    docker rm -f overlay-test

    2.2.5 overlay2的whiteout文件

    # 演示whiteout机制
    docker run -d –name whiteout-test ubuntu:22.04 sleep 3600

    # 获取UpperDir
    UPPER=$(docker inspect whiteout-test –format '{{.GraphDriver.Data.UpperDir}}')

    # 删除容器中的一个文件(该文件来自镜像层)
    docker exec whiteout-test rm /etc/hostname

    # 查看UpperDir中的whiteout文件
    sudo ls -la $UPPER/etc/
    # 可以看到一个名为 hostname 的字符设备文件(whiteout)
    # crwxr-xr-x 1 root root 0, 0 … hostname

    # 清理
    docker rm -f whiteout-test

    2.3 aufs驱动详解

    2.3.1 aufs简介

    aufs(Advanced Multi-Layered Unification Filesystem)是Docker早期使用的存储驱动之一,也是最早被Docker支持的联合文件系统。aufs功能强大,支持多层叠加,但由于未能被Linux内核主线接受,目前使用较少。

    aufs的特点:

    • 支持多层叠加(最多127层)
    • 功能成熟稳定
    • 未被Linux内核主线接受,需要额外安装
    • 在Ubuntu/Debian上较常见,在其他发行版上可能不可用
    • Docker已不再推荐使用aufs
    2.3.2 aufs工作原理

    aufs的工作原理与overlay2类似,都是通过联合挂载实现多层叠加:

    容器视角
    ┌──────────┐
    │ /usr/ │
    │ /etc/ │
    │ /var/ │
    └────┬─────┘

    ┌───────────────┼───────────────┐
    │ │ │
    ▼ ▼ ▼
    ┌────────┐ ┌────────┐ ┌────────┐
    │ 可写层 │ │ 只读层3 │ … │ 只读层1 │
    │(container)│ │(image) │ │(base) │
    └────────┘ └────────┘ └────────┘

    与overlay2不同的是:

    • aufs使用aufs挂载类型,而非overlay
    • aufs的whiteout实现方式不同(使用.wh.前缀文件)
    • aufs支持更多的分支策略(round-robin等)
    2.3.3 aufs的目录结构

    # aufs驱动下,容器的存储目录
    # /var/lib/docker/aufs/diff/<container_id>/ → 可写层
    # /var/lib/docker/aufs/diff/<layer_id>/ → 镜像层
    # /var/lib/docker/aufs/mnt/<container_id>/ → 合并视图
    # /var/lib/docker/aufs/layers/<container_id> → 层依赖关系

    2.3.4 为什么aufs逐渐被淘汰
  • 内核不支持:aufs未被Linux内核主线接受,需要打补丁
  • 维护困难:需要为每个内核版本维护补丁
  • overlay2更优:overlay2被内核原生支持,性能和功能都优于aufs
  • 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的两种模式
  • loop-lvm模式(默认但不推荐):使用回环设备(loop device)模拟块设备,性能差,仅用于测试。
  • 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模式

    // /etc/docker/daemon.json
    {
    "storage-driver": "devicemapper",
    "storage-opts": [
    "dm.directlvm_device=/dev/sdb",
    "dm.thinp_percent=95",
    "dm.thinp_metapercent=1",
    "dm.thinp_autoextend_threshold=80",
    "dm.thinp_autoextend_percent=20",
    "dm.directlvm_device_force=false"
    ]
    }

    # 配置完成后重启Docker
    sudo systemctl restart docker

    # 验证配置
    docker info | grep -A 10 "Storage Driver"
    # 输出应显示:
    # Storage Driver: devicemapper
    # Pool Name: docker-thinpool
    # Pool Blocksize: 65.54kB
    # …

    2.5 btrfs驱动详解

    2.5.1 btrfs简介

    btrfs(B-tree file system,通常发音为"Butter FS"或"B-tree FS")是现代的写时复制文件系统,Docker可以利用btrfs的子卷(Subvolume)和快照(Snapshot)功能来实现存储驱动。

    btrfs的特点:

    • 基于子卷和快照,原生支持写时复制
    • 支持透明压缩,节省存储空间
    • 支持子卷配额,可以限制容器存储大小
    • 适合大量小文件场景
    • 稳定性曾受质疑,但在不断改善
    • 需要将/var/lib/docker格式化为btrfs文件系统
    2.5.2 btrfs工作原理

    ┌─────────────────────────────────┐
    │ btrfs 文件系统 │
    │ │
    │ ┌───────────────────────────┐ │
    │ │ /var/lib/docker/btrfs/ │ │
    │ │ │ │
    │ │ ┌─────────┐ ┌─────────┐ │ │
    │ │ │ 子卷:容器 │ │ 子卷:镜像 │ │ │
    │ │ │ (可写) │ │ (只读) │ │ │
    │ │ │ (快照) │ │ │ │ │
    │ │ └─────────┘ └─────────┘ │ │
    │ │ │ │
    │ └───────────────────────────┘ │
    │ │
    │ 透明压缩 │ 快照 │ 配额 │ 去重 │
    └─────────────────────────────────┘

    • 镜像层:btrfs子卷(只读)
    • 容器可写层:镜像层子卷的快照(可读写)
    • 写时复制:btrfs原生支持,在文件系统层面实现
    2.5.3 配置btrfs驱动

    # 1. 格式化设备为btrfs
    sudo mkfs.btrfs /dev/sdb

    # 2. 挂载到/var/lib/docker
    sudo mkdir -p /var/lib/docker
    sudo mount /dev/sdb /var/lib/docker

    # 3. 配置Docker使用btrfs
    # /etc/docker/daemon.json
    # {
    # "storage-driver": "btrfs"
    # }

    # 4. 重启Docker
    sudo systemctl restart docker

    # 5. 验证
    docker info | grep "Storage Driver"
    # 输出: Storage Driver: btrfs

    2.5.4 btrfs的配额管理

    # 启用配额
    sudo btrfs quota enable /var/lib/docker

    # 设置子卷配额(限制为10GB)
    sudo btrfs qgroup limit 10G /var/lib/docker/btrfs/subvolumes/<container_id>

    # 查看配额使用情况
    sudo btrfs qgroup show /var/lib/docker

    2.6 zfs驱动详解

    2.6.1 zfs简介

    ZFS(Zettabyte File System)是由Sun Microsystems开发的高级文件系统,以其数据完整性、快照功能和海量存储支持而闻名。Docker可以利用ZFS的数据集(Dataset)和快照(Snapshot)功能实现存储驱动。

    zfs的特点:

    • 高级文件系统,支持数据完整性校验
    • 原生快照和克隆功能
    • 内置压缩和去重
    • ARC(Adaptive Replacement Cache)缓存,性能优异
    • 内存占用较高
    • 许可证(CDDL)与Linux内核(GPL)兼容性存在争议
    2.6.2 zfs工作原理

    ┌─────────────────────────────────┐
    │ zfs 存储池 (zpool) │
    │ │
    │ ┌───────────────────────────┐ │
    │ │ docker/zfs (文件系统) │ │
    │ │ │ │
    │ │ ┌─────────┐ ┌─────────┐ │ │
    │ │ │数据集:容器│ │数据集:镜像│ │ │
    │ │ │ (快照) │ │ (基础) │ │ │
    │ │ └─────────┘ └─────────┘ │ │
    │ │ │ │
    │ └───────────────────────────┘ │
    │ │
    │ 数据完整性 │ 快照 │ 压缩 │ 缓存 │
    └─────────────────────────────────┘

    • 镜像层:ZFS数据集(只读快照)
    • 容器可写层:镜像层的克隆(Clone)
    • 写时复制:ZFS原生支持
    2.6.3 配置zfs驱动

    # 1. 安装ZFS
    sudo apt-get install zfsutils-linux # Ubuntu/Debian
    # 或
    sudo yum install zfs # CentOS/RHEL

    # 2. 创建zpool
    sudo zpool create -f zpool-docker /dev/sdb

    # 3. 创建docker数据集
    sudo zfs create zpool-docker/docker

    # 4. 设置/var/lib/docker挂载点
    sudo zfs set mountpoint=/var/lib/docker zpool-docker/docker

    # 5. 配置Docker使用zfs
    # /etc/docker/daemon.json
    # {
    # "storage-driver": "zfs"
    # }

    # 6. 重启Docker
    sudo systemctl restart docker

    # 7. 验证
    docker info | grep "Storage Driver"
    # 输出: Storage Driver: zfs

    2.6.4 zfs配额和压缩

    # 设置容器数据集配额
    sudo zfs set quota=10G zpool-docker/docker/<container_id>

    # 启用压缩
    sudo zfs set compression=lz4 zpool-docker/docker

    # 查看压缩效果
    sudo zfs get compressratio zpool-docker/docker

    2.7 vfs驱动详解

    2.7.1 vfs简介

    vfs(Virtual File System)是Docker最简单的存储驱动,它不使用任何联合文件系统技术,而是通过简单的文件复制来实现层的叠加。

    vfs的特点:

    • 实现简单,兼容性最好
    • 没有任何写时复制优化,每个层都是完整的文件复制
    • 磁盘空间占用极大
    • 性能最差
    • 不推荐在生产环境使用
    • 适合测试环境或不支持其他驱动的系统
    2.7.2 vfs工作原理

    容器视角
    ┌──────────┐
    │ /usr/ │
    │ /etc/ │
    │ /var/ │
    └────┬─────┘

    ┌────┴────┐
    │ 可写层 │ ← 完整复制镜像层的所有文件
    └────┬────┘
    ┌────┴────┐
    │ 镜像层3 │ ← 完整复制前一层的所有文件
    └────┬────┘
    ┌────┴────┐
    │ 镜像层2 │ ← 完整复制前一层的所有文件
    └────┬────┘
    ┌────┴────┐
    │ 镜像层1 │ ← 基础层
    └─────────┘

    在vfs驱动中,每一层都是完整复制前一层的所有文件,然后在其基础上进行修改。这意味着:

    • 创建容器时会复制整个镜像的所有文件
    • 磁盘空间占用是叠加的(不是共享的)
    • 没有层共享机制
    2.7.3 何时使用vfs

    # vfs驱动通常在不支持其他驱动的环境中使用
    # 例如某些NAS环境或受限的容器运行时

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

    # 重启Docker
    sudo systemctl restart docker

    2.8 存储驱动对比表

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

    # 查看Docker信息
    docker info | grep -i "storage"

    # 输出示例:
    # Storage Driver: overlay2
    # Backing Filesystem: extfs
    # Supports d_type: true
    # Native Overlay Diff: true
    # userxattr: false

    2.9.3 切换存储驱动

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

    # 1. 停止Docker服务
    sudo systemctl stop docker

    # 2. 备份现有数据(可选但强烈推荐)
    sudo cp -au /var/lib/docker /var/lib/docker.backup

    # 3. 编辑Docker配置
    sudo tee /etc/docker/daemon.json <<'EOF'
    {
    "storage-driver": "overlay2"
    }
    EOF

    # 4. 删除旧数据(谨慎操作!)
    # 如果是全新切换,可以删除旧数据
    # sudo rm -rf /var/lib/docker

    # 5. 启动Docker服务
    sudo systemctl start docker

    # 6. 验证新的存储驱动
    docker info | grep "Storage Driver"

    # 7. 重新拉取镜像(因为旧镜像在新驱动下不可用)
    docker pull nginx:latest

    2.9.4 各发行版默认存储驱动
    发行版默认存储驱动推荐存储驱动
    Ubuntu 20.04+ overlay2 overlay2
    Debian 10+ overlay2 overlay2
    CentOS 8+ overlay2 overlay2
    RHEL 8+ overlay2 overlay2
    Alpine overlay2 overlay2
    SUSE btrfs(如在btrfs上) overlay2

    2.10 存储驱动配置

    2.10.1 daemon.json中的存储驱动配置

    // /etc/docker/daemon.json 完整配置示例
    {
    "storage-driver": "overlay2",
    "storage-opts": [
    "overlay2.override_kernel_check=true"
    ],
    "data-root": "/var/lib/docker",
    "max-concurrent-downloads": 10,
    "max-concurrent-uploads": 5,
    "log-driver": "json-file",
    "log-opts": {
    "max-size": "10m",
    "max-file": "3"
    }
    }

    2.10.2 验证存储驱动支持

    # 检查内核是否支持overlay
    grep overlay /proc/filesystems
    # 输出: nodev overlay

    # 检查文件系统是否支持d_type(overlay2需要)
    sudo xfs_info / | grep ftype
    # 输出: ftype=1 (需要为1)

    # 检查内核版本
    uname -r
    # 输出应 >= 4.0

    2.10.3 修改数据根目录

    在某些情况下,系统盘空间不足,需要将Docker数据目录迁移到其他磁盘:

    # 1. 停止Docker
    sudo systemctl stop docker

    # 2. 创建新目录
    sudo mkdir -p /data/docker

    # 3. 修改配置
    sudo tee /etc/docker/daemon.json <<'EOF'
    {
    "data-root": "/data/docker",
    "storage-driver": "overlay2"
    }
    EOF

    # 4. 迁移现有数据
    sudo rsync -aP /var/lib/docker/ /data/docker/

    # 5. 启动Docker
    sudo systemctl start docker

    # 6. 验证
    docker info | grep "Docker Root Dir"
    # 输出: Docker Root Dir: /data/docker

    # 7. 确认无误后删除旧目录
    sudo rm -rf /var/lib/docker

    2.10.4 存储驱动相关的故障排查

    # 查看Docker守护进程日志
    sudo journalctl -u docker.service –no-pager | tail -50

    # 常见存储驱动错误及解决方法:

    # 错误1: "overlay2 not supported"
    # 原因: 内核版本过低或文件系统不支持
    # 解决: 升级内核(>=4.0)或更换文件系统(使用ext4/xfs并设置ftype=1)

    # 错误2: "Failed to start Docker Container Engine"
    # 原因: 存储驱动配置错误或数据损坏
    # 解决: 检查daemon.json配置,必要时清除/var/lib/docker重新初始化

    # 错误3: "no space left on device"
    # 原因: 磁盘空间不足
    # 解决: 清理无用镜像和容器(docker system prune),或迁移数据根目录


    第三章 Docker Volume(数据卷)

    3.1 数据卷的概念与特点

    3.1.1 什么是数据卷

    数据卷(Volume)是Docker原生提供的数据持久化机制。它是一个由Docker守护进程管理的特殊目录,存在于宿主机上,可以被一个或多个容器挂载使用。数据卷完全独立于容器的生命周期——即使所有使用它的容器都被删除,数据卷及其数据依然存在。

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

    数据卷默认存储在宿主机的/var/lib/docker/volumes/目录下:

    /var/lib/docker/volumes/
    ├── my-volume/ # 命名数据卷
    │ ├── _data/ # 实际数据存储目录
    │ │ ├── file1.txt
    │ │ └── subdir/
    │ │ └── file2.txt
    │ └── metadata.json # 数据卷元数据? (实际不存在此文件)
    ├── f3b4…45e/ # 匿名数据卷(使用哈希值命名)
    │ └── _data/
    │ └── …
    └── backingFsBlockDev # 系统文件

    在不同平台上,数据卷的存储位置有所不同:

    平台存储位置
    Linux /var/lib/docker/volumes/
    Docker Desktop (macOS) 在Linux VM内部
    Docker Desktop (Windows) 在WSL2 VM内部
    Windows Containers C:\\ProgramData\\docker\\volumes\\

    3.2 docker volume create创建数据卷

    3.2.1 基本创建命令

    # 创建一个命名数据卷
    docker volume create my-data-volume

    # 输出: my-data-volume

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

    3.2.2 创建时指定选项

    # 创建数据卷时指定标签(label)
    docker volume create –label environment=production –label app=web my-labeled-volume

    # 创建数据卷时指定驱动(默认为local)
    docker volume create –driver local my-local-volume

    # 创建数据卷时指定驱动选项
    # 例如:创建一个NFS数据卷
    docker volume create \\
    –driver local \\
    –opt type=nfs \\
    –opt o=addr=192.168.1.100,rw \\
    –opt device=:/path/to/nfs/share \\
    my-nfs-volume

    # 创建一个tmpfs类型的数据卷(存储在内存中)
    docker volume create \\
    –driver local \\
    –opt type=tmpfs \\
    –opt device=tmpfs \\
    –opt o=size=100m,mode=0755 \\
    my-tmpfs-volume

    # 创建一个btrfs子卷
    docker volume create \\
    –driver local \\
    –opt type=btrfs \\
    –opt device=/dev/sdb \\
    –opt o=subvol=/docker-volumes/my-subvol \\
    my-btrfs-volume

    3.2.3 查看创建结果

    # 查看数据卷详细信息
    docker volume inspect my-data-volume

    # 输出:
    # [
    # {
    # "CreatedAt": "2024-01-15T10:30:00Z",
    # "Driver": "local",
    # "Labels": {},
    # "Mountpoint": "/var/lib/docker/volumes/my-data-volume/_data",
    # "Name": "my-data-volume",
    # "Options": {},
    # "Scope": "local"
    # }
    # ]

    # 在宿主机上查看数据卷目录
    sudo ls -la /var/lib/docker/volumes/my-data-volume/
    # 输出:
    # total 4
    # drwxr-xr-x 3 root root 4096 Jan 15 10:30 .
    # drwxr-xr-x 3 root root 4096 Jan 15 10:30 ..
    # drwxr-xr-x 2 root root 40 Jan 15 10:30 _data

    3.3 docker volume ls/rm/inspect/prune管理数据卷

    3.3.1 docker volume ls – 列出数据卷

    # 列出所有数据卷
    docker volume ls

    # 输出示例:
    # DRIVER VOLUME NAME
    # local my-data-volume
    # local my-labeled-volume
    # local nginx-html
    # local f3b4a2c5e8d1… # 匿名数据卷(哈希命名)

    # 按名称过滤
    docker volume ls –filter name=my

    # 输出:
    # DRIVER VOLUME NAME
    # local my-data-volume
    # local my-labeled-volume

    # 按标签过滤
    docker volume ls –filter label=environment=production

    # 输出:
    # DRIVER VOLUME NAME
    # local my-labeled-volume

    # 只显示数据卷名称(不显示DRIVER列)
    docker volume ls -q

    # 输出:
    # my-data-volume
    # my-labeled-volume

    3.3.2 docker volume inspect – 查看详情

    # 查看单个数据卷详情
    docker volume inspect my-data-volume

    # 查看多个数据卷详情
    docker volume inspect my-data-volume my-labeled-volume

    # 只查看挂载点路径
    docker volume inspect my-data-volume –format '{{.Mountpoint}}'
    # 输出: /var/lib/docker/volumes/my-data-volume/_data

    # 只查看驱动名称
    docker volume inspect my-data-volume –format '{{.Driver}}'
    # 输出: local

    # 查看标签
    docker volume inspect my-labeled-volume –format '{{.Labels}}'
    # 输出: map[app:web environment:production]

    3.3.3 docker volume rm – 删除数据卷

    # 删除单个数据卷
    docker volume rm my-data-volume

    # 删除多个数据卷
    docker volume rm volume1 volume2 volume3

    # 注意:正在被容器使用的数据卷无法删除
    docker volume rm my-data-volume
    # 报错: Error response from daemon: unable to remove volume:
    # remove my-data-volume: volume is in use

    # 强制删除使用中的数据卷(需要先删除使用它的容器)
    docker rm -f container_using_volume
    docker volume rm my-data-volume

    3.3.4 docker volume prune – 清理无用数据卷

    # 删除所有未被容器使用的数据卷
    docker volume prune

    # 输出:
    # WARNING! This will remove all local volumes not used by at least one container.
    # Are you sure you want to continue? [y/N] y
    # Deleted Volumes:
    # unused-volume-1
    # unused-volume-2
    # Total reclaimed space: 1.2GB

    # 跳过确认提示
    docker volume prune -f

    # 按标签过滤删除
    docker volume prune –filter label!=keep=true

    # 删除超过24小时未使用的数据卷
    docker volume prune –filter "until=24h"

    3.3.5 数据卷管理最佳实践

    # 1. 为数据卷添加描述性标签
    docker volume create –label app=mysql –label env=prod –label backup=daily mysql-data

    # 2. 使用有意义的名称
    docker volume create mysql_prod_data # 好的命名
    # docker volume create vol1 # 不好的命名

    # 3. 定期检查无用数据卷
    docker volume ls -f dangling=true

    # 4. 定期清理
    docker volume prune -f # 谨慎使用,确保没有正在使用的匿名卷

    # 5. 备份重要的数据卷(后续章节详细讲解)

    3.4 在docker run中使用数据卷

    3.4.1 基本用法(-v参数)

    # 使用命名数据卷
    docker run -d \\
    –name web-server \\
    -v web-data:/usr/share/nginx/html \\
    nginx:latest

    # 参数说明:
    # -v web-data:/usr/share/nginx/html
    # web-data: 数据卷名称(Docker会自动创建如果不存在)
    # /usr/share/nginx/html: 容器内的挂载路径

    3.4.2 使用–mount参数(推荐)

    # 使用–mount参数(语法更明确,推荐使用)
    docker run -d \\
    –name web-server \\
    –mount type=volume,src=web-data,dst=/usr/share/nginx/html \\
    nginx:latest

    # 参数说明:
    # –mount type=volume: 指定挂载类型为数据卷
    # src=web-data: 源数据卷名称(也可以用source=)
    # dst=/usr/share/nginx/html: 目标路径(也可以用destination=或target=)

    # 等价于:
    # docker run -d –name web-server -v web-data:/usr/share/nginx/html nginx:latest

    3.4.3 -v与–mount的对比

    # -v 参数: 简洁但不够明确
    # 格式: -v <volume_name>:<container_path>[:<options>]
    docker run -d -v web-data:/data:ro nginx:latest

    # –mount 参数: 语法明确,功能更丰富
    # 格式: –mount type=volume,src=<name>,dst=<path>[,<options>]
    docker run -d \\
    –mount type=volume,src=web-data,dst=/data,readonly \\
    nginx:latest

    # –mount 的优势:
    # 1. 语法更明确,每个参数都有键名
    # 2. 更容易阅读和理解
    # 3. 支持更多选项(如volume-nocopy)
    # 4. 适用于Swarm服务

    3.4.4 挂载多个数据卷

    # 一个容器可以挂载多个数据卷
    docker run -d \\
    –name app-server \\
    -v app-data:/app/data \\
    -v app-logs:/app/logs \\
    -v app-config:/app/config \\
    my-app:latest

    # 使用–mount语法
    docker run -d \\
    –name app-server \\
    –mount type=volume,src=app-data,dst=/app/data \\
    –mount type=volume,src=app-logs,dst=/app/logs \\
    –mount type=volume,src=app-config,dst=/app/config \\
    my-app:latest

    3.4.5 volume-nocopy选项

    默认情况下,当命名数据卷首次挂载到一个非空目录时,Docker会将容器镜像中该目录的内容复制到数据卷中。可以使用volume-nocopy选项禁用此行为:

    # 默认行为:复制镜像内容到数据卷
    docker run -d \\
    –name web1 \\
    –mount type=volume,src=web-data,dst=/usr/share/nginx/html \\
    nginx:latest
    # web-data会包含nginx默认的index.html和50x.html

    # 禁止复制:不复制镜像内容
    docker run -d \\
    –name web2 \\
    –mount type=volume,src=web-data2,dst=/usr/share/nginx/html,volume-nocopy \\
    nginx:latest
    # web-data2为空,不会包含nginx默认文件

    3.5 数据卷的自动创建行为

    3.5.1 自动创建命名数据卷

    # 如果指定的命名数据卷不存在,Docker会自动创建
    docker run -d \\
    –name auto-create-test \\
    -v non-existent-volume:/data \\
    alpine:latest sleep 3600

    # 查看是否自动创建了数据卷
    docker volume ls | grep non-existent-volume
    # 输出: local non-existent-volume

    # 自动创建的数据卷没有任何特殊标签
    docker volume inspect non-existent-volume

    3.5.2 自动创建匿名数据卷

    # 如果只指定容器路径而不指定数据卷名称,Docker会创建匿名数据卷
    docker run -d \\
    –name anonymous-volume-test \\
    -v /data \\
    alpine:latest sleep 3600

    # 查看创建的匿名数据卷(名称为长哈希值)
    docker volume ls
    # 输出包含:
    # DRIVER VOLUME NAME
    # local f3b4a2c5e8d1234567890abcdef1234567890abcdef1234567890abcdef1234

    # 查看匿名数据卷的详细信息
    docker inspect anonymous-volume-test –format '{{json .Mounts}}' | python3 -m json.tool
    # 输出:
    # [
    # {
    # "Type": "volume",
    # "Name": "f3b4a2c5…",
    # "Source": "/var/lib/docker/volumes/f3b4a2c5…/_data",
    # "Destination": "/data",
    # "Driver": "local",
    # "Mode": "",
    # "RW": true,
    # "Propagation": ""
    # }
    # ]

    3.5.3 自动创建的风险与注意事项

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

    # 演示数据卷泄漏
    for i in $(seq 1 10); do
    docker run –rm -v /tmp/data alpine echo "creating anonymous volume $i"
    done

    # 查看
    docker volume ls
    # 会发现有10个匿名数据卷残留

    # 清理
    docker volume prune -f

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

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

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

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

    3.6.1 默认存储位置

    # 数据卷默认存储在
    # /var/lib/docker/volumes/<volume_name>/_data/

    # 查看数据卷的存储路径
    docker volume inspect my-data-volume –format '{{.Mountpoint}}'
    # 输出: /var/lib/docker/volumes/my-data-volume/_data

    # 直接在宿主机上操作数据卷内容
    sudo ls -la /var/lib/docker/volumes/my-data-volume/_data/

    # 在宿主机上创建文件(会反映到容器中)
    sudo echo "Hello from host" > /var/lib/docker/volumes/my-data-volume/_data/host-created.txt

    # 在容器中验证
    docker run –rm -v my-data-volume:/data alpine cat /data/host-created.txt
    # 输出: Hello from host

    3.6.2 修改数据卷存储位置

    // /etc/docker/daemon.json
    {
    "data-root": "/data/docker"
    }

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

    3.6.3 数据卷的文件系统权限

    # 数据卷在宿主机上默认由root拥有
    sudo ls -la /var/lib/docker/volumes/
    # drwxr-xr-x root root …
    # drwxr-xr-x root root …

    # 如果容器内以非root用户运行,可能遇到权限问题
    # 解决方法1:在Dockerfile中调整权限
    # Dockerfile:
    # RUN mkdir -p /data && chown 1000:1000 /data
    # USER 1000

    # 解决方法2:在宿主机上修改数据卷权限
    sudo chown -R 1000:1000 /var/lib/docker/volumes/my-data-volume/_data/

    # 解决方法3:使用初始化容器设置权限
    docker run –rm -v my-data-volume:/data alpine chown -R 1000:1000 /data

    3.7 只读数据卷

    3.7.1 创建只读数据卷

    # 使用-v参数创建只读数据卷
    docker run -d \\
    –name readonly-test \\
    -v readonly-data:/data:ro \\
    alpine:latest sleep 3600

    # 使用–mount参数创建只读数据卷
    docker run -d \\
    –name readonly-test2 \\
    –mount type=volume,src=readonly-data,dst=/data,readonly \\
    alpine:latest sleep 3600

    # :ro 或 readonly 表示只读

    3.7.2 验证只读属性

    # 读取数据卷中的文件(正常)
    docker exec readonly-test cat /data/somefile.txt

    # 写入数据卷(会失败)
    docker exec readonly-test sh -c 'echo "test" > /data/newfile.txt'
    # 报错: sh: can't create /data/newfile.txt: Read-only file system

    # 创建子目录(也会失败)
    docker exec readonly-test mkdir /data/newdir
    # 报错: mkdir: can't create directory '/data/newdir': Read-only file system

    3.7.3 只读数据卷的应用场景

    # 场景1:配置文件只读挂载(防止应用意外修改配置)
    docker run -d \\
    –name config-server \\
    -v app-config:/etc/app/config:ro \\
    my-app:latest

    # 场景2:静态资源只读挂载
    docker run -d \\
    –name static-server \\
    -v static-assets:/var/www/static:ro \\
    nginx:latest

    # 场景3:只读数据 + 可写日志(一个只读,一个可写)
    docker run -d \\
    –name mixed-server \\
    -v app-data:/app/data:ro \\
    -v app-logs:/app/logs \\
    my-app:latest

    3.8 数据卷的权限与所有权

    3.8.1 权限问题分析

    当容器内以非root用户运行时,可能会遇到数据卷权限问题。这是因为数据卷在宿主机上由root创建,容器内的非root用户可能没有读写权限。

    # 示例:以非root用户运行的容器遇到权限问题
    docker run -d \\
    –name permission-test \\
    –user 1000:1000 \\
    -v perm-data:/data \\
    alpine:latest sleep 3600

    # 尝试写入文件
    docker exec permission-test sh -c 'echo "test" > /data/test.txt'
    # 可能报错: sh: can't create /data/test.txt: Permission denied

    3.8.2 解决权限问题的方法

    方法1:在Dockerfile中预设权限

    # Dockerfile
    FROM alpine:latest

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

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

    CMD ["sleep", "3600"]

    方法2:使用初始化容器

    # 步骤1:使用root用户初始化数据卷权限
    docker run –rm \\
    -v perm-data:/data \\
    alpine:latest chown -R 1000:1000 /data

    # 步骤2:以非root用户运行应用
    docker run -d \\
    –name app \\
    –user 1000:1000 \\
    -v perm-data:/data \\
    my-app:latest

    方法3:在宿主机上直接修改权限

    # 获取数据卷路径
    VOLUME_PATH=$(docker volume inspect perm-data –format '{{.Mountpoint}}')

    # 修改所有权
    sudo chown -R 1000:1000 $VOLUME_PATH

    # 修改权限
    sudo chmod -R 755 $VOLUME_PATH

    方法4:使用Docker Compose的init容器

    # docker-compose.yml
    version: '3.8'

    services:
    init-permissions:
    image: alpine:latest
    volumes:
    appdata:/data
    command: chown R 1000:1000 /data
    user: root

    app:
    image: myapp:latest
    depends_on:
    initpermissions
    user: "1000:1000"
    volumes:
    appdata:/data

    volumes:
    app-data:

    3.9 命名数据卷 vs 匿名数据卷

    3.9.1 命名数据卷(Named Volume)

    # 命名数据卷:有明确的名称
    docker run -d \\
    –name app \\
    -v my-app-data:/data \\
    my-app:latest

    # 特点:
    # 1. 有明确的、可读的名称
    # 2. 可以通过名称引用和管理
    # 3. 适合生产环境
    # 4. 便于备份、迁移和共享

    3.9.2 匿名数据卷(Anonymous Volume)

    # 匿名数据卷:只指定容器路径,不指定数据卷名称
    docker run -d \\
    –name app \\
    -v /data \\
    my-app:latest

    # 特点:
    # 1. 名称是随机生成的哈希值
    # 2. 难以管理和识别
    # 3. 容器删除后容易成为"孤儿"数据卷
    # 4. 不推荐在生产环境使用

    3.9.3 对比
    特性命名数据卷匿名数据卷
    名称 有明确名称 随机哈希值
    可读性
    管理难度
    可复用性
    适用场景 生产环境 临时测试
    清理难度 低(按名称删除) 高(需找出对应哈希)
    3.9.4 Dockerfile中的VOLUME指令产生的匿名卷

    # Dockerfile中使用VOLUME指令会创建匿名数据卷
    FROM nginx:latest
    VOLUME /data

    # 构建并运行
    docker build -t my-volume-image .
    docker run -d –name vol-test my-volume-image

    # 查看挂载信息,会发现一个匿名数据卷
    docker inspect vol-test –format '{{json .Mounts}}' | python3 -m json.tool

    3.10 数据卷实战: MySQL数据持久化

    3.10.1 需求分析

    我们需要运行一个MySQL容器,要求:

  • 数据库数据持久化,容器删除后数据不丢失
  • 配置文件外部管理,方便修改
  • 日志文件持久化
  • 初始化SQL脚本自动执行
  • 3.10.2 完整实现

    # 步骤1:创建数据卷
    docker volume create mysql_data
    docker volume create mysql_logs
    docker volume create mysql_config

    # 步骤2:创建配置文件
    mkdir -p /opt/mysql/conf
    cat > /opt/mysql/conf/my.cnf << 'EOF'
    [mysqld]
    character-set-server=utf8mb4
    collation-server=utf8mb4_unicode_ci
    max_connections=500
    innodb_buffer_pool_size=256M
    slow_query_log=1
    long_query_time=2
    EOF

    # 步骤3:创建初始化SQL脚本
    mkdir -p /opt/mysql/init
    cat > /opt/mysql/init/init.sql << 'EOF'
    CREATE DATABASE IF NOT EXISTS appdb DEFAULT CHARACTER SET utf8mb4;
    CREATE USER IF NOT EXISTS 'appuser'@'%' IDENTIFIED BY 'apppass123';
    GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'%';
    FLUSH PRIVILEGES;
    EOF

    # 步骤4:启动MySQL容器
    docker run -d \\
    –name mysql-prod \\
    –restart unless-stopped \\
    -p 3306:3306 \\
    -e MYSQL_ROOT_PASSWORD=RootPass123! \\
    -e MYSQL_DATABASE=appdb \\
    -e MYSQL_USER=appuser \\
    -e MYSQL_PASSWORD=apppass123 \\
    -v mysql_data:/var/lib/mysql \\
    -v mysql_logs:/var/log/mysql \\
    -v /opt/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro \\
    -v /opt/mysql/init:/docker-entrypoint-initdb.d:ro \\
    mysql:8.0

    # 步骤5:验证MySQL运行状态
    docker exec mysql-prod mysql -uroot -pRootPass123! -e "SHOW DATABASES;"
    # 输出:
    # +——————–+
    # | Database |
    # +——————–+
    # | appdb |
    # | information_schema |
    # | mysql |
    # | performance_schema |
    # | sys |
    # +——————–+

    # 步骤6:写入测试数据
    docker exec mysql-prod mysql -uroot -pRootPass123! appdb -e "
    CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    INSERT INTO users (name, email) VALUES ('张三', 'zhangsan@example.com');
    INSERT INTO users (name, email) VALUES ('李四', 'lisi@example.com');
    SELECT * FROM users;
    "

    # 步骤7:模拟容器故障 – 删除并重建容器
    docker rm -f mysql-prod

    # 重新启动容器(使用相同的数据卷)
    docker run -d \\
    –name mysql-prod \\
    –restart unless-stopped \\
    -p 3306:3306 \\
    -e MYSQL_ROOT_PASSWORD=RootPass123! \\
    -v mysql_data:/var/lib/mysql \\
    -v mysql_logs:/var/log/mysql \\
    -v /opt/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro \\
    mysql:8.0

    # 步骤8:验证数据依然存在
    docker exec mysql-prod mysql -uroot -pRootPass123! appdb -e "SELECT * FROM users;"
    # 输出:
    # +—-+——–+———————-+———————+
    # | id | name | email | created_at |
    # +—-+——–+———————-+———————+
    # | 1 | 张三 | zhangsan@example.com | 2024-01-15 10:30:00 |
    # | 2 | 李四 | lisi@example.com | 2024-01-15 10:30:00 |
    # +—-+——–+———————-+———————+

    # 数据完好无损!这就是数据卷持久化的威力。

    3.10.3 使用Docker Compose管理

    # docker-compose.yml
    version: '3.8'

    services:
    mysql:
    image: mysql:8.0
    container_name: mysqlprod
    restart: unlessstopped
    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:/dockerentrypointinitdb.d:ro
    healthcheck:
    test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-pRootPass123!"]
    interval: 10s
    timeout: 5s
    retries: 5

    volumes:
    mysql_data:
    name: mysql_data
    mysql_logs:
    name: mysql_logs

    # 启动
    docker-compose up -d

    # 查看状态
    docker-compose ps

    # 查看日志
    docker-compose logs -f mysql

    # 停止
    docker-compose down
    # 注意:down不会删除数据卷,但down -v会删除数据卷!
    # docker-compose down -v # 危险!会删除所有数据!


    第四章 Bind Mount(绑定挂载)

    4.1 绑定挂载的概念与特点

    4.1.1 什么是绑定挂载

    绑定挂载(Bind Mount)是Docker提供的另一种数据持久化机制。与数据卷不同,绑定挂载将宿主机上的一个绝对路径直接挂载到容器中。容器对该路径下文件的读写操作直接作用于宿主机的文件系统,两者之间是"双向同步"的。

    绑定挂载在Docker的早期版本中就已经存在,可以说是最古老的数据持久化方式之一。它的概念来源于Linux的mount –bind命令,即将一个目录树挂载到另一个目录树上。

    4.1.2 绑定挂载的核心特点
  • 依赖宿主机路径:必须指定宿主机上的绝对路径,路径不存在时Docker会自动创建。
  • 双向同步:容器内修改文件会反映到宿主机,宿主机修改文件也会反映到容器内。
  • 绕过存储驱动:与数据卷一样,绑定挂载直接操作宿主机文件系统,不经过存储驱动。
  • 不自动复制内容:与命名数据卷不同,绑定挂载不会将镜像中的内容复制到宿主机路径。如果宿主机路径为空,容器内挂载点也是空的(会"遮盖"镜像中的原有内容)。
  • 无法通过docker volume管理:绑定挂载不是数据卷,不能用docker volume命令管理。
  • 路径依赖性强:在不同主机上运行时,宿主机路径必须一致,可移植性较差。
  • 4.1.3 绑定挂载的工作原理

    ┌─────────────────────────────────────┐
    │ 宿主机 │
    │ │
    │ /home/user/project/ │
    │ ├── src/ │
    │ │ ├── app.py │
    │ │ └── utils.py │
    │ ├── config/ │
    │ │ └── settings.json │
    │ └── requirements.txt │
    │ │
    │ ┌─────────────────────────────────┐│
    │ │ Docker 容器 ││
    │ │ ││
    │ │ /app/ ← 绑定挂载点 ││
    │ │ ├── src/ ← 同步 ││
    │ │ │ ├── app.py ← 同步 ││
    │ │ │ └── utils.py ← 同步 ││
    │ │ ├── config/ ← 同步 ││
    │ │ │ └── settings.json ← 同步 ││
    │ │ └── requirements.txt ← 同步 ││
    │ │ ││
    │ └─────────────────────────────────┘│
    └─────────────────────────────────────┘

    当绑定挂载生效时,容器内挂载点路径上的内容会被宿主机路径的内容"覆盖"。即使镜像中该路径下有文件,这些文件也会被隐藏。

    4.2 在docker run中使用绑定挂载

    4.2.1 基本用法(-v参数)

    # 使用-v参数创建绑定挂载
    docker run -d \\
    –name dev-server \\
    -v /home/user/project:/app \\
    node:18-alpine

    # 参数说明:
    # -v /home/user/project:/app
    # /home/user/project: 宿主机绝对路径
    # /app: 容器内挂载路径
    # 注意:宿主机路径以 / 开头表示绑定挂载(不是以/开头表示数据卷名称)

    4.2.2 区分绑定挂载和数据卷

    # 绑定挂载:宿主机路径以/开头(或./、~/等绝对/相对路径)
    docker run -v /home/user/data:/data alpine sleep 3600 # 绑定挂载
    docker run -v ./data:/data alpine sleep 3600 # 绑定挂载(相对路径)
    docker run -v ~/data:/data alpine sleep 3600 # 绑定挂载(家目录)

    # 数据卷:不以/开头的是数据卷名称
    docker run -v my-volume:/data alpine sleep 3600 # 命名数据卷
    docker run -v /data alpine sleep 3600 # 匿名数据卷(只有容器路径)

    4.2.3 挂载单个文件

    # 绑定挂载也可以挂载单个文件(而不仅仅是目录)
    docker run -d \\
    –name nginx-custom \\
    -v /opt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \\
    nginx:latest

    # 注意:挂载单个文件时,宿主机上该文件必须存在
    # 如果文件不存在,Docker会将其创建为一个目录!

    # 演示文件挂载的错误情况
    # 如果文件不存在,Docker会创建目录
    docker run –rm -v /tmp/nonexist.conf:/etc/config.conf alpine ls -la /etc/config.conf
    # /etc/config.conf 实际上是一个目录!

    # 正确做法:先创建文件
    touch /tmp/config.conf
    docker run –rm -v /tmp/config.conf:/etc/config.conf alpine cat /etc/config.conf
    # 正常工作

    4.2.4 挂载多个绑定挂载

    docker run -d \\
    –name full-dev-env \\
    -v /home/user/project/src:/app/src \\
    -v /home/user/project/config:/app/config:ro \\
    -v /home/user/project/logs:/app/logs \\
    -v /home/user/.ssh:/root/.ssh:ro \\
    node:18-alpine

    4.3 使用–mount参数(更明确的语法)

    4.3.1 –mount语法

    # 使用–mount参数创建绑定挂载
    docker run -d \\
    –name dev-server \\
    –mount type=bind,src=/home/user/project,dst=/app \\
    node:18-alpine

    # 参数说明:
    # type=bind: 指定挂载类型为绑定挂载
    # src=/home/user/project: 宿主机源路径(也可以用source=)
    # dst=/app: 容器内目标路径(也可以用destination=或target=)

    4.3.2 –mount的完整选项

    docker run -d \\
    –name dev-server \\
    –mount \\
    type=bind,\\
    src=/home/user/project,\\
    dst=/app,\\
    readonly,\\
    bind-propagation=rprivate \\
    node:18-alpine

    # 选项说明:
    # type=bind: 绑定挂载类型
    # src: 宿主机源路径
    # dst: 容器目标路径
    # readonly: 只读挂载
    # bind-propagation: 绑定传播模式(rprivate/private/rslave/slave/rshared/shared)

    4.3.3 绑定传播(Bind Propagation)

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

    # 演示绑定传播
    # 准备目录结构
    mkdir -p /tmp/propagation/source
    mkdir -p /tmp/propagation/dest
    echo "source content" > /tmp/propagation/source/file.txt

    # rprivate(默认): 不会传播任何挂载
    docker run –rm \\
    –mount type=bind,src=/tmp/propagation/source,dst=/tmp/propagation/dest,bind-propagation=rprivate \\
    alpine ls /tmp/propagation/dest

    # shared: 挂载点中的挂载会传播到原挂载,反之亦然
    # slave: 只接收原挂载的传播,不反向传播
    # rshared: 递归shared
    # rslave: 递归slave
    # private: 不传播
    # rprivate: 递归private(默认)

    传播模式说明
    rprivate 递归私有(默认),不传播任何挂载
    private 私有,不传播挂载
    rshared 递归共享,双向传播所有挂载
    shared 共享,双向传播直接挂载
    rslave 递归从属,只接收原挂载的传播
    slave 从属,只接收原挂载的直接传播

    4.4 绑定挂载的绝对路径要求

    4.4.1 绝对路径规则

    # 绑定挂载的宿主机路径必须是绝对路径
    # 正确:绝对路径
    docker run -v /home/user/data:/data alpine sleep 3600

    # 正确:使用$(pwd)展开为绝对路径
    docker run -v $(pwd)/data:/data alpine sleep 3600

    # 错误:相对路径(不以/开头会被当作数据卷名称)
    docker run -v data:/data alpine sleep 3600
    # Docker会创建一个名为"data"的数据卷,而不是绑定挂载!

    # 正确:使用相对路径展开
    docker run -v "$(pwd)/data":/data alpine sleep 3600

    4.4.2 使用相对路径的正确方式

    # 在项目目录中使用绑定挂载
    cd /home/user/myproject

    # 使用$(pwd)获取当前目录的绝对路径
    docker run -d \\
    –name dev \\
    -v "$(pwd)":/app \\
    node:18-alpine

    # 或者使用–mount语法(同样需要绝对路径)
    docker run -d \\
    –name dev \\
    –mount type=bind,src=$(pwd),dst=/app \\
    node:18-alpine

    # 在Windows上使用PowerShell
    docker run -d `
    –name dev `

    -v "${PWD}:/app" `
    node:18-alpine

    # 在Windows上使用CMD
    docker run -d ^
    –name dev ^
    -v "%cd%:/app" ^
    node:18-alpine

    4.4.3 路径不存在时的行为

    # 如果宿主机路径不存在,Docker会自动创建一个目录
    docker run –rm -v /tmp/nonexistent-dir:/data alpine ls -la /data
    # /data目录会被创建(为空)

    # 但如果挂载的是文件且文件不存在,Docker会创建一个目录(而非文件)
    docker run –rm -v /tmp/nonexistent-file.txt:/config.txt alpine ls -la /config.txt
    # /config.txt实际上是一个目录,而非文件!
    # 这可能导致应用报错

    # 解决方法:先在宿主机上创建文件
    touch /tmp/config.txt
    docker run –rm -v /tmp/config.txt:/config.txt alpine cat /config.txt

    4.5 只读绑定挂载

    4.5.1 创建只读绑定挂载

    # 使用-v参数
    docker run -d \\
    –name config-server \\
    -v /opt/app/config:/app/config:ro \\
    my-app:latest

    # 使用–mount参数
    docker run -d \\
    –name config-server \\
    –mount type=bind,src=/opt/app/config,dst=/app/config,readonly \\
    my-app:latest

    4.5.2 只读绑定挂载的应用场景

    # 场景1:只读配置文件
    docker run -d \\
    –name nginx-server \\
    -v /opt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \\
    -v /opt/nginx/conf.d:/etc/nginx/conf.d:ro \\
    nginx:latest

    # 场景2:只读静态资源
    docker run -d \\
    –name static-server \\
    -v /opt/static:/usr/share/nginx/html:ro \\
    nginx:latest

    # 场景3:只读SSL证书
    docker run -d \\
    –name https-server \\
    -v /opt/ssl/certs:/etc/ssl/certs:ro \\
    -v /opt/ssl/private:/etc/ssl/private:ro \\
    nginx:latest

    # 场景4:可读写的代码目录 + 只读的配置目录
    docker run -d \\
    –name dev-server \\
    -v /home/user/project/src:/app/src \\
    -v /opt/app/config:/app/config:ro \\
    node:18-alpine

    4.6 绑定挂载与文件同步

    4.6.1 实时双向同步

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

    # 启动一个绑定挂载的容器
    mkdir -p /tmp/sync-test
    docker run -d \\
    –name sync-test \\
    -v /tmp/sync-test:/data \\
    alpine:latest sleep 3600

    # 在宿主机创建文件
    echo "Hello from host" > /tmp/sync-test/host-file.txt

    # 在容器中查看(立即可见)
    docker exec sync-test cat /data/host-file.txt
    # 输出: Hello from host

    # 在容器中创建文件
    docker exec sync-test sh -c 'echo "Hello from container" > /data/container-file.txt'

    # 在宿主机查看(立即可见)
    cat /tmp/sync-test/container-file.txt
    # 输出: Hello from container

    # 清理
    docker rm -f sync-test
    rm -rf /tmp/sync-test

    4.6.2 inotify事件传播

    绑定挂载支持文件系统事件(inotify)的传播,这使得文件监听工具(如nodemon、webpack-dev-server等)可以在容器内正常运行:

    # 在开发环境中使用nodemon监听文件变化
    docker run -d \\
    –name node-dev \\
    -v $(pwd):/app \\
    -w /app \\
    node:18-alpine \\
    npx nodemon app.js

    # 当你在宿主机上修改app.js时,nodemon会检测到变化并自动重启

    4.7 绑定挂载在开发环境中的应用

    4.7.1 代码热重载(Node.js)

    # 项目结构
    # /home/user/myapp/
    # ├── package.json
    # ├── src/
    # │ ├── index.js
    # │ └── routes.js
    # ├── node_modules/
    # └── Dockerfile

    # Dockerfile
    # FROM node:18-alpine
    # WORKDIR /app
    # COPY package.json .
    # RUN npm install
    # EXPOSE 3000
    # CMD ["npm", "start"]

    # 使用绑定挂载实现热重载
    docker run -d \\
    –name node-dev \\
    -p 3000:3000 \\
    -v /home/user/myapp:/app \\
    -w /app \\
    node:18-alpine \\
    npx nodemon –watch src src/index.js

    # 修改宿主机上的src/index.js,容器内的应用会自动重启

    4.7.2 Python Flask热重载

    # 使用Flask的debug模式实现热重载
    docker run -d \\
    –name flask-dev \\
    -p 5000:5000 \\
    -v $(pwd):/app \\
    -w /app \\
    python:3.11-slim \\
    bash -c "pip install flask && flask –app app run –host=0.0.0.0 –debug –reload"

    # 修改app.py,Flask会自动重载

    4.7.3 避免node_modules覆盖问题

    # 问题:绑定挂载会覆盖容器内的node_modules
    # 解决方法:使用匿名数据卷保护node_modules

    docker run -d \\
    –name node-dev \\
    -p 3000:3000 \\
    -v $(pwd):/app \\
    -v /app/node_modules \\
    -w /app \\
    node:18-alpine \\
    npm start

    # 说明:
    # -v $(pwd):/app: 绑定挂载项目目录
    # -v /app/node_modules: 匿名数据卷,保护容器内的node_modules不被宿主机覆盖
    # Docker会先处理绑定挂载,再处理匿名卷,所以node_modules会被单独挂载

    4.8 绑定挂载的权限问题

    4.8.1 Linux上的权限问题

    # 在Linux上,绑定挂载直接使用宿主机的UID/GID
    # 如果容器内以非root用户运行,需要确保UID匹配

    # 查看宿主机当前用户的UID
    id -u # 例如:1000

    # 方法1:Dockerfile中创建相同UID的用户
    # Dockerfile:
    # FROM node:18-alpine
    # RUN addgroup -g 1000 appgroup && adduser -u 1000 -G appgroup -s /bin/sh -D appuser
    # USER appuser

    # 方法2:运行时指定用户
    docker run -d \\
    –name dev \\
    –user $(id -u):$(id -g) \\
    -v $(pwd):/app \\
    node:18-alpine

    4.8.2 macOS上的特殊处理

    在macOS上使用Docker Desktop时,绑定挂载通过一个Linux虚拟机中转,存在以下特点:

    # macOS上Docker Desktop使用VirtioFS或osxfs进行文件共享
    # 1. 文件权限自动转换
    # 2. 性能比Linux原生绑定挂载稍差
    # 3. 可以通过Docker Desktop设置配置文件共享路径

    # 在Docker Desktop中设置:
    # Settings → Resources → File Sharing
    # 确保绑定的路径在共享列表中

    # 性能优化:使用缓存选项(仅macOS)
    # 在docker-compose.yml中:
    # volumes:
    # – type: bind
    # source: ./src
    # target: /app/src
    # consistency: cached # macOS专用,提高读取性能
    # # consistency: delegated # 容器内的修改延迟同步到宿主机
    # # consistency: consistent # 默认,完全一致

    4.8.3 Windows上的特殊处理

    # 在Windows上,路径格式不同
    # 使用PowerShell:
    docker run d `
    name dev `
    v "${PWD}\\src:/app/src" `
    node:18-alpine

    # 或者使用正斜杠(推荐):
    docker run d `
    name dev `
    v "${PWD}/src:/app/src" `
    node:18-alpine

    # Windows路径的特殊处理:
    # 1. 盘符路径(C:\\…)会被转换为Linux路径
    # 2. 使用Docker Desktop的WSL2后端性能更好
    # 3. 需要在Docker Desktop设置中开启文件共享

    # 注意Windows上的换行符问题(CRLF vs LF)
    # 建议:.gitattributes中设置 * text=auto eol=lf

    4.9 Bind Mount vs Volume对比

    对比项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
    FROM node:18-alpine

    # 设置工作目录
    WORKDIR /app

    # 复制package.json并安装依赖
    COPY package.json package-lock.json ./
    RUN npm ci –only=production

    # 安装开发依赖
    RUN npm install –only=dev

    # 暴露端口
    EXPOSE 3000

    # 使用nodemon启动(支持热重载)
    CMD ["npx", "nodemon", "–watch", "src", "src/index.js"]

    4.10.3 docker-compose.yml

    # docker-compose.yml
    version: '3.8'

    services:
    webapp:
    build: .
    container_name: webappdev
    ports:
    "3000:3000"
    volumes:
    # 绑定挂载:源代码目录(支持热重载)
    ./src:/app/src
    # 绑定挂载:配置文件(只读)
    ./config:/app/config:ro
    # 匿名数据卷:保护node_modules不被覆盖
    /app/node_modules
    environment:
    NODE_ENV=development
    CHOKIDAR_USEPOLLING=true # 确保文件变化检测在所有平台上正常工作
    restart: unlessstopped

    4.10.4 运行与验证

    # 启动开发环境
    docker-compose up -d

    # 查看日志
    docker-compose logs -f webapp

    # 修改src/index.js(在宿主机上)
    echo 'app.get("/health", (req, res) => res.json({status: "ok"}));' >> src/index.js

    # nodemon会自动检测到变化并重启应用
    # 日志中会出现: [nodemon] restarting due to changes…

    # 测试新添加的路由
    curl http://localhost:3000/health
    # 输出: {"status":"ok"}

    # 修改配置文件
    echo '{"debug": true}' > config/config.json

    # 应用可以读取最新的配置文件内容(因为绑定挂载是实时的)

    4.10.5 开发环境的最佳实践

    # 1. 使用.dockerignore避免不必要的文件被COPY
    # .dockerignore:
    # node_modules
    # .git
    # *.md
    # .env

    # 2. 使用docker-compose.override.yml覆盖开发配置
    # docker-compose.yml(基础配置)
    # docker-compose.override.yml(开发覆盖配置,自动加载)

    # 3. 使用环境变量管理配置
    # .env文件:
    # NODE_ENV=development
    # PORT=3000

    # 4. 开发工具集成
    # VS Code的Dev Containers扩展可以直接在容器内开发
    # 使用.devcontainer/devcontainer.json配置

    // .devcontainer/devcontainer.json
    {
    "name": "Node.js Dev",
    "build": {
    "dockerfile": "../Dockerfile"
    },
    "forwardPorts": [3000],
    "mounts": [
    "source=${localWorkspaceFolder}/src,target=/app/src,type=bind,consistency=cached",
    "source=${localWorkspaceFolder}/config,target=/app/config,type=bind"
    ],
    "remoteEnv": {
    "NODE_ENV": "development"
    },
    "postCreateCommand": "npm install"
    }


    第五章 tmpfs Mount(临时文件系统)

    5.1 tmpfs挂载的概念与特点

    5.1.1 什么是tmpfs挂载

    tmpfs挂载是Docker提供的第三种存储方式。与数据卷和绑定挂载不同,tmpfs将数据存储在宿主机的**内存(RAM)**中,而不是磁盘上。这意味着tmpfs中的数据读写速度极快,但容器停止后数据就会消失,不会持久化到磁盘。

    tmpfs是Linux内核提供的临时文件系统,它将一部分内存作为文件系统使用。Docker利用这一机制,允许容器将特定路径挂载为tmpfs,实现高速临时存储。

    5.1.2 tmpfs挂载的核心特点
  • 存储在内存中:数据存储在宿主机RAM中,不写入磁盘。
  • 极快的读写速度:内存读写速度远超磁盘,适合高频读写场景。
  • 临时性:容器停止后数据消失,不会在磁盘上留下痕迹。
  • 安全性高:数据不落盘,适合存储敏感信息(如密钥、令牌)。
  • 受内存限制:可用空间受宿主机可用内存限制。
  • 仅支持Linux:tmpfs是Linux特性,在Windows容器上不支持。
  • 单容器使用:tmpfs挂载不能在多个容器之间共享。
  • 无法通过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参数

    # 使用–tmpfs参数创建tmpfs挂载
    docker run -d \\
    –name tmpfs-test \\
    –tmpfs /app/temp \\
    alpine:latest sleep 3600

    # 参数说明:
    # –tmpfs /app/temp: 在容器内的/app/temp路径挂载tmpfs

    # 验证tmpfs
    docker exec tmpfs-test df -h /app/temp
    # 输出:
    # Filesystem Size Used Available Use% Mounted on
    # tmpfs 64.0M 0 64.0M 0% /app/temp

    # 写入数据
    docker exec tmpfs-test sh -c 'echo "temp data" > /app/temp/test.txt'
    docker exec tmpfs-test cat /app/temp/test.txt
    # 输出: temp data

    5.2.2 使用–mount参数

    # 使用–mount参数创建tmpfs挂载(语法更明确)
    docker run -d \\
    –name tmpfs-test2 \\
    –mount type=tmpfs,dst=/app/temp \\
    alpine:latest sleep 3600

    # 带选项的tmpfs挂载
    docker run -d \\
    –name tmpfs-test3 \\
    –mount type=tmpfs,dst=/app/temp,tmpfs-size=100m,tmpfs-mode=1777 \\
    alpine:latest sleep 3600

    # 参数说明:
    # type=tmpfs: 指定挂载类型为tmpfs
    # dst=/app/temp: 容器内目标路径
    # tmpfs-size=100m: tmpfs大小限制为100MB
    # tmpfs-mode=1777: tmpfs的权限模式

    5.2.3 –tmpfs与–mount的区别
    特性–tmpfs–mount type=tmpfs
    语法 简洁 明确
    大小限制 不支持单独设置 支持(tmpfs-size)
    权限模式 不支持单独设置 支持(tmpfs-mode)
    Swarm支持 不支持 支持
    推荐度 简单场景 复杂场景/生产环境

    5.3 tmpfs挂载的选项

    5.3.1 tmpfs-size(大小限制)

    # 设置tmpfs大小为64MB
    docker run -d \\
    –name tmpfs-sized \\
    –mount type=tmpfs,dst=/app/temp,tmpfs-size=64m \\
    alpine:latest sleep 3600

    # 验证大小限制
    docker exec tmpfs-sized df -h /app/temp
    # 输出:
    # Filesystem Size Used Available Use% Mounted on
    # tmpfs 64.0M 0 64.0M 0% /app/temp

    # 测试写入超过限制的数据(会失败)
    docker exec tmpfs-sized sh -c 'dd if=/dev/zero of=/app/temp/bigfile bs=1M count=100'
    # 报错: dd: writing '/app/temp/bigfile': No space left on device

    # 注意:如果不设置tmpfs-size,默认大小为宿主机内存的一半

    5.3.2 tmpfs-mode(权限模式)

    # 设置tmpfs权限为1777(所有用户可读写执行)
    docker run -d \\
    –name tmpfs-mode-test \\
    –mount type=tmpfs,dst=/app/temp,tmpfs-mode=1777 \\
    alpine:latest sleep 3600

    # 验证权限
    docker exec tmpfs-mode-test ls -ld /app/temp
    # 输出: drwxrwxrwt 2 root root 40 Jan 15 10:30 /app/temp

    # 设置权限为700(仅root可访问)
    docker run -d \\
    –name tmpfs-private \\
    –mount type=tmpfs,dst=/app/temp,tmpfs-mode=1700 \\
    alpine:latest sleep 3600

    docker exec tmpfs-private ls -ld /app/temp
    # 输出: drwx—— 2 root root 40 Jan 15 10:30 /app/temp

    5.3.3 完整选项列表
    选项说明示例
    tmpfs-size tmpfs最大大小(字节) tmpfs-size=100m
    tmpfs-mode 文件权限模式(八进制) tmpfs-mode=1777
    tmpfs-mode 可以与其他tmpfs选项组合 –tmpfs /app/temp:rw,size=100m,mode=1777

    5.4 tmpfs的内存使用

    5.4.1 内存消耗机制

    tmpfs使用宿主机的内存来存储数据。需要注意的是:

  • 动态分配:tmpfs不会预分配全部内存,而是按需分配。
  • 可被交换:如果系统内存不足,tmpfs中的数据可能被交换到swap分区。
  • 影响容器内存限制:tmpfs占用的内存计入容器的内存使用。
  • # 查看宿主机内存使用情况
    free -h

    # 启动使用tmpfs的容器
    docker run -d \\
    –name mem-test \\
    –mount type=tmpfs,dst=/app/temp,tmpfs-size=512m \\
    alpine:latest sleep 3600

    # 在容器中写入数据
    docker exec mem-test sh -c 'dd if=/dev/zero of=/app/temp/bigfile bs=1M count=200'

    # 查看宿主机内存变化
    free -h
    # 可以看到可用内存减少了约200MB

    # 清理
    docker rm -f mem-test

    5.4.2 与容器内存限制的关系

    # 如果容器设置了内存限制,tmpfs的内存使用也计入限制
    docker run -d \\
    –name mem-limited \\
    –memory=256m \\
    –mount type=tmpfs,dst=/app/temp,tmpfs-size=512m \\
    alpine:latest sleep 3600

    # 尝试写入超过内存限制的数据
    docker exec mem-limited sh -c 'dd if=/dev/zero of=/app/temp/bigfile bs=1M count=200'
    # 可能会触发OOM Killer,导致进程被杀

    # 查看容器是否因OOM被杀
    docker inspect mem-limited –format '{{.State.OOMKilled}}'
    # 输出: true (如果被OOM杀掉)

    5.5 tmpfs适用场景

    5.5.1 存储敏感信息

    # 场景:临时存储API密钥或令牌
    docker run -d \\
    –name secure-app \\
    –mount type=tmpfs,dst=/app/secrets,tmpfs-mode=1700 \\
    my-app:latest

    # 应用将敏感信息写入/app/secrets,容器停止后信息消失
    # 不会在磁盘上留下任何痕迹

    5.5.2 高速临时缓存

    # 场景:应用缓存目录
    docker run -d \\
    –name cache-app \\
    –mount type=tmpfs,dst=/app/cache,tmpfs-size=256m \\
    my-app:latest

    # 缓存数据存储在内存中,读写速度极快
    # 容器重启后缓存自动清空(从后端重新加载)

    5.5.3 减少磁盘I/O

    # 场景:临时文件处理
    docker run -d \\
    –name file-processor \\
    –mount type=tmpfs,dst=/tmp/processing,tmpfs-size=512m \\
    my-app:latest

    # 临时文件在内存中处理,减少磁盘I/O
    # 处理完成后,结果写入持久化存储(数据卷)

    5.5.4 不适合tmpfs的场景

    # 不适合:需要持久化的数据
    # 数据库数据 → 使用Volume
    # 应用日志 → 使用Volume或日志驱动
    # 用户上传文件 → 使用Volume

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

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

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

    5.6.1 需求场景

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

    5.6.2 完整实现

    # 步骤1:创建应用镜像
    mkdir -p /tmp/secure-app
    cat > /tmp/secure-app/Dockerfile << 'EOF'
    FROM python:3.11-slim

    WORKDIR /app

    RUN pip install flask

    COPY app.py .

    EXPOSE 5000

    CMD ["python", "app.py"]
    EOF

    cat > /tmp/secure-app/app.py << 'PYEOF'
    import os
    import json
    from flask import Flask, request, jsonify

    app = Flask(__name__)

    SECRETS_DIR = '/app/secrets'

    @app.route('/secret', methods=['POST'])
    def store_secret():
    """临时存储密钥(仅存在于内存中)"""
    data = request.json
    key = data.get('key')
    value = data.get('value')

    if not key or not value:
    return jsonify({'error': 'key and value required'}), 400

    # 写入tmpfs(内存中)
    with open(f'{SECRETS_DIR}/{key}.txt', 'w') as f:
    f.write(value)

    return jsonify({'message': f'Secret "{key}" stored in memory'}), 200

    @app.route('/secret/<key>', methods=['GET'])
    def get_secret(key):
    """读取临时密钥"""
    try:
    with open(f'{SECRETS_DIR}/{key}.txt', 'r') as f:
    value = f.read()
    return jsonify({'key': key, 'value': value}), 200
    except FileNotFoundError:
    return jsonify({'error': 'Secret not found'}), 404

    @app.route('/secrets', methods=['GET'])
    def list_secrets():
    """列出所有临时密钥"""
    files = os.listdir(SECRETS_DIR)
    return jsonify({'secrets': files}), 200

    @app.route('/health', methods=['GET'])
    def health():
    return jsonify({'status': 'healthy'}), 200

    if __name__ == '__main__':
    # 确保密钥目录存在
    os.makedirs(SECRETS_DIR, exist_ok=True)
    app.run(host='0.0.0.0', port=5000)
    PYEOF

    # 步骤2:构建镜像
    docker build -t secure-app /tmp/secure-app/

    # 步骤3:使用tmpfs启动容器
    docker run -d \\
    –name secure-service \\
    -p 5000:5000 \\
    –mount type=tmpfs,dst=/app/secrets,tmpfs-size=64m,tmpfs-mode=1700 \\
    –memory=256m \\
    secure-app

    # 步骤4:存储密钥
    curl -X POST http://localhost:5000/secret \\
    -H "Content-Type: application/json" \\
    -d '{"key": "db_password", "value": "SuperSecret123!"}'
    # 输出: {"message":"Secret \\"db_password\\" stored in memory"}

    curl -X POST http://localhost:5000/secret \\
    -H "Content-Type: application/json" \\
    -d '{"key": "api_key", "value": "ak_1234567890abcdef"}'
    # 输出: {"message":"Secret \\"api_key\\" stored in memory"}

    # 步骤5:读取密钥
    curl http://localhost:5000/secret/db_password
    # 输出: {"key":"db_password","value":"SuperSecret123!"}

    # 步骤6:列出所有密钥
    curl http://localhost:5000/secrets
    # 输出: {"secrets":["db_password.txt","api_key.txt"]}

    # 步骤7:验证密钥不在磁盘上
    # 查看容器挂载信息
    docker inspect secure-service –format '{{json .Mounts}}' | python3 -m json.tool
    # 输出显示tmpfs挂载,没有磁盘路径

    # 步骤8:重启容器,验证密钥消失
    docker restart secure-service
    sleep 3

    curl http://localhost:5000/secrets
    # 输出: {"secrets":[]}
    # 密钥已消失!因为tmpfs数据在容器重启后清除

    # 清理
    docker rm -f secure-service
    rm -rf /tmp/secure-app

    5.6.3 使用Docker Compose

    # docker-compose.yml
    version: '3.8'

    services:
    secure-service:
    build: .
    container_name: secureservice
    ports:
    "5000:5000"
    tmpfs:
    /app/secrets:size=64m,mode=1700
    mem_limit: 256m
    restart: unlessstopped
    healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
    interval: 30s
    timeout: 5s
    retries: 3

    # 启动
    docker-compose up -d

    # 查看tmpfs挂载
    docker exec secure-service df -h /app/secrets
    # 输出:
    # Filesystem Size Used Available Use% Mounted on
    # tmpfs 64.0M 0 64.0M 0% /app/secrets

    5.6.4 tmpfs安全注意事项

    # 1. tmpfs数据可能被交换到swap分区
    # 如果需要确保数据绝不落盘,需要禁用swap
    docker run -d \\
    –name ultra-secure \\
    –mount type=tmpfs,dst=/app/secrets \\
    –memory=256m \\
    –memory-swap=0 \\
    secure-app
    # –memory-swap=0: 禁止内存交换,确保tmpfs数据不被写入swap

    # 2. 使用tmpfs时注意内存监控
    # tmpfs占用的内存会影响容器和宿主机的可用内存

    # 3. tmpfs不适合长期存储大量数据
    # 仅适合临时、少量的敏感数据


    第六章 数据卷进阶操作

    6.1 数据卷驱动与远程数据卷

    6.1.1 数据卷驱动概述

    数据卷驱动(Volume Driver)是Docker数据卷系统的扩展机制。默认的数据卷驱动是local,它将数据存储在宿主机的本地文件系统上。通过安装第三方数据卷驱动插件,可以将数据卷存储在各种远程存储系统上,如NFS、Amazon EBS、Google Persistent Disk、Azure File Storage等。

    数据卷驱动的作用:

    • 抽象底层存储:为Docker提供统一的存储接口,屏蔽底层存储差异
    • 支持远程存储:将数据存储在网络存储上,实现跨主机数据共享
    • 支持云存储:与云厂商的块存储、文件存储服务集成
    • 支持高级特性:如加密、压缩、快照等
    6.1.2 内置数据卷驱动

    Docker内置了local驱动,它支持几种存储类型:

    # 1. 默认本地目录(最常见)
    docker volume create my-local-volume
    # 存储: /var/lib/docker/volumes/my-local-volume/_data/

    # 2. NFS类型
    docker volume create \\
    –driver local \\
    –opt type=nfs \\
    –opt o=addr=192.168.1.100,nfsvers=4,rw \\
    –opt device=:/path/to/nfs/share \\
    my-nfs-volume

    # 3. CIFS/SMB类型(Windows共享)
    docker volume create \\
    –driver local \\
    –opt type=cifs \\
    –opt o=addr=192.168.1.200,username=user,password=pass,rw \\
    –opt device=/sharename \\
    my-smb-volume

    # 4. tmpfs类型
    docker volume create \\
    –driver local \\
    –opt type=tmpfs \\
    –opt device=tmpfs \\
    –opt o=size=100m,mode=0755 \\
    my-tmpfs-volume

    # 5. btrfs子卷
    docker volume create \\
    –driver local \\
    –opt type=btrfs \\
    –opt device=/dev/sdb \\
    –opt o=subvol=/docker-volumes/my-subvol \\
    my-btrfs-volume

    # 6. 指定已有的块设备
    docker volume create \\
    –driver local \\
    –opt type=ext4 \\
    –opt device=/dev/sdc1 \\
    my-block-volume

    6.1.3 第三方数据卷驱动插件

    # 查看已安装的插件
    docker plugin ls

    # 安装插件示例(以Docker NFS插件为例)
    docker plugin install leapbit/docker-nfs:latest

    # 使用插件创建数据卷
    docker volume create \\
    –driver leapbit/docker-nfs:latest \\
    –opt nfsserver=192.168.1.100 \\
    –opt nfspath=/shared/data \\
    –opt nfsoptions=nfsvers=4 \\
    my-remote-volume

    6.2 使用数据卷插件(local、nfs、block)

    6.2.1 local驱动详解

    local驱动是Docker默认的数据卷驱动,它支持多种存储后端:

    # 查看local驱动支持的选项
    docker volume create –driver local –help

    # local驱动支持的–opt选项:
    # type: 文件系统类型(nfs, cifs, tmpfs, ext4, btrfs等)
    # device: 设备路径或远程共享路径
    # o: 挂载选项(对应mount命令的-o参数)

    # 示例:创建一个基于ext4文件系统的数据卷
    # 前提:/dev/sdb1已经格式化为ext4
    docker volume create \\
    –driver local \\
    –opt type=ext4 \\
    –opt device=/dev/sdb1 \\
    my-ext4-volume

    # 使用该数据卷
    docker run -d \\
    –name db \\
    -v my-ext4-volume:/var/lib/mysql \\
    mysql:8.0

    6.2.2 NFS数据卷插件

    NFS(Network File System)是常用的网络文件系统协议,适合多主机共享数据:

    # 方法1:使用local驱动创建NFS数据卷(推荐)
    docker volume create \\
    –driver local \\
    –opt type=nfs \\
    –opt o=addr=192.168.1.100,nfsvers=4,soft,timeo=60,retrans=2 \\
    –opt device=:/data/shared \\
    –name nfs-shared-data

    # 参数说明:
    # type=nfs: 使用NFS文件系统
    # o=addr=192.168.1.100: NFS服务器地址
    # nfsvers=4: NFS版本4
    # soft: 软挂载(NFS不可用时返回错误而非挂起)
    # timeo=60: 超时时间(60秒)
    # retrans=2: 重试次数
    # device=:/data/shared: NFS服务器上的共享路径

    # 使用NFS数据卷
    docker run -d \\
    –name shared-app \\
    -v nfs-shared-data:/app/data \\
    my-app:latest

    # 在多台主机上创建同名NFS数据卷,实现跨主机数据共享
    # 主机A:
    docker volume create –driver local –opt type=nfs –opt o=addr=nfs-server-ip,nfsvers=4 –opt device=:/data/shared nfs-shared-data
    # 主机B:
    docker volume create –driver local –opt type=nfs –opt o=addr=nfs-server-ip,nfsvers=4 –opt device=:/data/shared nfs-shared-data
    # 两台主机上的容器挂载同一个NFS数据卷,数据实时共享

    6.2.3 block设备数据卷

    # 使用块设备创建数据卷
    # 前提:块设备已格式化
    sudo mkfs.ext4 /dev/sdb1

    # 创建基于块设备的数据卷
    docker volume create \\
    –driver local \\
    –opt type=ext4 \\
    –opt device=/dev/sdb1 \\
    block-data-volume

    # 使用块设备数据卷(适合高性能数据库场景)
    docker run -d \\
    –name high-perf-db \\
    -v block-data-volume:/var/lib/postgresql/data \\
    postgres:15

    # 查看数据卷信息
    docker volume inspect block-data-volume
    # 输出:
    # [
    # {
    # "CreatedAt": "2024-01-15T10:30:00Z",
    # "Driver": "local",
    # "Labels": null,
    # "Mountpoint": "/var/lib/docker/volumes/block-data-volume/_data",
    # "Name": "block-data-volume",
    # "Options": {
    # "device": "/dev/sdb1",
    # "type": "ext4"
    # },
    # "Scope": "local"
    # }
    # ]

    6.3 创建NFS数据卷

    6.3.1 完整NFS数据卷配置

    # ============================================
    # 步骤1:在NFS服务器上配置共享(假设IP:192.168.1.100)
    # ============================================

    # 安装NFS服务
    sudo apt-get install nfs-kernel-server # Ubuntu/Debian
    # 或
    sudo yum install nfs-utils # CentOS/RHEL

    # 创建共享目录
    sudo mkdir -p /data/docker-shared
    sudo chown -R nobody:nogroup /data/docker-shared
    sudo chmod 777 /data/docker-shared

    # 配置NFS共享
    sudo tee -a /etc/exports << 'EOF'
    /data/docker-shared 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)
    EOF

    # 启动NFS服务
    sudo exportfs -ra
    sudo systemctl restart nfs-kernel-server
    sudo systemctl enable nfs-kernel-server

    # 验证共享
    showmount -e localhost
    # 输出: /data/docker-shared 192.168.1.0/24

    # ============================================
    # 步骤2:在Docker主机上创建NFS数据卷
    # ============================================

    # 安装NFS客户端
    sudo apt-get install nfs-common # Ubuntu/Debian
    # 或
    sudo yum install nfs-utils # CentOS/RHEL

    # 创建NFS数据卷
    docker volume create \\
    –driver local \\
    –opt type=nfs \\
    –opt o=addr=192.168.1.100,nfsvers=4,soft,timeo=60,retrans=2 \\
    –opt device=:/data/docker-shared \\
    nfs-docker-data

    # ============================================
    # 步骤3:使用NFS数据卷运行容器
    # ============================================

    # 运行一个web应用,数据存储在NFS上
    docker run -d \\
    –name web-app-1 \\
    -p 8080:80 \\
    -v nfs-docker-data:/usr/share/nginx/html \\
    nginx:latest

    # 在容器中创建文件
    docker exec web-app-1 sh -c 'echo "<h1>Hello from NFS</h1>" > /usr/share/nginx/html/index.html'

    # 在另一台Docker主机上运行容器,挂载同一个NFS数据卷
    # (需要在另一台主机上也创建同名的NFS数据卷)
    docker run -d \\
    –name web-app-2 \\
    -p 8080:80 \\
    -v nfs-docker-data:/usr/share/nginx/html \\
    nginx:latest

    # 验证:web-app-2也能看到web-app-1创建的文件
    docker exec web-app-2 cat /usr/share/nginx/html/index.html
    # 输出: <h1>Hello from NFS</h1>

    6.3.2 NFS数据卷在Docker Compose中的使用

    # docker-compose.yml
    version: '3.8'

    services:
    web:
    image: nginx:latest
    ports:
    "8080:80"
    volumes:
    nfsdata:/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/dockershared

    6.4 数据卷共享与多容器同时挂载

    6.4.1 多容器共享同一个数据卷

    # 创建共享数据卷
    docker volume create shared-data

    # 容器1:写入数据
    docker run -d \\
    –name writer \\
    -v shared-data:/data \\
    alpine:latest \\
    sh -c 'while true; do echo "$(date): writer" >> /data/log.txt; sleep 5; done'

    # 容器2:读取数据
    docker run -d \\
    –name reader \\
    -v shared-data:/data:ro \\
    alpine:latest \\
    sh -c 'while true; do tail -1 /data/log.txt; sleep 5; done'

    # 容器3:同时读写
    docker run -d \\
    –name processor \\
    -v shared-data:/data \\
    alpine:latest \\
    sh -c 'while true; do echo "$(date): processor" >> /data/log.txt; sleep 10; done'

    # 查看日志
    docker logs -f reader
    # 输出:
    # Wed Jan 15 10:30:05 UTC 2024: writer
    # Wed Jan 15 10:30:10 UTC 2024: writer
    # Wed Jan 15 10:30:10 UTC 2024: processor
    # …

    6.4.2 并发写入的注意事项

    # 多个容器同时写入同一个数据卷时,需要注意并发问题
    # Docker不提供文件锁机制,需要应用层面处理并发

    # 场景:多个容器写入同一个文件
    # 问题:可能导致数据交错或丢失

    # 解决方法1:每个容器写入不同的文件
    docker run -d –name app1 -v shared-data:/data alpine sh -c 'while true; do echo "$(date)" >> /data/app1.log; sleep 5; done'
    docker run -d –name app2 -v shared-data:/data alpine sh -c 'while true; do echo "$(date)" >> /data/app2.log; sleep 5; done'

    # 解决方法2:使用文件锁(flock)
    docker run -d –name app1 -v shared-data:/data alpine sh -c '
    while true; do
    (
    flock -x 200
    echo "$(date): app1" >> /data/shared.log
    ) 200>/data/.lock
    sleep 5
    done
    '

    # 解决方法3:使用消息队列或数据库处理并发

    6.5 数据卷容器模式(–volumes-from)

    6.5.1 数据卷容器概念

    数据卷容器(Data Volume Container)是一种特殊的容器,它本身不运行应用服务,仅用于提供数据卷给其他容器挂载。其他容器通过–volumes-from参数来挂载数据卷容器中的所有数据卷。

    虽然在新版本的Docker中,直接使用命名数据卷是更推荐的做法,但数据卷容器模式在某些场景下仍然有用。

    6.5.2 创建和使用数据卷容器

    # 步骤1:创建数据卷容器(不运行实际应用)
    docker create \\
    –name data-container \\
    -v /app/data \\
    -v /app/config \\
    -v /app/logs \\
    busybox:latest

    # 注意:使用docker create而非docker run,容器创建但不启动

    # 步骤2:其他容器通过–volumes-from挂载数据卷容器的卷
    docker run -d \\
    –name app1 \\
    –volumes-from data-container \\
    my-app:latest

    docker run -d \\
    –name app2 \\
    –volumes-from data-container \\
    my-app:latest

    # app1和app2都挂载了/app/data、/app/config、/app/logs
    # 它们共享这些路径的数据

    # 步骤3:验证共享
    docker exec app1 sh -c 'echo "data from app1" > /app/data/test.txt'
    docker exec app2 cat /app/data/test.txt
    # 输出: data from app1

    6.5.3 –volumes-from的权限继承

    # –volumes-from会继承数据卷的读写权限
    # 如果数据卷容器中是只读挂载,继承的也是只读

    # 创建只读数据卷容器
    docker create \\
    –name readonly-data-container \\
    -v /app/config:ro \\
    busybox:latest

    # 继承的容器也是只读
    docker run -d \\
    –name app-readonly \\
    –volumes-from readonly-data-container \\
    my-app:latest

    # app-readonly无法写入/app/config
    docker exec app-readonly sh -c 'echo "test" > /app/config/test.txt'
    # 报错: Read-only file system

    6.5.4 链式继承

    # –volumes-from支持链式继承
    docker create –name base-data -v /data busybox:latest
    docker run -d –name mid-data –volumes-from base-data -v /logs busybox:latest sleep 3600
    docker run -d –name app –volumes-from mid-data my-app:latest

    # app同时挂载了/data(来自base-data)和/logs(来自mid-data)

    6.5.5 数据卷容器模式的优缺点

    # 优点:
    # 1. 集中管理数据卷定义
    # 2. 多个容器共享同一组数据卷
    # 3. 可以方便地添加新的共享容器

    # 缺点:
    # 1. 数据卷容器本身不运行,容易被遗忘和误删
    # 2. 匿名数据卷难以管理
    # 3. Docker官方更推荐使用命名数据卷

    # 推荐的现代做法:直接使用命名数据卷
    docker volume create app-data
    docker volume create app-logs

    docker run -d –name app1 -v app-data:/app/data -v app-logs:/app/logs my-app:latest
    docker run -d –name app2 -v app-data:/app/data -v app-logs:/app/logs my-app:latest

    6.6 数据卷的备份(docker run + tar)

    6.6.1 备份原理

    数据卷虽然存储在宿主机上,但Docker推荐通过容器来操作数据卷内容。备份数据卷的标准方法是:启动一个新容器,同时挂载要备份的数据卷和宿主机上的备份目录,然后使用tar命令将数据卷内容打包到备份目录中。

    6.6.2 基本备份操作

    # 假设有一个名为mysql-data的数据卷需要备份

    # 方法1:备份到宿主机目录
    mkdir -p /backup

    docker run –rm \\
    -v mysql-data:/data:ro \\
    -v /backup:/backup \\
    alpine:latest \\
    tar czf /backup/mysql-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /data .

    # 参数说明:
    # –rm: 容器退出后自动删除
    # -v mysql-data:/data:ro: 以只读方式挂载要备份的数据卷
    # -v /backup:/backup: 挂载宿主机的备份目录
    # tar czf: 创建gzip压缩的tar包
    # -C /data .: 切换到/data目录并打包当前目录下的所有内容

    # 查看备份文件
    ls -lh /backup/
    # 输出: -rw-r–r– 1 root root 50M Jan 15 10:30 mysql-data-20240115-103000.tar.gz

    6.6.3 备份脚本

    #!/bin/bash
    # backup-volume.sh – 数据卷备份脚本

    # 参数
    VOLUME_NAME=$1
    BACKUP_DIR=${2:-/backup}
    RETENTION_DAYS=${3:-7}

    # 检查参数
    if [ -z "$VOLUME_NAME" ]; then
    echo "Usage: $0 <volume_name> [backup_dir] [retention_days]"
    exit 1
    fi

    # 创建备份目录
    mkdir -p "$BACKUP_DIR"

    # 生成备份文件名
    TIMESTAMP=$(date +%Y%m%d-%H%M%S)
    BACKUP_FILE="$BACKUP_DIR/${VOLUME_NAME}${TIMESTAMP}.tar.gz"

    echo "[$(date)] Backing up volume: $VOLUME_NAME"
    echo " Backup file: $BACKUP_FILE"

    # 执行备份
    docker run –rm \\
    -v "$VOLUME_NAME":/data:ro \\
    -v "$BACKUP_DIR":/backup \\
    alpine:latest \\
    tar czf "/backup/${VOLUME_NAME}${TIMESTAMP}.tar.gz" -C /data .

    # 检查备份结果
    if [ $? -eq 0 ]; then
    BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
    echo "[$(date)] Backup completed successfully"
    echo " Size: $BACKUP_SIZE"
    else
    echo "[$(date)] Backup FAILED!"
    exit 1
    fi

    # 清理过期备份
    echo "[$(date)] Cleaning up backups older than $RETENTION_DAYS days…"
    find "$BACKUP_DIR" -name "${VOLUME_NAME}-*.tar.gz" -mtime +$RETENTION_DAYS -delete

    echo "[$(date)] Done."

    # 使用备份脚本
    chmod +x backup-volume.sh
    ./backup-volume.sh mysql-data /backup 7

    6.7 数据卷的恢复

    6.7.1 从备份恢复数据卷

    # 方法1:恢复到新数据卷
    # 步骤1:创建新数据卷
    docker volume create mysql-data-restored

    # 步骤2:从备份恢复
    docker run –rm \\
    -v mysql-data-restored:/data \\
    -v /backup:/backup:ro \\
    alpine:latest \\
    sh -c 'tar xzf /backup/mysql-data-20240115-103000.tar.gz -C /data'

    # 步骤3:验证恢复
    docker run –rm -v mysql-data-restored:/data alpine ls -la /data

    # 步骤4:使用恢复的数据卷启动容器
    docker run -d \\
    –name mysql-restored \\
    -v mysql-data-restored:/var/lib/mysql \\
    -e MYSQL_ROOT_PASSWORD=RootPass123! \\
    mysql:8.0

    6.7.2 恢复脚本

    #!/bin/bash
    # restore-volume.sh – 数据卷恢复脚本

    VOLUME_NAME=$1
    BACKUP_FILE=$2

    if [ -z "$VOLUME_NAME" ] || [ -z "$BACKUP_FILE" ]; then
    echo "Usage: $0 <volume_name> <backup_file>"
    echo "Example: $0 mysql-data /backup/mysql-data-20240115-103000.tar.gz"
    exit 1
    fi

    if [ ! -f "$BACKUP_FILE" ]; then
    echo "Error: Backup file not found: $BACKUP_FILE"
    exit 1
    fi

    BACKUP_DIR=$(dirname "$BACKUP_FILE")
    BACKUP_FILENAME=$(basename "$BACKUP_FILE")

    echo "[$(date)] Restoring volume: $VOLUME_NAME"
    echo " From: $BACKUP_FILE"

    # 检查数据卷是否存在,不存在则创建
    if ! docker volume inspect "$VOLUME_NAME" > /dev/null 2>&1; then
    echo "[$(date)] Creating new volume: $VOLUME_NAME"
    docker volume create "$VOLUME_NAME"
    fi

    # 执行恢复
    docker run –rm \\
    -v "$VOLUME_NAME":/data \\
    -v "$BACKUP_DIR":/backup:ro \\
    alpine:latest \\
    sh -c "tar xzf /backup/$BACKUP_FILENAME -C /data"

    if [ $? -eq 0 ]; then
    echo "[$(date)] Restore completed successfully"
    else
    echo "[$(date)] Restore FAILED!"
    exit 1
    fi

    # 使用恢复脚本
    chmod +x restore-volume.sh
    ./restore-volume.sh mysql-data-restored /backup/mysql-data-20240115-103000.tar.gz

    6.8 数据卷迁移

    6.8.1 同主机迁移

    # 场景:将数据从旧数据卷迁移到新数据卷(可能使用不同的驱动)

    # 步骤1:创建新数据卷(例如使用NFS驱动)
    docker volume create \\
    –driver local \\
    –opt type=nfs \\
    –opt o=addr=192.168.1.100,nfsvers=4 \\
    –opt device=:/data/new-storage \\
    new-data-volume

    # 步骤2:迁移数据
    docker run –rm \\
    -v old-data-volume:/source:ro \\
    -v new-data-volume:/target \\
    alpine:latest \\
    sh -c 'cp -a /source/. /target/'

    # 步骤3:验证数据
    docker run –rm -v new-data-volume:/data alpine ls -la /data

    # 步骤4:更新容器使用新数据卷
    docker rm -f my-app
    docker run -d –name my-app -v new-data-volume:/app/data my-app:latest

    # 步骤5:确认无误后删除旧数据卷
    docker volume rm old-data-volume

    6.8.2 跨主机迁移

    # 场景:将数据卷从主机A迁移到主机B

    # 主机A:导出数据卷
    docker run –rm \\
    -v source-volume:/data:ro \\
    -v /tmp:/backup \\
    alpine:latest \\
    tar czf /backup/source-volume-backup.tar.gz -C /data .

    # 将备份文件传输到主机B
    scp /tmp/source-volume-backup.tar.gz user@hostB:/tmp/

    # 主机B:创建新数据卷并恢复
    docker volume create source-volume
    docker run –rm \\
    -v source-volume:/data \\
    -v /tmp:/backup:ro \\
    alpine:latest \\
    sh -c 'tar xzf /backup/source-volume-backup.tar.gz -C /data'

    # 验证
    docker run –rm -v source-volume:/data alpine ls -la /data

    6.8.3 使用rsync增量迁移

    # 对于大数据卷,使用rsync进行增量迁移更高效
    # 需要在两端都支持rsync

    # 步骤1:在目标主机创建数据卷
    docker volume create migrated-data

    # 步骤2:使用rsync迁移数据
    # 方法:在源主机上临时启动rsync服务端
    docker run -d \\
    –name rsync-server \\
    -v source-volume:/data \\
    -p 873:873 \\
    alpine:latest \\
    sh -c 'apk add rsync && rsync –daemon –no-detach'

    # 在目标主机上拉取数据
    docker run –rm \\
    -v migrated-data:/data \\
    alpine:latest \\
    sh -c 'apk add rsync && rsync -avz rsync://source-host:/data/ /data/'

    # 步骤3:清理rsync服务端
    # 在源主机上
    docker rm -f rsync-server

    6.9 数据卷大小限制

    6.9.1 使用配额限制数据卷大小

    Docker本身不直接支持为数据卷设置大小限制,但可以通过以下方法实现:

    # 方法1:使用btrfs配额(需要Docker使用btrfs存储驱动)
    # 前提:/var/lib/docker在btrfs文件系统上
    sudo btrfs quota enable /var/lib/docker
    sudo btrfs qgroup limit 10G /var/lib/docker/volumes/my-volume/_data

    # 方法2:使用LVM创建指定大小的逻辑卷
    # 创建一个10GB的逻辑卷
    sudo lvcreate -L 10G -n docker-volume-data vg0
    sudo mkfs.ext4 /dev/vg0/docker-volume-data

    # 创建基于该逻辑卷的数据卷
    docker volume create \\
    –driver local \\
    –opt type=ext4 \\
    –opt device=/dev/vg0/docker-volume-data \\
    size-limited-volume

    # 方法3:使用loop设备创建固定大小的文件系统
    # 创建一个10GB的文件
    sudo dd if=/dev/zero of=/tmp/volume-image.img bs=1M count=10240
    sudo mkfs.ext4 /tmp/volume-image.img
    sudo mount -o loop /tmp/volume-image.img /mnt/volume-mount

    # 创建数据卷时指向挂载点
    docker volume create \\
    –driver local \\
    –opt type=none \\
    –opt device=/mnt/volume-mount \\
    –opt o=bind \\
    loop-volume

    # 方法4:使用XFS配额
    # 挂载XFS时启用配额
    sudo mount -o prjquota /dev/sdb1 /var/lib/docker
    # 设置项目配额
    sudo xfs_quota -x -c 'project -s -p /var/lib/docker/volumes/my-volume/_data 1'
    sudo xfs_quota -x -c 'limit -p bhard=10g 1'

    6.9.2 监控数据卷使用量

    # 方法1:使用docker system df
    docker system df -v
    # 会显示每个数据卷的大小

    # 方法2:直接检查宿主机上的数据卷目录
    sudo du -sh /var/lib/docker/volumes/*/

    # 方法3:通过容器检查
    docker run –rm -v my-volume:/data alpine du -sh /data

    # 方法4:监控脚本
    #!/bin/bash
    # monitor-volumes.sh
    for vol in $(docker volume ls -q); do
    SIZE=$(docker run –rm -v "$vol":/data alpine du -sh /data 2>/dev/null | cut -f1)
    echo "$vol: $SIZE"
    done

    6.10 数据卷生命周期管理策略

    6.10.1 生命周期阶段

    创建 → 使用(挂载到容器) → 备份 → 迁移(可选) → 归档 → 清理/删除

    6.10.2 生命周期管理最佳实践

    # 1. 使用标签管理数据卷
    docker volume create \\
    –label app=mysql \\
    –label env=production \\
    –label backup=daily \\
    –label retention=30d \\
    –label owner=dba-team \\
    mysql-prod-data

    # 2. 定期备份脚本(crontab)
    # /etc/crontab
    # 0 2 * * * root /opt/scripts/backup-all-volumes.sh >> /var/log/volume-backup.log 2>&1

    #!/bin/bash
    # backup-all-volumes.sh – 备份所有标记了backup=daily的数据卷
    BACKUP_DIR="/backup/volumes/$(date +%Y%m%d)"
    mkdir -p "$BACKUP_DIR"

    for vol in $(docker volume ls –filter label=backup=daily -q); do
    echo "Backing up $vol…"
    docker run –rm \\
    -v "$vol":/data:ro \\
    -v "$BACKUP_DIR":/backup \\
    alpine:latest \\
    tar czf "/backup/${vol}.tar.gz" -C /data .
    done

    # 清理30天前的备份
    find /backup/volumes/ -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \\;

    # 3. 清理无用数据卷
    # 谨慎使用!先检查再清理
    docker volume ls -f dangling=true
    # 确认后清理
    docker volume prune -f

    # 4. 数据卷归档
    # 将不再使用但需要保留的数据卷归档
    docker run –rm \\
    -v old-data-volume:/data:ro \\
    -v /archive:/archive \\
    alpine:latest \\
    tar czf /archive/old-data-volume-$(date +%Y%m%d).tar.gz -C /data .

    # 归档后删除数据卷
    docker volume rm old-data-volume

    # 5. 定期完整性检查
    #!/bin/bash
    # check-volumes.sh – 检查数据卷完整性
    for vol in $(docker volume ls -q); do
    echo "Checking $vol…"
    docker run –rm -v "$vol":/data alpine sh -c '
    if [ -d /data ]; then
    FILE_COUNT=$(find /data -type f | wc -l)
    SIZE=$(du -sh /data | cut -f1)
    echo " Files: $FILE_COUNT, Size: $SIZE"
    else
    echo " ERROR: /data directory not found!"
    fi
    '

    done


    第七章 Dockerfile中的存储配置

    7.1 VOLUME指令详解

    7.1.1 VOLUME指令语法

    VOLUME指令在Dockerfile中用于声明数据卷挂载点。当容器启动时,如果用户没有通过-v参数指定数据卷,Docker会自动为这些路径创建匿名数据卷。

    # Dockerfile中使用VOLUME指令

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

    # 语法2:空格分隔的字符串形式
    VOLUME /data /var/log/app

    7.1.2 VOLUME指令的作用

    # 示例Dockerfile
    FROM ubuntu:22.04

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

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

    # 设置默认命令
    CMD ["sleep", "3600"]

    # 构建镜像
    docker build -t volume-test .

    # 运行容器(不指定-v参数)
    docker run -d –name auto-volume volume-test

    # 查看自动创建的匿名数据卷
    docker inspect auto-volume –format '{{json .Mounts}}' | python3 -m json.tool
    # 输出:
    # [
    # {
    # "Type": "volume",
    # "Name": "abc123def456…", # 匿名数据卷(哈希命名)
    # "Source": "/var/lib/docker/volumes/abc123def456…/_data",
    # "Destination": "/app/data",
    # "Driver": "local",
    # "Mode": "",
    # "RW": true,
    # "Propagation": ""
    # },
    # {
    # "Type": "volume",
    # "Name": "xyz789ghi012…",
    # "Source": "/var/lib/docker/volumes/xyz789ghi012…/_data",
    # "Destination": "/app/logs",
    # "Driver": "local",
    # "Mode": "",
    # "RW": true,
    # "Propagation": ""
    # }
    # ]

    7.2 VOLUME指令的注意事项

    7.2.1 匿名数据卷问题

    # Dockerfile
    FROM nginx:latest
    VOLUME /usr/share/nginx/html

    # 每次运行容器都会创建一个匿名数据卷
    docker run -d –name web1 volume-nginx # 创建匿名卷1
    docker run -d –name web2 volume-nginx # 创建匿名卷2
    docker run -d –name web3 volume-nginx # 创建匿名卷3

    # 删除容器后,匿名数据卷不会被自动删除
    docker rm -f web1 web2 web3

    # 查看遗留的匿名数据卷
    docker volume ls
    # 会看到3个哈希命名的匿名数据卷

    # 清理
    docker volume prune -f

    7.2.2 VOLUME指令后的RUN指令

    # 重要:VOLUME指令后的RUN指令对VOLUME声明的路径无效!
    # 因为VOLUME声明后,Docker知道该路径将使用外部存储,
    # 所以不会将RUN指令对该路径的修改保存到镜像中

    # 错误示例:
    FROM ubuntu:22.04
    VOLUME /app/data
    RUN echo "initial data" > /app/data/initial.txt # 这行不会被保存到镜像!
    # 原因:VOLUME声明后,/app/data被认为是外部挂载点,
    # RUN指令中的修改只会影响临时容器的可写层,不会写入镜像层

    # 正确示例:先写入数据,再声明VOLUME
    FROM ubuntu:22.04
    RUN mkdir -p /app/data && echo "initial data" > /app/data/initial.txt
    VOLUME /app/data
    # 这样initial.txt会被包含在镜像中,首次挂载时会被复制到数据卷

    7.2.3 VOLUME指令与镜像层的关系

    # VOLUME指令本身不创建新层,它只是在镜像元数据中添加声明
    # 但VOLUME指令会影响后续指令的行为

    FROM ubuntu:22.04

    # 这一层会包含文件
    RUN mkdir -p /app/data && echo "content" > /app/data/file.txt

    # VOLUME声明(不影响已有的镜像层)
    VOLUME /app/data

    # 这一层不会影响/app/data(因为VOLUME已声明)
    RUN echo "new content" > /app/data/newfile.txt
    # newfile.txt不会被保存到镜像中!

    # 构建并验证
    # docker build -t test .
    # docker run –rm test ls /app/data/
    # 输出: file.txt (没有newfile.txt!)

    7.3 VOLUME与docker run -v的交互

    7.3.1 -v覆盖VOLUME声明的路径

    # Dockerfile
    FROM nginx:latest
    VOLUME ["/usr/share/nginx/html"]

    # 情况1:不指定-v,Docker创建匿名数据卷(包含镜像内容)
    docker run -d –name web1 volume-nginx
    docker exec web1 ls /usr/share/nginx/html
    # 输出: 50x.html index.html (从镜像复制)

    # 情况2:指定命名数据卷
    docker run -d –name web2 -v my-html:/usr/share/nginx/html volume-nginx
    docker exec web2 ls /usr/share/nginx/html
    # 输出: 50x.html index.html (首次挂载时从镜像复制)

    # 情况3:指定绑定挂载(不会复制镜像内容)
    docker run -d –name web3 -v /tmp/empty:/usr/share/nginx/html volume-nginx
    docker exec web3 ls /usr/share/nginx/html
    # 输出: (空,绑定挂载不复制镜像内容)

    # 情况4:挂载已有内容的数据卷(不复制镜像内容)
    docker volume create pre-filled-volume
    docker run –rm -v pre-filled-volume:/data alpine sh -c 'echo "existing" > /data/existing.txt'
    docker run -d –name web4 -v pre-filled-volume:/usr/share/nginx/html volume-nginx
    docker exec web4 ls /usr/share/nginx/html
    # 输出: existing.txt (已有内容,不复制镜像内容)

    7.3.2 行为总结
    场景镜像内容是否复制到数据卷
    VOLUME声明 + 不指定-v(创建匿名卷)
    VOLUME声明 + 命名数据卷(首次创建)
    VOLUME声明 + 命名数据卷(已有内容)
    VOLUME声明 + 绑定挂载
    无VOLUME声明 + 命名数据卷(首次创建)
    无VOLUME声明 + 绑定挂载

    7.4 在Dockerfile中定义数据卷的最佳实践

    7.4.1 最佳实践建议

    # ============================================
    # 最佳实践1:谨慎使用VOLUME指令
    # ============================================

    # 不推荐:在基础镜像中使用VOLUME
    # 这会导致所有继承的镜像都被强制创建匿名数据卷
    # FROM ubuntu:22.04
    # VOLUME /data # 不推荐

    # 推荐:让用户在docker run时通过-v指定
    # 在文档中说明需要持久化的路径即可

    # ============================================
    # 最佳实践2:对于数据库镜像,声明VOLUME是合理的
    # ============================================

    # MySQL官方Dockerfile中声明了VOLUME
    # FROM mysql:8.0
    # VOLUME /var/lib/mysql
    # 这样即使用户忘记指定-v,数据也会保存在匿名数据卷中
    # 虽然匿名卷难以管理,但至少不会因容器删除而丢失

    # ============================================
    # 最佳实践3:在VOLUME之前准备初始数据
    # ============================================

    FROM ubuntu:22.04

    # 先创建目录和初始数据
    RUN mkdir -p /app/data && \\
    echo '{"initialized": true}' > /app/data/config.json && \\
    echo '#!/bin/sh' > /app/data/init.sh && \\
    chmod +x /app/data/init.sh

    # 然后声明VOLUME
    VOLUME ["/app/data"]

    # 这样config.json和init.sh会被包含在镜像中
    # 首次挂载时会复制到数据卷

    CMD ["sleep", "3600"]

    7.4.2 避免在VOLUME路径下执行写操作

    # 不推荐
    FROM node:18-alpine
    WORKDIR /app
    VOLUME ["/app"] # 整个/app目录声明为VOLUME
    COPY . . # COPY操作发生在VOLUME声明之后,可能有问题
    RUN npm install # npm install的结果可能不被保存

    # 推荐
    FROM node:18-alpine
    WORKDIR /app
    COPY . . # 先COPY
    RUN npm install # 先安装依赖
    # 不声明VOLUME,让用户决定如何持久化
    # 或者只声明需要持久化的子目录
    VOLUME ["/app/data"] # 只持久化数据目录

    7.5 多阶段构建中的数据卷处理

    7.5.1 多阶段构建与VOLUME

    # 多阶段构建中,VOLUME指令只在最终阶段生效

    # 阶段1:构建阶段
    FROM node:18-alpine AS builder
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci
    COPY . .
    RUN npm run build

    # 阶段2:运行阶段
    FROM nginx:alpine
    # 从构建阶段复制产物
    COPY –from=builder /app/dist /usr/share/nginx/html
    # 声明数据卷(仅在此阶段生效)
    VOLUME ["/var/log/nginx"]
    EXPOSE 80
    CMD ["nginx", "-g", "daemon off;"]

    7.5.2 多阶段构建中的数据传递

    # 在多阶段构建中,数据卷声明不会跨阶段传递
    # 每个阶段是独立的

    # 阶段1:数据准备阶段
    FROM alpine AS data-prep
    RUN mkdir -p /data && \\
    echo "prepared data" > /data/ready.txt

    # 阶段2:运行阶段
    FROM alpine
    # 从阶段1复制数据(这不是数据卷,是镜像层复制)
    COPY –from=data-prep /data /app/data
    # 声明数据卷
    VOLUME ["/app/data"]
    CMD ["cat", "/app/data/ready.txt"]

    7.6 镜像中预填充数据卷内容

    7.6.1 预填充机制

    当命名数据卷首次挂载到一个非空目录时(目录中有镜像层的内容),Docker会将这些内容复制到数据卷中。这被称为"预填充"。

    # Dockerfile – 预填充数据卷内容
    FROM ubuntu:22.04

    # 创建目录并填充初始数据
    RUN mkdir -p /app/data /app/config /app/templates

    # 填充初始数据文件
    RUN echo '{"version": "1.0", "initialized": false}' > /app/data/default.json

    # 填充配置文件
    RUN echo 'server.port=8080' > /app/config/application.properties

    # 填充模板文件
    RUN echo '<html><body>Hello {{name}}</body></html>' > /app/templates/index.html

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

    CMD ["sleep", "3600"]

    # 构建镜像
    docker build -t pre-filled-app .

    # 创建命名数据卷(首次挂载会复制镜像内容)
    docker volume create app-data
    docker volume create app-config

    # 运行容器
    docker run -d –name app \\
    -v app-data:/app/data \\
    -v app-config:/app/config \\
    pre-filled-app

    # 验证数据卷中包含了镜像的初始内容
    docker exec app cat /app/data/default.json
    # 输出: {"version": "1.0", "initialized": false}

    docker exec app cat /app/config/application.properties
    # 输出: server.port=8080

    # 用户可以修改数据卷中的内容
    docker exec app sh -c 'echo "{\\"version\\": \\"1.0\\", \\"initialized\\": true}" > /app/data/default.json'

    # 下次创建容器时,数据卷中是修改后的内容(不再复制镜像内容)
    docker rm -f app
    docker run -d –name app2 \\
    -v app-data:/app/data \\
    -v app-config:/app/config \\
    pre-filled-app

    docker exec app2 cat /app/data/default.json
    # 输出: {"version": "1.0", "initialized": true}

    7.6.2 初始化脚本模式

    # Dockerfile – 使用初始化脚本填充数据卷
    FROM ubuntu:22.04

    # 复制初始化脚本
    COPY init-data.sh /docker-entrypoint-initdb.d/
    RUN chmod +x /docker-entrypoint-initdb.d/init-data.sh

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

    # 自定义entrypoint
    COPY entrypoint.sh /entrypoint.sh
    RUN chmod +x /entrypoint.sh
    ENTRYPOINT ["/entrypoint.sh"]
    CMD ["sleep", "3600"]

    #!/bin/bash
    # entrypoint.sh – 自定义入口脚本
    set -e

    # 如果数据卷为空,执行初始化
    if [ -z "$(ls -A /app/data 2>/dev/null)" ]; then
    echo "Initializing data volume…"
    /docker-entrypoint-initdb.d/init-data.sh
    fi

    # 执行主命令
    exec "$@"

    #!/bin/bash
    # init-data.sh – 初始化脚本
    mkdir -p /app/data
    echo '{"initialized": true, "timestamp": "'$(date -Iseconds)'"}' > /app/data/init.json
    echo "Data volume initialized at $(date)"

    # 构建并运行
    docker build -t init-app .
    docker volume create app-fresh-data
    docker run -d –name app -v app-fresh-data:/app/data init-app

    # 验证初始化
    docker exec app cat /app/data/init.json
    # 输出: {"initialized": true, "timestamp": "2024-01-15T10:30:00+00:00"}

    # 重启容器,不会重复初始化(因为数据卷不为空)
    docker restart app
    docker logs app | grep "Initializing"
    # 只在首次启动时出现


    第八章 常见数据库的存储方案

    数据库是容器化部署中最需要数据持久化的应用类型。本章将详细讲解MySQL、PostgreSQL、MongoDB、Redis和Elasticsearch五种常见数据库在Docker中的完整持久化方案,包括数据卷配置、配置文件挂载、日志管理、备份恢复和性能优化。

    8.1 MySQL数据持久化方案(完整配置)

    8.1.1 需求分析

    MySQL是使用最广泛的关系型数据库之一。在Docker中部署MySQL,需要考虑以下持久化需求:

    • 数据文件(/var/lib/mysql):存储所有数据库和表数据
    • 配置文件(/etc/mysql):存储MySQL服务器配置
    • 日志文件:包括错误日志、慢查询日志、二进制日志
    • 初始化脚本:首次启动时自动执行SQL脚本
    8.1.2 完整Docker Compose配置

    # docker-compose.yml – MySQL完整持久化方案
    version: '3.8'

    services:
    mysql:
    image: mysql:8.0
    container_name: mysqlprod
    restart: unlessstopped
    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:/dockerentrypointinitdb.d:ro
    # 日志持久化
    mysql_logs:/var/log/mysql
    # 二进制日志(用于主从复制)
    mysql_binlog:/var/lib/mysqlbin
    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: mysqlbackup
    depends_on:
    mysql:
    condition: service_healthy
    volumes:
    mysql_data:/var/lib/mysql:ro
    ./backup:/backup
    ./scripts/backupmysql.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: unlessstopped

    volumes:
    mysql_data:
    name: mysql_data
    labels:
    app: mysql
    env: production
    mysql_logs:
    name: mysql_logs
    mysql_binlog:
    name: mysql_binlog

    8.1.3 MySQL配置文件

    # mysql/conf/my.cnf – MySQL配置文件

    [mysqld]
    # 基本设置
    character-set-server = utf8mb4
    collation-server = utf8mb4_unicode_ci
    default-storage-engine = InnoDB
    skip-name-resolve

    # 连接设置
    max_connections = 500
    max_connect_errors = 1000
    wait_timeout = 28800
    interactive_timeout = 28800

    # InnoDB设置
    innodb_buffer_pool_size = 512M
    innodb_buffer_pool_instances = 4
    innodb_log_file_size = 128M
    innodb_log_buffer_size = 16M
    innodb_flush_log_at_trx_commit = 1
    innodb_file_per_table = 1
    innodb_flush_method = O_DIRECT

    # 日志设置
    slow_query_log = 1
    long_query_time = 2
    slow_query_log_file = /var/log/mysql/slow.log
    log_error = /var/log/mysql/error.log

    # 二进制日志设置(用于主从复制和数据恢复)
    log_bin = /var/lib/mysql-bin/mysql-bin
    binlog_format = ROW
    binlog_expire_logs_seconds = 604800
    server_id = 1

    # 安全设置
    local_infile = 0
    sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

    [client]
    default-character-set = utf8mb4

    [mysql]
    default-character-set = utf8mb4

    8.1.4 初始化脚本

    — mysql/init/01-create-databases.sql
    — 创建应用数据库
    CREATE DATABASE IF NOT EXISTS appdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    CREATE DATABASE IF NOT EXISTS logdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

    — 创建应用用户
    CREATE USER IF NOT EXISTS 'appuser'@'%' IDENTIFIED WITH mysql_native_password BY 'apppass123';
    GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'%';
    GRANT INSERT, SELECT ON logdb.* TO 'appuser'@'%';

    — 创建只读用户
    CREATE USER IF NOT EXISTS 'readonly'@'%' IDENTIFIED WITH mysql_native_password BY 'readonly123';
    GRANT SELECT ON appdb.* TO 'readonly'@'%';

    — 刷新权限
    FLUSH PRIVILEGES;

    — mysql/init/02-create-tables.sql
    USE appdb;

    — 用户表
    CREATE TABLE IF NOT EXISTS users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_email (email)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    — 商品表
    CREATE TABLE IF NOT EXISTS products (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_name (name)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    8.1.5 备份与恢复

    #!/bin/bash
    # scripts/backup-mysql.sh – MySQL备份脚本

    BACKUP_DIR="/backup/mysql/$(date +%Y%m%d)"
    mkdir -p "$BACKUP_DIR"

    # 1. 逻辑备份(mysqldump)
    echo "[$(date)] Starting mysqldump backup…"
    docker exec mysql-prod \\
    mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \\
    –all-databases \\
    –single-transaction \\
    –routines \\
    –triggers \\
    –events \\
    –set-gtid-purged=OFF \\
    > "$BACKUP_DIR/alldatabases-$(date +%H%M%S).sql"

    # 2. 单独备份每个数据库
    for db in appdb logdb; do
    echo "[$(date)] Backing up database: $db"
    docker exec mysql-prod \\
    mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \\
    –single-transaction \\
    –routines \\
    –triggers \\
    "$db" > "$BACKUP_DIR/${db}$(date +%H%M%S).sql"
    done

    # 3. 物理备份(备份数据卷)
    echo "[$(date)] Backing up data volume…"
    docker run –rm \\
    -v mysql_data:/var/lib/mysql:ro \\
    -v "$BACKUP_DIR":/backup \\
    alpine:latest \\
    tar czf /backup/mysql-data-$(date +%H%M%S).tar.gz -C /var/lib/mysql .

    # 4. 压缩备份文件
    echo "[$(date)] Compressing backups…"
    gzip "$BACKUP_DIR"/*.sql

    # 5. 清理过期备份
    find /backup/mysql/ -maxdepth 1 -type d -mtime +${BACKUP_RETENTION_DAYS:-7} -exec rm -rf {} \\;

    echo "[$(date)] Backup completed."
    echo " Location: $BACKUP_DIR"
    ls -lh "$BACKUP_DIR"/

    #!/bin/bash
    # scripts/restore-mysql.sh – MySQL恢复脚本

    BACKUP_FILE=$1

    if [ -z "$BACKUP_FILE" ]; then
    echo "Usage: $0 <backup_sql_file>"
    exit 1
    fi

    echo "[$(date)] Restoring MySQL from: $BACKUP_FILE"

    # 如果是gzip压缩文件,先解压
    if [[ "$BACKUP_FILE" == *.gz ]]; then
    gunzip -c "$BACKUP_FILE" | docker exec -i mysql-prod \\
    mysql -uroot -p"$MYSQL_ROOT_PASSWORD"
    else
    docker exec -i mysql-prod \\
    mysql -uroot -p"$MYSQL_ROOT_PASSWORD" < "$BACKUP_FILE"
    fi

    echo "[$(date)] Restore completed."

    8.2 PostgreSQL数据持久化方案(完整配置)

    8.2.1 完整Docker Compose配置

    # docker-compose.yml – PostgreSQL完整持久化方案
    version: '3.8'

    services:
    postgres:
    image: postgres:15alpine
    container_name: postgresprod
    restart: unlessstopped
    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:/dockerentrypointinitdb.d:ro
    # WAL归档日志
    postgres_wal:/var/lib/postgresql/wal_archive
    # 日志
    postgres_logs:/var/log/postgresql
    command: >
    postgres
    -c config_file=/etc/postgresql/postgresql.conf
    -c hba_file=/etc/postgresql/pg_hba.conf
    -c max_connections=200
    -c shared_buffers=256MB
    -c effective_cache_size=768MB
    -c work_mem=4MB
    -c maintenance_work_mem=64MB
    -c wal_level=replica
    -c archive_mode=on
    -c archive_command='test ! -f /var/lib/postgresql/wal_archive/%f && cp %p /var/lib/postgresql/wal_archive/%f'
    -c logging_collector=on
    -c log_directory=/var/log/postgresql
    -c log_filename='postgresql-%Y-%m-%d.log'
    -c log_rotation_age=1d
    -c log_rotation_size=100MB

    healthcheck:
    test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
    interval: 10s
    timeout: 5s
    retries: 5
    start_period: 30s
    shm_size: '256m'

    volumes:
    postgres_data:
    name: postgres_data
    postgres_wal:
    name: postgres_wal
    postgres_logs:
    name: postgres_logs

    8.2.2 PostgreSQL配置文件

    # postgres/conf/postgresql.conf – PostgreSQL配置文件

    # 连接设置
    listen_addresses = '*'
    max_connections = 200
    superuser_reserved_connections = 3

    # 内存设置
    shared_buffers = 256MB
    effective_cache_size = 768MB
    work_mem = 4MB
    maintenance_work_mem = 64MB
    dynamic_shared_memory_type = posix

    # WAL设置
    wal_level = replica
    archive_mode = on
    archive_command = 'test ! -f /var/lib/postgresql/wal_archive/%f && cp %p /var/lib/postgresql/wal_archive/%f'
    max_wal_size = 1GB
    min_wal_size = 80MB
    wal_buffers = 16MB
    checkpoint_completion_target = 0.9

    # 查询优化
    random_page_cost = 1.1
    effective_io_concurrency = 200
    default_statistics_target = 100

    # 日志设置
    logging_collector = on
    log_directory = '/var/log/postgresql'
    log_filename = 'postgresql-%Y-%m-%d.log'
    log_rotation_age = 1d
    log_rotation_size = 100MB
    log_min_duration_statement = 1000
    log_checkpoints = on
    log_connections = on
    log_disconnections = on
    log_line_prefix = '%t [%p] %u@%d '

    # 自动清理
    autovacuum = on
    autovacuum_max_workers = 3
    autovacuum_naptime = 1min
    autovacuum_vacuum_threshold = 50
    autovacuum_analyze_threshold = 50

    # 时区
    timezone = 'Asia/Shanghai'
    log_timezone = 'Asia/Shanghai'

    # 字符集
    client_encoding = utf8
    lc_messages = 'C'
    lc_monetary = 'C'
    lc_numeric = 'C'
    lc_time = 'C'
    default_text_search_config = 'pg_catalog.english'

    # postgres/conf/pg_hba.conf – 客户端认证配置

    # TYPE DATABASE USER ADDRESS METHOD
    local all all trust
    host all all 127.0.0.1/32 md5
    host all all ::1/128 md5
    host all all 0.0.0.0/0 md5
    host replication all 0.0.0.0/0 md5

    8.2.3 初始化脚本

    — postgres/init/01-setup.sql
    — 创建额外数据库
    CREATE DATABASE logdb;
    CREATE DATABASE testdb;

    — 创建只读用户
    CREATE USER readonly WITH PASSWORD 'readonly123';
    GRANT CONNECT ON DATABASE appdb TO readonly;

    — 连接到appdb并设置权限
    \\c appdb
    GRANT USAGE ON SCHEMA public TO readonly;
    GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
    ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly;

    — 创建扩展
    CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
    CREATE EXTENSION IF NOT EXISTS "pgcrypto";

    — 创建表
    CREATE TABLE IF NOT EXISTS users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password_hash TEXT NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
    );

    CREATE TABLE IF NOT EXISTS products (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name VARCHAR(200) NOT NULL,
    price NUMERIC(10,2) NOT NULL,
    stock INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT NOW()
    );

    — 创建索引
    CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
    CREATE INDEX IF NOT EXISTS idx_products_name ON products(name);

    8.2.4 备份与恢复

    #!/bin/bash
    # scripts/backup-postgres.sh – PostgreSQL备份脚本

    BACKUP_DIR="/backup/postgres/$(date +%Y%m%d)"
    mkdir -p "$BACKUP_DIR"
    TIMESTAMP=$(date +%H%M%S)

    # 1. 逻辑备份(pg_dump)
    echo "[$(date)] Starting pg_dump backup…"

    # 备份所有数据库
    docker exec postgres-prod \\
    pg_dumpall -U appuser \\
    > "$BACKUP_DIR/alldatabases-$TIMESTAMP.sql"

    # 单独备份appdb(自定义格式,支持并行恢复)
    docker exec postgres-prod \\
    pg_dump -U appuser -Fc -d appdb \\
    > "$BACKUP_DIR/appdb-$TIMESTAMP.dump"

    # 单独备份appdb(纯文本格式)
    docker exec postgres-prod \\
    pg_dump -U appuser -d appdb \\
    > "$BACKUP_DIR/appdb-$TIMESTAMP.sql"

    # 2. 物理备份(备份数据卷)
    echo "[$(date)] Backing up data volume…"
    docker run –rm \\
    -v postgres_data:/var/lib/postgresql/data:ro \\
    -v "$BACKUP_DIR":/backup \\
    alpine:latest \\
    tar czf /backup/postgres-data-$TIMESTAMP.tar.gz -C /var/lib/postgresql/data .

    # 3. WAL归档备份
    docker run –rm \\
    -v postgres_wal:/wal:ro \\
    -v "$BACKUP_DIR":/backup \\
    alpine:latest \\
    tar czf /backup/postgres-wal-$TIMESTAMP.tar.gz -C /wal .

    # 4. 压缩
    gzip "$BACKUP_DIR"/*.sql

    # 5. 清理过期备份
    find /backup/postgres/ -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \\;

    echo "[$(date)] Backup completed."
    ls -lh "$BACKUP_DIR"/

    #!/bin/bash
    # scripts/restore-postgres.sh – PostgreSQL恢复脚本

    BACKUP_FILE=$1
    DB_NAME=${2:-appdb}

    echo "[$(date)] Restoring PostgreSQL from: $BACKUP_FILE"

    if [[ "$BACKUP_FILE" == *.dump ]]; then
    # 自定义格式恢复
    docker exec -i postgres-prod \\
    pg_restore -U appuser -d "$DB_NAME" –clean –if-exists < "$BACKUP_FILE"
    elif [[ "$BACKUP_FILE" == *.sql.gz ]]; then
    # gzip压缩的SQL恢复
    gunzip -c "$BACKUP_FILE" | docker exec -i postgres-prod \\
    psql -U appuser -d "$DB_NAME"
    elif [[ "$BACKUP_FILE" == *.sql ]]; then
    # 纯文本SQL恢复
    docker exec -i postgres-prod \\
    psql -U appuser -d "$DB_NAME" < "$BACKUP_FILE"
    fi

    echo "[$(date)] Restore completed."

    8.3 MongoDB数据持久化方案(完整配置)

    8.3.1 完整Docker Compose配置

    # docker-compose.yml – MongoDB完整持久化方案
    version: '3.8'

    services:
    mongodb:
    image: mongo:6.0
    container_name: mongodbprod
    restart: unlessstopped
    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:/dockerentrypointinitdb.d:ro
    # 日志
    mongo_logs:/var/log/mongodb
    command: ["mongod", "–config", "/etc/mongo/mongod.conf"]
    healthcheck:
    test: ["CMD", "mongosh", "–eval", "db.adminCommand('ping')"]
    interval: 10s
    timeout: 5s
    retries: 5
    start_period: 30s

    volumes:
    mongo_data:
    name: mongo_data
    mongo_logs:
    name: mongo_logs

    8.3.2 MongoDB配置文件

    # mongo/conf/mongod.conf – MongoDB配置文件

    # 存储设置
    storage:
    dbPath: /data/db
    journal:
    enabled: true
    wiredTiger:
    engineConfig:
    cacheSizeGB: 1
    journalCompressor: snappy
    collectionConfig:
    blockCompressor: snappy
    indexConfig:
    prefixCompression: true

    # 网络设置
    net:
    port: 27017
    bindIp: 0.0.0.0
    maxIncomingConnections: 1000

    # 日志设置
    systemLog:
    destination: file
    path: /var/log/mongodb/mongod.log
    logAppend: true
    logRotate: reopen
    verbosity: 0

    # 进程管理
    processManagement:
    fork: false
    pidFilePath: /var/run/mongod.pid

    # 复制集(可选,用于高可用)
    # replication:
    # replSetName: rs0
    # oplogSizeMB: 1024

    # 安全设置
    security:
    authorization: enabled

    # 性能分析
    operationProfiling:
    mode: slowOp
    slowOpThresholdMs: 100

    # 分片设置(如需要)
    # sharding:
    # clusterRole: shardsvr

    8.3.3 初始化脚本

    // mongo/init/01-init.js – MongoDB初始化脚本

    // 切换到admin数据库
    db = db.getSiblingDB('admin');

    // 创建应用用户
    db.createUser({
    user: 'appuser',
    pwd: 'apppass123',
    roles: [
    { role: 'readWrite', db: 'appdb' }
    ]
    });

    // 创建只读用户
    db.createUser({
    user: 'readonly',
    pwd: 'readonly123',
    roles: [
    { role: 'read', db: 'appdb' }
    ]
    });

    // 切换到appdb
    db = db.getSiblingDB('appdb');

    // 创建集合
    db.createCollection('users');
    db.createCollection('products');
    db.createCollection('orders');

    // 创建索引
    db.users.createIndex({ "email": 1 }, { unique: true });
    db.users.createIndex({ "username": 1 }, { unique: true });
    db.products.createIndex({ "name": "text" });
    db.products.createIndex({ "category": 1, "price": 1 });
    db.orders.createIndex({ "userId": 1, "createdAt": 1 });

    // 插入初始数据
    db.products.insertMany([
    { name: "笔记本电脑", price: 5999, stock: 100, category: "electronics" },
    { name: "机械键盘", price: 299, stock: 200, category: "accessories" },
    { name: "显示器", price: 1599, stock: 50, category: "electronics" }
    ]);

    8.3.4 备份与恢复

    #!/bin/bash
    # scripts/backup-mongo.sh – MongoDB备份脚本

    BACKUP_DIR="/backup/mongo/$(date +%Y%m%d)"
    mkdir -p "$BACKUP_DIR"
    TIMESTAMP=$(date +%H%M%S)

    # 1. 逻辑备份(mongodump)
    echo "[$(date)] Starting mongodump backup…"
    docker exec mongodb-prod \\
    mongodump \\
    –uri="mongodb://root:RootPass123!@localhost:27017" \\
    –gzip \\
    –archive > "$BACKUP_DIR/mongodb-$TIMESTAMP.gz"

    # 备份单个数据库
    docker exec mongodb-prod \\
    mongodump \\
    –uri="mongodb://root:RootPass123!@localhost:27017/appdb" \\
    –gzip \\
    –archive > "$BACKUP_DIR/appdb-$TIMESTAMP.gz"

    # 2. 物理备份(备份数据卷)
    echo "[$(date)] Backing up data volume…"
    docker run –rm \\
    -v mongo_data:/data/db:ro \\
    -v "$BACKUP_DIR":/backup \\
    alpine:latest \\
    tar czf /backup/mongo-data-$TIMESTAMP.tar.gz -C /data/db .

    # 3. 清理过期备份
    find /backup/mongo/ -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \\;

    echo "[$(date)] Backup completed."
    ls -lh "$BACKUP_DIR"/

    #!/bin/bash
    # scripts/restore-mongo.sh – MongoDB恢复脚本

    BACKUP_FILE=$1

    echo "[$(date)] Restoring MongoDB from: $BACKUP_FILE"

    # 逻辑恢复(mongorestore)
    docker exec -i mongodb-prod \\
    mongorestore \\
    –uri="mongodb://root:RootPass123!@localhost:27017" \\
    –gzip \\
    –drop \\
    –archive < "$BACKUP_FILE"

    echo "[$(date)] Restore completed."

    8.4 Redis数据持久化方案(RDB + AOF)

    8.4.1 完整Docker Compose配置

    # docker-compose.yml – Redis完整持久化方案
    version: '3.8'

    services:
    redis:
    image: redis:7alpine
    container_name: redisprod
    restart: unlessstopped
    ports:
    "6379:6379"
    environment:
    TZ: Asia/Shanghai
    volumes:
    # 数据持久化
    redis_data:/data
    # 配置文件
    ./redis/conf/redis.conf:/usr/local/etc/redis/redis.conf:ro
    # 日志
    redis_logs:/var/log/redis
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
    healthcheck:
    test: ["CMD", "redis-cli", "ping"]
    interval: 10s
    timeout: 5s
    retries: 5

    volumes:
    redis_data:
    name: redis_data
    redis_logs:
    name: redis_logs

    8.4.2 Redis配置文件

    # redis/conf/redis.conf – Redis配置文件

    # 网络设置
    bind 0.0.0.0
    protected-mode yes
    port 6379
    timeout 300
    tcp-keepalive 60

    # 安全设置
    requirepass YourRedisPassword123!

    # 通用设置
    daemonize no
    supervised no
    pidfile /var/run/redis_6379.pid
    loglevel notice
    logfile /var/log/redis/redis.log
    databases 16

    # 快照(RDB)持久化设置
    save 900 1 # 900秒内至少1个key变化则保存
    save 300 10 # 300秒内至少10个key变化则保存
    save 60 10000 # 60秒内至少10000个key变化则保存
    stop-writes-on-bgsave-error yes
    rdbcompression yes
    rdbchecksum yes
    dbfilename dump.rdb
    dir /data

    # AOF(Append Only File)持久化设置
    appendonly yes
    appendfilename "appendonly.aof"
    appendfsync everysec # 每秒同步一次(兼顾性能和安全)
    no-appendfsync-on-rewrite no
    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb
    aof-load-truncated yes
    aof-use-rdb-preamble yes

    # 内存管理
    maxmemory 512mb
    maxmemory-policy allkeys-lru

    # 慢查询日志
    slowlog-log-slower-than 10000
    slowlog-max-len 128

    # 客户端缓冲区限制
    client-output-buffer-limit normal 0 0 0
    client-output-buffer-limit replica 256mb 64mb 60
    client-output-buffer-limit pubsub 32mb 8mb 60

    # 惰性释放
    lazyfree-lazy-eviction yes
    lazyfree-lazy-expire yes
    lazyfree-lazy-server-del yes

    8.4.3 备份与恢复

    #!/bin/bash
    # scripts/backup-redis.sh – Redis备份脚本

    BACKUP_DIR="/backup/redis/$(date +%Y%m%d)"
    mkdir -p "$BACKUP_DIR"
    TIMESTAMP=$(date +%H%M%S)

    # 1. 触发RDB快照
    echo "[$(date)] Triggering BGSAVE…"
    docker exec redis-prod redis-cli -a YourRedisPassword123! BGSAVE

    # 等待BGSAVE完成
    while true; do
    STATUS=$(docker exec redis-prod redis-cli -a YourRedisPassword123! INFO persistence | grep rdb_bgsave_in_progress | cut -d: -f2 | tr -d '\\r')
    if [ "$STATUS" = "0" ]; then
    break
    fi
    sleep 1
    done
    echo "[$(date)] BGSAVE completed."

    # 2. 复制RDB文件
    docker cp redis-prod:/data/dump.rdb "$BACKUP_DIR/dump-$TIMESTAMP.rdb"

    # 3. 复制AOF文件
    docker cp redis-prod:/data/appendonly.aof "$BACKUP_DIR/appendonly-$TIMESTAMP.aof" 2>/dev/null || true

    # 4. 物理备份(备份数据卷)
    docker run –rm \\
    -v redis_data:/data:ro \\
    -v "$BACKUP_DIR":/backup \\
    alpine:latest \\
    tar czf /backup/redis-data-$TIMESTAMP.tar.gz -C /data .

    # 5. 清理过期备份
    find /backup/redis/ -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \\;

    echo "[$(date)] Backup completed."
    ls -lh "$BACKUP_DIR"/

    #!/bin/bash
    # scripts/restore-redis.sh – Redis恢复脚本

    BACKUP_FILE=$1

    echo "[$(date)] Stopping Redis…"
    docker-compose stop redis

    echo "[$(date)] Restoring data…"
    if [[ "$BACKUP_FILE" == *.rdb ]]; then
    # 恢复RDB文件
    docker run –rm \\
    -v redis_data:/data \\
    -v "$(dirname "$BACKUP_FILE")":/backup:ro \\
    alpine:latest \\
    sh -c "cp /backup/$(basename $BACKUP_FILE) /data/dump.rdb && rm -f /data/appendonly.aof"
    fi

    echo "[$(date)] Starting Redis…"
    docker-compose start redis

    echo "[$(date)] Restore completed."

    8.5 Elasticsearch数据持久化方案

    8.5.1 完整Docker Compose配置

    # docker-compose.yml – Elasticsearch完整持久化方案
    version: '3.8'

    services:
    elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    container_name: esprod
    restart: unlessstopped
    ports:
    "9200:9200"
    "9300:9300"
    environment:
    discovery.type=singlenode
    xpack.security.enabled=false
    ES_JAVA_OPTS=Xms512m Xmx512m
    cluster.name=escluster
    node.name=esnode1
    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: kibanaprod
    restart: unlessstopped
    ports:
    "5601:5601"
    environment:
    ELASTICSEARCH_HOSTS=http://esprod:9200
    depends_on:
    elasticsearch:
    condition: service_healthy

    volumes:
    es_data:
    name: es_data
    es_logs:
    name: es_logs
    es_plugins:
    name: es_plugins

    8.5.2 Elasticsearch配置文件

    # es/conf/elasticsearch.yml – Elasticsearch配置文件

    cluster.name: escluster
    node.name: esnode1

    # 数据和日志路径
    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: singlenode

    # 内存设置
    bootstrap.memory_lock: true

    # 安全设置
    xpack.security.enabled: false
    xpack.security.enrollment.enabled: false
    xpack.security.http.ssl.enabled: false
    xpack.security.transport.ssl.enabled: false

    # 索引设置
    action.destructive_requires_name: true

    # 慢查询日志
    indices.queries.cache.size: 10%
    indices.fielddata.cache.size: 40%

    # 线程池
    thread_pool.search.size: 12
    thread_pool.search.queue_size: 1000

    8.5.3 备份与恢复

    #!/bin/bash
    # scripts/backup-es.sh – Elasticsearch备份脚本

    BACKUP_DIR="/backup/es/$(date +%Y%m%d)"
    mkdir -p "$BACKUP_DIR"
    TIMESTAMP=$(date +%H%M%S)
    ES_HOST="http://localhost:9200"

    # 1. 注册快照仓库(使用共享文件系统)
    # 需要在elasticsearch.yml中设置 path.repo: ["/usr/share/elasticsearch/backup"]
    docker exec es-prod curl -s -X PUT "$ES_HOST/_snapshot/backup_repo" \\
    -H 'Content-Type: application/json' \\
    -d '{
    "type": "fs",
    "settings": {
    "location": "/usr/share/elasticsearch/backup",
    "compress": true
    }
    }'

    # 2. 创建快照
    docker exec es-prod curl -s -X PUT "$ES_HOST/_snapshot/backup_repo/snapshot_$TIMESTAMP?wait_for_completion=true" \\
    -H 'Content-Type: application/json' \\
    -d '{
    "indices": "*",
    "ignore_unavailable": true,
    "include_global_state": false
    }'

    # 3. 物理备份(备份数据卷)
    echo "[$(date)] Backing up data volume…"
    docker run –rm \\
    -v es_data:/usr/share/elasticsearch/data:ro \\
    -v "$BACKUP_DIR":/backup \\
    alpine:latest \\
    tar czf /backup/es-data-$TIMESTAMP.tar.gz -C /usr/share/elasticsearch/data .

    # 4. 清理过期备份
    find /backup/es/ -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \\;

    echo "[$(date)] Backup completed."
    ls -lh "$BACKUP_DIR"/

    8.6 数据库存储通用最佳实践

    8.6.1 通用配置建议

    # 通用最佳实践(适用于所有数据库)

    # 1. 始终使用命名数据卷,不要使用匿名数据卷
    volumes:
    db_data:/var/lib/database # 命名数据卷(好)
    # – /var/lib/database # 匿名数据卷(差)

    # 2. 为数据卷添加标签
    volumes:
    db_data:
    name: mysql_prod_data
    labels:
    app: mysql
    env: production
    backup: daily
    retention: 30d

    # 3. 设置合理的健康检查
    healthcheck:
    test: ["CMD-SHELL", "检查命令"]
    interval: 10s
    timeout: 5s
    retries: 5
    start_period: 30s # 给数据库足够的启动时间

    # 4. 设置资源限制
    deploy:
    resources:
    limits:
    memory: 2G
    cpus: '2.0'
    reservations:
    memory: 1G

    # 5. 设置重启策略
    restart: unlessstopped

    # 6. 日志限制
    logging:
    driver: "json-file"
    options:
    max-size: "10m"
    max-file: "3"

    # 7. 使用环境变量管理密码(配合.env文件)
    environment:
    DB_PASSWORD: ${DB_PASSWORD}

    8.6.2 数据卷分离策略

    # 将不同类型的数据存储在不同的数据卷中,便于管理

    volumes:
    # 核心数据(最高优先级备份)
    db_data:/var/lib/mysql

    # 二进制日志/WAL日志(用于复制和恢复)
    db_binlog:/var/lib/mysqlbin

    # 慢查询日志(用于性能优化)
    db_slowlog:/var/log/mysql/slow

    # 错误日志
    db_errorlog:/var/log/mysql/error

    # 临时文件(tmpfs,不需要持久化)
    # 通过tmpfs挂载

    8.7 数据库存储性能优化

    8.7.1 存储驱动优化

    # 1. 使用overlay2存储驱动(性能最佳)
    # /etc/docker/daemon.json
    # {
    # "storage-driver": "overlay2"
    # }

    # 2. 将数据目录放在高性能磁盘上
    # 使用SSD存储数据库数据卷
    # 修改Docker数据根目录
    # /etc/docker/daemon.json
    # {
    # "data-root": "/mnt/ssd/docker"
    # }

    # 3. 使用块设备数据卷(绕过文件系统开销)
    # 格式化SSD
    sudo mkfs.ext4 /dev/nvme0n1
    # 创建块设备数据卷
    docker volume create \\
    –driver local \\
    –opt type=ext4 \\
    –opt device=/dev/nvme0n1 \\
    mysql-block-data

    8.7.2 内存与缓存优化

    # 数据库容器内存优化建议

    services:
    mysql:
    # 设置足够的内存
    deploy:
    resources:
    limits:
    memory: 4G # 根据实际情况调整

    # 调整InnoDB缓冲池(MySQL)
    command: >
    –innodb_buffer_pool_size=2G
    –innodb_buffer_pool_instances=4
    –innodb_flush_method=O_DIRECT
    –innodb_io_capacity=2000
    –innodb_io_capacity_max=4000

    # 使用tmpfs存储临时表
    tmpfs:
    /tmp:size=256m
    /var/tmp:size=256m

    8.7.3 I/O调度优化

    # 1. 为数据库磁盘选择合适的I/O调度器
    # 查看当前调度器
    cat /sys/block/sda/queue/scheduler

    # 设置为deadline(适合数据库)
    echo deadline > /sys/block/sda/queue/scheduler

    # 对于NVMe SSD,使用none调度器
    echo none > /sys/block/nvme0n1/queue/scheduler

    # 2. 调整文件系统挂载选项(如果是绑定挂载或自定义数据根目录)
    # /etc/fstab
    # /dev/sdb1 /var/lib/docker ext4 defaults,noatime,nodiratime 0 0
    # noatime: 不更新文件访问时间,减少I/O
    # nodiratime: 不更新目录访问时间

    # 3. 禁用透明大页(THP),对数据库性能有负面影响
    echo never > /sys/kernel/mm/transparent_hugepage/enabled
    echo never > /sys/kernel/mm/transparent_hugepage/defrag

    8.7.4 数据库特定优化总结
    数据库关键优化项推荐值
    MySQL innodb_buffer_pool_size 物理内存的50-70%
    MySQL innodb_flush_log_at_trx_commit 1(安全)/2(性能)
    MySQL innodb_flush_method O_DIRECT
    PostgreSQL shared_buffers 物理内存的25%
    PostgreSQL effective_cache_size 物理内存的50-75%
    PostgreSQL wal_buffers 16MB
    MongoDB wiredTiger.cacheSizeGB 物理内存的50%
    Redis maxmemory 根据需求设置
    Redis appendfsync everysec(兼顾安全和性能)
    Elasticsearch ES_JAVA_OPTS -Xms -Xmx 相同值,物理内存的50%

    第九章 存储安全与备份策略

    9.1 数据卷加密

    9.1.1 为什么需要加密数据卷

    在生产环境中,数据卷中可能存储着敏感信息,如用户数据、密码、密钥、商业机密等。如果宿主机被入侵或磁盘被盗,未加密的数据将直接暴露。加密数据卷可以确保即使物理存储介质泄露,数据也无法被读取。

    9.1.2 使用LUKS加密(块设备级别)

    # 步骤1:创建并格式化LUKS加密设备
    sudo apt-get install cryptsetup # 安装cryptsetup
    sudo cryptsetup luksFormat /dev/sdb1
    # 输入加密密码(务必记住!)

    # 步骤2:打开加密设备
    sudo cryptsetup luksOpen /dev/sdb1 encrypted_docker
    # 输入密码

    # 步骤3:在加密设备上创建文件系统
    sudo mkfs.ext4 /dev/mapper/encrypted_docker

    # 步骤4:挂载加密设备
    sudo mkdir -p /mnt/encrypted-docker
    sudo mount /dev/mapper/encrypted_docker /mnt/encrypted-docker

    # 步骤5:创建Docker数据卷指向加密设备
    docker volume create \\
    –driver local \\
    –opt type=none \\
    –opt device=/mnt/encrypted-docker \\
    –opt o=bind \\
    encrypted-data

    # 步骤6:使用加密数据卷
    docker run -d \\
    –name secure-db \\
    -v encrypted-data:/var/lib/mysql \\
    -e MYSQL_ROOT_PASSWORD=RootPass123! \\
    mysql:8.0

    # 开机自动挂载加密设备
    # /etc/crypttab:
    # encrypted_docker /dev/sdb1 none luks
    # /etc/fstab:
    # /dev/mapper/encrypted_docker /mnt/encrypted-docker ext4 defaults 0 0

    9.1.3 使用Docker加密插件

    # 安装Docker加密卷插件(以vieux/sshfs为例,实际可使用专用加密插件)
    # 常见的加密卷插件包括:
    # – docker-volume-crypt(基于LUKS)
    # – Rancher Longhorn(支持加密)
    # – Portworx(企业级存储,支持加密)

    # 示例:使用第三方加密插件
    docker plugin install <encryption-plugin-name>

    # 创建加密数据卷
    docker volume create \\
    –driver <encryption-plugin-name> \\
    –opt encrypted=true \\
    –opt key=<encryption-key-id> \\
    encrypted-volume

    9.1.4 应用层加密

    # 应用层加密示例(Python)
    # 在写入数据卷之前加密数据

    from cryptography.fernet import Fernet
    import os

    # 生成或加载密钥
    key_file = '/app/keys/fernet.key'
    if os.path.exists(key_file):
    with open(key_file, 'rb') as f:
    key = f.read()
    else:
    key = Fernet.generate_key()
    os.makedirs(os.path.dirname(key_file), exist_ok=True)
    with open(key_file, 'wb') as f:
    f.write(key)

    cipher = Fernet(key)

    # 加密数据并写入数据卷
    def write_encrypted(filepath, data):
    encrypted_data = cipher.encrypt(data.encode())
    with open(filepath, 'wb') as f:
    f.write(encrypted_data)

    # 读取并解密数据
    def read_encrypted(filepath):
    with open(filepath, 'rb') as f:
    encrypted_data = f.read()
    return cipher.decrypt(encrypted_data).decode()

    # 使用示例
    write_encrypted('/app/data/secret.txt', '这是机密信息')
    print(read_encrypted('/app/data/secret.txt'))
    # 输出: 这是机密信息

    9.2 数据卷访问控制

    9.2.1 文件系统级别权限控制

    # 1. 使用Linux用户和组控制访问
    # 创建专用的Docker数据组
    sudo groupadd docker-data
    sudo usermod -aG docker-data $(whoami)

    # 设置数据卷目录的组和权限
    VOLUME_PATH=$(docker volume inspect my-volume –format '{{.Mountpoint}}')
    sudo chgrp -R docker-data $VOLUME_PATH
    sudo chmod -R 770 $VOLUME_PATH

    # 2. 使用ACL(访问控制列表)进行更细粒度的控制
    sudo setfacl -R -m u:appuser:rwx $VOLUME_PATH
    sudo setfacl -R -m u:readonlyuser:rx $VOLUME_PATH
    sudo setfacl -R -m o::— $VOLUME_PATH

    # 查看ACL设置
    sudo getfacl $VOLUME_PATH

    9.2.2 容器级别权限控制

    # 1. 使用–user参数限制容器内用户
    docker run -d \\
    –name app \\
    –user 1000:1000 \\
    -v my-data:/data \\
    my-app:latest

    # 2. 使用–read-only使整个根文件系统只读
    docker run -d \\
    –name secure-app \\
    –read-only \\
    -v my-data:/data \\
    –tmpfs /tmp \\
    my-app:latest

    # 3. 使用–cap-drop移除不必要的Linux capabilities
    docker run -d \\
    –name secure-app \\
    –cap-drop ALL \\
    –cap-add CHOWN \\
    –cap-add SETGID \\
    –cap-add SETUID \\
    -v my-data:/data \\
    my-app:latest

    # 4. 使用–security-opt增强安全性
    docker run -d \\
    –name secure-app \\
    –security-opt no-new-privileges \\
    –security-opt label=type:container_runtime_t \\
    -v my-data:/data \\
    my-app:latest

    9.2.3 数据卷隔离策略

    # docker-compose.yml – 多租户数据隔离
    version: '3.8'

    services:
    # 租户A的服务
    tenant-a-app:
    image: myapp:latest
    user: "1001:1001"
    volumes:
    tenant_a_data:/data # 租户A专用数据卷
    networks:
    tenant_a_network # 租户A专用网络

    # 租户B的服务
    tenant-b-app:
    image: myapp:latest
    user: "1002:1002"
    volumes:
    tenant_b_data:/data # 租户B专用数据卷
    networks:
    tenant_b_network # 租户B专用网络

    volumes:
    tenant_a_data:
    name: tenant_a_data
    labels:
    tenant: "A"
    security: "isolated"
    tenant_b_data:
    name: tenant_b_data
    labels:
    tenant: "B"
    security: "isolated"

    networks:
    tenant_a_network:
    driver: bridge
    tenant_b_network:
    driver: bridge

    9.3 备份策略设计(全量/增量/差异备份)

    9.3.1 备份类型详解
    备份类型说明优点缺点适用场景
    全量备份 备份所有数据 恢复简单,数据完整 耗时长,空间大 周期性基础备份
    增量备份 只备份自上次备份以来变化的数据 速度快,空间小 恢复复杂,依赖链长 频繁备份
    差异备份 只备份自上次全量备份以来变化的数据 恢复较简单 空间逐渐增大 中等频率备份
    9.3.2 全量备份实现

    #!/bin/bash
    # full-backup.sh – 全量备份脚本

    VOLUME_NAME=$1
    BACKUP_DIR="/backup/full/$(date +%Y%m%d)"
    mkdir -p "$BACKUP_DIR"

    echo "[$(date)] Starting full backup of $VOLUME_NAME…"

    # 全量备份:打包整个数据卷
    docker run –rm \\
    -v "$VOLUME_NAME":/data:ro \\
    -v "$BACKUP_DIR":/backup \\
    alpine:latest \\
    tar czf "/backup/${VOLUME_NAME}-full-$(date +%H%M%S).tar.gz" -C /data .

    echo "[$(date)] Full backup completed."
    ls -lh "$BACKUP_DIR"/

    9.3.3 增量备份实现(使用rsync)

    #!/bin/bash
    # incremental-backup.sh – 增量备份脚本(基于rsync)

    VOLUME_NAME=$1
    BACKUP_BASE="/backup/incremental"
    LATEST_LINK="$BACKUP_BASE/latest"
    CURRENT_BACKUP="$BACKUP_BASE/$(date +%Y%m%d-%H%M%S)"

    mkdir -p "$CURRENT_BACKUP"

    echo "[$(date)] Starting incremental backup of $VOLUME_NAME…"

    # 使用rsync的–link-dest进行增量备份
    # 原理:与上次备份比较,只传输变化的文件
    # 未变化的文件通过硬链接引用,不占用额外空间
    docker run –rm \\
    -v "$VOLUME_NAME":/data:ro \\
    -v "$BACKUP_BASE":/backup \\
    alpine:latest \\
    sh -c "
    apk add –no-cache rsync
    if [ -d /backup/latest ]; then
    rsync -a –delete –link-dest=/backup/latest /data/ /backup/current/
    else
    rsync -a /data/ /backup/current/
    fi
    "

    # 重命名current为带时间戳的目录
    # (在实际实现中需要更完善的逻辑)

    echo "[$(date)] Incremental backup completed."

    9.3.4 差异备份实现

    #!/bin/bash
    # differential-backup.sh – 差异备份脚本

    VOLUME_NAME=$1
    BACKUP_BASE="/backup/differential"
    LAST_FULL=$(ls -d "$BACKUP_BASE"/full-* 2>/dev/null | sort | tail -1)
    CURRENT_BACKUP="$BACKUP_BASE/diff-$(date +%Y%m%d-%H%M%S)"

    if [ -z "$LAST_FULL" ]; then
    echo "Error: No full backup found. Please run full backup first."
    exit 1
    fi

    mkdir -p "$CURRENT_BACKUP"

    echo "[$(date)] Starting differential backup of $VOLUME_NAME…"
    echo " Based on full backup: $LAST_FULL"

    # 差异备份:与最近的全量备份比较
    # 使用find命令找出变化的文件
    docker run –rm \\
    -v "$VOLUME_NAME":/data:ro \\
    -v "$BACKUP_BASE":/backup \\
    alpine:latest \\
    sh -c "
    REFERENCE_DIR=/backup/$(basename $LAST_FULL)
    # 找出与全量备份不同的文件并打包
    cd /data
    find . -type f -newer "
    $REFERENCE_DIR" -print0 | \\
    tar –null -czf /backup/$(basename $CURRENT_BACKUP).tar.gz -T –
    "

    echo "[$(date)] Differential backup completed."

    9.3.5 综合备份策略

    #!/bin/bash
    # backup-strategy.sh – 综合备份策略
    # 策略:每周一次全量备份,每天一次增量备份
    # 保留:4个全量备份(4周),28个增量备份(28天)

    VOLUME_NAME=$1
    DAY_OF_WEEK=$(date +%u) # 1=Monday, 7=Sunday
    BACKUP_DIR="/backup"

    # 周日执行全量备份
    if [ "$DAY_OF_WEEK" = "7" ]; then
    echo "[$(date)] Running weekly full backup…"
    /opt/scripts/full-backup.sh "$VOLUME_NAME"

    # 清理超过4周的全量备份
    find "$BACKUP_DIR/full/" -maxdepth 1 -type d -mtime +28 -exec rm -rf {} \\;
    else
    echo "[$(date)] Running daily incremental backup…"
    /opt/scripts/incremental-backup.sh "$VOLUME_NAME"
    fi

    # 清理超过28天的增量备份
    find "$BACKUP_DIR/incremental/" -maxdepth 1 -type d -mtime +28 -exec rm -rf {} \\;

    echo "[$(date)] Backup strategy executed."

    # 设置crontab定时执行
    # 每天凌晨2点执行备份
    # crontab -e
    # 0 2 * * * /opt/scripts/backup-strategy.sh mysql_data >> /var/log/backup.log 2>&1

    9.4 使用定时任务自动备份

    9.4.1 宿主机crontab方案

    #!/bin/bash
    # /opt/scripts/auto-backup-all.sh – 自动备份所有数据卷

    BACKUP_ROOT="/backup"
    LOG_FILE="/var/log/docker-backup.log"
    TIMESTAMP=$(date +%Y%m%d-%H%M%S)

    log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
    }

    log "=== Starting automatic backup ==="

    # 获取所有标记了backup=yes的数据卷
    VOLUMES=$(docker volume ls –filter label=backup=yes -q)

    for vol in $VOLUMES; do
    log "Backing up volume: $vol"

    # 获取保留天数
    RETENTION=$(docker volume inspect "$vol" –format '{{index .Labels "retention"}}' 2>/dev/null)
    RETENTION=${RETENTION:-7}

    BACKUP_DIR="$BACKUP_ROOT/$vol/$(date +%Y%m%d)"
    mkdir -p "$BACKUP_DIR"

    # 执行备份
    docker run –rm \\
    -v "$vol":/data:ro \\
    -v "$BACKUP_DIR":/backup \\
    alpine:latest \\
    tar czf "/backup/${vol}${TIMESTAMP}.tar.gz" -C /data . 2>> "$LOG_FILE"

    if [ $? -eq 0 ]; then
    SIZE=$(du -sh "$BACKUP_DIR" | cut -f1)
    log " Backup OK. Size: $SIZE"

    # 清理过期备份
    find "$BACKUP_ROOT/$vol/" -maxdepth 1 -type d -mtime +"$RETENTION" -exec rm -rf {} \\;
    log " Cleaned backups older than $RETENTION days"
    else
    log " Backup FAILED for $vol!"
    fi
    done

    log "=== Automatic backup completed ==="

    # crontab配置
    # 编辑crontab
    sudo crontab -e

    # 添加以下内容:
    # 每天凌晨2:00执行备份
    0 2 * * * /opt/scripts/auto-backup-all.sh

    # 每周日凌晨1:00执行全量备份
    0 1 * * 0 /opt/scripts/full-backup-all.sh

    # 每小时执行增量备份(仅特定数据卷)
    0 * * * * /opt/scripts/incremental-backup-all.sh

    9.4.2 使用备份容器方案

    # docker-compose.yml – 备份容器
    version: '3.8'

    services:
    backup:
    image: alpine:latest
    container_name: dockerbackup
    restart: unlessstopped
    volumes:
    # 需要备份的数据卷(只读)
    mysql_data:/data/mysql:ro
    postgres_data:/data/postgres:ro
    # 备份存储目录
    /backup:/backup
    # 备份脚本
    ./scripts/autobackup.sh:/backup.sh:ro
    # crontab配置
    ./scripts/crontab:/etc/crontabs/root:ro
    environment:
    TZ: Asia/Shanghai
    entrypoint: ["/bin/sh", "-c"]
    command: ["apk add –no-cache docker-cli && crond -f -l 2"]

    volumes:
    mysql_data:
    external: true
    postgres_data:
    external: true

    # scripts/crontab – 备份容器的crontab配置
    # 每天2:00备份MySQL
    0 2 * * * /backup.sh mysql /data/mysql /backup/mysql

    # 每天2:30备份PostgreSQL
    30 2 * * * /backup.sh postgres /data/postgres /backup/postgres

    # 每周日1:00清理旧备份
    0 1 * * 0 find /backup -maxdepth 2 -type d -mtime +30 -exec rm -rf {} \\;

    9.5 备份验证与恢复测试

    9.5.1 备份验证

    #!/bin/bash
    # verify-backup.sh – 验证备份完整性

    BACKUP_FILE=$1

    if [ -z "$BACKUP_FILE" ]; then
    echo "Usage: $0 <backup_file>"
    exit 1
    fi

    echo "[$(date)] Verifying backup: $BACKUP_FILE"

    # 1. 检查文件是否存在
    if [ ! -f "$BACKUP_FILE" ]; then
    echo "ERROR: Backup file not found!"
    exit 1
    fi

    # 2. 检查文件大小
    SIZE=$(stat -c%s "$BACKUP_FILE" 2>/dev/null || stat -f%z "$BACKUP_FILE")
    if [ "$SIZE" -lt 1024 ]; then
    echo "WARNING: Backup file is suspiciously small ($SIZE bytes)"
    fi

    # 3. 验证tar包完整性
    echo "[$(date)] Checking tar integrity…"
    tar tzf "$BACKUP_FILE" > /dev/null 2>&1
    if [ $? -ne 0 ]; then
    echo "ERROR: Backup file is corrupted!"
    exit 1
    fi

    # 4. 列出备份内容统计
    FILE_COUNT=$(tar tzf "$BACKUP_FILE" | wc -l)
    echo " File count: $FILE_COUNT"
    echo " File size: $(du -h "$BACKUP_FILE" | cut -f1)"

    # 5. 验证特定文件是否存在
    echo "[$(date)] Checking key files…"
    tar tzf "$BACKUP_FILE" | grep -q "ibdata1" && echo " MySQL data file: OK" || echo " MySQL data file: MISSING"
    tar tzf "$BACKUP_FILE" | grep -q "mysql" && echo " MySQL directory: OK" || echo " MySQL directory: MISSING"

    echo "[$(date)] Verification completed."

    9.5.2 自动化恢复测试

    #!/bin/bash
    # restore-test.sh – 自动化恢复测试
    # 在隔离环境中测试备份恢复

    BACKUP_FILE=$1
    TEST_VOLUME="test-restore-$(date +%s)"

    echo "[$(date)] Starting restore test…"
    echo " Backup file: $BACKUP_FILE"
    echo " Test volume: $TEST_VOLUME"

    # 1. 创建测试数据卷
    docker volume create "$TEST_VOLUME"

    # 2. 从备份恢复到测试数据卷
    echo "[$(date)] Restoring backup to test volume…"
    docker run –rm \\
    -v "$TEST_VOLUME":/data \\
    -v "$(dirname "$BACKUP_FILE")":/backup:ro \\
    alpine:latest \\
    tar xzf "/backup/$(basename "$BACKUP_FILE")" -C /data

    # 3. 验证恢复的数据
    echo "[$(date)] Verifying restored data…"
    FILE_COUNT=$(docker run –rm -v "$TEST_VOLUME":/data alpine find /data -type f | wc -l)
    echo " Restored file count: $FILE_COUNT"

    # 4. 启动测试容器验证数据
    echo "[$(date)] Starting test container…"
    docker run -d \\
    –name restore-test \\
    -v "$TEST_VOLUME":/var/lib/mysql \\
    -e MYSQL_ROOT_PASSWORD=testpass \\
    mysql:8.0

    # 等待MySQL启动
    sleep 30

    # 5. 验证数据库可用性
    echo "[$(date)] Testing database connectivity…"
    docker exec restore-test mysql -uroot -ptestpass -e "SHOW DATABASES;" > /dev/null 2>&1
    if [ $? -eq 0 ]; then
    echo " Database is accessible!"
    docker exec restore-test mysql -uroot -ptestpass -e "SHOW DATABASES;"
    else
    echo " ERROR: Database is not accessible!"
    fi

    # 6. 清理测试环境
    echo "[$(date)] Cleaning up…"
    docker rm -f restore-test
    docker volume rm "$TEST_VOLUME"

    echo "[$(date)] Restore test completed."

    9.6 灾难恢复方案

    9.6.1 灾难恢复计划

    灾难恢复计划(DR Plan)

    1. 灾难场景
    – 宿主机硬件故障
    – 磁盘损坏
    – 数据中心故障
    – 误操作导致数据丢失
    – 勒索软件攻击

    2. 恢复目标
    – RTO(恢复时间目标): < 4小时
    – RPO(恢复点目标): < 24小时

    3. 恢复步骤
    Step 1: 评估损失 (15分钟)
    Step 2: 准备恢复环境 (30分钟)
    Step 3: 恢复数据卷 (1-2小时)
    Step 4: 启动服务 (30分钟)
    Step 5: 验证数据完整性 (30分钟)
    Step 6: 切换流量 (15分钟)

    9.6.2 异地备份方案

    #!/bin/bash
    # offsite-backup.sh – 异地备份脚本

    LOCAL_BACKUP_DIR="/backup"
    REMOTE_HOST="backup-server.example.com"
    REMOTE_DIR="/mnt/backup/docker"
    REMOTE_USER="backup"

    echo "[$(date)] Starting offsite backup sync…"

    # 使用rsync同步备份到远程服务器
    rsync -avz –progress \\
    –delete \\
    –bwlimit=10000 \\
    -e "ssh -p 22 -i /home/backup/.ssh/id_rsa" \\
    "$LOCAL_BACKUP_DIR/" \\
    "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"

    if [ $? -eq 0 ]; then
    echo "[$(date)] Offsite backup completed successfully."
    else
    echo "[$(date)] Offsite backup FAILED!"
    # 发送告警
    echo "Docker backup offsite sync failed!" | mail -s "Backup Alert" admin@example.com
    fi

    # 验证远程备份
    echo "[$(date)] Verifying remote backup…"
    ssh "$REMOTE_USER@$REMOTE_HOST" \\
    "ls -lh $REMOTE_DIR/$(date +%Y%m%d)/"

    9.6.3 完整灾难恢复流程

    #!/bin/bash
    # disaster-recovery.sh – 灾难恢复脚本

    REMOTE_HOST="backup-server.example.com"
    REMOTE_DIR="/mnt/backup/docker"
    REMOTE_USER="backup"

    echo "=========================================="
    echo " Docker Disaster Recovery Script"
    echo " Started at: $(date)"
    echo "=========================================="

    # Step 1: 评估环境
    echo "[Step 1] Checking environment…"
    docker version > /dev/null 2>&1
    if [ $? -ne 0 ]; then
    echo "ERROR: Docker is not running!"
    exit 1
    fi

    # Step 2: 从远程下载最新备份
    echo "[Step 2] Downloading latest backup…"
    LATEST_BACKUP=$(ssh "$REMOTE_USER@$REMOTE_HOST" \\
    "ls -d $REMOTE_DIR/*/ | sort | tail -1")

    echo " Latest backup: $LATEST_BACKUP"

    mkdir -p /tmp/restore
    scp -r "$REMOTE_USER@$REMOTE_HOST:$LATEST_BACKUP" /tmp/restore/

    # Step 3: 恢复数据卷
    echo "[Step 3] Restoring volumes…"
    for backup_file in /tmp/restore/$(basename $LATEST_BACKUP)/*.tar.gz; do
    vol_name=$(basename "$backup_file" .tar.gz)
    vol_name=${vol_name%%-*} # 提取卷名

    echo " Restoring volume: $vol_name"
    docker volume create "$vol_name" 2>/dev/null || true

    docker run –rm \\
    -v "$vol_name":/data \\
    -v /tmp/restore:/backup:ro \\
    alpine:latest \\
    tar xzf "/backup/$(basename $LATEST_BACKUP)/$(basename $backup_file)" -C /data
    done

    # Step 4: 启动服务
    echo "[Step 4] Starting services…"
    cd /opt/docker-compose
    docker-compose up -d

    # Step 5: 等待服务就绪
    echo "[Step 5] Waiting for services…"
    sleep 60

    # Step 6: 验证
    echo "[Step 6] Verifying services…"
    docker-compose ps
    docker exec mysql-prod mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW DATABASES;"

    echo "=========================================="
    echo " Disaster recovery completed at: $(date)"
    echo "=========================================="

    9.7 存储安全审计

    9.7.1 审计脚本

    #!/bin/bash
    # storage-audit.sh – Docker存储安全审计

    echo "=========================================="
    echo " Docker Storage Security Audit"
    echo " Date: $(date)"
    echo "=========================================="

    echo ""
    echo "=== 1. Docker Configuration ==="
    echo "Storage Driver:"
    docker info 2>/dev/null | grep "Storage Driver"
    echo "Data Root:"
    docker info 2>/dev/null | grep "Docker Root Dir"

    echo ""
    echo "=== 2. Volume Security ==="
    echo "Total volumes: $(docker volume ls -q | wc -l)"
    echo "Dangling volumes: $(docker volume ls -f dangling=true -q | wc -l)"

    echo ""
    echo "Checking volume permissions…"
    for vol in $(docker volume ls -q); do
    MOUNTPOINT=$(docker volume inspect "$vol" –format '{{.Mountpoint}}')
    PERMS=$(stat -c "%a %U:%G" "$MOUNTPOINT" 2>/dev/null)
    echo " $vol: $PERMS"
    done

    echo ""
    echo "=== 3. Container Security ==="
    echo "Running containers with root user:"
    docker ps -q | while read cid; do
    USER=$(docker inspect "$cid" –format '{{.Config.User}}')
    NAME=$(docker inspect "$cid" –format '{{.Name}}')
    if [ -z "$USER" ] || [ "$USER" = "root" ] || [ "$USER" = "0:0" ]; then
    echo " WARNING: $NAME is running as root"
    fi
    done

    echo ""
    echo "=== 4. Bind Mount Security ==="
    echo "Containers with bind mounts to sensitive paths:"
    docker ps -q | while read cid; do
    NAME=$(docker inspect "$cid" –format '{{.Name}}')
    MOUNTS=$(docker inspect "$cid" –format '{{json .Mounts}}')
    if echo "$MOUNTS" | grep -q '"Type":"bind"'; then
    echo " $NAME has bind mounts"
    echo "$MOUNTS" | python3 -m json.tool 2>/dev/null | grep -E '"Source"|"Destination"'
    fi
    done

    echo ""
    echo "=== 5. Backup Status ==="
    echo "Recent backups:"
    ls -lt /backup/*/*.tar.gz 2>/dev/null | head -5

    echo ""
    echo "=== 6. Disk Usage ==="
    docker system df

    echo ""
    echo "=========================================="
    echo " Audit completed."
    echo "=========================================="

    9.8 合规性要求与数据保留策略

    9.8.1 数据保留策略

    # 数据保留策略配置示例

    # 数据保留级别
    retention_policy:
    # 关键业务数据 – 长期保留
    critical:
    description: "核心业务数据(用户数据、交易数据)"
    daily_backup: true
    weekly_full_backup: true
    retention_days: 90 # 每日备份保留90天
    retention_months: 12 # 每月备份保留12个月
    retention_years: 7 # 每年备份保留7年
    encryption: true
    offsite_backup: true

    # 重要业务数据 – 中期保留
    important:
    description: "重要业务数据(日志、配置)"
    daily_backup: true
    weekly_full_backup: true
    retention_days: 30
    retention_months: 6
    encryption: true
    offsite_backup: true

    # 一般数据 – 短期保留
    normal:
    description: "一般数据(缓存、临时数据)"
    daily_backup: false
    weekly_full_backup: true
    retention_days: 14
    encryption: false
    offsite_backup: false

    # 临时数据 – 不保留
    temporary:
    description: "临时数据(临时文件、处理中间结果)"
    backup: false
    use_tmpfs: true

    9.8.2 合规性检查脚本

    #!/bin/bash
    # compliance-check.sh – 合规性检查脚本

    echo "=== Docker Storage Compliance Check ==="
    echo "Date: $(date)"
    echo ""

    # 1. 检查数据卷是否有标签
    echo "— Volume Labels —"
    for vol in $(docker volume ls -q); do
    LABELS=$(docker volume inspect "$vol" –format '{{json .Labels}}')
    if [ "$LABELS" = "null" ] || [ "$LABELS" = "{}" ]; then
    echo "WARNING: Volume '$vol' has no labels"
    fi
    done

    # 2. 检查敏感数据卷是否加密
    echo ""
    echo "— Encryption Status —"
    for vol in $(docker volume ls –filter label=sensitive=true -q); do
    echo "Checking encryption for sensitive volume: $vol"
    MOUNTPOINT=$(docker volume inspect "$vol" –format '{{.Mountpoint}}')
    # 检查是否在加密文件系统上
    FS_TYPE=$(df "$MOUNTPOINT" –output=fstype 2>/dev/null | tail -1)
    echo " Filesystem: $FS_TYPE"
    done

    # 3. 检查备份状态
    echo ""
    echo "— Backup Compliance —"
    for vol in $(docker volume ls –filter label=backup=daily -q); do
    LATEST_BACKUP=$(find /backup/$vol/ -name "*.tar.gz" -mtime -1 2>/dev/null | head -1)
    if [ -z "$LATEST_BACKUP" ]; then
    echo "VIOLATION: Volume '$vol' has no backup in the last 24 hours!"
    else
    echo "OK: Volume '$vol' has recent backup"
    fi
    done

    # 4. 检查数据保留合规性
    echo ""
    echo "— Retention Compliance —"
    for vol in $(docker volume ls -q); do
    RETENTION=$(docker volume inspect "$vol" –format '{{index .Labels "retention"}}' 2>/dev/null)
    if [ -n "$RETENTION" ]; then
    OLD_BACKUPS=$(find /backup/$vol/ -maxdepth 1 -type d -mtime +"${RETENTION%d}" 2>/dev/null | wc -l)
    if [ "$OLD_BACKUPS" -gt 0 ]; then
    echo "WARNING: Volume '$vol' has $OLD_BACKUPS backups exceeding retention policy ($RETENTION)"
    fi
    fi
    done

    echo ""
    echo "=== Compliance check completed ==="


    第十章 存储管理与故障排查

    10.1 docker system df查看存储使用情况

    10.1.1 基本用法

    # 查看Docker整体磁盘使用情况
    docker system df

    # 输出示例:
    # TYPE TOTAL ACTIVE SIZE RECLAIMABLE
    # Images 15 5 5.2GB 2.1GB (40%)
    # Containers 8 3 120MB 80MB (66%)
    # Local Volumes 12 6 8.5GB 3.2GB (37%)
    # Build Cache 0 0 0B 0B

    # 列说明:
    # TYPE: 资源类型(镜像/容器/数据卷/构建缓存)
    # TOTAL: 总数量
    # ACTIVE: 正在使用的数量
    # SIZE: 总大小
    # RECLAIMABLE: 可回收的大小(未使用的资源)

    10.1.2 详细视图

    # 查看详细的使用情况(-v参数)
    docker system df -v

    # 输出示例(镜像部分):
    # Images space usage:
    # REPOSITORY TAG IMAGE ID CREATED SIZE SHARED SIZE UNIQUE SIZE CONTAINERS
    # nginx latest 605c77e624dd 2 weeks ago 141MB 80MB 61MB 2
    # mysql 8.0 3f01e6e1e1e1 3 weeks ago 521MB 80MB 441MB 1
    # …

    # 输出示例(数据卷部分):
    # VOLUME NAME LINKS SIZE
    # mysql_data 1 2.3GB
    # postgres_data 1 1.5GB
    # redis_data 1 120MB
    # unused_volume_1 0 500MB ← 未使用的(可清理)
    # unused_volume_2 0 300MB ← 未使用的(可清理)

    10.1.3 监控脚本

    #!/bin/bash
    # monitor-docker-storage.sh – Docker存储监控脚本

    THRESHOLD=80 # 磁盘使用率阈值(%)

    echo "=== Docker Storage Monitor ==="
    echo "Time: $(date)"
    echo ""

    # 1. 整体磁盘使用
    echo "— Disk Usage —"
    df -h /var/lib/docker
    echo ""

    # 2. Docker资源使用
    echo "— Docker Resources —"
    docker system df
    echo ""

    # 3. 磁盘使用率告警
    USAGE=$(df /var/lib/docker –output=pcent | tail -1 | tr -d ' %')
    if [ "$USAGE" -gt "$THRESHOLD" ]; then
    echo "WARNING: Disk usage is ${USAGE}% (threshold: ${THRESHOLD}%)"
    echo "Consider cleaning up unused resources:"
    echo " docker system prune"
    echo " docker volume prune"
    fi
    echo ""

    # 4. 数据卷使用详情
    echo "— Volume Details —"
    for vol in $(docker volume ls -q); do
    SIZE=$(docker run –rm -v "$vol":/data alpine du -sh /data 2>/dev/null | cut -f1)
    CONTAINERS=$(docker ps -a –filter volume="$vol" -q | wc -l)
    echo " $vol: Size=$SIZE, Containers=$CONTAINERS"
    done

    10.2 清理无用数据(docker system prune, volume prune)

    10.2.1 docker system prune

    # 清理所有未使用的资源(停止的容器、未使用的网络、悬空镜像、构建缓存)
    docker system prune

    # 输出:
    # WARNING! This will remove:
    # – all stopped containers
    # – all networks not used by at least one container
    # – all dangling images
    # – all dangling build cache
    # Are you sure you want to continue? [y/N] y

    # 清理所有未使用的资源(包括未被容器引用的镜像)
    docker system prune -a

    # 同时清理未被使用的数据卷(危险!谨慎使用!)
    docker system prune -a –volumes

    # 跳过确认
    docker system prune -f

    # 带过滤条件的清理(清理24小时前创建的)
    docker system prune –filter "until=24h"

    10.2.2 分别清理各类资源

    # 1. 清理停止的容器
    docker container prune
    # 或
    docker container prune -f # 跳过确认

    # 2. 清理悬空镜像(没有标签的镜像)
    docker image prune
    # 清理所有未使用的镜像(不只是悬空镜像)
    docker image prune -a

    # 3. 清理未使用的网络
    docker network prune

    # 4. 清理未使用的数据卷
    docker volume prune
    # 带过滤条件
    docker volume prune –filter "until=24h"
    # 按标签过滤
    docker volume prune –filter "label!=keep=true"

    # 5. 清理构建缓存
    docker builder prune
    docker builder prune -a # 清理所有构建缓存

    10.2.3 安全清理脚本

    #!/bin/bash
    # safe-cleanup.sh – 安全清理脚本

    echo "=== Docker Safe Cleanup ==="
    echo "Time: $(date)"

    # 1. 显示当前使用情况
    echo "— Before cleanup —"
    docker system df
    echo ""

    # 2. 清理停止的容器(安全)
    echo "Cleaning stopped containers…"
    docker container prune -f

    # 3. 清理悬空镜像(安全)
    echo "Cleaning dangling images…"
    docker image prune -f

    # 4. 清理未使用的网络(安全)
    echo "Cleaning unused networks…"
    docker network prune -f

    # 5. 清理构建缓存(安全)
    echo "Cleaning build cache…"
    docker builder prune -f

    # 6. 显示清理后的使用情况
    echo ""
    echo "— After cleanup —"
    docker system df

    # 7. 显示可进一步清理的资源
    echo ""
    echo "— Further cleanup options —"
    echo "Unused images (not used by any container):"
    docker images -f "dangling=false" –format "{{.Repository}}:{{.Tag}} {{.Size}}" | head -10
    echo ""
    echo "Unused volumes (not used by any container):"
    docker volume ls -f dangling=true

    echo ""
    echo "=== Cleanup completed ==="

    10.3 磁盘空间不足的处理

    10.3.1 诊断磁盘空间

    # 1. 查看磁盘整体使用情况
    df -h

    # 2. 查看Docker目录使用情况
    sudo du -sh /var/lib/docker/
    sudo du -sh /var/lib/docker/*/

    # 3. 查找最大的文件和目录
    sudo du -ah /var/lib/docker/ | sort -rh | head -20

    # 4. 查看容器日志大小
    sudo du -sh /var/lib/docker/containers/*/

    # 5. 查看数据卷大小
    sudo du -sh /var/lib/docker/volumes/*/

    # 6. 查看镜像层大小
    sudo du -sh /var/lib/docker/overlay2/*/

    10.3.2 清理容器日志

    # 容器日志可能占用大量空间
    # 查看容器日志大小
    sudo sh -c 'du -sh /var/lib/docker/containers/*/*-json.log'

    # 清理单个容器的日志
    sudo truncate -s 0 /var/lib/docker/containers/<container_id>/<container_id>-json.log

    # 清理所有容器日志
    sudo sh -c 'truncate -s 0 /var/lib/docker/containers/*/*-json.log'

    # 配置日志轮转(防止日志过大)
    # /etc/docker/daemon.json
    # {
    # "log-driver": "json-file",
    # "log-opts": {
    # "max-size": "10m",
    # "max-file": "3"
    # }
    # }

    10.3.3 迁移Docker数据目录

    # 当系统盘空间不足时,将Docker数据迁移到更大的磁盘

    # 1. 停止Docker
    sudo systemctl stop docker
    sudo systemctl stop docker.socket

    # 2. 创建新目录
    sudo mkdir -p /data/docker

    # 3. 迁移数据(使用rsync保留权限)
    sudo rsync -aP /var/lib/docker/ /data/docker/

    # 4. 修改Docker配置
    sudo tee /etc/docker/daemon.json << 'EOF'
    {
    "data-root": "/data/docker"
    }
    EOF

    # 5. 启动Docker
    sudo systemctl start docker

    # 6. 验证
    docker info | grep "Docker Root Dir"
    # 输出: Docker Root Dir: /data/docker

    # 7. 确认所有容器正常运行后,删除旧目录
    sudo rm -rf /var/lib/docker

    10.3.4 紧急空间释放

    #!/bin/bash
    # emergency-cleanup.sh – 紧急空间释放脚本

    echo "=== Emergency Space Cleanup ==="

    # 1. 清理所有停止的容器
    echo "Removing stopped containers…"
    docker rm -v $(docker ps -aq –filter status=exited) 2>/dev/null

    # 2. 清理所有未使用的镜像
    echo "Removing unused images…"
    docker rmi $(docker images -q –filter "dangling=true") 2>/dev/null
    docker image prune -a -f 2>/dev/null

    # 3. 清理未使用的数据卷(谨慎!)
    echo "Removing unused volumes…"
    docker volume prune -f

    # 4. 清理构建缓存
    echo "Cleaning build cache…"
    docker builder prune -a -f

    # 5. 清理容器日志
    echo "Truncating container logs…"
    sudo sh -c 'truncate -s 0 /var/lib/docker/containers/*/*-json.log' 2>/dev/null

    # 6. 显示结果
    echo ""
    echo "=== After cleanup ==="
    docker system df
    echo ""
    df -h /var/lib/docker

    10.4 数据卷损坏修复

    10.4.1 诊断数据卷损坏

    # 1. 检查数据卷是否可访问
    docker run –rm -v my-volume:/data alpine ls -la /data

    # 如果报错,可能数据卷已损坏

    # 2. 检查宿主机上的数据卷目录
    VOLUME_PATH=$(docker volume inspect my-volume –format '{{.Mountpoint}}')
    sudo ls -la $VOLUME_PATH

    # 3. 检查文件系统
    sudo fsck.ext4 /dev/sda1 # 如果使用ext4
    # 或
    sudo xfs_repair /dev/sda1 # 如果使用xfs

    # 4. 查看dmesg中的文件系统错误
    dmesg | grep -i error

    10.4.2 修复步骤

    # 步骤1:停止使用该数据卷的所有容器
    docker ps –filter volume=my-volume -q | xargs docker stop

    # 步骤2:尝试从备份恢复
    # 如果有备份,直接恢复
    docker volume rm my-volume
    docker volume create my-volume
    docker run –rm \\
    -v my-volume:/data \\
    -v /backup:/backup:ro \\
    alpine:latest \\
    tar xzf /backup/my-volume-backup.tar.gz -C /data

    # 步骤3:如果没有备份,尝试手动修复
    # 使用fsck修复底层文件系统
    sudo umount /var/lib/docker/volumes/my-volume/_data 2>/dev/null
    sudo fsck -y /dev/sda1
    sudo mount -a

    # 步骤4:如果数据卷目录损坏,尝试恢复文件
    sudo mkdir -p /tmp/recovery
    sudo cp -a /var/lib/docker/volumes/my-volume/_data/ /tmp/recovery/ 2>/dev/null
    # 检查恢复的文件
    ls -la /tmp/recovery/

    10.5 权限问题排查

    10.5.1 常见权限问题

    # 问题1:容器内无法写入数据卷
    # 错误信息: Permission denied

    # 诊断步骤
    # 1. 检查容器内进程的用户
    docker exec <container> id
    # 输出: uid=0(root) gid=0(root)

    # 2. 检查数据卷的所有者
    VOLUME_PATH=$(docker volume inspect my-volume –format '{{.Mountpoint}}')
    sudo ls -la $VOLUME_PATH
    # 输出: drwxr-xr-x 2 root root 4096 …

    # 3. 检查容器挂载信息
    docker inspect <container> –format '{{json .Mounts}}' | python3 -m json.tool

    10.5.2 解决权限问题

    # 解决方案1:修改数据卷权限
    VOLUME_PATH=$(docker volume inspect my-volume –format '{{.Mountpoint}}')

    # 修改为容器内用户的UID
    # 例如容器内用户UID为1000
    sudo chown -R 1000:1000 $VOLUME_PATH
    sudo chmod -R 755 $VOLUME_PATH

    # 解决方案2:使用初始化容器设置权限
    docker run –rm \\
    -v my-volume:/data \\
    alpine:latest \\
    chown -R 1000:1000 /data

    # 解决方案3:在Dockerfile中设置
    # Dockerfile:
    # FROM alpine
    # RUN mkdir -p /data && chown 1000:1000 /data
    # USER 1000:1000

    # 解决方案4:运行时指定用户
    docker run -d \\
    –user 1000:1000 \\
    -v my-volume:/data \\
    my-app

    # 解决方案5:使用entrypoint脚本动态设置权限
    # entrypoint.sh:
    # #!/bin/sh
    # if [ "$(id -u)" = "0" ]; then
    # chown -R appuser:appgroup /data
    # exec su-exec appuser "$@"
    # fi
    # exec "$@"

    10.5.3 SELinux/AppArmor权限问题

    # SELinux导致的权限问题(在CentOS/RHEL上常见)

    # 查看SELinux状态
    getenforce
    # 输出: Enforcing

    # 查看文件SELinux标签
    sudo ls -Z /var/lib/docker/volumes/my-volume/_data/

    # 解决方案1:添加:z或:Z后缀(Docker自动设置SELinux标签)
    docker run -v my-volume:/data:z my-app # 共享标签
    docker run -v my-volume:/data:Z my-app # 私有标签

    # 解决方案2:临时设置为permissive模式(不推荐生产使用)
    sudo setenforce 0

    # 解决方案3:正确设置SELinux策略
    sudo semanage fcontext -a -t container_file_t "/var/lib/docker/volumes/my-volume/_data(/.*)?"
    sudo restorecon -Rv /var/lib/docker/volumes/my-volume/_data/

    10.6 存储性能监控与优化

    10.6.1 使用docker stats监控

    # 实时监控容器资源使用
    docker stats

    # 输出示例:
    # CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
    # a1b2c3d4 mysql 2.35% 512MiB / 2GiB 25.00% 1.2kB / 648B 25MB / 15MB 120
    # e5f6g7h8 redis 0.50% 50MiB / 512MiB 9.77% 648B / 1.2kB 5MB / 2MB 15

    # BLOCK I/O列显示磁盘读写量,可用于判断存储性能

    # 监控特定容器
    docker stats mysql-prod

    # 只输出一次(不持续监控)
    docker stats –no-stream

    10.6.2 使用iostat监控磁盘I/O

    # 安装sysstat包
    sudo apt-get install sysstat # Ubuntu/Debian
    # 或
    sudo yum install sysstat # CentOS/RHEL

    # 监控磁盘I/O
    iostat -dx 1

    # 输出示例:
    # Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await %util
    # sda 0.00 1.00 2.00 5.00 16.00 48.00 9.14 0.03 4.29 3.00
    # nvme0n1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

    # 关注指标:
    # %util: 磁盘使用率(接近100%说明磁盘瓶颈)
    # await: I/O等待时间(过高说明磁盘慢)
    # r/s, w/s: 每秒读写次数
    # rkB/s, wkB/s: 每秒读写数据量

    10.6.3 性能优化建议

    # 1. 使用SSD替换HDD
    # SSD的随机读写性能远超HDD,对数据库等I/O密集型应用影响巨大

    # 2. 使用块设备数据卷(绕过文件系统开销)
    docker volume create \\
    –driver local \\
    –opt type=ext4 \\
    –opt device=/dev/nvme0n1 \\
    high-perf-volume

    # 3. 调整文件系统挂载选项
    # /etc/fstab
    # /dev/sdb1 /var/lib/docker ext4 defaults,noatime,nodiratime,data=writeback 0 0
    # noatime: 不更新访问时间
    # data=writeback: 写回模式(性能优先,但安全性降低)

    # 4. 使用I/O调度器
    # 对于SSD,使用none调度器
    echo none > /sys/block/nvme0n1/queue/scheduler

    # 5. 调整内核I/O参数
    sudo sysctl -w vm.dirty_ratio=10
    sudo sysctl -w vm.dirty_background_ratio=5
    sudo sysctl -w vm.swappiness=10

    # 6. 使用内存缓存(tmpfs)减少磁盘I/O
    docker run -d \\
    –name my-app \\
    –mount type=tmpfs,dst=/app/cache,tmpfs-size=512m \\
    my-app:latest

    10.7 生产环境存储架构设计

    10.7.1 单机存储架构

    ┌─────────────────────────────────────────┐
    │ 单机存储架构 │
    │ │
    │ ┌─────────────────────────────────────┐│
    │ │ Docker 主机 ││
    │ │ ││
    │ │ ┌──────────┐ ┌──────────┐ ││
    │ │ │ App容器 │ │ DB容器 │ ││
    │ │ └────┬─────┘ └────┬─────┘ ││
    │ │ │ │ ││
    │ │ ┌────▼──────────────▼─────┐ ││
    │ │ │ Docker Volumes │ ││
    │ │ │ /var/lib/docker/volumes │ ││
    │ │ └────────────┬────────────┘ ││
    │ │ │ ││
    │ │ ┌────────────▼────────────┐ ││
    │ │ │ 本地磁盘(SSD) │ ││
    │ │ │ ext4/xfs 文件系统 │ ││
    │ │ └─────────────────────────┘ ││
    │ └─────────────────────────────────────┘│
    │ │
    │ 适用场景:小型应用、开发测试环境 │
    │ 优点:简单、性能好 │
    │ 缺点:无高可用、无法扩展 │
    └─────────────────────────────────────────┘

    10.7.2 分布式存储架构

    ┌──────────────────────────────────────────────────────┐
    │ 分布式存储架构 │
    │ │
    │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
    │ │ Docker │ │ Docker │ │ Docker │ │
    │ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
    │ │ │ │ │ │ │ │
    │ │ App容器 │ │ App容器 │ │ App容器 │ │
    │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
    │ │ │ │ │
    │ └──────────┬───┴──────────────┘ │
    │ │ │
    │ ┌────────▼────────┐ │
    │ │ NFS / GlusterFS │ │
    │ │ / Ceph / 集群存储 │ │
    │ └────────┬────────┘ │
    │ │ │
    │ ┌────────▼────────┐ │
    │ │ 存储集群 │ │
    │ │ ┌───┐ ┌───┐ │ │
    │ │ │SSD│ │SSD│ │ │
    │ │ └───┘ └───┘ │ │
    │ │ ┌───┐ ┌───┐ │ │
    │ │ │SSD│ │SSD│ │ │
    │ │ └───┘ └───┘ │ │
    │ └─────────────────┘ │
    │ │
    │ 适用场景:中大型应用、需要高可用 │
    │ 优点:高可用、可扩展、数据共享 │
    │ 缺点:复杂、网络延迟 │
    └──────────────────────────────────────────────────────┘

    10.7.3 生产环境Docker Compose完整配置

    # docker-compose.prod.yml – 生产环境完整配置
    version: '3.8'

    services:
    # 反向代理
    nginx:
    image: nginx:alpine
    container_name: nginxproxy
    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: myapp: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:7alpine
    container_name: redis
    restart: always
    command: ["redis-server", "–requirepass", "${REDIS_PASSWORD}", "–appendonly", "yes"]
    volumes:
    redis_data:/data
    networks:
    backend
    deploy:
    resources:
    limits:
    memory: 512M

    volumes:
    nginx_conf:
    nginx_certs:
    nginx_logs:
    app_uploads:
    app_logs:
    mysql_data:
    labels:
    backup: daily
    retention: 30d
    mysql_conf:
    mysql_logs:
    redis_data:
    labels:
    backup: daily
    retention: 7d

    networks:
    frontend:
    driver: bridge
    backend:
    driver: bridge
    internal: true # 后端网络不对外暴露

    10.8 本章总结与下一期预告

    10.8.1 全文总结

    本文全面深入地讲解了Docker存储与数据持久化的全部知识,从底层原理到生产实践,涵盖了以下核心内容:

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

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

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

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

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

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

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

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

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

  • 存储管理与故障排查:掌握了docker system df、资源清理、磁盘空间处理、数据卷损坏修复、权限排查、性能监控和生产环境架构设计。

  • 10.8.2 关键要点回顾
    主题关键要点
    存储方式选择 生产用Volume,开发用Bind Mount,敏感数据用tmpfs
    存储驱动 优先使用overlay2,内核4.0+
    数据卷管理 使用命名数据卷,添加标签,定期清理
    数据库持久化 数据+配置+日志分离,设置健康检查和资源限制
    备份策略 3-2-1原则(3份备份,2种介质,1份异地)
    安全 加密敏感数据,最小权限原则,定期审计
    监控 docker system df, docker stats, iostat
    故障排查 先诊断后处理,权限问题最常见
    10.8.3 下一期预告

    在下一篇文章中,我们将探讨Docker网络与容器间通信的主题,内容包括:

    • Docker网络模型详解(bridge、host、overlay、macvlan、none)
    • 容器间通信机制
    • 端口映射与端口暴露
    • Docker DNS与服务发现
    • 自定义网络创建与管理
    • Docker Swarm覆盖网络
    • 网络安全与隔离
    • 负载均衡与流量分发
    • 生产环境网络架构设计

    敬请期待!


    结语

    Docker存储与数据持久化是容器化部署中最关键的主题之一。正确理解和运用Docker的存储机制,不仅能确保数据的安全性和持久性,还能显著提升应用的性能和可靠性。

    在实际生产环境中,存储方案的选择和配置需要根据具体的业务需求、数据类型、性能要求和安全要求来综合考虑。没有一种方案适用于所有场景,关键在于理解每种存储方式的原理和特点,然后在合适的场景使用合适的工具。

    希望本文能够帮助你全面掌握Docker存储与数据持久化的知识,在容器化部署的道路上更加得心应手。如果你在实践中遇到任何问题,欢迎留言讨论。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 06-Docker存储与数据持久化
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!