Docker 数据卷
容器有个铁律:容器删除,数据丢失。这对临时服务无所谓,但数据库、用户上传文件、日志这种需要持久化的数据就是灾难。Docker 提供 Volume(数据卷)机制,把数据放到容器之外,让数据拥有"独立于容器生命周期"的存在。本章彻底讲清。
为什么需要 Volume
先看一个反例,直观感受"数据丢失":
# 容器有个铁律:容器删除,数据丢失
# 跑一个 postgres 容器,建几张表
docker run -d --name db -e POSTGRES_PASSWORD=secret postgres:16
docker exec -it db psql -U postgres -c "CREATE TABLE users (id int);"
# 删除容器(数据没了!)
docker stop db && docker rm db
# 重新跑一个,表全没了
docker run -d --name db -e POSTGRES_PASSWORD=secret postgres:16
docker exec -it db psql -U postgres -c "\dt"
# 找不到任何表,因为数据跟着旧容器一起被删了
# 解决:用 Volume 把数据放到容器外
docker run -d --name db -e POSTGRES_PASSWORD=secret -v dbdata:/var/lib/postgresql/data postgres:16
# 现在删容器、重建容器,数据都还在容器本质是"镜像 + 一层可写层"。可写层跟着容器走,容器删了它就没了。Volume 的作用就是把需要长期保留的数据从可写层剥离,放到主机上由 Docker 管理的目录里。容器删除、重建,只要挂同一个卷,数据就还在。
三种挂载方式
Docker 提供三种把主机存储接进容器的机制,各有用途:
# 三种挂载方式
# 1. 命名卷(named volume):Docker 管理,跨容器共享,生产首选
docker volume create mydata
docker run -v mydata:/app/data myapp
# 2. 匿名卷(anonymous volume):没名字,Docker 自动起一长串哈希
docker run -v /app/data myapp # 主机路径自动分配
# 删除容器时如果不带 -v,匿名卷会变成"悬空卷",占空间
# 3. 绑定挂载(bind mount):映射主机目录,开发常用
docker run -v /home/me/project:/app myapp # 老语法
docker run -v $(pwd)/src:/app/src myapp # 当前目录
docker run --mount type=bind,source=$(pwd)/src,target=/app/src myapp # 新语法,更明确
# 4. tmpfs:只存内存,容器停止即消失(适合临时密钥、缓存)
docker run --tmpfs /app/cache myapp
docker run --mount type=tmpfs,target=/app/cache myapp- 命名卷:Docker 全权管理,路径在
/var/lib/docker/volumes/下。跨容器共享、跨主机迁移都方便,生产首选。 - 匿名卷:没起名字的卷,Docker 自动生成哈希名。容易"悬空"(容器删了卷还在,占空间),不推荐手动用。
- 绑定挂载:直接映射主机任意目录,能改代码立即生效,本地开发首选。缺点是依赖主机路径,换台机器可能不存在。
- tmpfs:只存内存,容器停止即消失。适合临时密钥、敏感数据(不想落盘)。
选型口诀:生产用命名卷,开发用绑定挂载,敏感数据用 tmpfs。
docker volume 全套命令
# docker volume 全套管理命令
# 创建卷
docker volume create mydata # 默认 local 驱动
docker volume create --driver local --opt type=nfs mydata # NFS 等高级驱动
# 列出所有卷
docker volume ls
# DRIVER VOLUME NAME
# local mydata
# local dbdata
# 看卷的详细信息(在主机上的真实路径、挂载点、驱动)
docker volume inspect mydata
# Mountpoint: /var/lib/docker/volumes/mydata/_data
# 删除卷(没有容器在用才能删)
docker volume rm mydata
# 删除所有未被使用的卷(释放磁盘)
docker volume prune
docker volume prune -a # 删除所有未挂载的卷
# 删除容器时连带删除其匿名卷
docker rm -v container_name几个常用场景:docker volume prune 清理无用卷(磁盘满了时常用);docker volume inspect 能看到卷在主机上的真实路径(Linux 是 /var/lib/docker/volumes/xxx/_data);docker rm -v 删容器时同时删匿名卷,避免悬空卷累积。
绑定挂载:本地开发的神器
绑定挂载最大的价值是实时同步——主机改代码,容器里立刻看到,实现热重载:
# 绑定挂载:本地开发的杀手锏
# 把当前目录的源码映射进容器,改代码立即生效
docker run -d --name dev \
-v $(pwd)/src:/app/src \
-v $(pwd)/package.json:/app/package.json:ro \
-p 3000:3000 \
node:20-alpine
# :ro 只读挂载(容器内不能改,但主机能改,适合配置文件)
# :rw 读写(默认)
# 用 --mount 新语法更清晰(推荐)
docker run -d --name dev \
--mount type=bind,source=$(pwd)/src,target=/app/src \
--mount type=bind,source=$(pwd)/.env,target=/app/.env,readonly \
node:20-alpine
# 经典用法:本地代码 + 容器内运行时,实现"热重载"
# 改源码 → nodemon / webpack-dev-server 监听到变化 → 自动重启
# 整个开发流程不用在主机装 Node,只装 Docker 就够了这种"主机代码 + 容器运行时"的开发模式好处巨大:不用在主机装 Node/Python/Ruby 一堆版本管理工具,每个项目用自己镜像里的版本,互不污染。新人入职 clone 项目 + docker compose up,几分钟就有完整开发环境。:ro(readonly)适合配置文件,防止容器内意外修改主机文件。
备份与迁移
命名卷虽然由 Docker 管理,但本质还是主机上的文件,可以打包搬运:
# 数据卷的备份与迁移
# 思路:用临时容器挂载卷,把数据 tar 出来
# 备份:把 mydata 卷的内容打包成 tar
docker run --rm -v mydata:/data -v $(pwd):/backup alpine \
tar czf /backup/mydata-backup.tar.gz -C /data .
# 恢复:把 tar 解到新卷
docker volume create mydata-restored
docker run --rm -v mydata-restored:/data -v $(pwd):/backup alpine \
tar xzf /backup/mydata-backup.tar.gz -C /data
# 迁移到另一台机器
# 1. 备份成 tar
# 2. scp / rsync 拷到新机器
# 3. 在新机器上恢复
# 也可以用 save/load 搬运整个镜像,但卷里的数据不包含在镜像里
# 镜像 = 应用代码,卷 = 运行时数据,两者要分开管理记住一个关键认知:镜像(docker save/load)包含应用代码,不包含卷里的运行时数据。数据库的表、上传的文件都在卷里,迁移时要单独备份卷。生产环境通常用云厂商的托管数据库或 NFS 卷,避免单机故障丢数据。
实战:给 Postgres 配持久化
把上面的知识串起来,做一个"删了容器数据还在"的真实例子:
# 实战:给一个 Postgres 配持久化
# 1. 创建命名卷
docker volume create pgdata
# 2. 跑 postgres,把数据目录挂到卷
docker run -d --name db \
-e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=app \
-v pgdata:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16-alpine
# 3. 建表、插数据
docker exec -it db psql -U app -c "CREATE TABLE users (id serial, name text);"
docker exec -it db psql -U app -c "INSERT INTO users (name) VALUES ('alice');"
# 4. 模拟"灾难":删除容器
docker stop db && docker rm db
# 5. 重新跑(用同一个卷),数据还在!
docker run -d --name db \
-e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=app \
-v pgdata:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16-alpine
docker exec -it db psql -U app -c "SELECT * FROM users;"
# 仍然能看到 alice 这条记录
# 6. 看卷的真实位置
docker volume inspect pgdata走完这一遍,你就理解了 Volume 的全部价值。生产里 99% 的有状态服务(数据库、消息队列、文件存储)都要这么配。无状态服务(Web 应用、API)则故意不挂卷,这样能随时水平扩容、随时重建。
常见坑
- 权限问题:绑定挂载时,容器内进程 UID 和主机 UID 不一致会导致"Permission denied"。Linux 上常需
--user $(id -u):$(id -g)对齐。 - 挂载点覆盖:
-v mydata:/app会覆盖镜像里/app的内容。如果镜像里这个目录有文件,挂载后会"消失"(被卷的内容盖住)。要么挂子目录,要么用 Dockerfile 里VOLUME声明让首次启动时自动复制。 - Mac/Windows 性能:绑定挂载在 macOS/Windows 上(因为跨虚拟机文件系统)比 Linux 慢很多,文件多时会明显卡。Docker Desktop 4 起的 VirtioFS / gRPC FUSE 缓解不少。
- 卷里别装代码:代码属于镜像,卷应该只放运行时产生的数据。把源码放卷里会让镜像构建失去意义。
← 上一篇 Dockerfile 详解
下一篇 Docker 网络 →