06. 数据卷 Volume:容器重启数据不丢,多容器还能共享文件
06. 数据卷 Volume:容器重启数据不丢,多容器还能共享文件
上篇文章你学会了用 Docker 跑 Nginx、MySQL,感觉挺爽的。直到有一天,你
docker rm删了一个 MySQL 容器重新创建,打开一看——数据库全空了,几个月的数据灰飞烟灭。那一刻的绝望,经历过的人都懂。本文就来彻底解决这个问题:让容器的数据"活"在容器之外,不管你怎么删、怎么重建,数据都稳稳地在那儿。

▲ 把数据"搬出"容器:不管容器怎么删、怎么重建,数据都像放在保险箱里一样稳。
一、先搞清楚:为什么容器里的数据会丢?
Docker 容器的设计理念是无状态的——容器就像一个"临时工",用完就走。容器里的所有数据,都存在于容器的可写层(还记得上篇讲的分层文件系统吗?)。
这意味着:
| 操作 | 数据会丢吗? |
|---|---|
docker stop 停止容器 | ❌ 不会 |
docker restart 重启容器 | ❌ 不会 |
docker rm 删除容器 | ✅ 数据全丢 |
| 容器崩溃后被 K8s 重建 | ✅ 数据全丢 |
来,我们用实际操作感受一下这种"恐怖":
# 1. 启动一个 MySQL 容器,写入一条数据 docker run -d --name my-mysql \ -e MYSQL_ROOT_PASSWORD=123456 \ mysql:8.0 # 等几秒 MySQL 启动完毕,进入容器写入数据 docker exec -it my-mysql mysql -uroot -p123456 -e " CREATE DATABASE testdb; USE testdb; CREATE TABLE users(id INT, name VARCHAR(50)); INSERT INTO users VALUES(1, '张三'); SELECT * FROM users; "
输出:
+------+--------+ | id | name | +------+--------+ | 1 | 张三 | +------+--------+
数据写好了,现在模拟"手滑删容器":
# 2. 删除容器 docker rm -f my-mysql # 3. 用同样的命令重新创建 docker run -d --name my-mysql \ -e MYSQL_ROOT_PASSWORD=123456 \ mysql:8.0 # 4. 再进去看 docker exec -it my-mysql mysql -uroot -p123456 -e "USE testdb; SELECT * FROM users;"
输出:
ERROR 1049 (42000): Unknown database 'testdb'
数据库没了。 张三也没了。这就是没有数据卷的下场。
金句:容器是临时的,数据不能是临时的。 任何重要数据都必须存在容器之外。
二、三种挂载方式:一张表看清楚
Docker 提供了三种把"外部存储"接入容器的方式,统称 Mount:
| 维度 | Volume(数据卷) | Bind Mount(绑定挂载) | tmpfs(内存挂载) |
|---|---|---|---|
| 管理方式 | Docker 托管 | 用户自己管路径 | Docker 托管 |
| 宿主机位置 | /var/lib/docker/volumes/ 下自动生成 | 你指定的任意目录 | 仅在内存中 |
| 跨平台 | ✅ Linux / Windows / Mac 都支持 | ⚠️ 路径格式因系统而异 | ✅ 支持 |
| 性能 | 优秀 | 优秀 | 极快(内存速度) |
| 持久化 | ✅ 持久 | ✅ 持久 | ❌ 容器停就没 |
| 共享 | ✅ 多容器共享 | ✅ 多容器共享 | ❌ 不支持 |
| 适用场景 | 数据库、应用数据 | 开发环境、配置文件 | 敏感信息临时存储 |
| 推荐度 | ⭐⭐⭐⭐⭐ 首选 | ⭐⭐⭐⭐ 开发必备 | ⭐⭐ 特殊场景 |
结论:生产环境用 Volume,开发环境用 Bind Mount,敏感数据临时用 tmpfs。

