Docker 持久化存储三大方式实战深度指南:docker run / Volumes / Bind Mounts 全面精通

作者:云原生架构师
技术栈:Docker, Linux Kernel, OverlayFS, Mount Namespace, fio
难度等级:★★★★★(专家级)
预计阅读时间:60 分钟


目录

  1. 引言:从理论到实操
  2. docker run 命令的存储参数深度剖析
  3. -v 与 --mount 语法对比与陷阱
  4. Volumes 全生命周期实战
  5. Bind Mounts 高级实战
  6. MySQL 容器持久化完整方案
  7. Nginx 配置文件挂载实战
  8. 多容器共享数据实战
  9. docker-compose 持久化编排实战
  10. 生产级存储监控与维护
  11. 故障排查实战手册
  12. 总结与最佳实践清单

1. 引言:从理论到实操

1.1 本文定位

上一篇文章从内核原理层面解析了容器数据持久化的必要性和三种存储方式的架构差异。本文将完全聚焦实操,通过大量可直接复制执行的命令,深入演示每种持久化方式的使用方法、参数含义、注意事项和生产级配置。

1.2 实验环境准备

# 本文所有实验基于以下环境
# Docker Engine 26.x (Linux) / Docker Desktop 4.x (Windows/macOS)

# 检查 Docker 版本
docker version
# Client: Docker Engine - Community
#  Version:           26.1.4
# Server: Docker Engine - Community
#  Version:           26.1.4

# 检查存储驱动
docker info | grep "Storage Driver"
# Storage Driver: overlay2

# 创建实验工作目录
mkdir -p ~/docker-storage-lab && cd ~/docker-storage-lab

# Windows PowerShell 用户
mkdir -Force $HOME\docker-storage-lab; cd $HOME\docker-storage-lab

2. docker run 命令的存储参数深度剖析

2.1 docker run 的完整存储相关参数

docker run [存储相关参数] IMAGE [COMMAND]

# 核心参数列表:
# -v, --volume        挂载 Volume 或 Bind Mount(旧语法,兼容性好)
# --mount             挂载存储(新语法,更明确,推荐用于生产)
# --tmpfs             挂载 tmpfs 内存文件系统
# --volumes-from      从另一个容器继承所有挂载
# --storage-opt       设置存储驱动选项(如容器层大小限制)
# -w, --workdir       设置工作目录(影响相对路径解析)
# --read-only         将容器根文件系统设为只读

2.2 docker run 存储参数的内核行为

docker run -v mydata:/data -v /host/cfg:/etc/app:ro --tmpfs /tmp image

Docker Engine 处理流程:
                                                     
 ┌────────────────────────────────────────────────────────────────┐
 │ 1. 创建容器的 Mount Namespace                                   │
 │    → clone(CLONE_NEWNS)                                        │
 │                                                                 │
 │ 2. 准备 OverlayFS 根文件系统                                    │
 │    → mount -t overlay ... merged/                               │
 │                                                                 │
 │ 3. 处理 -v mydata:/data                                        │
 │    → 查找/创建 Named Volume "mydata"                            │
 │    → mount --bind /var/lib/docker/volumes/mydata/_data merged/data │
 │                                                                 │
 │ 4. 处理 -v /host/cfg:/etc/app:ro                               │
 │    → mount --bind /host/cfg merged/etc/app                      │
 │    → mount -o remount,ro merged/etc/app                         │
 │                                                                 │
 │ 5. 处理 --tmpfs /tmp                                           │
 │    → mount -t tmpfs tmpfs merged/tmp                            │
 │                                                                 │
 │ 6. pivot_root 到 merged 目录                                    │
 │    → 容器进程以 merged 为根文件系统启动                           │
 └────────────────────────────────────────────────────────────────┘

2.3 理解 -v 参数的三种行为模式

-v 参数根据冒号左侧的内容,有截然不同的行为:

# 模式 1:Named Volume(左侧是名称,无 / 开头)
docker run -v mysql-data:/var/lib/mysql mysql:8.0
#          ↑ 名称          ↑ 容器路径
# 行为:创建/使用名为 mysql-data 的 Volume
# 数据存储在 /var/lib/docker/volumes/mysql-data/_data/

# 模式 2:Bind Mount(左侧是绝对路径,以 / 或盘符开头)
docker run -v /home/user/config:/etc/app/config nginx
#          ↑ 宿主机绝对路径    ↑ 容器路径
# 行为:将宿主机目录直接映射到容器
# ⚠️ 如果宿主机路径不存在,Docker 会自动创建空目录

# 模式 3:Anonymous Volume(只有容器路径)
docker run -v /var/lib/mysql mysql:8.0
#          ↑ 仅容器路径
# 行为:创建匿名 Volume(64位哈希名称)
# ⚠️ 难以管理,不推荐在生产环境使用

三种模式的快速判断规则

-v 参数左侧:
  ┌─────────────────────────────────────────────┐
  │  以 / 开头(Linux)或 C:\ 开头(Windows)     │
  │  → Bind Mount                                │
  ├─────────────────────────────────────────────┤
  │  普通名称(无 / 开头,如 mydata)              │
  │  → Named Volume                              │
  ├─────────────────────────────────────────────┤
  │  没有冒号左侧(仅容器路径)                    │
  │  → Anonymous Volume                          │
  └─────────────────────────────────────────────┘

3. -v 与 --mount 语法对比与陷阱

3.1 同一操作的两种写法

# ═══════════════════════════════════════════════════════
# 示例 1:Named Volume
# ═══════════════════════════════════════════════════════

# -v 写法(简洁)
docker run -d --name db1 \
  -v pg-data:/var/lib/postgresql/data \
  postgres:16

# --mount 写法(明确)
docker run -d --name db2 \
  --mount type=volume,source=pg-data,target=/var/lib/postgresql/data \
  postgres:16

# ═══════════════════════════════════════════════════════
# 示例 2:Bind Mount (只读)
# ═══════════════════════════════════════════════════════

# -v 写法
docker run -d --name web1 \
  -v /home/user/nginx.conf:/etc/nginx/nginx.conf:ro \
  nginx:alpine

# --mount 写法
docker run -d --name web2 \
  --mount type=bind,source=/home/user/nginx.conf,target=/etc/nginx/nginx.conf,readonly \
  nginx:alpine

