第08章:Docker 数据持久化

本章目标:理解容器数据的存储方式,掌握 Volume 和 Bind Mount 的使用,实现数据的持久化和共享。


8.1 为什么需要数据持久化

8.1.1 容器数据的生命周期

容器的可写层(Write Layer):

┌─────────────────────────────────┐
│  Container Layer (可写层)        │
│  - /app/data/db.sqlite          │
│  - /var/log/app.log             │
│  - /tmp/session.cache           │
└─────────────────────────────────┘

⚠️ 容器被删除时,可写层中的数据也会丢失!

docker rm <container>  →  所有数据消失!

8.1.2 需要持久化的数据类型

数据类型 示例 重要性
数据库文件 MySQL 数据、Redis RDB ⭐⭐⭐ 生命线
用户上传 图片、视频、文档 ⭐⭐⭐ 不可再生
应用日志 访问日志、错误日志 ⭐⭐ 排查问题
配置文件 证书、密钥、配置 ⭐⭐ 安全相关
临时文件 缓存、会话 ⭐ 可重建

8.2 数据存储方式概览

8.2.1 三种数据存储方式

┌────────────────────────────────────────────────────┐
│              Docker 数据存储方式                     │
│                                                    │
│  ┌──────────────────┐  ┌────────────────────────┐│
│  │  Volume (卷)      │  │  Bind Mount (绑定挂载)  ││
│  │                   │  │                        ││
│  │  Docker 管理      │  │  用户指定目录           ││
│  │  独立于容器        │  │  直接映射宿主机目录     ││
│  │  推荐使用         │  │  开发环境常用           ││
│  └──────────────────┘  └────────────────────────┘│
│                                                    │
│  ┌──────────────────┐                              │
│  │  tmpfs (临时文件) │                              │
│  │                   │                              │
│  │  内存中存储        │                              │
│  │  容器停止即消失    │                              │
│  │  敏感数据使用      │                              │
│  └──────────────────┘                              │
└────────────────────────────────────────────────────┘

8.2.2 三种方式对比

特性 Volume Bind Mount tmpfs
存储位置 Docker 管理的目录 宿主机目录 内存
持久性 ✅ 容器删除后保留 ✅ 宿主机目录保留 ❌ 容器停止即丢失
多容器共享 ✅ 支持 ✅ 支持 ❌ 不支持
备份迁移 ✅ 方便 ✅ 直接复制 ❌ 不支持
性能 最高(内存)
配置复杂度
推荐场景 生产环境 开发环境 敏感数据/缓存

8.3 Volume(数据卷)

8.3.1 Volume 的工作原理

Volume 存储结构:

宿主机文件系统
┌──────────────────────────────────────┐
│  /var/lib/docker/volumes/            │
│  ┌──────────────────────────────┐   │
│  │  mydata/                     │   │
│  │  ┌──────────────────────┐   │   │
│  │  │  _data/              │   │   │
│  │  │  ├── db/             │   │   │
│  │  │  │   ├── ibdata1     │   │   │
│  │  │  │   ├── ib_logfile0 │   │   │
│  │  │  │   └── ...         │   │   │
│  │  │  └── ...             │   │   │
│  │  └──────────────────────┘   │   │
│  └──────────────────────────────┘   │
└──────────────────────────────────────┘
                  │
                  │ 挂载
                  ▼
容器文件系统
┌──────────────────────────────────────┐
│  /var/lib/mysql/                     │
│  ├── ibdata1                         │
│  ├── ib_logfile0                     │
│  └── ...                             │
└──────────────────────────────────────┘

8.3.2 创建和使用 Volume

# 1. 创建 Volume
docker volume create mydata
docker volume create --driver local \
  --opt type=none \
  --opt device=/data/mysql \
  --opt o=bind \
  mysql-data

# 2. 查看所有 Volume
docker volume ls
# DRIVER    VOLUME NAME
# local     mydata
# local     mysql-data
# local     abc123def456...  ← 匿名卷

# 3. 查看 Volume 详情
docker volume inspect mydata
# [
#     {
#         "CreatedAt": "2024-01-15T10:30:00+08:00",
#         "Driver": "local",
#         "Labels": {},
#         "Mountpoint": "/var/lib/docker/volumes/mydata/_data",
#         "Name": "mydata",
#         "Options": {},
#         "Scope": "local"
#     }
# ]

