TL;DR(太长不看版)
- 生产数据库选命名卷,开发热更新选绑定挂载
- 配置文件用
:ro只读挂载,更安全 - 权限问题三招:chown、
--user、命名卷 - 容器 UID 与宿主 UID 不一致是权限根源
- 备份用
docker exec+ mysqldump / pg_dump - 恢复先重建容器,再导入 SQL 或 dump 文件
- 卷迁移用
--volumes-from或 tar 打包 - 三大坑:1000:1000、chown 重建、匿名卷泄漏
1. 引言
容器是无状态的,这是 Docker 设计哲学的核心。但现实中的数据库、配置文件、用户上传内容都需要持久化存储。如果容器被删除,数据也随之消失,那将是灾难性的。
本篇文章是「Docker 进阶系列」的第二篇,前置依赖为《数据卷基础》。我们将深入探讨:
- 命名卷 / 绑定挂载 / 匿名卷的对比与选型
- 文件权限问题:容器 UID vs 宿主 UID、
:ro、SELinux(:z/:Z) - 数据库持久化实战:MySQL / PostgreSQL 卷 + 定时备份 + 恢复演练
- 卷迁移与备份:
docker run --volumes-from、tar 打包 - 一键备份 + 恢复脚本模板
- 常见坑位总结:权限 1000:1000 问题、chown 与容器重建、匿名卷泄漏
前置知识:建议先阅读《数据卷基础》,了解
docker volume create、docker run -v与docker run --mount的基本用法。
2. 三种持久化方式对比与选型
2.1 命名卷(Named Volume)
命名卷由 Docker 管理,存储在宿主机 /var/lib/docker/volumes/<volume-name>/_data 目录下。
bash
# 创建命名卷
docker volume create mysql-data
# 挂载命名卷
docker run -d \
--name mysql-demo \
-v mysql-data:/var/lib/mysql \
mysql:8.0
优点:
- Docker 自动管理目录,无需关心宿主机具体路径
- 跨平台一致性好(Linux / macOS / Windows)
- 支持卷驱动扩展(如 NFS、云存储驱动)
- 适合生产环境数据库持久化
缺点:
- 数据位于 Docker 管理目录,直接访问不便
- 备份需要额外命令(
docker run --rm -v ... tar)
2.2 绑定挂载(Bind Mount)
绑定挂载直接将宿主机的任意目录/文件挂载到容器中。
bash
# 挂载宿主机目录
docker run -d \
--name nginx-demo \
-v /home/user/nginx/html:/usr/share/nginx/html:ro \
nginx:latest
# 使用 --mount 语法(推荐,语义更清晰)
docker run -d \
--name nginx-demo \
--mount type=bind,source=/home/user/nginx/html,target=/usr/share/nginx/html,readonly \
nginx:latest
优点:
- 宿主机路径直观,方便直接编辑文件
- 适合开发环境热更新(改代码即时生效)
- 适合配置文件、日志目录等场景
缺点:
- 依赖宿主机目录结构,跨平台可移植性差
- 存在权限问题(见第 3 节)
- 生产环境需谨慎管理路径
2.3 匿名卷(Anonymous Volume)
匿名卷是未指定名称的卷,由 Docker 自动生成随机名称。
bash
# 未指定卷名,Docker 自动创建匿名卷
docker run -d --name temp-db -v /var/lib/mysql mysql:8.0
特点:
- 主要用于镜像中
VOLUME指令声明的路径 - 容器删除时匿名卷不会 自动删除(除非加
-v参数) - 容易造成「匿名卷泄漏」------磁盘空间被孤儿卷占用
2.4 选型决策表
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 生产数据库 | 命名卷 | 独立管理、易备份、跨主机迁移 |
| 开发环境代码热更新 | 绑定挂载 | 宿主机直接编辑,即时生效 |
| 配置文件注入 | 绑定挂载(:ro) |
只读安全,宿主机路径直观 |
| 临时容器数据 | 匿名卷 | 随用随弃,注意清理 |
| 多容器共享数据 | 命名卷 + --volumes-from |
数据一致性、权限统一 |
3. 文件权限问题:容器 UID vs 宿主 UID
3.1 问题根源
容器内进程以特定 UID 运行,宿主机文件也有属主 UID。当两者不一致时,就会出现「权限不足」或「文件属主错乱」问题。
典型场景:MySQL 官方镜像以 mysql 用户(UID 999)运行,宿主机挂载目录属主是 root(UID 0),容器内写入就会报 Permission denied。
3.2 经典坑位:1000:1000 问题
很多基础镜像(如 Node、Python 官方镜像)默认以 UID 1000 的用户运行。宿主机上普通用户 UID 通常也是 1000,看起来「刚好匹配」,但实际会引发困惑:
bash
# 宿主机用户 UID 1000
$ id -u
1000
# 容器内用户 UID 1000
$ docker run --rm node:20 id -u
1000
问题在于: 当你在宿主机用 root 创建挂载目录时,目录属主是 root:root,容器内 UID 1000 的用户无法写入。
解决方案:
bash
# 方案一:宿主机目录属主改为容器内 UID
sudo chown -R 1000:1000 ./data
# 方案二:运行时指定用户(需镜像支持)
docker run --user 1000:1000 -v $(pwd)/data:/app/data node:20
# 方案三:使用命名卷,让 Docker 初始化属主
docker volume create app-data
docker run -v app-data:/app/data node:20
3.3 chown 与容器重建的坑
坑位: 容器重建后,挂载目录属主被重置。
bash
# 第一次运行,容器内 chown 了挂载目录
docker run --name app -v app-data:/app/data app-image
# 容器删除后重建
docker rm app
docker run --name app -v app-data:/app/data app-image
现象: 某些镜像的 entrypoint 脚本会在启动时对挂载目录执行 chown,导致宿主机目录属主被修改,影响宿主机其他进程访问。
规避策略:
- 优先使用命名卷,避免直接 chown 宿主机目录
- 若必须绑定挂载,在宿主机预先设置好属主,容器内不要执行 chown
- 使用
--user显式指定 UID,保持与宿主机一致
3.4 只读挂载 :ro
对于配置文件、静态资源等不需要容器写入的目录,务必使用只读挂载:
bash
docker run -d \
--name nginx \
-v /etc/nginx/conf.d:/etc/nginx/conf.d:ro \
nginx:latest
好处:
- 防止容器内进程意外修改宿主机文件
- 提升安全性(即使容器被攻破,也无法篡改配置)
- 语义清晰,明确「只读」意图
3.5 SELinux 标签 :z 与 :Z
在启用 SELinux 的 Linux 发行版(如 RHEL、CentOS、Fedora)上,绑定挂载默认会被 SELinux 阻止访问。
bash
# :z 表示共享标签,多个容器可共享
docker run -v /data:/data:z ...
# :Z 表示私有标签,仅当前容器可用
docker run -v /data:/data:Z ...
区别:
| 选项 | 含义 | 适用场景 |
|---|---|---|
:z |
共享 SELinux 标签 | 多个容器共享同一挂载目录 |
:Z |
私有 SELinux 标签 | 仅当前容器独占该目录 |
注意:
:z/:Z会修改宿主机目录的 SELinux 标签,需谨慎使用。生产环境建议统一规划标签策略。
4. 数据库持久化实战
4.1 MySQL 持久化 + 定时备份
启动 MySQL 并挂载命名卷:
bash
# 创建命名卷
docker volume create mysql-data
# 启动 MySQL
docker run -d \
--name mysql-prod \
-e MYSQL_ROOT_PASSWORD=StrongPass123 \
-e MYSQL_DATABASE=appdb \
-v mysql-data:/var/lib/mysql \
-p 3306:3306 \
mysql:8.0
定时备份脚本(mysqldump):
bash
#!/bin/bash
# mysql-backup.sh
BACKUP_DIR="/backup/mysql"
CONTAINER_NAME="mysql-prod"
DB_USER="root"
DB_PASS="StrongPass123"
DB_NAME="appdb"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
# 使用 docker exec 执行 mysqldump
docker exec "$CONTAINER_NAME" \
mysqldump -u"$DB_USER" -p"$DB_PASS" \
--single-transaction --routines --triggers \
"$DB_NAME" > "$BACKUP_DIR/${DB_NAME}_${DATE}.sql"
# 保留最近 7 天备份
find "$BACKUP_DIR" -name "*.sql" -mtime +7 -delete
echo "MySQL 备份完成: $BACKUP_DIR/${DB_NAME}_${DATE}.sql"
恢复演练:
bash
# 模拟数据丢失:删除容器(保留卷)
docker stop mysql-prod
docker rm mysql-prod
# 重新创建容器(挂载同一卷)
docker run -d \
--name mysql-prod \
-e MYSQL_ROOT_PASSWORD=StrongPass123 \
-v mysql-data:/var/lib/mysql \
-p 3306:3306 \
mysql:8.0
# 恢复数据
docker exec -i mysql-prod \
mysql -uroot -pStrongPass123 appdb < /backup/mysql/appdb_20260927_000000.sql
4.2 PostgreSQL 持久化 + 定时备份
启动 PostgreSQL:
bash
docker volume create pg-data
docker run -d \
--name pg-prod \
-e POSTGRES_USER=appuser \
-e POSTGRES_PASSWORD=StrongPass456 \
-e POSTGRES_DB=appdb \
-v pg-data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
定时备份脚本(pg_dump):
bash
#!/bin/bash
# pg-backup.sh
BACKUP_DIR="/backup/postgres"
CONTAINER_NAME="pg-prod"
DB_USER="appuser"
DB_NAME="appdb"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
# 使用 docker exec 执行 pg_dump
docker exec "$CONTAINER_NAME" \
pg_dump -U"$DB_USER" \
--format=custom \
"$DB_NAME" > "$BACKUP_DIR/${DB_NAME}_${DATE}.dump"
# 保留最近 7 天备份
find "$BACKUP_DIR" -name "*.dump" -mtime +7 -delete
echo "PostgreSQL 备份完成: $BACKUP_DIR/${DB_NAME}_${DATE}.dump"
恢复演练:
bash
# 恢复 PostgreSQL 数据
docker exec -i pg-prod \
pg_restore -Uappuser -d appdb \
--clean --if-exists \
< /backup/postgres/appdb_20260927_000000.dump
4.3 定时任务配置(cron)
bash
# 编辑 crontab
crontab -e
# 每天凌晨 2 点执行备份
0 2 * * * /opt/scripts/mysql-backup.sh >> /var/log/backup.log 2>&1
0 3 * * * /opt/scripts/pg-backup.sh >> /var/log/backup.log 2>&1
5. 卷迁移与备份
5.1 使用 --volumes-from 共享数据
--volumes-from 允许新容器继承已有容器的卷挂载配置:
bash
# 启动数据容器
docker run -d --name data-container -v app-data:/app/data busybox sleep 3600
# 新容器继承卷挂载
docker run -d \
--name app-server \
--volumes-from data-container \
app-image
适用场景:
- 多个容器共享同一数据卷
- 备份容器临时挂载数据卷
- 迁移容器时保持数据一致
5.2 使用 tar 打包备份卷
bash
# 备份命名卷到 tar 文件
docker run --rm \
-v app-data:/source \
-v $(pwd):/backup \
alpine tar czf /backup/app-data-backup.tar.gz -C /source .
# 从 tar 文件恢复卷
docker run --rm \
-v app-data:/target \
-v $(pwd):/backup \
alpine tar xzf /backup/app-data-backup.tar.gz -C /target
5.3 卷迁移到另一台主机
bash
# 在源主机导出
docker run --rm \
-v app-data:/source \
-v $(pwd):/backup \
alpine tar czf /backup/app-data.tar.gz -C /source .
# 拷贝 tar 到目标主机
scp app-data.tar.gz user@target-host:/backup/
# 在目标主机导入
docker volume create app-data
docker run --rm \
-v app-data:/target \
-v $(pwd):/backup \
alpine tar xzf /backup/app-data.tar.gz -C /target
6. 一键备份 + 恢复脚本模板
6.1 通用备份脚本
bash
#!/bin/bash
# docker-backup.sh - 通用 Docker 卷备份脚本
set -euo pipefail
# 配置区
BACKUP_ROOT="/backup/docker"
DATE=$(date +%Y%m%d_%H%M%S)
VOLUMES=("mysql-data" "pg-data" "app-data") # 要备份的卷列表
mkdir -p "$BACKUP_ROOT"
backup_volume() {
local volume="$1"
local backup_file="$BACKUP_ROOT/${volume}_${DATE}.tar.gz"
echo "备份卷: $volume"
docker run --rm \
-v "${volume}:/source:ro" \
-v "$BACKUP_ROOT:/backup" \
alpine tar czf "/backup/$(basename "$backup_file")" -C /source .
echo "完成: $backup_file"
}
# 备份所有卷
for vol in "${VOLUMES[@]}"; do
backup_volume "$vol"
done
# 清理 7 天前的备份
find "$BACKUP_ROOT" -name "*.tar.gz" -mtime +7 -delete
echo "所有卷备份完成,目录: $BACKUP_ROOT"
6.2 通用恢复脚本
bash
#!/bin/bash
# docker-restore.sh - 通用 Docker 卷恢复脚本
set -euo pipefail
# 配置区
BACKUP_ROOT="/backup/docker"
RESTORE_FILE="${1:-}" # 第一个参数:备份文件路径
if [[ -z "$RESTORE_FILE" ]]; then
echo "用法: $0 <备份文件.tar.gz>"
exit 1
fi
if [[ ! -f "$RESTORE_FILE" ]]; then
echo "错误: 备份文件不存在: $RESTORE_FILE"
exit 1
fi
# 从文件名提取卷名(格式: <volume>_<timestamp>.tar.gz)
VOLUME_NAME=$(basename "$RESTORE_FILE" | sed 's/_[0-9_]*\.tar\.gz$//')
echo "恢复卷: $VOLUME_NAME"
echo "备份文件: $RESTORE_FILE"
# 确认操作
read -p "确认恢复?此操作将覆盖现有数据 (y/N): " confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
echo "已取消"
exit 0
fi
# 确保卷存在
docker volume create "$VOLUME_NAME" > /dev/null 2>&1 || true
# 恢复数据
docker run --rm \
-v "${VOLUME_NAME}:/target" \
-v "$(dirname "$RESTORE_FILE"):/backup" \
alpine tar xzf "/backup/$(basename "$RESTORE_FILE")" -C /target
echo "恢复完成: $VOLUME_NAME"
6.3 使用示例
bash
# 备份
chmod +x docker-backup.sh docker-restore.sh
./docker-backup.sh
# 恢复
./docker-restore.sh /backup/docker/mysql-data_20260927_000000.tar.gz
7. 坑位总结
7.1 权限 1000:1000 问题
现象: 容器内进程无法写入挂载目录,报 Permission denied。
原因: 容器内 UID 与宿主机目录属主 UID 不一致。
解决:
bash
# 查看容器内 UID
docker run --rm node:20 id -u
# 调整宿主机目录属主
sudo chown -R 1000:1000 ./data
# 或运行时指定用户
docker run --user 1000:1000 -v $(pwd)/data:/app/data node:20
7.2 chown 与容器重建
现象: 容器重建后,挂载目录属主被重置,或宿主机目录属主被容器内 entrypoint 脚本意外修改。
原因: 镜像的 entrypoint 在启动时对挂载目录执行 chown,或容器内 UID 与宿主机不一致导致属主错乱。
解决:
bash
# 方案一:优先使用命名卷,避免直接 chown 宿主机目录
docker volume create app-data
docker run -v app-data:/app/data app-image
# 方案二:绑定挂载时,在宿主机预先设置好属主
sudo chown -R 1000:1000 ./data
# 方案三:运行时显式指定用户,保持与宿主机一致
docker run --user 1000:1000 -v $(pwd)/data:/app/data app-image
规避: 容器内不要执行 chown,改用 --user 或命名卷让 Docker 初始化属主。