# ═══════════════════════════════════════════════════════
# 示例 3:tmpfs
# ═══════════════════════════════════════════════════════

# --tmpfs 写法(简单用法)
docker run -d --name app1 \
  --tmpfs /tmp:rw,noexec,nosuid,size=256m \
  myapp:latest

# --mount 写法(更精细控制)
docker run -d --name app2 \
  --mount type=tmpfs,target=/tmp,tmpfs-size=268435456,tmpfs-mode=1777 \
  myapp:latest

3.2 关键差异与陷阱

# ═══════════════════════════════════════════════════════
# 陷阱 1:-v 会自动创建不存在的宿主机路径,--mount 会报错
# ═══════════════════════════════════════════════════════

# -v:宿主机路径不存在时自动创建空目录
docker run -v /nonexistent/path:/data alpine ls /data
# 结果:成功运行,/nonexistent/path 被自动创建为空目录
# ⚠️ 这可能隐藏配置错误!你以为挂载了数据,其实是个空目录

# --mount:宿主机路径不存在时报错退出
docker run --mount type=bind,source=/nonexistent/path,target=/data alpine ls /data
# 结果:Error response from daemon: invalid mount config for type "bind":
#        bind source path does not exist: /nonexistent/path
# ✅ 这是更安全的行为,推荐生产环境使用 --mount

# ═══════════════════════════════════════════════════════
# 陷阱 2:-v 的名称与路径歧义
# ═══════════════════════════════════════════════════════

# 当前目录有个名为 data 的文件夹
mkdir data

# -v data:/app/data → 这是 Named Volume 还是 Bind Mount?
docker run -v data:/app/data alpine ls /app/data
# 答案:Named Volume!因为 "data" 不以 / 开头
# Docker 会创建一个名为 "data" 的 Volume,而不是挂载当前目录的 ./data

# 正确挂载当前目录的 data:
docker run -v $(pwd)/data:/app/data alpine ls /app/data
# 或
docker run -v ./data:/app/data alpine ls /app/data  # Docker Compose 支持

# --mount 不存在此歧义:
docker run --mount type=bind,source=$(pwd)/data,target=/app/data alpine ls /app/data
# type=bind 明确声明了这是 Bind Mount

# ═══════════════════════════════════════════════════════
# 陷阱 3:挂载到非空容器目录的行为差异
# ═══════════════════════════════════════════════════════

# 场景:容器镜像 /usr/share/nginx/html/ 有默认文件

# Volume 挂载到非空目录 → Volume 被初始化为容器目录的内容
docker volume create empty-vol
docker run -v empty-vol:/usr/share/nginx/html nginx:alpine
# 结果:empty-vol 中包含 nginx 默认的 index.html!
# Volume 自动"继承"了容器目录的原始内容

# Bind Mount 挂载到非空目录 → 完全覆盖容器目录
mkdir ~/empty-dir
docker run -v ~/empty-dir:/usr/share/nginx/html nginx:alpine
# 结果:容器中 /usr/share/nginx/html 变为空目录!
# Bind Mount 直接遮蔽了容器镜像层的文件
# nginx 会报 403 Forbidden(没有 index.html)

3.3 --mount 参数完整语法参考

--mount type=TYPE,KEY=VALUE,KEY=VALUE,...

# type 可选值:
# - volume  → Docker Volume
# - bind    → Bind Mount
# - tmpfs   → tmpfs Mount

# Volume 专用参数:
# source (src)          Volume 名称
# target (dst)          容器内挂载点
# readonly (ro)         只读
# volume-driver         Volume 驱动
# volume-label          Volume 标签
# volume-nocopy         禁止从容器初始化 Volume 内容
# volume-opt            驱动选项

# Bind 专用参数:
# source (src)          宿主机路径(必须是绝对路径)
# target (dst)          容器内挂载点
# readonly (ro)         只读
# bind-propagation      挂载传播(rprivate/rshared/rslave/...)
# bind-nonrecursive     非递归绑定

# tmpfs 专用参数:
# target (dst)          容器内挂载点
# tmpfs-size            大小限制(字节)
# tmpfs-mode            文件系统权限模式

4. Volumes 全生命周期实战

4.1 Volume CRUD 操作

# ═══════════════════════════════════════════════════════
# Create - 创建 Volume
# ═══════════════════════════════════════════════════════

# 创建基本 Volume
docker volume create app-data

# 创建带标签的 Volume(便于管理)
docker volume create \
  --label project=myapp \
  --label env=production \
  --label backup=daily \
  myapp-db

# 创建指定驱动选项的 Volume
docker volume create \
  --driver local \
  --opt type=none \
  --opt device=/data/fast-ssd \
  --opt o=bind \
  ssd-volume

# 创建 NFS Volume
docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100,rw,nfsvers=4.1 \
  --opt device=:/exports/shared \
  nfs-shared

# ═══════════════════════════════════════════════════════
# Read - 查看 Volume
# ═══════════════════════════════════════════════════════

# 列出所有 Volume
docker volume ls

# 输出:
# DRIVER    VOLUME NAME
# local     app-data
# local     myapp-db
# local     ssd-volume
# local     nfs-shared

# 按标签过滤
docker volume ls --filter label=env=production

# 按驱动过滤
docker volume ls --filter driver=local

# 查看悬空(未被使用)的 Volume
docker volume ls --filter dangling=true

# 查看 Volume 详细信息
docker volume inspect myapp-db

# 输出:
# [
#     {
#         "CreatedAt": "2026-03-12T10:00:00+08:00",
#         "Driver": "local",
#         "Labels": {
#             "backup": "daily",
#             "env": "production",
#             "project": "myapp"
#         },
#         "Mountpoint": "/var/lib/docker/volumes/myapp-db/_data",
#         "Name": "myapp-db",
#         "Options": {},
#         "Scope": "local"
#     }
# ]

# 查看 Volume 实际磁盘占用
sudo du -sh /var/lib/docker/volumes/myapp-db/_data/

# ═══════════════════════════════════════════════════════
# Update - 使用 Volume(没有直接的 update 命令)
# ═══════════════════════════════════════════════════════

# 挂载到容器
docker run -d --name db \
  -v myapp-db:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  postgres:16

# 查看哪些容器使用了该 Volume
docker ps -a --filter volume=myapp-db --format "table {{.Names}}\t{{.Status}}"