# 4. 使用 Volume 运行容器
docker run -d \
  --name mysql-db \
  -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret123 \
  mysql:8.0

# 5. 验证数据持久化
docker exec mysql-db mysql -uroot -psecret123 -e "CREATE DATABASE test;"
docker rm -f mysql-db

# 重新创建容器,数据仍然存在!
docker run -d \
  --name mysql-db-new \
  -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret123 \
  mysql:8.0

docker exec mysql-db-new mysql -uroot -psecret123 -e "SHOW DATABASES;"
# test 数据库仍然存在!

8.3.3 匿名卷 vs 命名卷

# 匿名卷(Docker 自动生成名称)
docker run -d -v /var/lib/mysql mysql:8.0
# Volume 名称:abc123def456...(随机)

# 命名卷(推荐!)
docker run -d -v mysql-data:/var/lib/mysql mysql:8.0
# Volume 名称:mysql-data(可读可管理)

# 在 Dockerfile 中声明匿名卷
VOLUME /data
VOLUME ["/data", "/logs"]

# ⚠️ 注意:Dockerfile 中的 VOLUME 声明会导致后续对该目录的修改无法保存到镜像层

8.3.4 Volume 的生命周期管理

# 删除未使用的 Volume
docker volume prune

# 删除指定 Volume
docker volume rm mydata

# 查看 Volume 使用情况
docker system df -v

# 备份 Volume
docker run --rm -v mysql-data:/source-data -v $(pwd):/backup \
  ubuntu tar czf /backup/mysql-data-backup.tar.gz -C /source-data .

# 恢复 Volume
docker volume create mysql-data-restored
docker run --rm -v mysql-data-restored:/target-data -v $(pwd):/backup \
  ubuntu tar xzf /backup/mysql-data-backup.tar.gz -C /target-data

8.4 Bind Mount(绑定挂载)

8.4.1 Bind Mount 的工作原理

Bind Mount 直接映射宿主机目录:

宿主机目录                          容器目录
┌──────────────────────┐          ┌──────────────────────┐
│  /home/user/project/ │  ──────► │  /app/               │
│  ├── src/            │          │  ├── src/            │
│  │   └── app.py      │          │  │   └── app.py      │
│  ├── config/         │          │  ├── config/         │
│  │   └── settings.yml│          │  │   └── settings.yml│
│  └── requirements.txt│          │  └── requirements.txt│
└──────────────────────┘          └──────────────────────┘

容器内的修改会直接影响宿主机!
宿主机的修改也会直接影响容器!

8.4.2 使用 Bind Mount

# 基本用法
docker run -d -v /host/path:/container/path nginx

# 推荐使用 --mount 语法(更清晰)
docker run -d \
  --mount type=bind,source=/host/path,target=/container/path \
  nginx

# 只读挂载
docker run -d \
  --mount type=bind,source=/host/config,target=/app/config,readonly \
  nginx

# 挂载单个文件
docker run -d \
  --mount type=bind,source=/host/nginx.conf,target=/etc/nginx/nginx.conf,readonly \
  nginx

# 开发环境示例:挂载代码目录
docker run -d \
  --name dev-app \
  -v $(pwd)/src:/app/src \
  -v $(pwd)/requirements.txt:/app/requirements.txt \
  -p 8080:8080 \
  python:3.11-slim

# 开发时修改代码后,容器内立即生效!

8.4.3 --volumes-from(从另一个容器挂载)

# 从已有容器挂载所有卷
docker run -d --name data-container -v mysql-data:/data ubuntu

# 新容器从 data-container 挂载卷
docker run -d --name app --volumes-from data-container myapp

# data-container 和 app 共享同一个 mysql-data 卷
# 适合数据容器模式(已不推荐,优先使用命名卷)

8.5 tmpfs(临时文件系统)

8.5.1 tmpfs 的特点

# tmpfs 挂载:数据存储在内存中
docker run -d \
  --tmpfs /tmp:rw,size=100m \
  nginx

# 指定 tmpfs 参数
docker run -d \
  --tmpfs /tmp:rw,noexec,nosuid,size=100m \
  myapp

# tmpfs 参数:
# rw     读写模式
# ro     只读模式
# noexec 不可执行
# nosuid 不允许 setuid
# size   大小限制

8.5.2 tmpfs 适用场景

场景 说明
敏感数据 密钥、证书、Token(不写入磁盘)
高速缓存 临时缓存文件(重启即清除)
日志缓冲 高频写入的临时日志
临时文件 应用运行时产生的临时文件

