Docker 数据卷实战:volume、bind mount、tmpfs 到底怎么选,数据持久化与权限坑
容器一删,数据库里的数据全没了------这是几乎每个人上手 Docker 都会栽的第一个跟头。原因很简单:容器的可写层是跟着容器生命周期走的,docker rm 之后就灰飞烟灭。想让数据活过容器,你得把它放到「卷」里。
但 Docker 的挂载方式有三种:volume、bind mount、tmpfs,新手常常混着用,结果要么数据没持久化,要么本地文件被容器改乱,要么权限报错启动不了。这篇把三种方式的区别、适用场景和真实的坑一次讲透。
先看最常见的错误:数据说没就没
bash
# 起一个 MySQL,不挂任何卷
docker run -d --name mydb -e MYSQL_ROOT_PASSWORD=123456 mysql:8
# 建库、写数据......然后手滑
docker rm -f mydb
# 重新起一个,进去一看:库没了
docker run -d --name mydb -e MYSQL_ROOT_PASSWORD=123456 mysql:8
数据写进了容器的可写层,容器删了数据就删了。任何需要「重启/重建容器后还在」的数据,都必须落到卷上。
三种挂载方式各是什么
用一张表先建立整体认知:
| 类型 | 数据存哪 | 谁管理 | 典型用途 |
|---|---|---|---|
| volume | Docker 管理的目录(/var/lib/docker/volumes/) |
Docker | 数据库、生产持久化数据 |
| bind mount | 你指定的宿主机任意路径 | 你自己 | 开发时挂源码、挂配置文件 |
| tmpfs | 宿主机内存,不落盘 | Docker | 临时敏感数据(密钥、session) |
关键区别一句话:volume 让 Docker 替你管目录,bind mount 是你自己指定宿主机路径,tmpfs 根本不落盘。
volume:生产持久化的默认选择
创建并挂载一个命名卷:
bash
# 显式创建(可选,run 时不存在会自动建)
docker volume create pgdata
# 挂到容器里。-v 卷名:容器内路径
docker run -d --name pg \
-e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data \
postgres:16
# 现在删容器再重建,数据还在
docker rm -f pg
docker run -d --name pg -e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data postgres:16
更推荐用语义更清晰的 --mount 写法(生产环境建议统一用它,参数拼错会直接报错而不是默默给你新建目录):
bash
docker run -d --name pg \
-e POSTGRES_PASSWORD=secret \
--mount type=volume,source=pgdata,target=/var/lib/postgresql/data \
postgres:16
管理命令:
bash
docker volume ls # 列出所有卷
docker volume inspect pgdata # 看卷的真实路径、驱动
docker volume rm pgdata # 删卷(卷被容器占用时会拒绝)
docker volume prune # 清理没有被任何容器引用的卷
docker volume prune 很好用,但它会删掉所有「悬空」卷------跑之前一定确认没有停着的容器还指望这些数据,否则一条命令抹掉你的测试库。
bind mount:开发时挂源码,改代码不用重建镜像
bind mount 把宿主机的某个路径直接映射进容器,最典型的用途是本地开发热更新:
bash
# 把当前目录的源码挂进容器,改代码容器里立刻生效
docker run -d --name web \
-v "$(pwd)":/app \
-w /app \
node:20 npm run dev
注意几个实战要点:
- 路径必须写绝对路径 。
-v ./src:/app里的./src在某些场景不被解析,用$(pwd)/src更稳。 - bind mount 会「盖住」容器内原有内容 。如果容器镜像里
/app本来有node_modules,你把宿主机/app(没装依赖)挂上去,容器里的node_modules就被空目录盖掉了。解法是给 node_modules 单独再挂一个匿名 volume 保护起来:
bash
docker run -d --name web \
-v "$(pwd)":/app \
-v /app/node_modules \
-w /app node:20 npm run dev
后一个 -v /app/node_modules(只写容器内路径)是匿名卷,优先级高于前面的 bind mount,把宿主机的空目录挡在外面,容器里镜像自带的依赖得以保留。
只读挂载:配置文件不想被容器改
挂配置文件时,加 :ro 让容器无法写回,防止程序或误操作改坏你的宿主机文件:
bash
docker run -d --name nginx \
-v "$(pwd)/nginx.conf":/etc/nginx/nginx.conf:ro \
-p 80:80 nginx
:ro = read-only。容器内对该路径的写操作会直接失败,这在挂密钥、挂共享配置时是一道很值的保险。
tmpfs:敏感临时数据不落盘
有些数据你既不想留在容器里,也不想写到宿主机磁盘上(比如临时解密出来的密钥、大量临时文件)。tmpfs 把它放内存,容器一停就蒸发:
bash
docker run -d --name app \
--mount type=tmpfs,destination=/app/cache,tmpfs-size=64m \
myapp:latest
内存有限,记得用 tmpfs-size 设上限,别让临时文件把内存吃爆。
最容易卡住的坑:权限不匹配
这是 bind mount 的高频翻车点。容器里的进程通常以某个特定 UID 运行(比如官方 postgres 镜像用 UID 999、很多镜像用非 root 的 UID 1000),而你宿主机目录的属主是你自己(比如 UID 501)。挂进去之后,容器内进程对这个目录没有写权限,启动就报 Permission denied。
先定位问题------看容器里进程是谁、目录属主是谁:
bash
# 容器内进程的 UID
docker exec app id
# 宿主机目录属主
ls -ln ./data
三种常见解法,按推荐程度排:
bash
# 方法一:把宿主机目录的属主改成容器进程的 UID(最干净)
sudo chown -R 999:999 ./pgdata
# 方法二:启动时用 --user 让容器进程以你的 UID 跑
docker run --user "$(id -u):$(id -g)" -v "$(pwd)/data":/data myapp
# 方法三(仅限本地开发图省事,别上生产):放开目录权限
chmod -R 777 ./data
生产环境优先方法一或方法二。方法三的 777 等于对所有用户放开读写执行,是安全红线,只在本机临时调试用。
补充一个容易忽略的点:volume 不会有这个权限烦恼。因为命名卷第一次挂载到一个空卷时,Docker 会把容器镜像里目标目录的属主和权限「复制」到卷上,属主自动对齐。这也是「数据库用 volume 而不是 bind mount」的一个隐藏理由。
备份与迁移:卷里的数据怎么导出
命名卷不像 bind mount 那样能直接 cp,但可以起一个临时容器把它打包出来:
bash
# 把 pgdata 卷的内容打包成宿主机当前目录的 backup.tar.gz
docker run --rm \
-v pgdata:/data \
-v "$(pwd)":/backup \
alpine tar czf /backup/backup.tar.gz -C /data .
# 恢复到另一个卷
docker run --rm \
-v pgdata_new:/data \
-v "$(pwd)":/backup \
alpine tar xzf /backup/backup.tar.gz -C /data
思路:用一个轻量的 alpine 容器同时挂上「源卷」和「宿主机备份目录」,在容器里做 tar 打包,--rm 让临时容器用完即删。
小结
- 持久化数据(数据库等)用命名 volume:Docker 管理、权限自动对齐、迁移备份有标准套路。
- 本地开发挂源码/配置用 bind mount:改文件立即生效;注意绝对路径、注意会盖住容器内原有目录、注意用匿名卷保护 node_modules。
- 敏感临时数据用 tmpfs:只在内存、不落盘、记得限大小。
- 挂配置文件加
:ro:防止容器写坏宿主机文件。 - 权限报错先
docker exec app id对比目录属主 :优先 chown 对齐或--user,777 只在本机调试用。
一句话记忆点:容器是牛,数据是奶------牛可以随时宰(重建),但奶(数据)得先挤到卷里存好。