# ═══════════════════════════════════════════════════════
# Delete - 删除 Volume
# ═══════════════════════════════════════════════════════

# 删除指定 Volume(必须没有容器在使用)
docker volume rm app-data
# 如果有容器使用中:
# Error: volume is in use - [container_id]

# 强制删除(先停止/删除使用它的容器)
docker stop db && docker rm db
docker volume rm myapp-db

# 清理所有悬空 Volume
docker volume prune
# WARNING! This will remove anonymous volumes not used by at least one container.
# Are you sure you want to continue? [y/N]

# 清理所有未使用的 Volume(包括 Named Volume)
docker volume prune --all
# ⚠️ 危险!会删除所有未被容器引用的 Volume
# 生产环境慎用!

4.2 Volume 数据备份与恢复

# ═══════════════════════════════════════════════════════
# 备份方案 1:使用临时容器 + tar 归档
# ═══════════════════════════════════════════════════════

# 备份 mysql-data Volume 的内容到宿主机
docker run --rm \
  -v mysql-data:/source:ro \
  -v $(pwd)/backups:/backup \
  alpine \
  tar czf /backup/mysql-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /source .

# 解析:
# --rm                         容器执行完自动删除
# -v mysql-data:/source:ro     将目标 Volume 以只读挂载
# -v $(pwd)/backups:/backup    将备份输出目录 Bind Mount
# tar czf ...                  压缩归档 Volume 内容

# 恢复:从备份恢复到新 Volume
docker volume create mysql-data-restored

docker run --rm \
  -v mysql-data-restored:/target \
  -v $(pwd)/backups:/backup:ro \
  alpine \
  sh -c "cd /target && tar xzf /backup/mysql-data-20260312-100000.tar.gz"

# ═══════════════════════════════════════════════════════
# 备份方案 2:Volume 间复制
# ═══════════════════════════════════════════════════════

# 创建 Volume 的精确副本
docker volume create mysql-data-clone

docker run --rm \
  -v mysql-data:/from:ro \
  -v mysql-data-clone:/to \
  alpine \
  sh -c "cd /from && cp -av . /to/"

# ═══════════════════════════════════════════════════════
# 备份方案 3:数据库专用备份(更安全)
# ═══════════════════════════════════════════════════════

# 对于数据库,推荐使用数据库自带的备份工具(保证数据一致性)
# 而不是直接复制数据文件

# PostgreSQL
docker exec postgres-container \
  pg_dump -U postgres mydb | gzip > backup-$(date +%Y%m%d).sql.gz

# MySQL
docker exec mysql-container \
  mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --all-databases | \
  gzip > backup-$(date +%Y%m%d).sql.gz

# 恢复
gunzip < backup-20260312.sql.gz | \
  docker exec -i postgres-container psql -U postgres mydb

4.3 Volume 的高级用法

# ═══════════════════════════════════════════════════════
# 高级 1:Volume 的只读挂载
# ═══════════════════════════════════════════════════════

# 场景:多个 Web 服务器共享静态资源,但不允许修改
docker run -d --name web1 \
  -v shared-assets:/usr/share/nginx/html:ro \
  nginx:alpine

docker run -d --name web2 \
  -v shared-assets:/usr/share/nginx/html:ro \
  nginx:alpine

# 只有一个管理容器有写权限
docker run -d --name content-manager \
  -v shared-assets:/data \
  content-manager:latest

# ═══════════════════════════════════════════════════════
# 高级 2:volume-nocopy 防止容器内容覆盖
# ═══════════════════════════════════════════════════════

# 默认行为:新 Volume 会从容器目录"继承"内容
docker volume create test-vol
docker run -v test-vol:/usr/share/nginx/html nginx:alpine
# test-vol 中出现了 nginx 默认页面

# 使用 volume-nocopy 阻止此行为
docker run \
  --mount type=volume,source=test-vol2,target=/usr/share/nginx/html,volume-nocopy \
  nginx:alpine
# test-vol2 保持为空,不复制容器内容

# ═══════════════════════════════════════════════════════
# 高级 3:使用指定子路径挂载(Docker 26+)
# ═══════════════════════════════════════════════════════

# 只挂载 Volume 中的某个子目录
docker run \
  --mount type=volume,source=app-data,target=/config,volume-subpath=configs \
  myapp:latest
# 仅将 app-data Volume 中的 configs/ 子目录挂载到容器的 /config

# ═══════════════════════════════════════════════════════
# 高级 4:限制容器层大小
# ═══════════════════════════════════════════════════════

# 使用 --storage-opt 限制容器可写层的大小
# 需要 overlay2 + xfs 文件系统 + pquota 挂载选项
docker run --storage-opt size=10G -d nginx:alpine
# 容器最多只能写入 10GB 到可写层
# 超出后 → ENOSPC (No space left on device)

5. Bind Mounts 高级实战

5.1 基础 Bind Mount 操作

# ═══════════════════════════════════════════════════════
# 挂载目录
# ═══════════════════════════════════════════════════════

# 准备宿主机目录
mkdir -p ~/web-content
echo "<h1>Hello from Host</h1>" > ~/web-content/index.html

# 挂载到容器
docker run -d --name web \
  -p 8080:80 \
  -v ~/web-content:/usr/share/nginx/html:ro \
  nginx:alpine

# 验证
curl http://localhost:8080
# <h1>Hello from Host</h1>

# 在宿主机修改文件(实时生效!)
echo "<h1>Updated!</h1>" > ~/web-content/index.html
curl http://localhost:8080
# <h1>Updated!</h1>

# ═══════════════════════════════════════════════════════
# 挂载单个文件
# ═══════════════════════════════════════════════════════

# 只挂载一个配置文件(不影响目录中的其他文件)
docker run -d --name web2 \
  -p 8081:80 \
  -v ~/web-content/index.html:/usr/share/nginx/html/index.html:ro \
  nginx:alpine

# 容器中 /usr/share/nginx/html/ 目录的其他文件来自镜像层
# 只有 index.html 被替换

# ⚠️ 注意:Bind Mount 单文件时,文件必须在容器启动前存在
# 如果文件不存在,Docker 会创建一个目录(而不是文件)!

5.2 SELinux 环境下的 Bind Mount

# ═══════════════════════════════════════════════════════
# 在 SELinux Enforcing 模式下(RHEL/CentOS/Fedora)
# ═══════════════════════════════════════════════════════