8.6 数据库容器化实战

8.6.1 MySQL 持久化部署

# 创建 Docker Compose 文件
cat > docker-compose.yml << 'EOF'
version: '3.8'

services:
  mysql:
    image: mysql:8.0
    container_name: mysql-server
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-secret123}
      MYSQL_DATABASE: ${DB_NAME:-myapp}
      MYSQL_USER: ${DB_USER:-appuser}
      MYSQL_PASSWORD: ${DB_PASSWORD:-apppass123}
    volumes:
      - mysql-data:/var/lib/mysql           # 数据文件
      - mysql-logs:/var/log/mysql           # 日志文件
      - ./mysql/conf.d:/etc/mysql/conf.d    # 配置文件(只读)
      - ./mysql/initdb:/docker-entrypoint-initdb.d  # 初始化脚本
    ports:
      - "3306:3306"
    networks:
      - backend
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: '1.0'

volumes:
  mysql-data:
    driver: local
  mysql-logs:
    driver: local

networks:
  backend:
    driver: bridge
EOF

# 创建必要的目录
mkdir -p mysql/conf.d mysql/initdb

# 添加自定义配置
cat > mysql/conf.d/custom.cnf << 'EOF'
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
max_connections = 200
innodb_buffer_pool_size = 512M
EOF

# 启动 MySQL
docker compose up -d

# 查看日志
docker compose logs -f mysql

# 测试连接
docker exec -it mysql-server mysql -uroot -psecret123

8.6.2 Redis 持久化部署

# Redis Docker Compose 配置
cat > docker-compose-redis.yml << 'EOF'
version: '3.8'

services:
  redis:
    image: redis:7-alpine
    container_name: redis-server
    restart: unless-stopped
    command: redis-server /etc/redis/redis.conf
    volumes:
      - redis-data:/data
      - ./redis/redis.conf:/etc/redis/redis.conf:ro
    ports:
      - "6379:6379"
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  redis-data:
    driver: local

networks:
  backend:
    driver: bridge
EOF

# 创建 Redis 配置
mkdir -p redis
cat > redis/redis.conf << 'EOF'
bind 0.0.0.0
protected-mode no
requirepass redis123
save 900 1
save 300 10
save 60 10000
maxmemory 256mb
maxmemory-policy allkeys-lru
appendonly yes
EOF

# 启动 Redis
docker compose -f docker-compose-redis.yml up -d

8.6.3 PostgreSQL 持久化部署

# PostgreSQL Docker Compose 配置
cat > docker-compose-pg.yml << 'EOF'
version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: postgres-server
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${PG_DB:-myapp}
      POSTGRES_USER: ${PG_USER:-appuser}
      POSTGRES_PASSWORD: ${PG_PASSWORD:-apppass123}
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - pg-data:/var/lib/postgresql/data
      - ./postgres/initdb:/docker-entrypoint-initdb.d
    ports:
      - "5432:5432"
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${PG_USER:-appuser}"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pg-data:
    driver: local

networks:
  backend:
    driver: bridge
EOF

# 创建初始化脚本
mkdir -p postgres/initdb
cat > postgres/initdb/01-init.sql << 'EOF'
-- 创建扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- 创建示例表
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
EOF

# 启动 PostgreSQL
docker compose -f docker-compose-pg.yml up -d

8.7 数据备份与恢复

8.7.1 MySQL 备份

# 备份 MySQL 数据
docker exec mysql-server mysqldump -uroot -psecret123 --all-databases > mysql-backup.sql

# 使用 Volume 备份
docker run --rm \
  -v mysql-data:/var/lib/mysql \
  -v $(pwd):/backup \
  ubuntu tar czf /backup/mysql-volume-backup.tar.gz -C /var/lib/mysql .

# 恢复 MySQL 数据
docker exec -i mysql-server mysql -uroot -psecret123 < mysql-backup.sql

# 使用 Volume 恢复
docker volume create mysql-data-new
docker run --rm \
  -v mysql-data-new:/var/lib/mysql \
  -v $(pwd):/backup \
  ubuntu tar xzf /backup/mysql-volume-backup.tar.gz -C /var/lib/mysql

8.7.2 Redis 备份

# 触发 Redis 保存
docker exec redis-server redis-cli -a redis123 BGSAVE

# 备份 RDB 文件
docker cp redis-server:/data/dump.rdb ./redis-backup.rdb

