Docker Compose 完全指南 / 第 5 章 · 数据卷:named volumes、bind mounts 与 tmpfs
第 5 章 · 数据卷管理
5.1 为什么需要数据卷?
Docker 容器是无状态的——容器删除后,其内部的所有数据都会丢失。数据卷解决了这个核心问题。
容器文件系统分层
┌────────────────────────────┐
│ 可写层 (Container Layer) │ ← 容器删除即丢失
├────────────────────────────┤
│ 只读层 (Image Layers) │ ← 镜像内容,共享
├────────────────────────────┤
│ 宿主机文件系统 │
└────────────────────────────┘
使用数据卷后:
┌────────────────────────────┐
│ 容器 │
│ /data → 宿主机卷 │ ← 数据持久化
├────────────────────────────┤
│ 数据卷 (Volume) │ ← 独立于容器生命周期
└────────────────────────────┘
三种挂载方式对比
| 特性 | Named Volume | Bind Mount | tmpfs |
|---|---|---|---|
| 存储位置 | Docker 管理的目录 | 宿主机指定路径 | 内存 |
| 持久化 | ✅ 是 | ✅ 是 | ❌ 否(容器停止即丢) |
| 跨主机共享 | ❌(需驱动支持) | ❌ | ❌ |
| 可移植性 | ✅ 高 | ❌ 低(依赖宿主机路径) | ✅ 高 |
| 性能 | 优秀 | 取决于存储类型 | 最快 |
| 典型用途 | 数据库数据、应用状态 | 开发代码同步、配置文件 | 临时缓存、密钥 |
| 容器删除后 | 数据保留 | 数据保留(宿主机文件) | 数据丢失 |
5.2 Named Volumes(命名卷)
命名卷是 Docker 管理的持久存储,推荐用于生产数据。
基本用法
services:
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: # 必须在顶级 volumes 中声明
命名卷的高级配置
volumes:
pgdata:
name: myapp_production_pgdata # 自定义卷名(避免默认的 <project>_<name> 格式)
driver: local # 默认驱动
driver_opts:
type: nfs # NFS 类型
o: addr=192.168.1.100,rw,nfsvers=4
device: ":/exports/pgdata"
external: false # 是否为外部卷
labels:
com.example.backup: "daily"
com.example.retention: "30d"
卷的生命周期管理
# 查看所有卷
docker volume ls
# 查看卷详情
docker volume inspect myapp_production_pgdata
# 创建卷(手动,非 Compose 管理)
docker volume create my-shared-data
# 删除卷(⚠️ 数据会丢失)
docker volume rm my-shared-data
# 清理无主卷(⚠️ 谨慎使用)
docker volume prune
# 清理无主卷 + 确认
docker volume prune -f
⚠️ 注意:
docker compose down只会删除容器和网络,不会删除命名卷。要同时删除卷需要docker compose down -v。
卷命名最佳实践
# ❌ 不好的做法:卷名依赖项目目录名
volumes:
data: # 实际卷名:myproject_data(目录名变化则卷名变化)
# ✅ 好的做法:显式指定卷名
volumes:
data:
name: myapp_pgdata # 不受项目目录名影响
5.3 Bind Mounts(绑定挂载)
绑定挂载将宿主机的文件或目录映射到容器中,推荐用于开发环境。
基本用法
services:
web:
image: nginx:alpine
volumes:
# 短语法:宿主机路径:容器路径
- ./html:/usr/share/nginx/html
# 只读挂载
- ./nginx.conf:/etc/nginx/nginx.conf:ro
# 读写挂载(默认)
- ./logs:/var/log/nginx
长语法(更清晰)
services:
web:
volumes:
- type: bind
source: ./html # 宿主机路径
target: /usr/share/nginx/html
read_only: false
bind:
create_host_path: true # 宿主机路径不存在时自动创建
propagation: rprivate # 挂载传播模式
传播模式(Propagation)
| 模式 | 说明 |
|---|---|
private | 默认,挂载事件不传播 |
rprivate | 递归 private |
shared | 挂载事件双向传播 |
rshared | 递归 shared |
slave | 从宿主机单向接收传播 |
rslave | 递归 slave |
💡 大多数场景不需要设置 propagation,保持默认即可。仅在嵌套挂载或特殊存储驱动场景下才需配置。
开发热重载示例
# Node.js 开发环境
services:
app:
image: node:20-alpine
working_dir: /app
command: npx nodemon src/index.js
volumes:
# 绑定挂载源码
- ./src:/app/src
# 使用命名卷缓存 node_modules(性能更好)
- node_modules:/app/node_modules
ports:
- "3000:3000"
environment:
NODE_ENV: development
volumes:
node_modules: # 命名卷,不受 bind mount 影响
💡 技巧:将
node_modules放在命名卷而非绑定挂载中,可以避免 macOS/Windows 上的 I/O 性能问题。
5.4 tmpfs Mounts(内存挂载)
tmpfs 将数据存储在内存中,适合临时数据和敏感信息。
基本用法
services:
app:
image: myapp:latest
tmpfs:
- /tmp # 简单语法
- /run:size=10M # 带大小限制
volumes:
- type: tmpfs
target: /app/cache
tmpfs:
size: 104857600 # 100MB (bytes)
mode: 1777 # 权限
适用场景
| 场景 | 说明 |
|---|---|
| 临时文件 | /tmp、/var/tmp |
| 会话缓存 | 应用运行时缓存 |
| 敏感数据 | 密钥文件,不落盘更安全 |
| 高性能缓存 | 内存 I/O 远快于磁盘 |
实际示例:NGINX 临时目录
services:
nginx:
image: nginx:alpine
tmpfs:
- /var/cache/nginx:size=100M
- /var/run:size=10M
- /tmp:size=50M
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "80:80"
5.5 从容器中复制数据到卷
场景:初始化卷数据
有时需要将文件从容器复制到卷中(如首次初始化配置)。
services:
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
# 挂载初始化脚本(仅在数据目录为空时执行)
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
手动复制数据到卷
# 创建一个临时容器来复制文件
docker compose run --rm -v myvolume:/target alpine sh -c \
"cp -r /source/* /target/"
# 或使用 docker cp
docker compose up -d db
docker cp ./backup.sql myproject-db-1:/tmp/
docker compose exec db psql -U postgres -f /tmp/backup.sql
5.6 卷备份与恢复
备份命名卷
# 方法一:使用临时容器导出
docker compose run --rm -v pgdata:/source -v $(pwd):/backup alpine \
tar czf /backup/pgdata-backup-$(date +%Y%m%d).tar.gz -C /source .
# 方法二:使用 docker exec(容器运行时)
docker compose exec db pg_dump -U postgres mydb > backup.sql
# 方法三:直接备份卷目录(需 root)
sudo tar czf pgdata-backup.tar.gz /var/lib/docker/volumes/myapp_pgdata/_data/
恢复命名卷
# 创建新卷
docker volume create pgdata-restored
# 解压到新卷
docker run --rm \
-v pgdata-restored:/target \
-v $(pwd):/backup \
alpine sh -c "cd /target && tar xzf /backup/pgdata-backup-20260510.tar.gz"
# 使用恢复的卷启动
# 在 compose.yaml 中修改卷名或使用 external volume
自动化备份脚本
#!/bin/bash
# backup-compose-volumes.sh
# 用法: ./backup-compose-volumes.sh <project_name>
PROJECT=${1:-$(basename $(pwd))}
BACKUP_DIR="./backups/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
echo "Backing up volumes for project: $PROJECT"
# 获取项目的所有卷
VOLUMES=$(docker volume ls --filter "label=com.docker.compose.project=$PROJECT" -q)
for VOL in $VOLUMES; do
echo " → Backing up $VOL..."
docker run --rm \
-v "$VOL":/source:ro \
-v "$(pwd)/$BACKUP_DIR":/backup \
alpine tar czf "/backup/${VOL}.tar.gz" -C /source .
done
echo "Backup complete: $BACKUP_DIR"
ls -lh "$BACKUP_DIR"
5.7 NFS 与远程存储
使用 NFS 卷
services:
app:
image: myapp:latest
volumes:
- nfsvol:/data
volumes:
nfsvol:
driver: local
driver_opts:
type: nfs
o: addr=192.168.1.100,rw,nfsvers=4,hard,timeo=600
device: ":/exports/appdata"
使用 CIFS/SMB(Windows 共享)
volumes:
smbdata:
driver: local
driver_opts:
type: cifs
o: addr=192.168.1.200,username=user,password=pass,file_mode=0777,dir_mode=0777
device: "//192.168.1.200/share"
使用 tmpfs 驱动(伪 tmpfs)
volumes:
ramdisk:
driver: local
driver_opts:
type: tmpfs
device: tmpfs
o: size=100m,uid=1000
5.8 数据卷的实际业务场景
场景一:数据库持久化
services:
postgres:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
- ./backups:/backups # 备份目录
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
mysql:
image: mysql:8.0
volumes:
- mysqldata:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
redis:
image: redis:7-alpine
volumes:
- redisdata:/data
command: redis-server --appendonly yes # 开启 AOF 持久化
volumes:
pgdata:
mysqldata:
redisdata:
场景二:文件上传共享
services:
api:
image: myapi:latest
volumes:
- uploads:/data/uploads # API 写入上传文件
worker:
image: myworker:latest
volumes:
- uploads:/data/uploads # Worker 处理上传文件
nginx:
image: nginx:alpine
volumes:
- uploads:/usr/share/nginx/uploads:ro # Nginx 直接服务静态文件
volumes:
uploads:
场景三:开发环境代码同步
services:
frontend:
image: node:20-alpine
working_dir: /app
command: npm run dev
volumes:
- ./frontend:/app # 源码绑定挂载
- frontend_modules:/app/node_modules # 依赖用命名卷
backend:
image: python:3.12-slim
working_dir: /app
command: python -m uvicorn main:app --reload
volumes:
- ./backend:/app # 源码绑定挂载
- pip_cache:/root/.cache/pip # pip 缓存
volumes:
frontend_modules:
pip_cache:
5.9 卷的性能考量
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 数据库数据 | Named Volume | Docker 优化的存储路径,性能最佳 |
| 开发代码同步 | Bind Mount | 实时同步,开发便利 |
| macOS/Windows 开发 | Named Volume + Bind Mount 混合 | node_modules 用命名卷避免 I/O 瓶颈 |
| 临时缓存 | tmpfs | 内存最快 |
| CI/CD 构建缓存 | Named Volume | 持久化缓存层,加速构建 |
macOS/Windows I/O 优化
# macOS/Windows 上的优化写法
services:
node-app:
image: node:20-alpine
volumes:
- .:/app # 代码同步
- node_modules:/app/node_modules # 命名卷避免跨文件系统 I/O
- /app/dist # 匿名卷排除构建产物
volumes:
node_modules:
5.10 常见问题与排错
| 问题 | 原因 | 解决方案 |
|---|---|---|
权限拒绝 (Permission denied) | UID/GID 不匹配 | 使用 user 指令或修改挂载目录权限 |
| 数据丢失 | 使用了匿名卷或 down -v | 声明命名卷,避免 -v 参数 |
| 挂载路径不存在 | Bind Mount 源路径不存在 | Compose 会自动创建目录(bind mount) |
node_modules 冲突 | Bind Mount 覆盖了容器内目录 | 使用命名卷隔离 node_modules |
| 卷空间不足 | Docker 默认存储空间限制 | 配置 daemon.json 的 storage-opts |
解决权限问题
services:
app:
image: myapp:latest
user: "1000:1000" # 指定容器内运行用户
volumes:
- ./data:/app/data
# 或在 Dockerfile 中处理权限
# 修改挂载目录权限
chmod -R 777 ./data # 临时方案(不安全)
sudo chown -R 1000:1000 ./data # 推荐方案
5.11 小结
| 概念 | 说明 |
|---|---|
| Named Volume | Docker 管理,持久化,推荐用于生产数据 |
| Bind Mount | 宿主机路径映射,适合开发环境 |
| tmpfs | 内存存储,适合临时/敏感数据 |
| 卷备份 | 使用临时容器导出 + tar 归档 |
| 性能优化 | macOS/Windows 上混合使用命名卷和绑定挂载 |
| 安全注意 | down -v 会删除所有命名卷,谨慎使用 |
扩展阅读
上一章:第 4 章 · 网络 ← | 下一章:第 6 章 · 环境变量 →