# 检查 SELinux 状态
getenforce
# Enforcing

# 不加标签的 Bind Mount → Permission Denied
docker run -v /data/shared:/data alpine ls /data
# ls: /data: Permission denied

# 解决方案 1::z 标签(shared label,多容器可访问)
docker run -v /data/shared:/data:z alpine ls /data
# 成功!Docker 自动执行 chcon -Rt svirt_sandbox_file_t /data/shared

# 解决方案 2::Z 标签(private label,仅当前容器可访问)
docker run -v /data/private:/data:Z alpine ls /data
# 成功!但只有这个容器能访问

# :z 和 :Z 的区别:
# :z → chcon -Rt svirt_sandbox_file_t path  → 所有容器可访问
# :Z → chcon -Rt svirt_sandbox_file_t path  → 加私有 MCS 标签,仅本容器

# ⚠️ 警告:对系统目录使用 :Z 可能导致系统故障!
# 永远不要对 /etc、/var、/home 等使用 :Z

5.3 Bind Mount 的权限管理

# ═══════════════════════════════════════════════════════
# 权限问题:容器内外 UID/GID 不一致
# ═══════════════════════════════════════════════════════

# 场景:宿主机 UID 1000 创建文件,容器内以 root (UID 0) 运行
mkdir ~/app-data
echo "config" > ~/app-data/config.txt
ls -la ~/app-data/
# -rw-rw-r-- 1 user user 7 Mar 12 10:00 config.txt  (UID=1000)

# 容器以 root 运行,可以读写
docker run -v ~/app-data:/data alpine sh -c "cat /data/config.txt && echo 'new' > /data/new.txt"
ls -la ~/app-data/
# -rw-rw-r-- 1 user user 7 config.txt
# -rw-r--r-- 1 root root 4 new.txt  ← ⚠️ 属主变成了 root!

# 解决方案 1:容器内使用与宿主机相同的 UID 运行
docker run --user 1000:1000 \
  -v ~/app-data:/data \
  alpine sh -c "echo 'owned by 1000' > /data/test.txt"
ls -la ~/app-data/test.txt
# -rw-r--r-- 1 user user 15 test.txt  ← 属主正确

# 解决方案 2:Dockerfile 中创建匹配的用户
# FROM alpine
# RUN adduser -D -u 1000 appuser
# USER appuser

# 解决方案 3:使用 User Namespace Remapping(高级)
# 在 /etc/docker/daemon.json 中配置 "userns-remap"

5.4 Bind Mount 的挂载传播

# ═══════════════════════════════════════════════════════
# 挂载传播模式(影响子挂载的可见性)
# ═══════════════════════════════════════════════════════

# rprivate(默认):完全隔离
docker run --mount type=bind,source=/mnt,target=/mnt,bind-propagation=rprivate \
  alpine mount
# 容器内 /mnt 下的子挂载不会传播到宿主机

# rslave:宿主机 → 容器 单向传播
docker run --mount type=bind,source=/mnt,target=/mnt,bind-propagation=rslave \
  alpine sh

# 在宿主机新增挂载:
# mount /dev/sdb1 /mnt/usb
# → 容器内可以看到 /mnt/usb ✓
# 在容器内新增挂载:
# mount -t tmpfs tmpfs /mnt/tmp
# → 宿主机看不到 ✗

# rshared:双向传播
docker run --mount type=bind,source=/mnt,target=/mnt,bind-propagation=rshared \
  --privileged \
  alpine sh
# 宿主机和容器之间的子挂载双向可见
# ⚠️ 需要 --privileged 权限
# 典型用途:容器内运行挂载工具(如 CSI 节点插件)

6. MySQL 容器持久化完整方案

6.1 方案设计

MySQL 容器持久化架构:

┌──────────────────────────────────────────────────┐
│                 MySQL Container                    │
│                                                    │
│  /var/lib/mysql ──────────► Named Volume (数据)    │
│  /etc/mysql/conf.d ──────► Bind Mount (配置,只读) │
│  /var/log/mysql ─────────► Named Volume (日志)    │
│  /tmp ───────────────────► tmpfs (临时文件)        │
│  /docker-entrypoint ─────► Bind Mount (初始化SQL) │
│                                                    │
└──────────────────────────────────────────────────┘

6.2 完整实操

# ═══════════════════════════════════════════════════════
# Step 1: 准备配置文件和初始化脚本
# ═══════════════════════════════════════════════════════

# 创建项目目录
mkdir -p ~/mysql-project/{config,init-sql,backups}

# 自定义 MySQL 配置
cat > ~/mysql-project/config/custom.cnf << 'EOF'
[mysqld]
# 性能优化
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
max_connections = 500
thread_cache_size = 50

# 字符集
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

# 慢查询日志
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2

# 二进制日志(用于主从复制和 PITR)
server-id = 1
log-bin = /var/lib/mysql/mysql-bin
binlog_format = ROW
binlog_expire_logs_seconds = 604800
EOF

# 初始化 SQL
cat > ~/mysql-project/init-sql/01-create-db.sql << 'EOF'
CREATE DATABASE IF NOT EXISTS myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'appuser'@'%' IDENTIFIED BY 'app_password_2026';
GRANT ALL PRIVILEGES ON myapp.* TO 'appuser'@'%';
FLUSH PRIVILEGES;
EOF

# ═══════════════════════════════════════════════════════
# Step 2: 创建 Named Volumes
# ═══════════════════════════════════════════════════════

docker volume create mysql-data
docker volume create mysql-logs

# ═══════════════════════════════════════════════════════
# Step 3: 启动 MySQL 容器
# ═══════════════════════════════════════════════════════

docker run -d \
  --name mysql-prod \
  --restart unless-stopped \
  -p 3306:3306 \
  -e MYSQL_ROOT_PASSWORD=RootPass_2026_Secure \
  -e MYSQL_DATABASE=myapp \
  -e TZ=Asia/Shanghai \
  \
  -v mysql-data:/var/lib/mysql \
  -v mysql-logs:/var/log/mysql \
  \
  --mount type=bind,source=$HOME/mysql-project/config,target=/etc/mysql/conf.d,readonly \
  --mount type=bind,source=$HOME/mysql-project/init-sql,target=/docker-entrypoint-initdb.d,readonly \
  \
  --tmpfs /tmp:rw,noexec,nosuid,size=256m \
  \
  --memory 2g \
  --memory-swap 2g \
  --cpus 2 \
  \
  mysql:8.0 \
  --default-authentication-plugin=mysql_native_password