# 恢复 Redis 数据
docker cp ./redis-backup.rdb redis-server:/data/dump.rdb
docker exec redis-server redis-cli -a redis123 SHUTDOWN NOSAVE
docker restart redis-server

8.7.3 自动化备份脚本

cat > backup.sh << 'EOF'
#!/bin/bash
# Docker 数据自动备份脚本

BACKUP_DIR="/backup/docker"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7

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

# 备份 MySQL
echo "正在备份 MySQL..."
docker exec mysql-server mysqldump -uroot -psecret123 --all-databases | \
  gzip > $BACKUP_DIR/mysql_$DATE.sql.gz

# 备份 Redis
echo "正在备份 Redis..."
docker exec redis-server redis-cli -a redis123 BGSAVE
sleep 2
docker cp redis-server:/data/dump.rdb $BACKUP_DIR/redis_$DATE.rdb

# 备份 PostgreSQL
echo "正在备份 PostgreSQL..."
docker exec postgres-server pg_dumpall -U appuser | \
  gzip > $BACKUP_DIR/postgres_$DATE.sql.gz

# 清理过期备份
echo "清理 $RETENTION_DAYS 天前的备份..."
find $BACKUP_DIR -name "*.gz" -mtime +$RETENTION_DAYS -delete
find $BACKUP_DIR -name "*.rdb" -mtime +$RETENTION_DAYS -delete

echo "备份完成!"
ls -lh $BACKUP_DIR/
EOF

chmod +x backup.sh

# 添加到 crontab(每天凌晨3点备份)
# crontab -e
# 0 3 * * * /path/to/backup.sh >> /var/log/docker-backup.log 2>&1

8.8 动手实验

实验 8.1:Volume 数据持久化验证

# 1. 创建命名卷
docker volume create test-data

# 2. 启动容器写入数据
docker run --rm -v test-data:/data ubuntu bash -c \
  "echo 'Hello Volume' > /data/test.txt && cat /data/test.txt"
# Hello Volume

# 3. 删除容器(数据仍在卷中)
# (上面使用了 --rm,容器已自动删除)

# 4. 启动新容器读取数据
docker run --rm -v test-data:/data ubuntu cat /data/test.txt
# Hello Volume ✅ 数据持久化成功!

# 5. 清理
docker volume rm test-data

实验 8.2:Bind Mount 开发环境

# 1. 创建项目目录
mkdir -p ~/docker-lab/webapp
cd ~/docker-lab/webapp

# 2. 创建 Flask 应用
cat > app.py << 'EOF'
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello from Docker! (with bind mount)"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)
EOF

# 3. 运行开发容器(挂载代码目录)
docker run -d --name dev-web \
  -v $(pwd):/app \
  -w /app \
  -p 5000:5000 \
  python:3.11-slim \
  pip install flask && python app.py

# 4. 修改 app.py,容器内自动更新!
# 5. 清理
docker rm -f dev-web

实验 8.3:MySQL 完整部署

# 使用上面的 docker-compose.yml 部署 MySQL

# 1. 启动 MySQL
docker compose up -d

# 2. 等待健康检查通过
docker compose ps

# 3. 连接并创建数据
docker exec -it mysql-server mysql -uroot -psecret123 -e "
  USE myapp;
  CREATE TABLE test (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50));
  INSERT INTO test (name) VALUES ('Docker'), ('MySQL'), ('Volume');
  SELECT * FROM test;
"

# 4. 停止并删除容器
docker compose down

# 5. 重新启动(数据仍在)
docker compose up -d

# 6. 验证数据
docker exec -it mysql-server mysql -uroot -psecret123 -e "
  USE myapp;
  SELECT * FROM test;
"
# 数据完整!✅

8.9 本章小结

存储方式 特点 推荐场景
Volume Docker 管理,独立于容器 生产环境数据库、持久化数据
Bind Mount 直接映射宿主机目录 开发环境、配置文件
tmpfs 内存存储,临时使用 敏感数据、高速缓存

8.10 课后练习

  1. 基础题:使用 Volume 部署 MySQL,验证数据持久化。
  2. 进阶题:使用 Bind Mount 搭建 Python 开发环境,实现代码热更新。
  3. 实践题:编写自动化备份脚本,备份 MySQL 和 Redis 数据。

📖 下一章:Docker Compose 编排 —— 学会多容器应用的编排和管理

Logo

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

更多推荐