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 编排多容器才能玩得转。

更多推荐