Docker训练营(二)-Docker存储管理
Docker 容器在运行时会产生大量数据,这些数据如何持久化和管理是一个重要的话题。
本节我们将通过一个 Nginx Web 服务器的案例,来深入探讨 Docker 的三种数据管理方式。
Docker 存储基础
Docker 提供了三种主要的数据管理方式:
- 默认存储:容器内的数据随容器删除而丢失
- Volumes(卷):由 Docker 管理的持久化存储空间,完全独立于容器的生命周期
- Bind Mounts(绑定挂载):将主机上的目录或文件直接挂载到容器中
让我们通过一个 Nginx Web 服务器的例子来理解这三种方式的区别。我们将在每种方式下执行相同的操作:创建一个 HTML 文件,然后测试数据的持久性。
场景一:默认存储(非持久化)
在这个场景中,我们直接在容器内创建文件,看看数据会发生什么:
1 | 运行一个 nginx 容器 |
核心思想:容器默认是“无状态”且“易消亡”的 (Stateless and Ephemeral)。 我的操作完美地证明了这一点。对容器内部文件系统的任何写入,都会随着容器的删除而消失。这在设计上保证了容器的可移植性和一致性——无论在哪里启动,它都基于同一个镜像,表现完全一致。
如何让数据持久化? 这就引出了 Docker 一个极其重要的概念:**卷 (Volume)**。如果你希望数据(比如网站文件、数据库数据、日志)在容器删除后依然存在,我就需要把这些数据存放在容器“外部”,也就是宿主机上,然后把宿主机的目录“挂载”到容器内部。这个“外部存储”就是卷。
场景二:使用 Volume
在这个场景中,我们使用 Docker 管理的卷来存储数据:
1 | 创建一个 Docker volume |
第 1 步: docker volume create nginx_data
docker volume create
: 这是一个专门用来创建卷的命令。nginx_data
: 你为这个卷起的名字。作用与原理
: 这条命令告诉 Docker:“请帮我创建一个由你来管理的、专门用来存放数据的区域,我以后就用
1
nginx_data
这个名字来引用它。”
- 关键点:你不需要关心这个数据区域到底存放在你电脑的哪个具体位置,Docker 会在它自己的一个专属目录(在 Linux 上通常是
/var/lib/docker/volumes/
)里进行统一管理。这实现了数据与宿主机文件系统的解耦。
- 关键点:你不需要关心这个数据区域到底存放在你电脑的哪个具体位置,Docker 会在它自己的一个专属目录(在 Linux 上通常是
第 2 步: docker run ... -v nginx_data:/usr/share/nginx/html nginx
- 这条命令的大部分我们已经熟悉了,核心在于
-v
参数的新用法。 -v nginx_data:/usr/share/nginx/html
: 这就是**卷挂载 (Volume Mount)**。- 格式:
-v <卷的名称>:<容器内的路径>
- 格式:
第 3、4、5 步: exec
, curl
, rm
- 这几步的逻辑和上次完全一样。
docker exec
修改了index.html
,由于/usr/share/nginx/html
目录现在已经链接到了nginx_data
卷,所以这个修改实际上是写进了nginx_data
卷里。curl
验证了写入成功。docker rm -f web-volume
销毁了web-volume
这个容器。但是,卷nginx_data
是一个独立于容器的资源,它并不会被删除,它静静地待在 Docker 的管理目录里,保存着我们写入的数据。
第 6、7 步: 再次 run
和 curl
- 你启动了一个全新的容器
web-volume-2
,并且再次使用-v nginx_data:/usr/share/nginx/html
将同一个卷挂载了进去。 - 当这个新容器启动后,它内部的
/usr/share/nginx/html
目录指向的正是那个包含了"<h1>Hello from Volume Storage</h1>"
的nginx_data
卷。 - 因此,
curl
的结果证明了数据被完美地持久化和复用了。
第 8 步: docker volume inspect nginx_data
- 这条命令是用来“解密”卷的,它让你能看到一个卷的详细信息。
docker volume inspect
: 查看一个或多个卷的元数据。- 输出解读:
"Name": "nginx_data"
: 卷的名字。"Driver": "local"
: 驱动类型。local
表示这个卷的数据存储在本机。对于更复杂的场景,还可以有其他驱动,比如将数据存到云存储上。"Mountpoint": "/var/lib/docker/volumes/nginx_data/_data"
: 这是最重要的信息。它揭示了nginx_data
这个卷在你的宿主机上实际存储数据的物理位置。虽然我们不应该手动去操作这个目录,但它清楚地告诉你,数据确实是存在宿主机上的,只是由 Docker 在这个特定路径下代为保管。
场景三:使用 Bind Mount
在这个场景中,我们将主机上的目录直接挂载到容器中:
1 | 创建本地目录 |
第 1 & 2 步: mkdir nginx-content
和 echo ... > nginx-content/index.html
- 这两条是标准的 Linux/macOS shell 命令。
mkdir nginx-content
: 在你当前所在的目录(/workspace
)下,创建一个名为nginx-content
的新目录。echo "..." > ...
: 将一段 HTML 文本写入到nginx-content
目录下的index.html
文件中。- 核心思想: 这个工作流的特点是先在宿主机上准备好数据。数据源是明确、可见、且由你完全控制的。
第 3 步: docker run ... -v $(pwd)/nginx-content:/usr/share/nginx/html nginx
- 这条命令是本次操作的核心。
-v $(pwd)/nginx-content:/usr/share/nginx/html
: 这就是我们反复强调的绑定挂载 (Bind Mount)。$(pwd)
: 这是一个 shell 变量,意思是“print working directory”(打印当前工作目录)。在这里,它会被替换成你的当前路径,例如/workspace
。所以完整的宿主机路径就是/workspace/nginx-content
。Docker 要求绑定挂载时必须使用绝对路径,使用$(pwd)
是一个确保路径正确的常用技巧。:
: 分隔符,左边是宿主机路径,右边是容器内路径。- 原理: 这条命令建立了一个直接的、实时的链接。它告诉 Docker:“把容器内的
/usr/share/nginx/html
目录下的所有文件操作,都直接映射到我宿主机上的/workspace/nginx-content
目录。” 这就像给容器里的目录创建了一个指向宿主机目录的“快捷方式”或“传送门”。容器内对这个目录的任何读写,实际上都是在读写你宿主机上的文件。
第 4 步:curl http://localhost:8082
curl http://localhost:8082
: 由于绑定挂载,Nginx 读取/usr/share/nginx/html/index.html
时,实际上读取的是你宿主机上的nginx-content/index.html
文件,所以你看到了 “Hello from Bind Mount Storage”。
第 5 & 6 步: echo "<h1>updated</h1>" > nginx-content/index.html
和 curl
- 关键操作: 注意,这次的
echo
命令是直接在你的宿主机终端里执行的。你没有使用docker exec
进入容器内部。 - 你修改了宿主机上的
nginx-content/index.html
文件。 - 当你再次
curl
时,请求到达容器内的 Nginx。Nginx 再次去读取/usr/share/nginx/html/index.html
,通过那个“实时传送门”,它读取到的是你刚刚在宿主机上修改的最新内容 “updated”。 - 这完美地展示了绑定挂载的实时性。宿主机和容器共享着同一个文件,任何一方的修改对另一方都立即可见。
第 7, 8, 9 步: rm
, run
, curl
docker rm -f web-bind
: 你删除了容器。但数据的源头——你宿主机上的nginx-content
目录和其中的文件——毫发无损。- 当你用同样的命令启动一个新容器
web-bind-2
时,Docker 重新建立了那个从容器到宿主机的链接。 - 新容器的 Nginx 服务自然就读取到了宿主机上那个依然是 “updated” 内容的
index.html
文件。数据持久化得以实现,因为数据的生命周期完全跟宿主机上的文件绑定,与容器无关。
三种方式的对比
默认存储
- 数据随容器删除而丢失
- 适合存储临时数据
- 容器间数据隔离
- 无需额外配置
Volume
- 数据持久化,独立于容器生命周期
- Docker 统一管理,方便备份和迁移
- 可以在多个容器间共享
- 数据存储在 Docker 管理区域,安全性好
Bind Mount
- 数据持久化,存储在主机指定位置
- 可以直接在主机上修改文件
- 开发环境中方便调试和修改
- 依赖主机文件系统结构
常见错误:挂载空目录导致容器内原有文件“消失”
这是一个非常常见的绑定挂载“陷阱”。假设 nginx
镜像的 /usr/share/nginx/html
目录里原本有 5 个默认文件。
如果你在宿主机上创建一个空目录 my-empty-dir
,然后运行: docker run -v $(pwd)/my-empty-dir:/usr/share/nginx/html nginx
这时,宿主机的空目录 my-empty-dir
会“覆盖”掉容器内的 /usr/share/nginx/html
目录。你进入容器查看,会发现 /usr/share/nginx/html
变成了一个空目录,镜像里原有的 5 个文件都看不到了。
而命名卷的行为不同:当你第一次将一个新的空卷挂载到包含内容的容器目录时,Docker 会聪明地将容器目录里的内容复制到这个空卷中。这样就避免了数据丢失。这也是在处理应用数据时,卷通常更安全、更方便的原因之一。
清理操作
完成实验后,可以进行清理:
1 | 清理容器 |
实践案例:使用 Volume 部署 MySQL 数据库
我们将通过一个 MySQL 数据库的例子来演示如何使用 Volume 持久化数据。
创建并管理 Volume
1 | 创建一个命名卷 |
docker volume create/inspect/ls
- 这三条命令是你管理卷的“三板斧”,我们在上一个练习中已经很熟悉了。
create mysql_data
: 申请一块由 Docker 管理的、名为mysql_data
的专用存储空间。inspect mysql_data
: 查看这块空间的详细信息,尤其是它的物理存储位置Mountpoint
。ls
: 列出当前 Docker 环境中存在的所有卷。- 目的: 这是运行任何有状态服务的第一步,即“先准备好放数据的保险箱”。
使用 Volume 运行 MySQL
1 | 运行 MySQL 容器并挂载卷 |
docker run ...
(首次运行)
这条命令是整个流程的重中之重,包含了两个新的关键知识点。
-e MYSQL_ROOT_PASSWORD=mysecret
:-e
是--env
的缩写,意思是设置**环境变量 (Environment Variable)**。- 什么是环境变量? 它们是注入到容器内部的键值对,可以用来动态配置容器里运行的程序,而无需修改程序代码或镜像本身。
- 为什么这么设计? 官方的
mysql
镜像被设计成在第一次启动时,会检查一个名为MYSQL_ROOT_PASSWORD
的环境变量。如果发现了这个变量,它就会在初始化数据库时,将root
用户的密码设置为你提供的值(这里是mysecret
)。这是一种非常通用和优雅的容器配置方式。
-v mysql_data:/var/lib/mysql
:- 这就是我们之前学的卷挂载。
/var/lib/mysql
: 这是 MySQL 数据库在 Linux 系统中默认的数据存储目录。所有的数据库、表、索引、日志等核心文件都存放在这里。- 作用: 这条命令的含义是:“将
mysql_data
这个卷挂载到容器的/var/lib/mysql
目录下”。这样一来,MySQL 服务写入的所有数据,实际上都直接被保存到了容器外部的mysql_data
卷里,从而实现了数据和容器运行环境的分离。
mysql:8.0
:- 使用
mysql
镜像,并明确指定了8.0
这个版本标签。在生产环境中,强烈建议指定明确的版本号,而不是依赖于默认的latest
标签。这可以确保你在任何地方重新部署时,使用的都是完全相同的软件版本,避免了因版本更新导致意外行为。
- 使用
docker exec -it mysql_db mysql ...
(进入数据库)
docker exec -it mysql_db
: 以交互模式进入mysql_db
容器。mysql -uroot -pmysecret
: 这是在容器内部执行的命令。它启动了 MySQL 的命令行客户端。
-u root
: 指定登录用户为root
。-pmysecret
: 提供root
用户的密码。注意-p
和密码之间没有空格。- 你看到
mysql: [Warning] Using a password on the command line...
的警告,是因为在命令行直接写密码有安全风险(可能会被系统日志或历史记录捕获),这在测试时很方便,但在生产环境中需要更安全的方式。
SQL 操作: 你执行的
create database
,create table
,insert
等所有操作,都在修改数据库文件。因为我们做了卷挂载,这些修改都真实地发生在mysql_data
卷里。
验证数据持久化
1 | 删除原容器 |
第 4 & 5 步: rm
和再次 run
docker rm -f mysql_db
: 容器被彻底销毁了。但存放着test_db
数据库的mysql_data
卷安然无恙。- 第二次
docker run
:- 你用完全相同的命令启动了一个新容器。
- 当这个新的 MySQL 实例启动时,它会检查它的数据目录
/var/lib/mysql
。 - 因为我们挂载了已经有数据的
mysql_data
卷,新实例会发现里面已经存在一个初始化好的数据库。它会跳过初始化步骤(不再需要MYSQL_ROOT_PASSWORD
来设置密码,而是用它来验证身份),直接加载现有数据并对外提供服务。
docker exec ... -e "..."
(验证数据)
- 这是一个非常高效的非交互式执行命令的方式。
docker exec mysql_db ...
: 注意这里没有-it
,因为我们不需要交互,只想执行一条命令并获取结果。mysql ... -e "use test_db; select * from users;"
:mysql
客户端的-e
参数(不要和docker run
的-e
搞混)表示“执行引号内的命令,然后立即退出”。- 你成功查询出了
id: 1, name: John
的记录。 - 这雄辩地证明了:数据与容器的生命周期是分离的,只要卷还在,数据就安全。
实践案例:使用 Bind Mounts 运行 DeepSeek-R1
在这个案例中,我们将演示如何使用 Bind Mounts 来运行 DeepSeek-R1 大语言模型。
这个案例很好地展示了 Bind Mounts 在处理大型文件时的优势:模型文件通常很大(几十GB),
如果打包到镜像中会导致镜像体积过大,而使用 Bind Mounts 可以直接挂载主机上的模型文件。
运行 ollama 容器
1 | 查看本地的模型文件是否存在 |
运行 deepseek r1 模型
1 | 查看模型是否被识别 |