# ═══════════════════════════════════════════════════════
# Step 4: 验证持久化效果
# ═══════════════════════════════════════════════════════

# 等待 MySQL 启动
docker logs -f mysql-prod 2>&1 | head -50

# 写入测试数据
docker exec -i mysql-prod mysql -u root -pRootPass_2026_Secure myapp << 'EOF'
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(200) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (name, email) VALUES
    ('张三', 'zhangsan@example.com'),
    ('李四', 'lisi@example.com'),
    ('王五', 'wangwu@example.com');
SELECT * FROM users;
EOF

# 验证数据已写入
docker exec mysql-prod mysql -u root -pRootPass_2026_Secure myapp \
  -e "SELECT COUNT(*) as total FROM users;"
# +-------+
# | total |
# +-------+
# |     3 |
# +-------+

# ═══════════════════════════════════════════════════════
# Step 5: 模拟容器销毁与恢复
# ═══════════════════════════════════════════════════════

# 停止并删除容器
docker stop mysql-prod
docker rm mysql-prod

# 确认 Volume 还在
docker volume ls | grep mysql
# local     mysql-data    ← 数据安全!
# local     mysql-logs    ← 日志安全!

# 用新容器挂载同一个 Volume
docker run -d \
  --name mysql-prod-v2 \
  -p 3306:3306 \
  -e MYSQL_ROOT_PASSWORD=RootPass_2026_Secure \
  -v mysql-data:/var/lib/mysql \
  -v mysql-logs:/var/log/mysql \
  --mount type=bind,source=$HOME/mysql-project/config,target=/etc/mysql/conf.d,readonly \
  --tmpfs /tmp:rw,noexec,nosuid,size=256m \
  mysql:8.0

# 验证数据完好
docker exec mysql-prod-v2 mysql -u root -pRootPass_2026_Secure myapp \
  -e "SELECT * FROM users;"
# +----+--------+----------------------+---------------------+
# | id | name   | email                | created_at          |
# +----+--------+----------------------+---------------------+
# |  1 | 张三   | zhangsan@example.com | 2026-03-12 10:00:00 |
# |  2 | 李四   | lisi@example.com     | 2026-03-12 10:00:00 |
# |  3 | 王五   | wangwu@example.com   | 2026-03-12 10:00:00 |
# +----+--------+----------------------+---------------------+

# ✅ 数据完整恢复!容器可以随意销毁重建,数据不受影响

7. Nginx 配置文件挂载实战

7.1 典型的 Nginx Bind Mount 方案

# ═══════════════════════════════════════════════════════
# 准备配置
# ═══════════════════════════════════════════════════════

mkdir -p ~/nginx-project/{conf,html,certs,logs}

