第4章:Docker 数据管理 - 持久化与共享
在前面的章节里,我们的 my-app 容器运行得非常完美。但它有一个致命的缺陷:它是一个“健忘”的家伙。
容器被设计为无状态 (Stateless) 的。当一个容器被删除时,它在运行期间产生的所有数据(比如日志文件、用户上传的图片、数据库记录等),都会随着那个可写的容器层一起被销毁,灰飞烟灭。
这对于像 Web 服务器这样不保存状态的应用来说可能没问题,但对于数据库、或者任何需要长期保存数据的应用来说,这绝对是场灾难。
本章,我们将学习如何为容器装上“记忆硬盘”,让数据得以持久化 (Persistence)。
4.1 为什么容器需要数据持久化?
设想一下,你运行了一个 mysql 数据库容器,辛辛苦苦在里面存储了大量的业务数据。某天,因为需要升级镜像版本,你不得不删除旧的容器,然后用新镜像启动一个新容器。当你做完这一切,连接上新的数据库容器时,你会惊恐地发现——里面空空如也,所有数据都消失了!
这就是因为数据被存储在了那个被删除的容器的可写层里。
为了解决这个问题,Docker 提供了两种主要的方式来将数据存储在容器之外,直接管理在主机上:
- 数据卷 (Volumes)
- 绑定挂载 (Bind Mounts)
这两种方式都允许我们将主机上的一个目录“挂载”到容器内部的一个路径上。这样,容器向这个路径读写数据时,实际上操作的是主机上的目录,数据也就自然而然地持久化了。即使容器被删除,主机上的目录和数据依然完好无损。
4.2 Volume(数据卷):官方推荐的正确姿势
数据卷 (Volume) 是存储容器数据的首选和推荐方式。
- 是什么:你可以把数据卷想象成一个由 Docker 亲自管理的、特殊的主机目录。你不需要关心这个目录具体在哪,Docker 会为你处理好一切。
- 优点:
- 解耦:数据卷将数据的生命周期与容器的生命周期彻底分开。
- 易于管理:Docker 提供了专门的命令(如
docker volume create,ls,rm)来管理数据卷。 - 高性能:在大多数情况下,数据卷能提供接近本机的 I/O 性能。
- 安全:由 Docker 管理,避免了容器内进程直接操作主机文件系统的风险。
如何使用数据卷
使用数据卷非常简单,在 docker run 命令中添加 -v 或 --volume 参数即可。
--volume 的格式是 <volume_name>:<container_path>。
我们来启动一个官方的 nginx 镜像,并为它的日志目录 /var/log/nginx 创建一个数据卷。
# 启动 nginx 容器,并创建一个名为 nginx-logs 的数据卷,挂载到容器的 /var/log/nginx
docker run -d -p 8080:80 --name my-nginx -v nginx-logs:/var/log/nginx nginx
nginx-logs: 这是我们给数据卷起的名字。如果这个数据卷不存在,Docker 会自动创建它。/var/log/nginx: 这是容器内 Nginx 存放日志的路径。
现在,即使我们 docker rm -f my-nginx,nginx-logs 这个数据卷和里面的日志文件依然存在。下次我们再启动任何容器并挂载这个数据卷,就能看到之前的日志。
管理数据卷
- 列出所有数据卷:
docker volume ls - 查看某个数据卷的详细信息 (包括它在主机上的真实路径):
docker volume inspect nginx-logs - 删除一个数据卷:
# 确保没有容器正在使用它 docker volume rm nginx-logs
4.3 Bind Mount(绑定挂载):与主机共享文件
绑定挂载 (Bind Mount) 是另一种强大的数据持久化方式,但它的使用场景与数据卷有所不同。
- 是什么:绑定挂载直接将主机上的一个已存在的、任意路径的目录或文件,挂载到容器的指定路径上。
- 优点:
- 实时同步:主机和容器之间可以实时看到对方对文件的修改。这在开发环境中非常有用,你可以在主机上用你喜欢的 IDE 修改代码,容器内的应用能立刻感知到变化。
- 缺点:
- 耦合度高:容器与主机的特定文件系统路径紧密耦合,降低了可移植性。
- 潜在风险:容器内的进程拥有了对主机特定目录的读写权限,如果应用有漏洞,可能会带来安全风险。
如何使用绑定挂载
绑定挂载同样使用 -v 或 --volume 参数,但格式稍有不同:<host_path>:<container_path>。
我们来实践一下它最经典的应用场景:开发时热更新代码。
回到我们第二章创建的 my-app 项目。假设我们想在不重启容器的情况下,修改 app.js 里的返回信息。
# 确保你当前在 my-app 文件夹下
# 使用 $(pwd) (Linux/macOS) 或 ${PWD} (Windows PowerShell) 来获取当前主机路径
docker run -d -p 4000:8080 --name dev-server \
-v $(pwd):/usr/src/app \
my-app:1.0
注意:
-v $(pwd):/usr/src/app: 这条命令将主机上当前目录(my-app文件夹)挂载到了容器的/usr/src/app目录。- 一个问题:
node_modules目录也会被挂载进去,可能会覆盖掉Dockerfile里RUN npm install生成的、适用于容器环境的node_modules。一个巧妙的解决方法是再加一个数据卷挂载来“覆盖”掉它:-v /usr/src/app/node_modules。这样,容器会使用自己在构建时生成的node_modules。
完整的命令:
docker run -d -p 4000:8080 --name dev-server \
-v $(pwd):/usr/src/app \
-v /usr/src/app/node_modules \
my-app:1.0
现在,启动容器后,尝试用你的代码编辑器修改 app.js 文件,比如把 Hello, Docker World! 改成 Hello, Bind Mount!。保存文件。
然后,你需要安装一个能监控文件变化并自动重启 Node 服务的工具,比如 nodemon。我们可以在 CMD 中使用它:CMD ["npx", "nodemon", "app.js"](需要修改 Dockerfile 并重新构建镜像)。
修改代码后,刷新浏览器 http://localhost:4000,你会发现无需重启容器,页面内容已经更新了!
4.4 [实战] 为我们的应用挂载一个数据库数据卷
我们来做一个更真实的练习。我们将启动一个 postgres 数据库容器,并使用数据卷来持久化它的数据。
PostgreSQL 官方镜像告诉我们,它的数据都存储在 /var/lib/postgresql/data 目录。
docker run -d --name my-postgres \
-e POSTGRES_PASSWORD=mysecretpassword \
-v pg-data:/var/lib/postgresql/data \
postgres:13
-e POSTGRES_PASSWORD=...: 这是通过环境变量为数据库设置一个初始密码,是postgres镜像要求的。-v pg-data:/var/lib/postgresql/data: 我们创建了一个名为pg-data的数据卷,并把它挂载到容器内的数据存储路径。
现在,你可以使用任何数据库客户端连接到这个容器(需要找到容器IP或进行端口映射),创建表,插入数据。然后,你可以 docker rm -f my-postgres,再用完全相同的命令重新启动一个新的 postgres 容器。当你再次连接上去时,你会发现——数据完好无损!因为它们都保存在 pg-data 这个数据卷里。
4.5 本章小结 & 避坑指南(Volume 和 Bind Mount 的选择题)
你已经掌握了 Docker 数据管理的精髓,再也不用担心数据丢失了。
-
本章回顾:
- 我们理解了数据持久化的必要性。
- 我们学习了 Docker 推荐的数据持久化方式——数据卷 (Volume),它由 Docker 管理,安全又高效。
- 我们学习了另一种方式——绑定挂载 (Bind Mount),它非常适合在开发环境中实现代码的热更新。
- 我们通过实战,为
nginx和postgres容器配置了数据持久化。
-
避坑指南:Volume 和 Bind Mount,我该用哪个? 这是一个经典问题。请遵循以下简单的原则:
-
当你需要持久化应用数据时(如数据库文件、用户上传内容、日志):
- 永远优先选择数据卷 (Volume)。这是最清晰、最解耦、最符合 Docker 理念的做法。
-
当你需要在开发环境中,将主机代码或配置文件同步到容器时:
- 使用绑定挂载 (Bind Mount)。这是它最闪光的应用场景。
-
一个黄金法则:
- 生产环境用 Volume,开发环境用 Bind Mount。
- 如果你不确定该用哪个,就用 Volume。
-
在下一章,我们将解决容器间的“沟通”问题。当我们的应用和数据库分别在两个容器里时,它们该如何找到对方并建立连接呢?敬请期待 Docker 网络的世界。