▲ 三种方式各有分工:Volume 管持久数据、Bind Mount 管实时同步、tmpfs 管临时秘密。
三、Volume(数据卷):Docker 的"亲儿子"
Volume 是 Docker 官方推荐的数据持久化方案。数据存储在 Docker 自己管理的目录下(Linux 上是 /var/lib/docker/volumes/),你不需要关心具体路径,Docker 帮你打理一切。
3.1 基本操作四件套
# 创建数据卷 docker volume create my-data # 查看所有数据卷 docker volume ls # DRIVER VOLUME NAME # local my-data # 查看详情(能看到挂载点、创建时间等) docker volume inspect my-data # 删除数据卷(数据会永久丢失!) docker volume rm my-data
3.2 挂载到容器
两种写法,效果一样:
# 写法一:--mount(推荐,更清晰) docker run -d --name web \ --mount source=my-data,target=/app/data \ nginx:latest # 写法二:-v(简写,老手爱用) docker run -d --name web \ -v my-data:/app/data \ nginx:latest
注意:
-v后面如果写的是名字(如my-data),就是 Volume;如果写的是路径(如/home/user/data),就是 Bind Mount。Docker 靠这个区分两种方式。
3.3 不创建直接用也行
如果你不提前 docker volume create,直接在 -v 里写个新名字,Docker 会自动创建:
# 自动创建名为 auto-data 的 Volume 并挂载 docker run -d -v auto-data:/data nginx:latest
四、Bind Mount(绑定挂载):开发环境的"实时同步盘"
Bind Mount 把宿主机的一个具体目录或文件直接映射进容器,你在宿主机上改文件,容器里立刻生效——这是开发环境的神器。
4.1 基本用法
# 把宿主机的 /home/user/my-website 目录挂载到容器的 /usr/share/nginx/html docker run -d --name my-web \ -v /home/user/my-website:/usr/share/nginx/html \ -p 8080:80 \ nginx:latest
4.2 开发场景实战:改代码秒生效
# 1. 准备一个项目目录 mkdir -p ~/my-app && cd ~/my-app echo '<h1>Hello from Bind Mount!</h1>' > index.html # 2. 启动容器,挂载项目目录 docker run -d --name dev-web \ -v $(pwd):/usr/share/nginx/html:ro \ -p 8080:80 \ nginx:latest # 3. 浏览器访问 http://localhost:8080,看到 "Hello from Bind Mount!" # 4. 在宿主机修改文件(不用重启容器!) echo '<h1>Updated! No restart needed!</h1>' > index.html # 5. 刷新浏览器,立刻看到新内容
:ro是什么意思? 它表示 read-only,容器里只能读、不能写这个目录。这是个好习惯——防止容器意外修改你的源代码。如果需要读写,不加:ro或加:rw。
4.3 Windows / Mac 上的路径差异
| 系统 | 路径写法示例 |
|---|---|
| Linux | -v /home/user/data:/data |
| Mac | -v /Users/yourname/data:/data |
| Windows (PowerShell) | -v ${PWD}:/data 或 -v C:\Users\you\data:/data |
| Windows (Git Bash) | -v //c/Users/you/data:/data |
Windows 用户注意:Docker Desktop 需要在 Settings → Resources → File Sharing 中授权共享目录,否则挂载会失败。
五、tmpfs:数据只活在内存里
tmpfs 挂载的数据只存在于内存中,容器一停,数据立刻消失。听起来很鸡肋?其实它有独特的用武之地:
-
敏感数据:密钥、令牌、证书——不落盘就不怕泄露
-
临时缓存:编译中间产物、临时文件——用完即弃
# 创建一块 100MB 的内存挂载 docker run -d --name secure-app \ --tmpfs /app/secrets:rw,size=100m \ my-app:latest # 容器内写入"密钥" docker exec -it secure-app sh -c "echo 'my-secret-key-123' > /app/secrets/api_key.txt" # 读取验证 docker exec -it secure-app cat /app/secrets/api_key.txt # 输出:my-secret-key-123 # 容器停止后,内存数据自动清除 docker stop secure-app # 重启后 /app/secrets/ 是空的
六、实战 1:MySQL 数据持久化(最重要的场景)
这是 Volume 最经典的使用场景。我们把 MySQL 的数据目录挂载到一个 Volume 上,这样不管容器怎么删,数据都在。
6.1 创建并挂载
# 创建专用数据卷 docker volume create mysql_data # 启动 MySQL,挂载数据卷 docker run -d --name mysql-safe \ -e MYSQL_ROOT_PASSWORD=123456 \ -v mysql_data:/var/lib/mysql \ -p 3306:3306 \ mysql:8.0
/var/lib/mysql是 MySQL 存储所有数据库文件的默认目录。挂载这个目录 = 保护所有数据。
6.2 写入数据
# 等 MySQL 启动(大约 10~20 秒)
sleep 15
# 创建数据库和表,插入数据
docker exec -it mysql-safe mysql -uroot -p123456 -e "
CREATE DATABASE shop;
USE shop;
CREATE TABLE products(
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
price DECIMAL(10,2)
);
INSERT INTO products(name, price) VALUES('iPhone 16', 7999.00), ('Pixel 9', 5999.00);
SELECT * FROM products;
"
输出:
+----+-----------+---------+ | id | name | price | +----+-----------+---------+ | 1 | iPhone 16 | 7999.00 | | 2 | Pixel 9 | 5999.00 | +----+-----------+---------+
6.3 删容器,数据还在!
# 删除容器(模拟"手滑"或"升级") docker rm -f mysql-safe # 用同样的 Volume 重新创建 docker run -d --name mysql-safe \ -e MYSQL_ROOT_PASSWORD=123456 \ -v mysql_data:/var/lib/mysql \ -p 3306:3306 \ mysql:8.0 # 等待启动 sleep 15 # 验证数据还在 docker exec -it mysql-safe mysql -uroot -p123456 -e "USE shop; SELECT * FROM products;"
输出:
+----+-----------+---------+ | id | name | price | +----+-----------+---------+ | 1 | iPhone 16 | 7999.00 | | 2 | Pixel 9 | 5999.00 | +----+-----------+---------+
数据完好无损! 这就是 Volume 的价值——容器可以随便删、随便重建,数据纹丝不动。
6.4 升级 MySQL 版本也不怕
# 删掉旧版本 docker rm -f mysql-safe # 换成 MySQL 8.4,Volume 不变 docker run -d --name mysql-safe \ -e MYSQL_ROOT_PASSWORD=123456 \ -v mysql_data:/var/lib/mysql \ -p 3306:3306 \ mysql:8.4 # 数据依然在
七、实战 2:Nginx 配置文件和静态文件挂载
这个场景用 Bind Mount——因为你需要在宿主机上频繁修改配置文件和 HTML,而且希望改了立刻生效。
7.1 准备文件
# 创建项目目录
mkdir -p ~/nginx-demo/{conf,html}
# 写一个自定义 Nginx 配置
cat > ~/nginx-demo/conf/default.conf << 'EOF'
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
}
location /api {
return 200 '{"status":"ok","msg":"Hello from Docker Nginx"}';
add_header Content-Type application/json;
}
}
EOF
# 写一个首页
cat > ~/nginx-demo/html/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>我的 Docker 网站</title></head>
<body>
<h1>🐳 欢迎来到 Docker 的世界!</h1>
<p>这个页面通过 Bind Mount 挂载,修改宿主机文件即可实时更新。</p>
</body>
</html>
EOF
7.2 启动 Nginx 并挂载
docker run -d --name my-nginx \ -v ~/nginx-demo/conf/default.conf:/etc/nginx/conf.d/default.conf:ro \ -v ~/nginx-demo/html:/usr/share/nginx/html:ro \ -p 8080:80 \ nginx:latest
这里挂载了两个东西:
-
配置文件(单个文件挂载)
-
静态文件目录(目录挂载)
7.3 修改文件实时生效
# 访问 http://localhost:8080 看到原始页面 # 在宿主机修改 HTML(不需要重启容器!) echo '<h1>🚀 页面已更新!无需重启容器!</h1>' > ~/nginx-demo/html/index.html # 刷新浏览器,立刻看到新内容
这就是 Bind Mount 的魅力:宿主机改文件 → 容器内实时生效 → 零重启零构建。 开发环境下用 Bind Mount + Docker,比在本地装 Nginx 还方便。
八、实战 3:多容器共享同一个 Volume
Volume 可以被多个容器同时挂载,实现数据共享。这在"多个服务读取同一份数据"的场景下非常实用。
8.1 场景:两个 Nginx 容器共享静态资源
# 创建共享 Volume docker volume create shared-static # 用一个"搬运工"容器往 Volume 里写入文件 docker run --rm \ -v shared-static:/data \ alpine sh -c " echo '<h1>共享页面 - 来自 Volume</h1>' > /data/index.html echo '这是两个容器共享的静态资源' >> /data/index.html " # 启动两个 Nginx 容器,都挂载同一个 Volume docker run -d --name nginx-a \ -v shared-static:/usr/share/nginx/html:ro \ -p 8081:80 \ nginx:latest docker run -d --name nginx-b \ -v shared-static:/usr/share/nginx/html:ro \ -p 8082:80 \ nginx:latest
8.2 验证共享效果
# 访问两个容器,内容完全一样 curl http://localhost:8081 # <h1>共享页面 - 来自 Volume</h1> # 这是两个容器共享的静态资源 curl http://localhost:8082 # <h1>共享页面 - 来自 Volume</h1> # 这是两个容器共享的静态资源
两个不同端口、不同容器,返回的内容完全一致——因为它们读的是同一个 Volume。
8.3 更新共享内容
# 再跑一个容器更新 Volume 内容 docker run --rm \ -v shared-static:/data \ alpine sh -c "echo '<h1>内容已更新!两个容器同时生效</h1>' > /data/index.html" # 验证 curl http://localhost:8081 # <h1>内容已更新!两个容器同时生效</h1> curl http://localhost:8082 # <h1>内容已更新!两个容器同时生效</h1>
这就是 Volume 共享的威力:更新一次,所有挂载的容器同时生效。 适合 CDN 回源、静态资源分发、多实例共享配置等场景。
九、数据卷的清理与维护
数据卷会一直存在,即使容器被删除了。时间长了,会有很多"孤儿卷"占用磁盘空间。
9.1 查看数据卷占用空间
docker system df -v
输出类似:
VOLUME NAME LINKS SIZE mysql_data 1 256.3MB shared-static 2 4.1kB orphan_vol_abc 0 1.2GB ← 没有容器使用,孤儿卷 orphan_vol_xyz 0 800MB ← 又一个孤儿卷
LINKS 为 0 的就是没有容器使用的孤儿卷。
9.2 一键清理无用卷
# 预览会删除哪些(不会真删) docker volume prune # 确认清理 docker volume prune -f
警告:
docker volume prune会删除所有未被任何容器引用的 Volume,数据不可恢复。执行前务必确认哪些卷是真正不需要的。
9.3 安全删除指定卷
# 先确认没有容器在用 docker volume inspect mysql_data # 查看挂载信息 # 确认不需要了再删 docker volume rm mysql_data
十、踩坑指南:权限问题
这是新手最容易踩的坑——容器里的进程和宿主机的用户 UID 不一致,导致权限不够、文件读写失败。
10.1 问题现象
docker run -d --name my-app \ -v /home/user/data:/app/data \ my-app:latest # 容器日志报错: # Permission denied: /app/data/upload/file.txt
容器内的进程以某个用户身份运行(比如 UID 1000),但挂载目录在宿主机上属于另一个用户(比如 UID 1001),于是权限不够。
10.2 解决办法
方法一:调整宿主机目录权限
# 把宿主机目录的所有者改成容器内用户的 UID sudo chown -R 1000:1000 /home/user/data # 或者给目录加写权限(简单粗暴,不推荐生产用) chmod 777 /home/user/data
方法二:在 Dockerfile 中指定用户
FROM node:18-alpine # 创建用户并切换 RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -D appuser USER appuser # ... 后续操作都以 appuser 身份执行
方法三:用 Volume 代替 Bind Mount
# Volume 由 Docker 管理,会自动处理权限问题 docker run -d -v my-data:/app/data my-app:latest
经验之谈:如果你不确定容器内进程用什么 UID,先用
docker exec -it 容器名 whoami和id看一下,然后对应调整宿主机权限。
十一、Docker Compose 中的 Volume 写法
实际项目中,Volume 通常在 docker-compose.yml 里定义。这里提前剧透一下写法(后面 Compose 专题会细讲):
version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: 123456 volumes: - mysql_data:/var/lib/mysql # Volume 挂载 - ./conf/my.cnf:/etc/mysql/my.cnf # Bind Mount 挂载配置文件 ports: - "3306:3306" nginx: image: nginx:latest volumes: - ./html:/usr/share/nginx/html:ro # Bind Mount 静态文件 - shared_files:/data # 与 mysql 共享的 Volume ports: - "8080:80" volumes: mysql_data: # 声明 Volume(Docker 自动创建) driver: local shared_files: # 共享 Volume driver: local
底部
volumes:声明的卷名,会被 Docker Compose 自动创建。上面的服务直接引用名字就行。
十二、本文要点回顾
| 要点 | 核心内容 |
|---|---|
| 为什么需要 Volume | 容器删除后数据全丢,重要数据必须持久化到容器外 |
| Volume(首选) | Docker 托管,-v 卷名:容器路径,适合数据库等生产数据 |
| Bind Mount | 挂载宿主机目录,改文件实时生效,适合开发环境和配置文件 |
| tmpfs | 数据只在内存中,容器停即消失,适合敏感临时数据 |
| 多容器共享 | 同一个 Volume 可被多个容器挂载,更新一次全部生效 |
| 清理无用卷 | docker volume prune 一键清理孤儿卷 |
| 权限坑 | 容器内 UID 和宿主机不一致会导致读写失败,注意对齐 |
一句话总结:把容器想象成一台"随时可能被回收的电脑",Volume 就是你的"移动硬盘"——电脑没了,硬盘里的东西还在,换台电脑插上继续用。
下集预告
Volume 搞定了数据持久化,但到目前为止,你每次启动容器都要敲一长串 docker run 命令,参数多到记不住。下一篇我们来学 Docker 网络模式——搞懂容器之间怎么通信、怎么让外部访问、四种网络模式各有什么特点。把网络打通了,后面 Docker Compose 编排多容器才能玩得转。
更多推荐

所有评论(0)