1. 为什么Docker Compose重启会丢数据?

"昨天还好好的服务,今天重启后数据库全空了!"——这是许多开发者初次使用Docker Compose时踩过的坑。问题的核心在于:默认情况下,Docker容器是临时(Ephemeral)的。当容器被销毁(比如执行docker-compose down)时,容器内部生成的文件会随容器生命周期结束而消失。这种特性在无状态服务中没问题,但对数据库、文件存储等场景就是灾难。

典型翻车现场

  • 开发环境用docker-compose up启动MySQL后存入测试数据
  • 修改配置后执行docker-compose down && docker-compose up -d
  • 发现数据库变成初始状态,测试数据全丢
  • 开发者陷入"我数据呢?"的哲学思考

2. 数据持久化的三种武器

2.1 方案一:用Docker卷(Volume)当"U盘"

技术栈:Docker Compose + Volume
原理:将容器内关键目录映射到宿主机的持久化存储空间

version: '3.8'

services:
  mysql:
    image: mysql:8.0
    volumes:
      - mysql_data:/var/lib/mysql  # 将数据库文件存放到命名卷
    environment:
      MYSQL_ROOT_PASSWORD: secret

volumes:
  mysql_data:  # 声明一个持久化卷

验证操作

# 启动服务
docker-compose up -d

# 进入容器插入测试数据
docker exec -it [容器ID] mysql -uroot -psecret -e "CREATE DATABASE demo;"

# 销毁容器
docker-compose down

# 重新启动
docker-compose up -d

# 检查数据库是否存在(应保留)
docker exec -it [新容器ID] mysql -uroot -psecret -e "SHOW DATABASES;"

优点

  • 数据独立于容器生命周期
  • 跨主机迁移方便(卷可导出)
  • 性能接近原生磁盘

缺点

  • 需要手动清理废弃卷
  • 不直观查看卷内文件(需通过docker volume inspect

2.2 方案二:绑定宿主机目录(Bind Mount)

技术栈:Docker Compose + 宿主机目录
适用场景:需要直接修改配置文件的开发环境

services:
  redis:
    image: redis:6
    volumes:
      - ./redis_data:/data  # 映射到当前目录的redis_data文件夹

文件结构

project/
├── docker-compose.yml
└── redis_data/  # 自动创建的空目录

验证方法

# 启动后执行数据写入
docker exec [容器ID] redis-cli SET foo "bar"

# 查看宿主机目录
ls ./redis_data
# 应看到dump.rdb文件

# 重启服务后检查数据
docker exec [新容器ID] redis-cli GET foo  # 应返回"bar"

优点

  • 开发调试直观(直接修改宿主机文件)
  • 无需管理卷生命周期

缺点

  • 路径依赖导致部署不够灵活
  • 可能引发权限问题(容器用户与宿主机用户UID不一致)

2.3 方案三:临时卷(Tmpfs)的特殊玩法

技术栈:Docker Compose + Tmpfs
适用场景:敏感数据内存暂存

services:
  temp_service:
    image: alpine
    tmpfs:
      - /app/temp_cache  # 内存挂载目录

特点

  • 数据仅存于内存,重启后消失
  • 适合存放临时会话、缓存等非持久数据

3. 混合模式实战:Web应用+数据库+文件存储

技术栈:Node.js + MySQL + MinIO
场景:一个包含用户上传功能的Web服务

version: '3.8'

services:
  web:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - ./uploads:/app/uploads  # 绑定上传目录
    depends_on:
      - db
      - minio

  db:
    image: mysql:8.0
    volumes:
      - mysql_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: supersecret

  minio:
    image: minio/minio
    volumes:
      - minio_data:/data
    command: server /data
    environment:
      MINIO_ROOT_USER: admin
      MINIO_ROOT_PASSWORD: admin123

volumes:
  mysql_data:
  minio_data:

关键点说明

  1. Web服务通过绑定挂载实现上传目录持久化
  2. 数据库使用命名卷保证事务安全
  3. 对象存储使用独立卷隔离业务数据

4. 避坑指南:那些你一定会遇到的坑

4.1 权限的幽灵

现象:容器报错"Permission Denied"
根因:容器内进程用户(如MySQL用mysql用户)无宿主机目录写权限

解决方案

volumes:
  - ./data:/var/lib/mysql:z  # SELinux上下文标记
  # 或
  - ./data:/var/lib/mysql:ro  # 只读挂载(慎用)
4.2 备份策略不能少

经典错误:以为用了卷就万事大吉
正确姿势

# 定期备份MySQL卷
docker run --rm -v mysql_data:/source -v $(pwd)/backups:/backup \
  alpine tar czf /backup/mysql_$(date +%Y%m%d).tar.gz -C /source .

# MinIO配置版本控制
docker exec minio mc admin bucket set mybucket versioning enable
4.3 不要忽视版本兼容性

血泪案例

  • Compose文件版本影响卷声明方式
  • 不同Docker引擎版本对tmpfs的支持差异

5. 终极武器:Docker卷的进阶管理

5.1 查看卷使用情况
docker system df -v  # 显示卷磁盘占用
5.2 跨主机迁移
# 导出卷
docker run --rm -v mysql_data:/source -v $(pwd):/backup \
  alpine tar cf /backup/mysql_data.tar -C /source .

# 新主机导入
docker volume create mysql_data
docker run --rm -v mysql_data:/target -v $(pwd):/backup \
  alpine tar xf /backup/mysql_data.tar -C /target
5.3 自动清理策略
# 删除所有未被使用的卷
docker volume prune

# 保留最近3天的备份
find ./backups -name "*.tar.gz" -mtime +3 -exec rm {} \;

6. 总结:数据无价,且用且珍惜

通过合理的卷策略,我们可以让Docker Compose既保持"一键启停"的便捷性,又实现关键数据的持久化。记住三个黄金法则:

  1. 重要数据必须显式声明存储位置
  2. 开发环境优先使用绑定挂载,生产环境用命名卷
  3. 备份方案要和存储方案同时设计