欢迎来到我为初学者解释Docker的无耻尝试的第二部分。我挣扎过,所以你不必挣扎!
在上周的文章中,我们介绍了Docker是什么,它的一些术语,以及为什么在当今典型的软件开发生命周期中,它在许多用例上都优于VM。本周,我们将更深入地在我们的机器上运行Docker,同时找出最有趣和最有用的底层技术。
如果您的计算机上没有安装Docker,请转到Docker下载页面获取可以在您的系统上运行的版本。
鉴于Docker是基于Linux实用程序构建的,Linux版本是最容易安装的。另一方面,在Mac和Windows上,使用Docker的主要方式有两种:
Docker Desktop较新的推荐版本Docker。这是一个本机解决方案,可直接与您的操作系统进程配合使用,因此我们可以使用您最喜欢的终端和IDE/文本编辑器与Docker自由交互。
Docker工具箱这是一个旧版本,适用于不符合Docker Desktop最低要求的计算机。它不是本地解决方案,因为它运行在VirtualBox上,所以我们被迫使用工具箱附带的工具,这使得一切都变得更加繁琐和容易出错。
主要的问题是,Docker Desktop并不容易安装在每个系统上。虽然在MacOS10.13+和Windows10专业版上很简单,但Windows10家庭版需要特别注意。
Windows上的Docker Desktop利用了Hyper-V,这是微软的Type 1虚拟机管理程序,仅在Windows 10专业版、企业版和教育版上可用。幸运的是,Windows Home支持Windows Subsystem for Linux(WSL),这是一个由Microsoft开发的Linux内核,允许我们在没有仿真的情况下运行Linux进程。从2020年3月开始,Docker Desktop在Windows Home上支持WSL版本2作为后端。
要在Windows Home上安装Docker Desktop,需要先安装WSL2,然后在安装过程中安装Docker Desktop,并指定WSL2为后端。我们可以在这里找到安装WSL2的整个过程,以及在这里可以找到后续的Docker Desktop安装过程。如果您想看安装过程的演练视频,请让我知道,我将努力制作一个可用的视频。
安装后,我们可以打开我们最喜欢的终端,并通过键入以下命令检查安装是否顺利。
注意:如果您无法使其在您的机器上运行,或者如果您还不想安装任何东西,Docker还提供在线游乐场来运行您的代码,您在本教程中找到的所有内容都将以相同的方式运行。
正如我们上周看到的,要运行容器,我们首先需要在Docker注册表中找到映像。像编程中的所有东西一样,Docker有一个Hello World容器,我们可以很容易地从它的Docker Hub页面获得。正如我们在网站上看到的,我们必须运行的命令是。
Using Default Tag:LatestLatest:正在从库中提取/hello-world0e03bdcc26d7:正在提取文件系统层0e03bdcc26d7:验证Checksum0e03bdcc26d7:下载完成0e03bdcc26d7:提取完成摘要:sha256:49a1c8800c94df04e9658809b006fd8a686cab8028d33cfba2cc049724254202Status:为hello-world:latestdocker.io/library/hello-world:latest下载了更新的映像。
我们可以看到一些我们需要涵盖的内容,即标记和层。
我们只需运行$docker image并检查其输出,即可检查机器中的所有图像:
我们可以看到,我们有一个标记为LATEST的hello-world映像,它有一个唯一的映像ID(您的可能不同),并且它只有13.3KB!那就是黄金的藏身之处!使用VM运行类似的基本作业需要安装整个操作系统才能输出";Hello world!";。
Docker图像输出的第一列称为存储库。如果你曾经使用过Git或GitHub,这个名字可能听起来很耳熟。在Docker Registry中,存储库是共享相同基本名称的不同版本映像的集合。每个版本都由一个标签标识,但是图像也可以有多个标签!
一般来说,Docker镜像遵循username/pository:tag的命名约定。举个简单的例子,假设我构建了一个名为mysimpleapp的映像,并在Docker Hub上共享了1.0版。您可以通过运行以下命令获得该图像。
如果我们不指定标签部分,Docker将默认下载标记为最新的镜像。这不是一个特殊的标签,只是Docker在构建时没有指定其他标签时默认分配给镜像的标签。
现在我们在本地有了映像,我们想要运行它,对吗?毫无疑问,运行映像的命令是$docker run<;image>;。引用图像主要有三种方式:
按存储库,但只有当我们只有一个版本的镜像时才有效(例如:如果我有来自同一存储库的两个版本,Docker会混淆我指的是哪一个版本)。
根据镜像ID是首选方式,因为每个镜像都有唯一的ID。请注意,我们可以只使用ID中的前几个字符,Docker会选择正确的。
$docker从Docker运行bf756Hello!此消息表明您的安装似乎工作正常。为生成此消息,Docker执行了以下步骤:1.Docker客户端联系了Docker守护程序。2.Docker守护进程从Docker Hub拉取";hello-world;镜像。(AMD64)3.Docker守护进程从该映像创建了一个新容器,该容器运行生成您当前正在阅读的输出的可执行文件。4.Docker守护进程将输出流式传输到Docker客户端,Docker客户端将其发送到您的终端。要尝试更有雄心的东西,您可以使用以下命令运行Ubuntu容器:$docker run-it ubuntu bashshare图像、自动化工作流等。使用免费Docker ID:https://hub.docker.com/For更多示例和想法,请访问:https://docs.docker.com/get-started/。
我们已经成功地提取并运行了我们的第一个Docker映像!🎉让我们通过可视化存储库结构以及以简单的方式提取或运行映像的所有方法来回顾我们到目前为止学到的所有知识
我们知道如何获取和运行简单的图像!然而,我们只是触及了码头的表面。你准备好进入下一阶段了吗?
我们从上周了解到Docker Image从Dockerfile开始,这是Docker打包应用程序时遵循的秘诀。它由一系列[action][target]命令组成,每个命令指定Docker需要按顺序运行的命令。
让我们来看看一个部署了一个简单Node.js Express服务器的Dockerfile,并尝试了解它是如何工作的。
📦docker-express-app┣📂src┃┗📜server.js┣📜.gitignore┣📜Docker文件┣📜包-lock.json┗📜包.json。
正如我们所看到的,Dockerfile位于我的项目文件夹的根级别。现在让我们来看看它的内容。
From node:11-alpine告诉Docker下载在Linux Alpine(一个非常轻量级的Linux发行版)上运行的Node.js版本11的Docker映像。这将是我们的容器使用的基本图像。
运行mkdir app会在环境的根目录下创建一个名为app的本地文件夹。
WORKDIR/app将所有后续命令的工作目录更改为我们在步骤2中创建的/app文件夹。
收到。。告诉Docker将我机器上当前文件夹(我的docker-express-app文件夹)中的所有文件复制到Docker镜像的当前目录(/app目录)中。
Run NPM install将安装我在Package.json文件中指定的所有依赖项,在本例中仅为express。
Cmd[";npm";,";run";,";start";]是容器在实例化时运行的命令,在本例中是Package.json中定义的启动脚本。
下一步是构建映像。正如您可以想象的那样,我们有一个docker build命令可以做到这一点!Build命令的标准用法是。
请记住,在为镜像添加标签时,如果我们只提供存储库名称,Docker会默认为镜像分配最新的标签。让我们看一下输出结果。
$docker build-t docker-express-app.将构建上下文发送到Docker后台进程2.013MB步骤1/6:从节点:11-alpine->;f18da2f58c3d步骤2/6:运行在37513fc60556中运行的mkdir app->;删除中间容器37513fc60556->;13ee4d6cccbcStep 3/6:。->;a3527fb353c6步骤5/6:在f4ab1726f363中运行NPM安装->;在0.855中审核了50个软件包,未发现任何漏洞删除中间容器f4ab1726f363->;8f2c4645e536步骤6/6:CMD[";npm&34;,";运行";,";
正如我们所看到的,每一条指示都得到了认真的遵循。在每个步骤上,Docker都会创建一个层。每个镜像都是由几个只读层构建而成,每一层都对应于Dockerfile中的特定指令。每一步都是在上一层的基础上构建的。
为此,Docker从上一层或基础映像(如果它是第一个命令)构建一个称为中间容器的临时容器。然后,它在此中间容器上运行new命令,将结果另存为新的临时容器,然后继续执行下一条指令。如果引入新文件,一些图层会增加图像的大小,而其他图层则不会。最后一个容器,也就是我们的最终图像,是前面所有层的组合,打包成一个层。就像洋葱一样!
当我们意识到docker在构建过程中将层缓存在内存中时,分层架构的用处就开始发挥作用了。为了说明这一点,让我们来看一下Docker如何构建映像的简化视图。
正如我们所看到的,Docker在图层构建后将其存储在内存中。在第一次构建全新的映像时,Dockers会保存一个层,并根据其内容为其分配散列。散列依赖于内容,因此如果两个文件仅相差一个逗号,则它们的散列将完全不同。
$docker build-t docker-express-app.将生成上下文发送到Docker后台进程2.013MB步骤1/6:从节点:11-alpine->;f18da2f58c3d步骤2/6:使用缓存运行mkdir app->;13ee4d6cccbcStep 3/6:WORKDIR/app->;使用缓存-->;。-&>;使用缓存-&>;a3527fb353c6步骤5/6:使用缓存运行NPM安装->;8f2c4645e536步骤6/6:CMD[";npm&34;,";运行";,";启动";]->;使用缓存->;ac71530。
虽然第一次构建花费了大约7秒,但这一次是瞬间完成的!在这个构建过程中,在每一步,Docker都足够聪明,能够记住他以前已经执行过相同的操作。在每个步骤之后打印的->;Using Cache消息就证明了这一点。如果我们查看前面的输出,我们还可以看到每个层的散列都与前面相同!即使是最终的图像也有相同的散列,因为没有任何内容更改。
Docker检查层生成的散列是否已经在其缓存中,如果已经存在,它将只使用缓存的层,而不会再次运行所有命令。让我们看看这个构建过程的图形表示。
现在我们可以明白为什么再次建立形象是如此之快了。它的所有片段都已经存储在内存中了!通过运行docker History<;tainer-id>;,我们可以看到每个层何时构建完毕,以及它在图像大小中占用了多少空间(页面大小对格式设置没有帮助)。
由大小COMMENTac715302eade创建的$docker历史ac715IMAGE 3小时前/bin/sh-c#(Nop)cmd[";npm&34;";运行";";开始";]0B8f2c4645e536 3小时前/bin/sh-c NPM安装405Ba3527fb353c6 3小时前/bin/sh-c#(Nop)copy。1.71MB579c23501f62 3小时前/bin/sh-c#(Nop)WORKDIR/app 0B13ee4d6cccbc 3小时前/bin/sh-c mkdir app 0Bf18da2f58c3d 14个月前/bin/sh-c#(Nop)cmd[";node";]0B<;丢失>;14个月前/bin/sh-c#(NOP)。0b<;14个月前丢失>;/bin/sh-c#(Nop)复制文件:238737301d473041…。116b<;15个月前缺失>;/bin/sh-c apk add--无缓存--虚拟.bui…。5.1MB<;15个月前缺失/bin/sh-c#(Nop)ENV YORAN_VERSION=1.15.2 0B;15个月前缺失&>t;/bin/sh-c addgroup-g 1000节点&;&;Addu…。64.9MB<;15个月前缺失&>t;/bin/sh-c#(NOP)ENV node_version=11.15.0 0B<;15个月前缺失>;/bin/sh-c#(NOP)CMD[";/bin/sh";]0B<;15个月前缺失/bin/sh-c#(NOP)添加文件:a86aea1f3a7d68f6a。5.53MB。
所有缺失的图层都是因为我们的图像基于节点:11-阿尔卑斯山基础图像。我们可以看到图像由哪些层组成,有点像账簿,但我们只下载了最后一层。既然我们知道层是作为中间容器一个接一个地构建的,我们就可以得出结论,所有这些缺失的层都是Node.js映像的维护者在其构建过程中使用的中间层。
然而,考虑到散列是依赖于内容的,如果我们决定对代码库的任何部分进行一些更改,我们将无法使用所有缓存层来构建映像。作为一个非常简单的示例,我们只需将包含server.js文件的文件夹的名称从src更改为code即可。
📦docker-express-app┣📂code┃┗📜server.js┣📜.gitignore┣📜Dockerfile┣📜Package-lock.json┗📜Package.json。
$docker build-t docker-express-app.将生成上下文发送到Docker后台进程2.013MB步骤1/6:从节点:11-alpine->;f18da2f58c3d步骤2/6:使用缓存运行mkdir app->;13ee4d6cccbcStep 3/6:WORKDIR/app->;使用缓存-->;。->;f10aa38c82f7步骤5/6:在2fa89543e5e5e中运行NPM安装->;在0.829中审核50个软件包,发现0个漏洞删除中间容器2fa89543e5ea->;1b1c5a70b2b7步骤6/6:cmd[";npm&34;,";run";,
正如我们所看到的,步骤4发生了变化,因为复制的文件不再相同。从那里开始,所有步骤都需要从头开始构建,因为它们是从不同的中间容器构建的,因此也就是不同的散列。让我们也把这个构建过程形象化。
现在我们可以将这张新图像的历史与以前的历史进行比较。在那里,我们可以确认后三层比前三层更新得更晚。
$docker History 5d9beIMAGE由大小COMMENT5d9be595c131约一小时前创建/bin/sh-c#(Nop)CMD[";npm";";run";";start";]0B1b1c5a70b2b7约一小时前/bin/sh-c NPM安装405Bf10aa38c82f7约一小时前/bin/。1.71MB579c23501f62 3小时前/bin/sh-c#(Nop)WORKDIR/app 0B13ee4d6cccbc 3小时前/bin/sh-c mkdir app 0Bf18da2f58c3d 14个月前/bin/sh-c#(Nop)cmd[";node";]0B<;丢失>;14个月前/bin/sh-c#(NOP)。0b<;14个月前丢失>;/bin/sh-c#(Nop)复制文件:238737301d473041…。116b<;15个月前缺失>;/bin/sh-c apk add--无缓存--虚拟.bui…。5.1MB<;15个月前缺失/bin/sh-c#(Nop)ENV YORAN_VERSION=1.15.2 0B;15个月前缺失&>t;/bin/sh-c addgroup-g 1000节点&;&;Addu…。64.9MB<;15个月前缺失&>t;/bin/sh-c#(NOP)ENV node_version=11.15.0 0B<;15个月前缺失>;/bin/sh-c#(NOP)CMD[";/bin/sh";]0B<;15个月前缺失/bin/sh-c#(NOP)添加文件:a86aea1f3a7d68f6a。5.53MB。
现在,我们已经构建了一个新映像,并准备将其作为容器运行。正如我们前面提到的,我们可以通过简单地将映像ID作为参数提供给docker run函数来运行映像,所以让我们这样做吧。
如果您查看src文件夹中的代码,我们知道它是一个简单的Express应用程序,在端口3000上提供简单的Hello World类型的消息。这也是航站楼似乎要做的事情。但是为什么我们不能到达我们的网站呢?让我们再考虑一下集装箱化意味着什么。
我们的应用程序与我们的主机操作系统进程一起在自己的私有空间上运行。容器与其他进程隔离,这意味着所有容器端口也与主机的端口隔离。默认情况下,Docker不会将容器的任何端口发布到主机。我们现在可以理解为什么这个网站无法访问了。这是因为我们连接的是主机上的端口3000,而该端口当前未提供任何服务。
为了确保容器内运行的应用程序将服务于我们可以从主机到达的端口,我们需要主动将必要的端口绑定到主机的端口。在Docker中执行端口映射有几种方法,但最直接的一种方法是在发出docker run命令时使用-p或Publish标志显式发布端口。
由于我们确实使用前面的命令运行了一个容器,因此我们必须手动停止正在运行的进程。为此,我们可以打开一个新的终端,并使用docker ps探索当前运行的容器。
$docker psCONTAINER ID IMAGE命令CREATED STATUS PORTS NAMES2a9465cdf730ac715";docker-entrypoint.s…。";10秒前上升了8秒flambiant_haslett。
如您所见,此命令向我们显示正在运行的容器,以及它们的ID、关联的映像ID、状态、映射的端口和随机生成的名称。我们可以在运行时使用标志--name提供一个更有意义的名称,以便更有效地跟踪我们正在运行的服务。要停止正在运行的容器,只需发出命令docker stop以及容器名称或ID即可。
如果返回ID,则容器已停止。我们知道我们的Express应用程序正在侦听端口3000,所以让我们尝试再次运行它,这次使用正确的端口映射。
-p标志采用的参数形式为<;hostPort>;:<;tainerPort>;。例如,如果我们想要将容器的端口3000映射到主机上的端口3000,我们可以简单地说。
值得一提的是,港口不必在集装箱和主机之间匹配。我可以将容器的端口3000映射到我的主机的端口5000,只要容器在自己的端口3000上提供内容,我就可以通过主机的端口5000访问它。
如果这看起来很难理解,请不要担心。Docker中的网络是一个完全不同的蠕虫罐头,我们将在下一篇文章中更详细地探讨这一问题。现在,让我们以正确的方式运行我们的容器。
$docker run--name express-app-p3000:3000ac715>;[email protected] start/app>;node src/server.js http://localhost:3000上侦听的示例应用程序。
祝贺你!。您运行了第一个Docker容器,从Dockerfile构建了第一个映像,了解了分层体系结构,并尝试了网络和端口映射。这值得我们热烈鼓掌!
下周,我们将更深入地讨论Docker卷、容器网络和Docker Compose的魔力,为本Docker教程中使用ECS、Fargate和Kubernetes在AWS上部署和协调选项的最终入门做好准备!
你可以通过Twitter与我保持联系,让我知道你对这本指南的看法。我希望你喜欢我们在码头山上的第二次徒步旅行,并敬请关注下周的登山之旅!