学习 Docker - 从零开始,第二部分
在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris
本文是系列文章的一部分:
- Docker — 从第一部分开始,涵盖了为什么使用 Docker 以及容器、镜像和 Dockerfile 等基本概念,当然还有管理它们所需的命令。
- Docker — 从头开始,第二部分,我们在这里
- Docker — 从第三部分开始,介绍如何处理数据库,将它们放入容器中,以及如何使用传统链接以及通过网络的新标准使容器与其他容器通信
- Docker — 从头开始,第 IV 部分,介绍如何使用 Docker Compose 管理多个服务(这是 Docker Compose 的一半)
- Docker - 从头开始,第五部分,本部分是 Docker Compose 的第二部分,也是最后一部分,其中我们介绍了卷、环境变量以及如何使用数据库和网络
欢迎阅读 Docker 系列的第二部分。希望您已经阅读了第一部分,对 Docker 的核心概念及其基本命令有了一定的了解,或者您在其他地方获得了这些知识。
在本文中,我们将尝试讨论以下主题
- 回顾和问题介绍,让我们回顾一下第一部分的经验教训,并尝试描述不使用卷可能会非常痛苦
- 持久数据,我们可以使用卷来持久化我们创建的文件或者我们更改的数据库(例如 Sqllite)。
- 将我们的工作目录变成一个卷,卷也为我们提供了一种很好的方式来处理我们的应用程序,而不必为每次更改设置和拆除容器。
资源
使用 Docker 和容器化就是将单体应用拆分成微服务。在本系列文章中,我们将学习掌握 Docker 及其所有命令。您迟早会想把容器部署到生产环境中。这个环境通常是云端。当您觉得自己拥有足够的 Docker 经验后,可以查看以下链接,了解如何在云端使用 Docker:
- 云中的容器精彩的概述页面,展示了有关云中容器的其他信息
- 在云中部署容器教程展示了如何轻松利用您现有的 Docker 技能并在云中运行您的服务
- 创建容器注册表您的 Docker 镜像可以存储在 Docker Hub 中,也可以存储在云端的容器注册表中。将镜像存储在某个地方,然后只需几分钟就能从该注册表创建服务,岂不是很棒?
回顾不使用卷的问题
好的,我们将继续研究我们在本系列第一部分中创建的应用程序,这是一个安装了 express 库的 Node.js 应用程序。
在本节中我们将执行以下操作:
- 运行一个容器,我们将启动一个容器,从而重复我们在本系列第一部分中学到的一些基本 Docker 命令
- 更新我们的应用程序,更新我们的源代码,启动和停止容器,并意识到为什么这种工作方式非常痛苦
运行容器
随着应用程序的扩展,我们可能需要添加路由,或者更改特定路由的渲染内容。让我们展示一下目前的源代码:
// app.js
const express = require('express')
const app = express()
const port = process.env.PORT
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
现在让我们看看我们是否还记得基本命令。我们输入:
docker ps
好的,看起来是空的。我们上次用 docker stop 或 docker kill 清理了容器,但无论用什么命令,都没有可以启动的容器,所以我们需要构建一个。让我们看看我们有哪些镜像:
Docker 镜像
好的,我们有了图像,让我们创建并运行一个容器:
docker run -d -p 8000:3000 chrisnoring/节点
这应该会导致容器在端口 8000 上启动并运行,并且由于我们指定了 -d 标志,它应该以分离模式运行。
上面我们得到了一个容器 ID,很好。让我们看看能否在http://localhost:8000 找到我们的应用程序:
好的,一切就绪。现在我们准备进行下一步,即更新源代码。
更新我们的应用程序
让我们首先更改默认路由以呈现 hello Chris ,即添加以下行:
app.get('/', (req, res) => res.send('Hello Chris!'))
好的,我们保存更改,然后返回浏览器,发现它仍然显示 Hello World。看来容器没有反映我们的更改。为此,我们需要关闭容器,将其移除,重建镜像,然后再次运行容器。因为我们需要执行一系列命令,所以我们需要改变构建和运行容器的方式,即主动给容器命名,而不是像这样运行容器:
docker run -d -p 8000:3000 chrisnoring/节点
我们现在输入:
docker run -d -p 8000:3000 --name my-container chrisnoring/node
这意味着我们的容器将获得名称 my-container,这也意味着当我们引用我们的容器时,我们现在可以使用它的名称而不是它的容器 ID,这对于我们的情况更好,因为容器 ID 会在每次设置和拆卸时发生变化。
docker stop my-container // this will stop the container, it can still be started if we want to
docker rm my-container // this will remove the container completely
docker build -t chrisnoring/node . // creates an image
docker run -d -p 8000:3000 --name my-container chrisnoring/node
您可以将这些命令链接起来,如下所示:
docker stop my-container && docker rm my-container && docker build -t chrisnoring/node . && docker run -d -p 8000:3000 --name my-container chrisnoring/node
我第一眼看到这个就觉得哇,这么多命令啊。肯定有更好的办法吧,尤其是在我开发阶段?