# 主配置文件
cat > ~/nginx-project/conf/nginx.conf << 'EOF'
user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
    use epoll;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" '
                    'rt=$request_time';

    access_log /var/log/nginx/access.log main;

    sendfile        on;
    tcp_nopush      on;
    keepalive_timeout  65;
    gzip  on;
    gzip_types text/plain text/css application/json application/javascript;

    include /etc/nginx/conf.d/*.conf;
}
EOF

# 站点配置
cat > ~/nginx-project/conf/default.conf << 'EOF'
server {
    listen 80;
    server_name localhost;

    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}
EOF

# 首页
cat > ~/nginx-project/html/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>Docker Storage Lab</title></head>
<body>
    <h1>Nginx with Bind Mount Configuration</h1>
    <p>Config: Bind Mount (read-only)</p>
    <p>Content: Bind Mount (read-only)</p>
    <p>Logs: Named Volume (persistent)</p>
</body>
</html>
EOF

# ═══════════════════════════════════════════════════════
# 启动 Nginx
# ═══════════════════════════════════════════════════════

docker volume create nginx-logs

docker run -d \
  --name nginx-web \
  -p 80:80 \
  --mount type=bind,source=$HOME/nginx-project/conf/nginx.conf,target=/etc/nginx/nginx.conf,readonly \
  --mount type=bind,source=$HOME/nginx-project/conf/default.conf,target=/etc/nginx/conf.d/default.conf,readonly \
  --mount type=bind,source=$HOME/nginx-project/html,target=/usr/share/nginx/html,readonly \
  -v nginx-logs:/var/log/nginx \
  nginx:alpine

# 验证
curl http://localhost/health
# healthy

curl http://localhost
# <!DOCTYPE html>...

# 查看日志(写入 Volume)
docker exec nginx-web tail -5 /var/log/nginx/access.log

# ═══════════════════════════════════════════════════════
# 配置热更新(不重建容器)
# ═══════════════════════════════════════════════════════

# 修改配置
echo '    # 新增缓存头' >> ~/nginx-project/conf/default.conf

# 测试配置是否合法
docker exec nginx-web nginx -t
# nginx: configuration file /etc/nginx/nginx.conf test is successful

# 热加载(不停机)
docker exec nginx-web nginx -s reload
# Bind Mount 的优势:宿主机修改立即可见,reload 即生效

8. 多容器共享数据实战

8.1 Volume 共享:生产者-消费者模式

# ═══════════════════════════════════════════════════════
# 场景:日志收集(App 写日志 → Filebeat 读日志)
# ═══════════════════════════════════════════════════════

docker volume create app-logs

# 容器 1:应用(写日志)
docker run -d --name app \
  -v app-logs:/var/log/app \
  alpine sh -c 'while true; do echo "[$(date)] request processed" >> /var/log/app/access.log; sleep 1; done'

# 容器 2:日志收集器(读日志)
docker run -d --name log-collector \
  -v app-logs:/logs:ro \
  alpine sh -c 'tail -f /logs/access.log'

# 查看效果
docker logs -f log-collector
# [Thu Mar 12 10:00:01 UTC 2026] request processed
# [Thu Mar 12 10:00:02 UTC 2026] request processed
# ...

# ═══════════════════════════════════════════════════════
# 场景:静态资源构建(Builder 生成 → Web 服务器提供)
# ═══════════════════════════════════════════════════════

docker volume create static-assets

# 容器 1:构建(运行完退出,但数据留在 Volume)
docker run --rm \
  -v static-assets:/output \
  node:20-alpine \
  sh -c 'echo "<h1>Built at $(date)</h1>" > /output/index.html'

# 容器 2:Web 服务器(使用构建产物)
docker run -d --name static-web \
  -p 8080:80 \
  -v static-assets:/usr/share/nginx/html:ro \
  nginx:alpine

curl http://localhost:8080
# <h1>Built at Thu Mar 12 10:00:00 UTC 2026</h1>

8.2 --volumes-from:继承另一个容器的所有挂载

# ═══════════════════════════════════════════════════════
# 数据容器模式(Data Container Pattern)
# ═══════════════════════════════════════════════════════

# 创建"数据容器"(不需要运行)
docker create --name data-container \
  -v mysql-data:/var/lib/mysql \
  -v mysql-logs:/var/log/mysql \
  -v mysql-config:/etc/mysql/conf.d \
  alpine /bin/true

# MySQL 容器继承所有 Volume
docker run -d --name mysql \
  --volumes-from data-container \
  -e MYSQL_ROOT_PASSWORD=secret \
  mysql:8.0

# 备份容器也继承同样的 Volume
docker run --rm \
  --volumes-from data-container \
  -v $(pwd)/backups:/backup \
  alpine tar czf /backup/full.tar.gz /var/lib/mysql /var/log/mysql

# ⚠️ 注意:--volumes-from 是旧模式
# 现代 Docker 推荐直接使用 Named Volume,更清晰可控

9. docker-compose 持久化编排实战

9.1 完整的有状态服务编排

# docker-compose.yml - 生产级持久化配置
version: "3.9"

services:
  # ─────────────────────────────────────
  # Web 前端(无状态 + 配置 Bind Mount)
  # ─────────────────────────────────────
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro     # 配置: Bind Mount 只读
      - ./nginx/conf.d:/etc/nginx/conf.d:ro              # 站点配置: 只读
      - ./nginx/certs:/etc/nginx/certs:ro                # SSL 证书: 只读
      - static-files:/usr/share/nginx/html:ro            # 静态资源: Volume 只读
      - nginx-logs:/var/log/nginx                        # 日志: Volume 可写
    tmpfs:
      - /var/cache/nginx:size=100m                       # 缓存: tmpfs
      - /var/run:size=10m                                # PID: tmpfs
    restart: unless-stopped

  # ─────────────────────────────────────
  # 应用服务(无状态 + 上传文件持久化)
  # ─────────────────────────────────────
  app:
    image: myapp:latest
    volumes:
      - ./config/app.yaml:/app/config/app.yaml:ro        # 配置: Bind Mount 只读
      - user-uploads:/app/uploads                         # 上传文件: Volume
      - app-logs:/app/logs                                # 应用日志: Volume
    tmpfs:
      - /tmp:size=200m,noexec,nosuid                     # 临时文件: tmpfs
    environment:
      - DB_HOST=postgres
      - REDIS_HOST=redis
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  # ─────────────────────────────────────
  # PostgreSQL(有状态 - 必须持久化)
  # ─────────────────────────────────────
  postgres:
    image: postgres:16
    volumes:
      - pg-data:/var/lib/postgresql/data                  # 数据: Named Volume
      - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
      - ./postgres/init:/docker-entrypoint-initdb.d:ro   # 初始化脚本
      - pg-backup:/backup                                 # 备份目录
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: myapp
      TZ: Asia/Shanghai
    secrets:
      - db_password
    shm_size: "256m"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  # ─────────────────────────────────────
  # Redis(有状态 - AOF 持久化)
  # ─────────────────────────────────────
  redis:
    image: redis:7-alpine
    command: >
      redis-server
      --appendonly yes
      --appendfsync everysec
      --maxmemory 512mb
      --maxmemory-policy allkeys-lru
    volumes:
      - redis-data:/data                                  # RDB + AOF: Volume
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

# ─────────────────────────────────────
# Volume 定义
# ─────────────────────────────────────
volumes:
  pg-data:
    name: "${PROJECT_NAME:-myapp}_pg-data"
    labels:
      backup: "daily"
      criticality: "high"

  redis-data:
    name: "${PROJECT_NAME:-myapp}_redis-data"
    labels:
      backup: "hourly"

  user-uploads:
    name: "${PROJECT_NAME:-myapp}_uploads"
    labels:
      backup: "daily"

  app-logs:
    name: "${PROJECT_NAME:-myapp}_app-logs"
    labels:
      retention: "30d"

  nginx-logs:
    name: "${PROJECT_NAME:-myapp}_nginx-logs"
    labels:
      retention: "7d"

  static-files:
    name: "${PROJECT_NAME:-myapp}_static"

  pg-backup:
    name: "${PROJECT_NAME:-myapp}_pg-backup"

# Secrets(避免在环境变量中明文传递密码)
secrets:
  db_password:
    file: ./secrets/db_password.txt

9.2 docker-compose 中 Volume 的生命周期

# ═══════════════════════════════════════════════════════
# docker compose 中 Volume 的行为
# ═══════════════════════════════════════════════════════

# 启动(首次运行会自动创建 Volume)
docker compose up -d

# 停止服务(容器停止,Volume 保留)
docker compose stop

# 重启服务(容器恢复,数据还在)
docker compose start

# 重建服务(容器删除重建,Volume 保留)
docker compose up -d --force-recreate

# 删除容器(Volume 保留!这是关键)
docker compose down
# Stopping myapp_postgres_1 ... done
# Removing myapp_postgres_1 ... done
# Volume "myapp_pg-data" is not removed  ← Volume 安全

# 删除容器 + Volume(⚠️ 危险操作)
docker compose down -v
# 这会同时删除所有 Volume!数据全部丢失!

# 查看当前项目的 Volume
docker compose ps --format json | jq '.[].Name'
docker volume ls --filter label=com.docker.compose.project=myapp

10. 生产级存储监控与维护

10.1 监控 Volume 使用量

# ═══════════════════════════════════════════════════════
# 方法 1:直接查看文件系统使用量
# ═══════════════════════════════════════════════════════

# 查看所有 Volume 的大小
docker system df -v | head -30

# 输出示例:
# VOLUME NAME           LINKS  SIZE
# pg-data               1      2.4GB
# redis-data            1      156MB
# user-uploads          1      890MB
# nginx-logs            1      45MB
# app-logs              1      230MB

# 单个 Volume 的精确大小
sudo du -sh /var/lib/docker/volumes/pg-data/_data/

# ═══════════════════════════════════════════════════════
# 方法 2:容器内查看挂载点使用量
# ═══════════════════════════════════════════════════════

docker exec postgres df -h /var/lib/postgresql/data
# Filesystem      Size  Used Avail Use% Mounted on
# /dev/sda1       100G  2.4G  93G   3% /var/lib/postgresql/data

# ═══════════════════════════════════════════════════════
# 方法 3:Prometheus 监控指标(推荐生产环境)
# ═══════════════════════════════════════════════════════

# 使用 cadvisor 暴露容器存储指标
docker run -d --name cadvisor \
  -p 8080:8080 \
  -v /:/rootfs:ro \
  -v /var/run:/var/run:ro \
  -v /sys:/sys:ro \
  -v /var/lib/docker:/var/lib/docker:ro \
  gcr.io/cadvisor/cadvisor:latest

# 关键指标:
# container_fs_usage_bytes{device="/dev/sda1"}     容器文件系统使用量
# container_fs_limit_bytes{device="/dev/sda1"}     文件系统限制
# container_fs_reads_total                         读操作次数
# container_fs_writes_total                        写操作次数

10.2 定期维护任务

# ═══════════════════════════════════════════════════════
# 日志轮转(防止日志 Volume 无限增长)
# ═══════════════════════════════════════════════════════

# Docker 内建的日志轮转(daemon.json 配置)
cat > /etc/docker/daemon.json << 'EOF'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "5"
  }
}
EOF
# 每个容器最多 5 个日志文件,每个 50MB = 最大 250MB/容器

# 应用自身日志的轮转(logrotate)
cat > ~/mysql-project/logrotate.conf << 'EOF'
/var/log/mysql/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    create 640 mysql mysql
    postrotate
        docker exec mysql-prod mysqladmin flush-logs
    endscript
}
EOF

# ═══════════════════════════════════════════════════════
# 清理不再使用的 Volume
# ═══════════════════════════════════════════════════════

# 列出悬空 Volume
docker volume ls --filter dangling=true

# 安全清理(仅删除匿名未使用 Volume)
docker volume prune

# 清理前先确认哪些 Volume 在使用中
docker ps -a --format '{{.Names}}' | while read name; do
    echo "=== $name ==="
    docker inspect "$name" --format='{{range .Mounts}}{{.Name}} ({{.Type}}){{println}}{{end}}'
done

11. 故障排查实战手册

11.1 常见故障与解决方案

# ═══════════════════════════════════════════════════════
# 故障 1:Permission denied 写入 Volume
# ═══════════════════════════════════════════════════════

# 症状:
docker logs myapp
# Error: EACCES: permission denied, open '/data/config.json'

# 诊断:
docker exec myapp ls -la /data/
docker exec myapp id
# uid=1000(appuser) gid=1000(appuser)

sudo ls -la /var/lib/docker/volumes/mydata/_data/
# drwxr-xr-x 2 root root 4096 ... .  ← 属主是 root!

# 修复方案 1:修改 Volume 目录权限
docker run --rm -v mydata:/data alpine chown -R 1000:1000 /data

# 修复方案 2:容器初始化时修改权限(Dockerfile)
# COPY --chown=1000:1000 . /data

# ═══════════════════════════════════════════════════════
# 故障 2:Volume 数据为空(预期有数据)
# ═══════════════════════════════════════════════════════

# 症状:数据库启动报告"需要初始化"
docker logs postgres
# PostgreSQL Database directory appears to contain a database;

# 诊断:
docker volume inspect pg-data
# 检查 Mountpoint 路径

sudo ls -la /var/lib/docker/volumes/pg-data/_data/
# 检查文件是否存在

# 常见原因:
# 1. docker compose down -v 误删了 Volume
# 2. 挂载路径写错(如 /var/lib/postgresql 而非 /var/lib/postgresql/data)
# 3. 用了匿名 Volume,容器重建后创建了新的匿名 Volume

# ═══════════════════════════════════════════════════════
# 故障 3:Bind Mount 文件不同步
# ═══════════════════════════════════════════════════════

# 症状:宿主机修改了文件,容器内看不到变化

# 原因 1:使用了 macOS/Windows 的 Docker Desktop
# Docker Desktop 的 Bind Mount 有缓存机制
# 解决方案:使用 :cached 或 :delegated 挂载选项
docker run -v $(pwd)/src:/app:cached myapp

# 原因 2:编辑器使用"创建新文件+重命名"策略(如 vim)
# Bind Mount 绑定的是 inode,vim 保存时创建了新 inode
# 解决方案:挂载目录而非单个文件
docker run -v $(pwd)/config:/etc/app:ro myapp
# 而不是
# docker run -v $(pwd)/config/app.yaml:/etc/app/app.yaml:ro myapp

# ═══════════════════════════════════════════════════════
# 故障 4:磁盘空间不足
# ═══════════════════════════════════════════════════════

# 快速诊断
docker system df
# TYPE          TOTAL     ACTIVE    SIZE      RECLAIMABLE
# Images        15        5         4.2GB     2.8GB (66%)
# Containers    8         5         1.2GB     800MB (66%)
# Local Volumes 12        5         8.5GB     3.2GB (37%)  ← 大头在这
# Build Cache   0         0         0B        0B

# 深入分析:找出最大的 Volume
docker system df -v | grep -A1 "VOLUME NAME" | sort -k3 -h -r | head -20

# 清理方案(从安全到激进)
docker container prune     # 清理停止的容器
docker image prune         # 清理悬空镜像
docker volume prune        # 清理悬空 Volume(⚠️ 确认后再执行)
docker system prune -a     # 清理所有未使用资源(⚠️ 不包含 Volume)

# ═══════════════════════════════════════════════════════
# 故障 5:容器无法启动 - Volume 被占用
# ═══════════════════════════════════════════════════════

# 症状:
# Error response from daemon: Conflict. The container name "mysql" is already in use

# 诊断:查看哪些容器在使用 Volume
docker ps -a --filter volume=mysql-data

# 解决:
docker rm -f mysql-old-container
docker run -d --name mysql -v mysql-data:/var/lib/mysql mysql:8.0

11.2 调试工具箱

# ═══════════════════════════════════════════════════════
# 工具 1:查看容器的所有挂载点
# ═══════════════════════════════════════════════════════

docker inspect myapp --format='{{json .Mounts}}' | python3 -m json.tool

# 输出:
# [
#     {
#         "Type": "volume",
#         "Name": "mysql-data",
#         "Source": "/var/lib/docker/volumes/mysql-data/_data",
#         "Destination": "/var/lib/mysql",
#         "Mode": "",
#         "RW": true,
#         "Propagation": ""
#     },
#     {
#         "Type": "bind",
#         "Source": "/home/user/config",
#         "Destination": "/etc/app",
#         "Mode": "ro",
#         "RW": false,
#         "Propagation": "rprivate"
#     }
# ]

# ═══════════════════════════════════════════════════════
# 工具 2:从容器内查看挂载信息
# ═══════════════════════════════════════════════════════

docker exec myapp mount | grep -E '/data|/var/lib|/etc/app'
docker exec myapp cat /proc/mounts | grep -E 'overlay|volume|bind'
docker exec myapp df -h

# ═══════════════════════════════════════════════════════
# 工具 3:实时监控 I/O
# ═══════════════════════════════════════════════════════

# 查看容器的 I/O 统计
docker stats --format "table {{.Name}}\t{{.BlockIO}}" --no-stream

# 输出:
# NAME           BLOCK I/O
# mysql-prod     1.2GB / 850MB
# redis          45MB / 120MB
# nginx          2MB / 15MB

12. 总结与最佳实践清单

12.1 docker run 持久化命令速查表

# ┌─────────────────────────────────────────────────────────────────┐
# │                    持久化命令速查表                               │
# ├─────────────────────────────────────────────────────────────────┤
# │                                                                  │
# │  Named Volume:                                                   │
# │  docker run -v mydata:/data image                               │
# │  docker run --mount type=volume,src=mydata,dst=/data image      │
# │                                                                  │
# │  Bind Mount:                                                     │
# │  docker run -v /host/path:/container/path image                 │
# │  docker run -v /host/path:/container/path:ro image              │
# │  docker run --mount type=bind,src=/host,dst=/cont,ro image      │
# │                                                                  │
# │  tmpfs:                                                          │
# │  docker run --tmpfs /tmp:size=256m image                        │
# │  docker run --mount type=tmpfs,dst=/tmp,tmpfs-size=256m image   │
# │                                                                  │
# │  只读根文件系统 + 可写 Volume:                                    │
# │  docker run --read-only -v mydata:/data --tmpfs /tmp image      │
# │                                                                  │
# │  Volume 管理:                                                    │
# │  docker volume create/ls/inspect/rm/prune                       │
# │                                                                  │
# │  备份:                                                           │
# │  docker run --rm -v mydata:/src:ro -v $(pwd):/bak alpine \      │
# │    tar czf /bak/backup.tar.gz -C /src .                         │
# └─────────────────────────────────────────────────────────────────┘

12.2 生产环境十大最佳实践

┌─────────────────────────────────────────────────────────────────┐
│                  生产环境持久化十大最佳实践                        │
├──────┬──────────────────────────────────────────────────────────┤
│  1   │ 数据库/消息队列一律使用 Named Volume                      │
│      │ → 生命周期独立,不随容器销毁                               │
├──────┼──────────────────────────────────────────────────────────┤
│  2   │ 配置文件使用 Bind Mount + :ro(只读)                     │
│      │ → 宿主机可直接编辑,容器无法篡改                           │
├──────┼──────────────────────────────────────────────────────────┤
│  3   │ 生产环境优先使用 --mount 而非 -v                          │
│      │ → 更严格(路径不存在会报错),避免隐性问题                  │
├──────┼──────────────────────────────────────────────────────────┤
│  4   │ 永远不要运行 docker compose down -v(除非你确定要删数据)   │
│      │ → 这会删除所有 Volume,数据不可恢复                        │
├──────┼──────────────────────────────────────────────────────────┤
│  5   │ 使用 --read-only 根文件系统 + tmpfs 配合 Volume           │
│      │ → 最小权限原则,容器只能写入明确授权的路径                  │
├──────┼──────────────────────────────────────────────────────────┤
│  6   │ 为 Volume 设置有意义的标签                                 │
│      │ → backup=daily, criticality=high, retention=30d          │
├──────┼──────────────────────────────────────────────────────────┤
│  7   │ 数据库备份使用数据库自带工具(pg_dump, mysqldump)         │
│      │ → 直接复制数据文件可能导致不一致                           │
├──────┼──────────────────────────────────────────────────────────┤
│  8   │ 监控 Volume 空间使用量 + 设置告警                         │
│      │ → docker system df -v 定期检查,80% 触发告警              │
├──────┼──────────────────────────────────────────────────────────┤
│  9   │ 日志配置轮转策略(max-size + max-file)                   │
│      │ → 防止日志 Volume 无限膨胀撑满磁盘                        │
├──────┼──────────────────────────────────────────────────────────┤
│ 10   │ 定期测试备份恢复流程                                      │
│      │ → 没有经过验证的备份等于没有备份                           │
└──────┴──────────────────────────────────────────────────────────┘

12.3 存储方式选型最终决策图

  你的数据需要容器删除后保留吗?
  │
  ├─ 不需要
  │    │
  │    ├─ 需要高速读写? → tmpfs
  │    └─ 不需要高速?   → 容器层(默认)
  │
  └─ 需要
       │
       ├─ 需要 Docker 管理生命周期?
       │    │
       │    ├─ 是 → Named Volume
       │    │         │
       │    │         ├─ 数据库数据 → volume (local driver, SSD 分区)
       │    │         ├─ 用户上传 → volume (NFS driver, 多机共享)
       │    │         └─ 应用日志 → volume (local driver, 日志轮转)
       │    │
       │    └─ 否 → Bind Mount
       │              │
       │              ├─ 配置文件 → bind mount (:ro)
       │              ├─ 开发源码 → bind mount (:cached on macOS)
       │              └─ SSL 证书 → bind mount (:ro) 或 Docker Secrets
       │
       └─ 需要跨主机迁移?
            │
            └─ 是 → 云存储 Volume Driver (EBS/NFS/Ceph)
                    或 Kubernetes PV/PVC + CSI

参考资料

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