是的,有一个更好的方法,那就是使用卷。接下来我们来看看卷。
使用卷
卷或数据卷是一种在主机上创建一个可以写入文件并持久化文件的地方的方法。我们为什么要这样做呢?因为在开发过程中,我们可能需要将应用程序置于特定状态,这样就不必从头开始。通常,我们会将日志文件、JSON 文件,甚至数据库(SQLite)存储在卷上。
创建卷非常容易,我们可以通过多种不同的方式来实现,但主要有两种方式:
- 在创建容器之前
- 懒惰地,例如在创建容器时
创建和管理卷
要创建卷,请输入以下内容:
dockervolumecreate[卷名称]
我们可以通过输入以下内容来验证我们的卷是否已创建:
docker 卷 ls
这将列出我们拥有的所有不同卷。一段时间后,这会导致您创建大量的卷,因此了解如何控制卷的数量是很重要的。为此,您可以输入:
docker 卷修剪
这将删除您当前未使用的所有卷。系统将询问您是否要继续。
如果要删除单个卷,可以输入以下命令:
dockervolumerm[卷名称]
您最有可能想要了解的另一个命令是检查命令,它允许我们查看有关所创建卷的更多详细信息,并且可能最重要的是它将持久文件放置在何处。
docker inspect [卷名称]
不过需要注意的是,大多数情况下你可能并不关心 Docker 将这些文件放在哪里,但有时出于调试的目的,你可能需要知道它们的位置。正如我们将在本节后面看到的那样,控制文件的持久化位置在开发应用程序时非常有用。
正如您所看到的,Mountpoint 字段告诉我们 Docker 计划将您的文件保存在何处。
在应用程序中安装卷
好的,现在我们想在应用程序中使用卷。我们希望能够在容器中更改或创建文件,这样当我们将其拉下来并重新启动时,更改仍然有效。
为此,我们可以使用两个不同的命令,它们以不同的语法实现相对相同的功能,它们是:
-v
,——volume,语法如下 -v [卷名称]:[容器中的目录],例如 -v my-volume:/app--mount
,语法如下所示 --mount source=[name ofvolume],target=[directory in container] ,例如 ——mount source=my-volume,target=/app
与运行容器结合使用,它看起来像这样:
docker run -d -p 8000:3000 --name my-container --volume my-volume:/logs chrisnoring/node
让我们尝试一下。首先,让我们运行容器:
然后,让我们运行检查命令,以确保卷已正确挂载到容器内。运行该命令时,我们会得到一个巨大的 JSON 输出,但我们要查找的是 Mounts 属性:
好的,我们的体积已经存在了,很好。下一步是将体积定位到容器内部。让我们使用以下命令进入容器:
docker exec -it 我的容器 bash
然后导航到我们的/logs
目录:
好的,现在如果我们关闭容器,我们在卷中创建的所有内容都应该被持久化,而所有未放入卷的内容都应该消失,对吗?没错,就是这个意思。很好,我们理解了卷的原理。
将子目录挂载为卷
到目前为止,我们已经创建了一个卷,并让 Docker 决定文件的持久化位置。如果我们决定了这些文件的持久化位置,会发生什么?
如果我们指向硬盘上的一个目录,它不仅会查看该目录并将文件放入其中,还会选择其中预先存在的文件,并将它们加载到容器中的挂载点。让我们执行以下操作来演示我的意思:
- 创建一个目录,让我们创建一个目录 /logs
- 创建一个文件,让我们创建一个文件 logs.txt 并在其中写入一些文本
- 运行我们的容器,让我们创建一个到本地目录 + /logs 的挂载点
前两个命令使我们拥有如下文件结构:
app.js
Dockerfile
/logs
logs.txt // contains 'logging host...'
package.json
package-lock.json
现在使用运行命令来启动并运行我们的容器:
上面我们注意到 --volume 命令有点不同。第一个参数$(pwd)/logs
表示当前工作目录及其子目录logs
。第二个参数/logs
表示将主机日志目录挂载到容器中同名目录。
让我们深入容器并确定容器确实从主机日志目录中提取了文件:
从上面的命令集中我们可以看出,我们使用 进入容器docker exec -it my-container bash
,然后导航到 logs 目录,最后使用 命令读取 logs.txt 的内容cat logs.txt
。结果就是记录主机……例如,主机上确切的文件和内容。
但这是一个卷,这意味着主机中的卷与容器之间存在连接。接下来我们在主机上编辑该文件,看看容器会发生什么:
哇,它也在容器中发生了变化,而我们无需拆除它或重新启动它。
将我们的应用程序视为一个卷
为了使我们的整个应用程序被视为一个卷,我们需要像这样拆除容器:
docker kill 我的容器 && docker rm 我的容器
为什么我们需要做这些?因为我们即将更改 Dockerfile 以及源代码,而我们的容器不会获取这些更改,除非我们使用 Volume,就像我将在下面向您展示的那样。
此后,我们需要这次使用不同的卷参数重新运行我们的容器--volume $(PWD):/app
。
注意:如果您的 PWD 包含一个包含空格的目录,您可能需要将参数指定为
"$(PWD)":/app
,即需要$(PWD)
用双引号括起来。感谢 Vitaly 指出了这一点 :)
完整命令如下:
这将有效地使我们的整个应用程序目录成为一个卷,并且每次我们在其中进行某些更改时,我们的容器都应反映这些更改。
因此,让我们尝试在 Node.js Express 应用程序中添加一条路由,如下所示:
app.get("/docker", (req, res) => {
res.send("hello from docker");
});
好的,从我们处理 express 库所了解到的情况来看,我们应该能够在浏览器中访问http://localhost:8000/docker ,是吗?
悲伤的脸 :(。它不起作用,我们做错了什么?嗯,事情是这样的。如果你在 Node.js Express 应用程序中更改了源代码,则需要重新启动它。这意味着我们需要退一步思考如何在文件更改后立即重新启动我们的 Node.js Express Web 服务器。有几种方法可以实现这一点,例如:
- 安装一个像 nodemon 或 forever 这样的库来重启 Web 服务器
- 运行PKILL 命令并终止正在运行的 node.js 进程和运行的 node app.js
感觉安装像 nodemon 这样的库不太麻烦,所以让我们这样做:
这意味着我们现在在 package.json 中又多了一个库依赖,但也意味着我们需要改变应用的启动方式。我们需要使用命令 来启动应用nodemon app.js
。这意味着nodemon
一旦发生更改,就会立即重启整个应用。既然如此,我们不妨在 package.json 中添加一个启动脚本,毕竟,这更符合 Node.js 的做事方式:
如果你是 Node.js 新手,我们来描述一下上面所做的工作。在 package.json 文件中添加启动脚本意味着我们进入一个名为“scripts”的部分,并添加一个 start 条目,如下所示:
// excerpt package.json
"scripts": {
"start": "nodemon app.js"
}
"scripts"
默认情况下,键入 即可运行中定义的命令npm run [name of command]
。但是,也有一些已知命令,例如start
和test
,对于已知命令,我们可以省略关键字,因此我们run
不必键入,而是键入。让我们添加另一个命令,如下所示:npm run start
npm start
"log"
// excerpt package.json
"scripts": {
"start": "nodemon app.js",
"log": "echo \"Logging something to screen\""
}
要运行这个新命令,"log"
我们需要输入npm run log
。
好的,还有一件事要做,那就是修改 Dockerfile,改变它启动应用程序的方式。我们只需要将最后一行从:
ENTRYPOINT ["node", "app.js"]
到
ENTRYPOINT ["npm", "start"]
因为我们修改了 Dockerfile,所以必须重建镜像。那么,我们来做一下:
docker build -t chrisnoring/node 。
好的,下一步是启动我们的容器:
docker run -d -p 8000:3000 --name my-container --volume $(PWD):/app chrisnoring/node
值得注意的是我们如何公开我们当前所在的整个目录并将其映射到/app
容器内部。
因为我们已经添加了 /docker 路由,所以我们需要添加一个新的路由,如下所示:
app.get('/nodemon', (req, res) => res.send('hello from nodemon'))
现在我们希望nodemon
当我们在 app.js 中保存更改时它已经完成了它的部分:
啊,我们成功了!它可以路由到 /nodemon 了。我不知道你是怎么想的,但我第一次让它工作的时候是这样的:

概括
本文到此结束。我们学习了卷(Volumes),这是一个非常酷炫且实用的功能。更重要的是,我展示了如何将整个开发环境变成一个卷,并在无需重启容器的情况下继续处理源代码。
在本系列的第三部分中,我们将介绍如何使用链接容器和数据库。敬请期待。
在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris
文章来源:https://dev.to/azure/docker-from-the-beginning---part-ii-5g8n