Docker-快速启动指南(全)

Docker 快速启动指南(全)

原文:zh.annas-archive.org/md5/23ECB0A103B038BBAFCFDE067D60BC3D

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

通常,当我提到 Docker 或这本书时,我得到的第一个问题是Docker 是什么?所以,我们也可以现在回答这个问题,把它解决掉…

在我交往的朋友圈里,我会回答这个问题,说Docker 是一种软件解决方案,用于构建、发布和在任何地方运行容器。但如果你不是计算机专业人士,那么这个答案对你来说几乎毫无意义。所以,让我们再试一次,以一种值得快速入门指南的方式回答什么是 Docker?

Docker 是一种工具,允许软件开发人员轻松创建应用程序,并将这些应用程序打包成一个称为容器的特殊包。正确使用时,作为容器打包的应用程序可以非常高效、非常安全地运行。由于容器包含应用程序运行所需的一切,它还允许开发人员在几乎任何地方分享他们的应用程序,而无需重新创建或重新打包。

这意味着,通过使用 Docker,开发人员可以在自己的笔记本电脑上创建、运行和测试他们的应用容器,然后与同行分享完全相同的容器,以便他们也可以运行和测试。然后,他们可以与质量保证团队分享相同的容器,以进一步验证质量,最终,完全相同的容器可以在生产环境中运行和使用。

使用 Docker,软件开发人员可以创建比以往更好、更安全的软件,可以比以往更快地进行测试和部署。

在这本书的页面中,您将找到所有您需要了解 Docker 是什么以及 Docker 提供了什么好处的信息。使用详细但易于理解的描述和示例,这本快速入门指南将教会您如何设置自己的 Docker 开发环境,以及如何创建利用 Docker 提供的所有重要功能的企业级 Docker 镜像。这本快速入门指南将教会您如何使用 Docker 网络和 Docker 的存储功能。您还将学习如何创建和部署多容器应用程序,以及如何使用 Docker Swarm 设置 Docker 集群。完成快速入门指南时,您将能够构建和共享自己的 Docker 镜像,并在 Docker 容器中运行最重要的应用程序。这本快速入门指南将充分准备您在未来的所有项目中使用 Docker。如果您准备好开始,请翻页...

这本书适合谁

这本快速入门指南适用于任何想了解 Docker 是什么以及为什么有这么多人对使用它感到兴奋的人。它适用于希望立即开始使用 Docker 并且没有时间翻阅完整的《精通 Docker》书籍或参加为期一周的培训课程的开发人员。这本指南适用于任何需要快速决定是否在下一个项目中使用 Docker 并立即开始的人。

为了充分利用这本书

您应该有一台开发者工作站,可以在上面安装 Docker 并用于测试本书中包含的示例。您应该通过实际尝试每个示例来学习,而不仅仅是阅读它们。此外,您应该至少有一台其他服务器,最好是两台或三台其他服务器,用于配置为 Docker 集群。这些服务器可以是 AWS 中的 EC2 实例;或者是 VMware Workstation 或 Fusion 上的虚拟机;或者在 VirtualBox 中的虚拟机,最坏的情况下。本书中使用的所有软件都是免费或开源的,因此您应该能够尝试您在这里学到的一切。大多数示例无论您使用的操作系统如何都能很好地工作,我已经尝试在适当的地方指出了差异。您应该在hub.docker.com上创建一个帐户,并在github.com上创建一个帐户。所有的代码示例都经过了我的测试,以及几位审阅者的测试,所以如果您无法使它们工作,请仔细检查代码并重试,或者从 Packt 下载代码并将其剪切粘贴到您的系统中,然后再试一次。您会掌握的。

下载示例代码文件

您可以从www.packtpub.com的帐户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. www.packtpub.com登录或注册。

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

文件下载后,请确保您使用最新版本的解压缩或提取文件夹:

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Docker-Quick-Start-Guide。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有来自丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。快去看看吧!

实际代码

访问以下链接查看代码运行的视频:bit.ly/2Q1DbPq

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。以下是一个例子:“在网络密钥部分,我们正在指示 Docker 创建两个网络,一个名为frontend,一个名为backend。”

任何命令行输入或输出都以以下方式编写:

# Enable autolock on your swarm cluster
docker swarm update --autolock=true
# Adjust certificate expiry to 30 days
docker swarm update --cert-expiry 720h

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。以下是一个例子:“一旦配置已保存,让我们通过单击“立即构建”链接来测试作业。”

警告或重要说明会以这种方式出现。提示和技巧会以这种方式出现。

保持联系

第一章:设置 Docker 开发环境

“突然间我想到:如果我的拖车可以被简单地吊起并放在船上,而不触及其中的货物,那不是很好吗?” - Malcolm McLean,美国卡车企业家

在本章中,我们将为我们的工作站设置 Docker 开发环境。我们将学习如何在 Linux、Windows 和 OS X 工作站上设置 Docker 开发环境。然后,我们将处理每个操作系统的一些后安装步骤。最后,我们将了解在每个操作系统上使用 Docker 的区别以及在它们之间需要注意的事项。

到本章结束时,您将了解以下内容:

  • 如何设置您的 Docker 开发环境,无论您的工作站运行在以下哪种操作系统上:

  • CentOS

  • Ubuntu

  • Windows

  • OS X

  • 在不同操作系统上使用 Docker 时需要注意的差异

技术要求

您需要使用您选择的操作系统(包括 Linux、Windows 或 OS X)的开发工作站。您需要在工作站上拥有 sudo 或管理员访问权限。由于您将安装从互联网上拉取的 Docker 软件,因此您需要工作站上的基本互联网连接。

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Docker-Quick-Start-Guide/tree/master/Chapter01

查看以下视频以查看代码的运行情况:bit.ly/2rbGXqy

设置您的 Docker 开发环境

现在是时候动手了。让我们开始设置我们的工作站。无论您的首选操作系统是什么,都有相应的 Docker。使用以下内容作为指南,我们将带您完成在工作站上设置 Docker 的过程。我们可以从设置 Linux 工作站开始,然后解决 Windows 系统的问题,最后完成可能是最常见的开发者选项,即 OS X 工作站。虽然 OS X 可能是最受欢迎的开发者选项,但我建议您考虑将 Linux 发行版作为您的首选工作站。稍后在在 OS X 工作站上安装 Docker部分中,我们将更多地讨论我为什么做出这样的建议。但现在,如果您被说服在 Linux 上开发,请在 Linux 安装讨论期间仔细关注。

一般来说,有两种 Docker 可供选择:Docker 企业版或 Docker EE,以及 Docker 社区版或 Docker CE。通常,在企业中,你会选择企业版,特别是对于生产环境。它适用于业务关键的用例,Docker EE 正如其名,是经过认证、安全且在企业级别得到支持的。这是一个商业解决方案,由 Docker 提供支持并购买。

另一种类型,Docker CE,是一个社区支持的产品。CE 是免费提供的,通常是小型企业的生产环境和开发人员工作站的选择。Docker CE 是一个完全有能力的解决方案,允许开发人员创建可以与团队成员共享、用于 CI/CD 的自动构建工具,并且如果需要,可以与 Docker 社区大规模共享的容器。因此,它是开发人员工作站的理想选择。值得注意的是,Docker CE 有两种发布路径:稳定版和测试版。在本章的所有安装示例中,我们将使用 Docker CE 的稳定发布路径。

我们将从 CentOS Linux 开始安装讨论,但如果你赶时间,可以直接跳到 Ubuntu、Windows 或 Mac 部分。

在 Linux 工作站上安装 Docker

我们将执行 Docker 的 Linux 安装步骤,分别针对基于 RPM 的工作站(使用 CentOS)和基于 DEB 的工作站(使用 Ubuntu),这样你就会得到最符合你当前使用的 Linux 发行版或将来打算使用的指导。我们将从 CentOS 开始我们的安装之旅。

你可以在参考部分找到所有操作系统安装中使用的下载链接。

在 CentOS 工作站上安装 Docker

CentOS 上的 Docker CE 需要一个维护的 CentOS 7 版本。虽然安装可能在存档版本上运行,但它们既没有经过测试也没有得到支持。

在 CentOS 上安装 Docker CE 有三种方法:

  • 通过 Docker 仓库

  • 下载并手动安装 RPM 包

  • 运行 Docker 的便利脚本

最常用的方法是通过 Docker 仓库,所以让我们从那里开始。

通过 Docker 仓库安装 Docker CE

首先,我们需要安装一些必需的软件包。打开终端窗口,输入以下命令:

# installing required packages sudo yum install -y yum-utils \
 device-mapper-persistent-data \
 lvm2

这将确保我们在系统上安装了yum-config-manager实用程序和设备映射器存储驱动程序。如下截图所示:

请注意,你的 CentOS 7 安装可能已经安装了这些,并且在这种情况下,yum install命令将报告没有需要安装的内容。

接下来,我们将为 Docker CE 设置 CentOS 稳定存储库。

值得注意的是,即使你想安装边缘版本,你仍然需要设置稳定的存储库。

输入以下命令设置稳定的存储库:

# adding the docker-ce repo sudo yum-config-manager \
 --add-repo \
 https://download.docker.com/linux/centos/docker-ce.repo

如果你想使用边缘版本,可以使用以下命令启用它:

# enable edge releases sudo yum-config-manager --enable docker-ce-edge

同样,你可以使用这个命令禁用对边缘版本的访问:

# disable edge releases sudo yum-config-manager --disable docker-ce-edge

现在开始有趣的部分...我们将安装 Docker CE。要这样做,请输入以下命令:

# install docker sudo yum -y install docker-ce 

如果出现关于需要安装container-selinux的错误,请使用以下命令进行安装,然后重试:

# install container-selinux sudo yum -y --enablerepo=rhui-REGION-rhel-server-extras \
   install container-selinux

sudo yum -y install docker-ce

就是这样!安装 Docker CE 比你想象的要容易得多,对吧?

让我们使用最基本的方法来确认安装成功,通过发出版本命令。

这个命令验证了我们安装了 Docker CE,并显示了刚刚安装的 Docker 的版本。输入以下命令:

# validate install with version command docker --version

在撰写本文时,最新版本的 Docker CE 是 18.03.1:

我们还有一个关键的步骤。虽然 Docker CE 已安装,但 Docker 守护程序尚未启动。要启动它,我们需要发出以下命令:

# start docker deamon sudo systemctl start docker

它应该悄悄地启动,看起来像这样:

我们看到了如何使用版本命令验证 Docker 的安装。这是一个很好的快速测试,但有一种简单的方法来确认不仅安装,而且一切都按预期启动和工作,那就是运行我们的第一个 Docker 容器。

让我们发出以下命令来运行 hello-world 容器:

# run a test container sudo docker run hello-world

如果一切顺利,你会看到类似以下的内容:

我们在我们的 CentOS 工作站上安装了 Docker CE,并且它已经在运行容器。我们有了一个很好的开始。现在我们知道如何使用 Docker 存储库进行安装,让我们看看如何手动使用下载的 RPM 进行安装。

使用下载的 RPM 手动安装 Docker CE

安装 Docker CE 的另一种方法是使用下载的 RPM。这种方法涉及下载您希望安装的版本的 Docker CE RPM。您需要浏览 Docker CE 稳定版 RPM 下载站点。其 URL 为download.docker.com/linux/centos/7/x86_64/stable/Packages

单击要下载的 Docker CE 版本,并在提示时告诉浏览器保存文件。接下来,发出yum install命令,提供已下载的 RPM 文件的路径和文件名。您的命令应该类似于这样:

# install the docker rpm sudo yum install ~/Downloads/docker-ce-18.03.1.ce-1.el7.centos.x86_64.rpm

您需要启动 Docker 守护程序。您将在存储库部分使用前面的命令:

# start docker sudo systemctl start docker

而且,正如我们之前学到的,您可以使用以下命令验证安装的功能:

# validate the install and functionality docker --version
sudo docker run hello-world

虽然这种方法可能看起来更简单、更容易执行,但它不太理想,因为它更多地是一个手动过程,特别是在更新 Docker CE 版本时。您必须再次浏览下载页面,找到更新版本,下载它,然后执行yum install。使用之前描述的 Docker 存储库方法,升级只需发出yum upgrade命令。现在让我们再看一种在您的 CentOS 工作站上安装 Docker CE 的方法。

通过运行便利脚本安装 Docker CE

安装 Docker 的第三种方法是使用 Docker 提供的便利脚本。这些脚本允许您安装 Docker 的最新边缘版本或最新测试版本。不建议在生产环境中使用其中任何一个,但它们确实在测试和开发最新的 Docker 版本时起到作用。这些脚本在某种程度上受限,因为它们不允许您在安装过程中自定义任何选项。相同的脚本可以用于各种 Linux 发行版,因为它们确定您正在运行的基本发行版,然后根据该确定进行安装。该过程很简单。

使用curl下载所需的脚本,然后使用 sudo 运行脚本。

运行最新的边缘版本的命令如下:

# download and run the install script curl -fsSL get.docker.com -o get-docker.sh
sudo sh get-docker.sh

执行脚本将产生以下输出:

docker 组已经由脚本为您创建,但由于 CentOS 是以 RPM 为中心,您仍需要自己启动 Docker 服务:

# start docker sudo systemctl start docker

如果这是一个基于 Debian 的系统,Docker 服务将会被脚本自动启动。

现在我们已经检查了在 CentOS 工作站上安装 Docker 的三种方法,现在是时候讨论一些推荐的后续安装设置。

您可能要考虑的后续安装步骤

所有三种安装方法都会自动为您创建一个 docker 组,但如果您希望能够在不使用root或 sudo 的情况下运行 Docker 命令,则需要将用户添加到 docker 组中。

请注意,许多 Docker 命令需要完整的管理员访问权限才能执行,因此将用户添加到 docker 组相当于授予他们 root 访问权限,应考虑安全影响。如果用户已经在其工作站上具有 root 访问权限,则将其添加到 docker 组只是为其提供方便。

通过以下命令轻松将当前用户添加到 docker 组:

# add the current user to the docker group sudo usermod -aG docker $USER

您需要注销并重新登录以更新您帐户的组成员资格,但一旦您这样做了,您应该可以执行任何 Docker 命令而不使用 sudo。

可以通过在不使用 sudo 的情况下运行 hello-world 容器来验证:

# test that sudo is not needed docker run hello-world

接下来,您将希望配置系统在系统启动时启动 Docker 服务:

# configure docker to start on boot sudo systemctl enable docker

您可能要考虑的另一个后续安装步骤是安装 docker-compose。

这个工具可以成为您的 Docker 工具箱的重要补充,我们将在第七章中讨论其用途,Docker Stacks。安装 docker-compose 的命令是:

# install docker compose
sudo curl -L \
 https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) \
 -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

恭喜,您的 CentOS 工作站现在已准备好开始开发您的 Docker 镜像并部署您的 Docker 容器。接下来,我们将学习如何在 Ubuntu 工作站上使用 DEB-based 系统安装 Docker。如果您准备好了,请继续阅读。

在 Ubuntu 工作站上安装 Docker

与在 CentOS 工作站上一样,我们将在 Ubuntu 工作站上安装 Docker CE。在 Ubuntu 上安装 Docker CE 的要求是您必须运行 64 位的最新 LTS 版本,例如 Bionic、Xenial 或 Trusty。您可以在 Artful 版本的 Ubuntu 上安装 Docker CE 的边缘版本。

在 Ubuntu 上安装 Docker CE 有三种方法:

  • 通过 Docker 仓库

  • 下载并手动安装 DEB 软件包

  • 运行方便脚本

最常用的方法是通过 Docker 存储库,所以让我们从那里开始。

通过 Docker 存储库安装 Docker CE

我们首先需要设置 Docker 存储库,然后我们可以进行安装,所以让我们现在处理存储库。

第一步是更新 apt 软件包索引。使用以下命令来执行:

# update apt-get libraries sudo apt-get update

现在我们需要安装一些支持软件包:

# install required packages sudo apt-get install \
 apt-transport-https \
 ca-certificates \
 curl \
 software-properties-common

接下来,我们需要获取 Docker 的 GPG 密钥:

# get the GPG key for docker curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
   sudo apt-key add -

您可以确认已成功添加了 Docker 的 GPG 密钥;它将具有9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88的指纹。

您可以通过使用以下命令检查最后八个字符是否与0EBFCD88匹配来验证密钥:

# validating the docker GPG key is installed sudo apt-key fingerprint 0EBFCD88

最后,我们需要实际设置存储库。我们将专注于我们的示例中的稳定存储库。

如果要安装 Docker CE 的边缘或测试版本,请确保在以下命令中的stable单词后添加edgetest(不要替换stable单词):

# adding the docker repository sudo add-apt-repository \
 "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
 $(lsb_release -cs) \
 stable"

现在我们的系统已经设置了正确的存储库来安装 Docker CE,让我们来安装它。

首先确保所有软件包都是最新的,通过发出apt-get update命令:

# update apt-get libraries again sudo apt-get update

现在我们将实际安装 Docker CE:

# install docker sudo apt-get install docker-ce

Docker 已安装。安装后,您可以检查 Docker 版本以确认安装成功:

# validate install with version command docker --version

版本命令应该类似于这样:

现在,让我们验证 Docker 安装是否按预期工作。为此,我们将使用以下命令运行 hello-world Docker 镜像:

# validating functionality by running a container
sudo docker run hello-world

您注意到了一些有趣的事情吗?

在安装后,我们不需要像在 CentOS 安装中那样启动 Docker。这是因为在基于 DEB 的 Linux 系统上,安装过程也会为我们启动 Docker。此外,Ubuntu 工作站已配置为在启动时启动 Docker。因此,在安装过程中,这两个 Docker 启动步骤都已为您处理。太棒了!您的 Ubuntu 工作站现在已安装了 Docker,并且我们已经验证它正在按预期工作。

虽然使用 Docker 存储库是在工作站上安装 Docker 的最佳方法,但让我们快速看一下在 Ubuntu 工作站上手动安装 Docker CE 的另一种方法,即通过使用 DEB 软件包手动安装它。

使用 DEB 软件包手动安装 Docker CE

现在我们将向您展示如何下载和安装 Docker CE DEB 软件包。如果由于某种原因,软件库对您的工作站不可用,您应该考虑使用此方法。

您需要下载 Docker CE 软件包,所以首先打开浏览器,访问 Ubuntu Docker CE 软件包下载站点download.docker.com/linux/ubuntu/dists/.

在那里,您将找到列出的 Ubuntu 版本文件夹的列表,看起来像这样:

您需要选择与工作站上安装的 Ubuntu 版本相匹配的文件夹,对我来说是xenial文件夹。

继续浏览到/pool/stable/,然后转到与您的工作站硬件相匹配的处理器文件夹。对我来说,那是 amd64,看起来是这样的:

现在单击要下载和安装的 Docker CE 版本。

在单击“确定”之前,请务必选择“保存文件”选项。

一旦软件包已下载到您的工作站,只需使用dpkg命令手动安装软件包即可安装它。

您将下载的 Docker CE 软件包的路径和文件名作为参数提供给dpkg。以下是我用于刚刚下载的软件包的命令:

# installing docker package
sudo dpkg -i ~/Downloads/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb

执行该命令如下:

现在 Docker 已安装,让我们使用版本命令来确认成功安装,然后运行 hello-world 容器来验证 Docker 是否按预期工作:

# validating the install and functionality
docker --version
sudo docker run hello-world

这很好。就像仓库安装一样,您的 docker 组已创建,并且在手动软件包安装中,这两个启动步骤都已为您处理。您不必启动 Docker,也不必配置 Docker 在启动时启动。因此,您已准备好开始创建 Docker 镜像和运行 Docker 容器。

然而,在我们开始创建和运行之前,还有一种在 Ubuntu 工作站上安装 Docker 的方法,我们将介绍。您可以使用 Docker 的便利脚本来安装 Docker CE 的最新边缘或测试版本。现在让我们看看如何做到这一点。

通过运行便利脚本安装 Docker CE

安装 Docker 的另一种方法是使用 Docker 提供的便利脚本。这些脚本允许您安装最新的边缘版本或最新的测试版本的 Docker。不建议在生产环境中使用其中任何一个,但它们确实在测试和开发最新的 Docker 版本时起到作用。这些脚本有一定的局限性,因为它们不允许您在安装中自定义任何选项。相同的脚本可以用于各种 Linux 发行版,因为它们确定您正在运行的基本发行版,然后根据该确定进行安装。这个过程很简单。使用curl拉取所需的脚本,然后使用 sudo 运行脚本。运行最新的边缘版本的命令如下。

使用以下命令安装 curl:

# install curl sudo apt-get install curl

现在获取脚本并运行 docker 脚本进行安装:

# download and run the docker install script curl -fsSL get.docker.com -o get-docker.sh
sudo sh get-docker.sh

执行脚本将产生以下输出:

脚本已为您创建了 docker 组。 Docker 服务已启动,并且工作站已配置为在启动时运行 Docker。因此,您又一次准备好开始使用 Docker。

我们已经研究了在 Ubuntu 工作站上安装 Docker 的三种方法,现在是讨论建议的后安装设置的好时机。

您可能要考虑的后安装步骤

这三种安装方法都会自动为您创建一个 docker 组,但如果您想要能够在不使用root或 sudo 的情况下运行 Docker 命令,您将需要将您的用户添加到 docker 组中。

请注意,许多 Docker 命令需要完全的管理员访问权限才能执行,因此将用户添加到 docker 组相当于授予他们 root 访问权限,应考虑安全性影响。如果用户已经在他们的工作站上具有 root 访问权限,则将他们添加到 docker 组只是为他们提供方便。

将当前用户添加到 docker 组中很容易通过以下命令完成:

# add the current user to the docker group sudo usermod -aG docker $USER

您需要注销并重新登录以更新您帐户的组成员资格,但一旦您这样做了,您就可以执行任何 Docker 命令而不使用 sudo。

这可以通过 hello-world 容器进行验证:

# validate that sudo is no longer needed docker run hello-world

您应该考虑的另一个后安装步骤是安装 docker-compose。

这个工具可以成为您的 Docker 工具箱的重要补充,我们将在第七章《Docker Stacks》中讨论其用途。安装 docker-compose 的命令是:

# install docker-compose
sudo curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

恭喜,您的 Ubuntu 工作站现在已准备好开始开发 Docker 镜像并部署 Docker 容器。接下来,我们将学习如何在基于 Windows 的工作站上安装 Docker。如果您准备好了,请继续阅读。

在 Windows 工作站上安装 Docker

Docker CE 的 Windows 版本与 Windows 10 专业版或企业版兼容。Windows 上的 Docker CE 通过与 Windows Hyper-V 虚拟化和网络集成,提供了完整的 Docker 开发解决方案。Windows 上的 Docker CE 支持创建和运行 Windows 和 Linux 容器。Windows 上的 Docker CE 可以从 Docker 商店下载:store.docker.com/editions/community/docker-ce-desktop-windows

您需要登录 Docker 商店以下载 Docker CE for Windows 安装程序,因此,如果您还没有帐户,请立即创建一个然后登录。

请务必安全地保存您的 Docker 凭据,因为您将在将来经常使用它们。

登录后,您应该会看到“获取 Docker”下载按钮。单击下载按钮,允许安装程序下载到您的工作站。一旦安装程序下载完成,您可以单击“运行”按钮开始安装。如果出现安全检查,请确认您要运行安装程序可执行文件,然后单击“运行”按钮。如果您的工作站启用了 UAC,您可能会看到用户账户控制警告,询问您是否要允许 Docker CE 安装程序对设备进行更改。您必须选择“是”才能继续,所以请立即单击。

Docker CE 安装程序将运行,并开始下载 Docker。一旦 Docker 安装文件成功下载,安装程序将要求您确认所需的配置。这里的选项很少。我建议您将快捷方式添加到桌面,并且不要选择使用 Windows 容器而不是 Linux 容器的选项:

安装程序将解压 Docker CE 文件。当文件解压缩后,您将收到安装成功的通知。根据当前的文档,安装程序将在安装结束时为您运行 Docker。根据我的经验,这并不总是发生。请耐心等待,但如果第一次它没有启动,您可能需要手动运行 Docker。

如果您选择了将 Docker 添加到桌面的快捷方式配置选项,现在您可以双击该快捷方式图标,第一次启动 Docker。

Docker 将运行,并且您将看到一个欢迎屏幕,告诉您 Docker 已经启动。建议您在此时提供您的 Docker 凭据并登录。

每当 Docker 运行时,您将在任务栏通知区域看到一个鲸鱼图标。如果您将鼠标悬停在该图标上,您可以获取 Docker 进程的状态。您将看到诸如 Docker 正在启动和 Docker 正在运行等状态。您可以右键单击该图标以打开 Docker for Windows 菜单:

一旦您在 Windows 工作站上运行 Docker,您可以打开 Windows PowerShell 命令窗口并开始使用 Docker。要验证安装是否成功,请打开 PowerShell 窗口并输入版本命令。为了确认 Docker 是否按预期工作,请运行 hello-world Docker 容器:

# validate install and functionality docker --version
docker run hello-world

您的 Windows 10 工作站现在已设置好,可以创建 Docker 镜像并运行 Docker 容器。Docker 也应该配置为在启动时启动,这样当您需要重新启动工作站时,它将自动启动。

请注意,在 Windows 工作站上使用 Docker CE 并不完全像在 Linux 工作站上使用 Docker CE 那样。在幕后隐藏着一个额外的虚拟化层。Docker 在 Hyper-V 中运行一个小型的 Linux 虚拟机,并且您所有的 Docker 交互都会通过这个 Linux 虚拟机进行。对于大多数用例,这永远不会出现任何问题,但它确实会影响性能。我们将在发现操作系统之间需要注意的差异部分详细讨论这一点。

我们还想看一下另一个设置,所以如果您准备好了,就直接进入下一节。

您可能想考虑的安装后步骤

以下是我建议您在 Docker Windows 工作站上进行的一些安装后步骤。

安装 Kitematic

Docker CE 的 Windows 安装集成了一个名为 Kitematic 的图形用户界面工具。如果您是图形界面类型的人(并且由于您正在使用 Windows 进行 Docker,我猜您是),您会想要安装此工具。

在任务栏通知区域找到Docker图标,右键单击它以打开 Windows 菜单。单击 Kitematic 菜单选项。Kitematic 不是默认安装的。您必须下载包含应用程序的存档。当您第一次单击 Kitematic 菜单选项时,将提示您下载它。单击下载按钮,并将存档文件保存到您的工作站。

您需要解压 Kitematic 存档才能使用它。未压缩的 Kitematic 文件夹需要位于C:\Program Files\Docker文件夹中,并且文件夹名称为Kitematic,以便 Docker 子菜单集成能够正常工作。一旦您在 Windows 工作站的正确路径上安装了 Kitematic,您可以右键单击任务栏通知区域中的Docker图标,然后再次选择 Kitematic 选项。

您将被提示再次输入您的 Docker 凭据以连接到 Docker Hub。您可以跳过此步骤,但我建议您现在就登录。一旦您登录(或跳过登录步骤),您将看到 Kitematic 用户界面。它允许您在工作站上下载和运行 Docker 容器。尝试一个,比如hello-world-nginx容器,或者如果您想玩游戏,可以尝试 Minecraft 容器。

您现在可以在 Windows 10 工作站上创建 Docker 镜像并运行 Docker 容器,但我们还有一个工作站操作系统需要学习如何在其上安装 Docker CE。让我们看看如何在 OS X 工作站上安装它。

为 PowerShell 设置 DockerCompletion

如果您曾经使用过命令行完成,您可能会考虑为 PowerShell 安装 DockerCompletion。此工具为 Docker 命令提供了命令行完成。它相当容易安装。您需要设置系统以允许执行已下载的模块。为此,请以管理员身份打开 PowerShell 命令窗口,并发出以下命令:

# allow remote signed scripts to run
Set-ExecutionPolicy RemoteSigned

您现在可以关闭管理员命令窗口,并打开普通用户 PowerShell 命令窗口。要安装DockerCompletion模块,请发出以下命令:

# install Docker completion
Install-Module DockerCompletion -Scope CurrentUser

最后,在当前的 PowerShell 窗口中激活模块,请使用以下命令:

# enable Docker completion
Import-Module DockerCompletion

现在您可以为所有 Docker 命令使用命令完成功能。这是一个很好的节省按键的功能!

请注意,Import-Module 命令仅在当前的 PowerShell 命令窗口中有效。如果您希望在所有未来的 PowerShell 会话中都可用,您需要将Import-Module DockerCompletion添加到您的 PowerShell 配置文件中。

您可以使用以下命令轻松编辑您的 PowerShell 配置文件(如果尚未创建,则创建一个新的):

# update your user profile to enable docker completion for every PowerShell command prompt
notepad $PROFILE

输入Import-Module DockerCompletion命令并保存配置文件。现在您的 Docker 命令行完成功能将在所有未来的 PowerShell 会话中激活。

在 OS X 工作站上安装 Docker

近年来,Mac 上的 Docker 故事有了很大进展,现在它是 Mac 工作站的一个真正可用的开发解决方案。Docker CE for Mac 需要 OS X El Capitan 10.11 或更新的 macOS 版本。Docker CE 应用程序与 OS X 中内置的 hypervisor、网络和文件系统集成。安装过程很简单:下载 Docker 安装程序镜像并启动它。您可以从 Docker 商店下载安装程序镜像。您必须登录 Docker 商店才能下载安装镜像,因此,如果尚未拥有帐户,请在那里创建一个。

请务必安全地保存您的凭据,因为以后会需要它们。

浏览到 Docker CE for Mac 的 Docker 商店页面store.docker.com/editions/community/docker-ce-desktop-mac。请记住,您必须登录 Docker 商店才能下载安装程序镜像。

一旦登录到 Docker 商店,Get Docker 按钮将可供单击。继续单击它开始下载。Docker CE for Mac 安装镜像可能需要一些时间来下载。下载完成后,双击Docker.dmg镜像文件以挂载和打开它:

一旦 Docker CE for Mac 镜像已挂载并打开,点击Docker图标并将其拖放到应用程序图标上以完成安装。将启动复制Docker应用程序的操作。当复制过程完成时,Docker 应用程序将可以从您的应用程序文件夹中运行。双击您的Docker图标来启动它。第一次启动 Docker 时,会警告您正在运行从互联网下载的应用程序,以确保您真的想要打开它。当 Docker 应用程序打开时,您将收到友好的欢迎消息。

在欢迎消息上点击下一步,会警告您 Docker 需要提升的权限才能运行,并告知您必须提供凭据来安装 Docker 的网络和应用链接。输入您的用户名和密码。Docker 应用程序将启动,将鲸鱼图标添加到菜单通知区域。

您还将被提示输入 Docker 商店凭据,以允许 Docker for Mac 登录商店。输入您的凭据,然后点击“登录”按钮。您将收到确认消息,显示您当前已登录。

为了验证我们的安装成功并确认我们的安装功能,我们将发出版本命令,然后运行 Docker 的 hello-world 容器:

# validate install and functionality docker --version
docker run hello-world

您的 macOS 工作站现在已设置好,可以创建 Docker 镜像和运行 Docker 容器。您已经准备好将应用程序容器化了!您可以轻松使用终端窗口进行所有 Docker 工作,但您可能对 Mac 上可用的图形 UI 工具Kitematic感兴趣。让我们接下来安装 Kitematic。

安装后你可能想考虑的步骤

以下是我建议您的 Docker OS X 工作站的一些安装后步骤。

安装 Kitematic

虽然您可以在 OS X 终端窗口中使用 Docker CLI,并且可能会在大部分 Docker 开发工作中使用它,但您也可以选择使用名为 Kitematic 的图形 UI 工具。要安装 Kitematic,请右键单击 OS X 菜单通知区域中的鲸鱼图标以打开 Docker for Mac 菜单。单击 Kitematic 菜单选项以下载(以及后来运行)Kitematic 应用程序。如果您尚未安装 Kitematic,当您单击 Docker for Mac 菜单时,将显示包含下载链接的消息。该消息还提醒您必须将 Kitematic 安装到您的Applications文件夹中以启用 Docker 菜单集成。单击此处链接下载 Kitematic 应用程序:

下载完成后,将下载的应用程序移动到您的Applications文件夹中,如之前所述。然后,使用 Docker for Mac 菜单,再次单击 Kitematic 菜单选项。这次它将运行 Kitematic 应用程序。第一次运行应用程序时,您将收到标准警告,询问您是否真的要打开它。单击“打开”按钮以打开。

一旦在您的 Mac 工作站上安装了 Kitematic,您可以单击菜单栏通知区域中的 Docker 鲸鱼图标,然后再选择 Kitematic 选项。

您将被提示输入您的 Docker 凭据以将 Kitematic 连接到 Docker Hub。您可以跳过此步骤,但我建议您现在登录。一旦您登录(或跳过登录步骤),您将看到 Kitematic 用户界面。这允许您在您的工作站上下载和运行 Docker 容器。尝试一个,比如hello-world-nginx容器,或者如果您想玩游戏,可以尝试 Minecraft 容器。

恭喜!您现在已经设置好了使用 Docker CLI 和 Kitematic 图形用户界面来运行 Docker 容器和管理 Docker 镜像。但是,您将使用 OS X 终端和您喜欢的代码编辑器来创建 Docker 镜像。

安装 Docker 命令行完成

安装 Homebrew。您的 Mac 上可能已经安装了 Homebrew,但如果没有,现在应该安装它。以下是安装它的命令:

# install homebrew
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

接下来,使用 Homebrew 安装bash-completion。以下是命令:

# use homebrew to install bash completion 
brew install bash-completion

安装bash-completion会指导你将以下行添加到你的~/.bash_profile文件中:

# update the bash profile to enable bash completion for every terminal session 
[ -f /usr/local/etc/bash_completion ] && . /usr/local/etc/bash_completion

现在,创建必要的链接以启用 Docker 命令行补全功能。每个 Docker 工具集都有一个链接。以下是 bash 的链接命令(如果你使用zsh,请查看下一个代码块中的链接命令):

# create links for bash shell
ln -s /Applications/Docker.app/Contents/Resources/etc/docker.bash-completion $(brew --prefix)/etc/bash_completion.d/docker
ln -s /Applications/Docker.app/Contents/Resources/etc/docker-machine.bash-completion $(brew --prefix)/etc/bash_completion.d/docker-machine
ln -s /Applications/Docker.app/Contents/Resources/etc/docker-compose.bash-completion $(brew --prefix)/etc/bash_completion.d/docker-compose

请注意,如果你使用的是zsh而不是 bash,链接命令是不同的。以下是zsh的链接命令:

# create links for zsh shell
ln -s /Applications/Docker.app/Contents/Resources/etc/docker.zsh-completion /usr/local/share/zsh/site-functions/_docker
ln -s /Applications/Docker.app/Contents/Resources/etc/docker-machine.zsh-completion /usr/local/share/zsh/site-functions/_docker-machine
ln -s /Applications/Docker.app/Contents/Resources/etc/docker-compose.zsh-completion /usr/local/share/zsh/site-functions/_docker-compose

最后,重新启动你的终端会话——现在你可以使用 Docker 命令补全了!尝试输入docker并按两次Tab键。

参考

操作系统之间需要注意的差异

Docker 镜像是自包含的软件包,包括运行它们所设计的应用程序所需的一切。Docker 的一个巨大优势是,Docker 镜像可以在几乎任何操作系统上运行。也就是说,在不同的操作系统上运行 Docker 镜像的体验会有一些差异。Docker 是在 Linux 上创建的,并且与一些关键的 Linux 构造深度集成。因此,当您在 Linux 上运行 Docker 时,一切都会直接无缝地与操作系统集成。Docker 原生地利用 Linux 内核和文件系统。

不幸的是,当您在 Windows 或 Mac 上运行 Docker 时,Docker 无法利用与 Linux 上原生支持的相同构造,因为这些构造在这些其他操作系统上不存在。Docker 通过在非 Linux 操作系统中的虚拟机中创建一个小型、高效的 Linux VM 来处理这个问题。在 Windows 上,这个 Linux VM 是在 Hyper-V 中创建的。在 macOS 上,这个 VM 是在一个名为hyperkit的自定义虚拟机中创建的。

正如您所期望的,辅助虚拟机会带来性能开销。然而,如果您确实使用 Windows 或 OS X 作为开发工作站,您会高兴地知道,Docker 在这两个平台上都取得了很多积极的进展,减少了开销,并且随着每个新的主要版本的发布,性能得到了显著改善。有很多关于 OS X 上 hyperkit 虚拟机高 CPU 利用率的报告,但我个人没有遇到这个问题。我相信,使用当前稳定版本的 Docker CE,Windows 和 OS X 都可以成功用于 Docker 开发。

除了处理性能之外,还有其他一些差异需要考虑。有两个你应该知道的:文件挂载和端点。

在 Linux 操作系统上,Docker CE 能够直接使用文件系统来进行运行容器中的文件挂载,从而提供本地磁盘性能水平。您还可以更改文件系统驱动程序以实现不同级别的性能。这在 Windows 或 Mac 上不可用。对于 Windows 和 OS X,还有一个额外的文件系统工具来处理文件挂载。在 Windows 上,您将使用 Windows 共享文件,在 OS X 上则使用 osxfs。不幸的是,对于 Windows 和 OS X 用户来说,文件挂载的性能损失是显著的。尽管 Docker 在改进 Windows 和 OS X 的文件挂载故事方面取得了长足进步,但与在 Linux 操作系统上本地运行相比,两者仍然明显较慢。特别是对于 Windows,文件挂载选项非常受限制。如果您正在开发一个对磁盘利用率很高的应用程序,这种差异可能足以让您立即考虑切换到 Linux 开发工作站。

Linux 上的 Docker 和 Windows 或 Mac 上的 Docker 之间的另一个区别是端口的利用。例如,在 Windows 上使用 Docker 时,无法使用 localhost 从主机访问容器的端点。这是一个已知的 bug,但唯一的解决方法是从与运行它们的主机不同的主机访问容器的端点。在 Mac 上使用 Docker 时,还存在其他端点限制,比如无法 ping 容器(因为 Docker for Mac 无法将 ping 流量路由到容器内部),也无法使用每个容器的 IP 地址(因为 Docker 桥接网络无法从 macOS 访问)。

这些任何限制可能足以让您考虑将开发工作站切换到 Ubuntu 或 CentOS 操作系统。对我来说是这样,您会发现本书中大多数示例都是在我的 Ubuntu 工作站上执行的。我会尽量指出如果您使用 Windows 或 OS X 可能会有显著不同的地方。

总结

哇!我们在这第一章涵盖了很多内容。现在,您应该能够在您的工作站上安装 Docker,无论它运行的是哪种操作系统。您应该能够使用三种不同的方法在 Linux 工作站上安装 Docker,并了解在基于 RPM 的系统和基于 DEB 的系统上安装之间的一些区别。

我们还介绍了一些非常重要的原因,为什么您可能会考虑使用 Linux 工作站进行开发,而不是使用 Windows 或 macOS 工作站。到目前为止,您应该能够通过检查安装的 Docker 版本轻松验证 Docker 的成功安装。

您应该能够通过运行一个 hello-world 容器轻松确认 Docker 是否按预期工作。对于你的第一章来说还不错,对吧?好了,有了这个基础和你新准备好的 Docker 工作站,让我们直接进入第二章 学习 Docker 命令,在那里我们将学习许多你每天都会使用的 Docker 命令。

参考资料

第二章:学习 Docker 命令

在本章中,我们将学习一些基本的 Docker 命令。虽然我们将重点关注最重要的命令之一,即container run命令,但我们也将涵盖许多其他您每天都会使用的命令。这些命令包括列出容器命令、停止容器命令和删除容器命令。在学习过程中,我们还将了解其他容器命令,如日志、检查、统计、附加、执行和提交。我认为您会发现本章对 Docker 教育是一个很好的基础。

BIC:国际集装箱局成立于 1933 年,是一个中立的、非营利性的国际组织,其使命是促进集装箱化和联运交通的安全、安全和可持续发展。

在本章结束时,您将了解以下内容:

  • 当前和以前的命令行语法

  • 使用版本命令的两种方式

  • 如何使用container run命令及其许多可选参数

  • 如何启动和停止容器、查看容器信息、与运行中的容器交互,以及如何保存和重用对容器所做的更改

技术要求

您将从 Docker 的公共仓库中拉取 Docker 镜像,并安装 jq 软件包,因此需要基本的互联网访问权限来执行本章中的示例。

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Docker-Quick-Start-Guide/tree/master/Chapter02

查看以下视频以查看代码的实际操作:bit.ly/2P43WNT

命令语法信息

在我们深入学习 Docker 命令及其众多选项之前,我想通知您的是 Docker CLI 在 2017 年 1 月发生了变化。

随着每个新版本的发布,Docker 命令和相关选项的数量都在增加。Docker 决定需要解决这种复杂性。因此,随着 Docker 版本 1.13 的发布(Docker 还在 2017 年更改了版本编号方案),CLI 命令已被划分为管理功能组。例如,现在有一个容器管理组的命令,以及一个镜像管理组的命令。这改变了您运行 Docker 命令的方式。以下是旧和新run命令的使用示例:

# the new command syntax...
docker container run hello-world
# the old command syntax...
docker run hello-world

这个变化提供了更好的命令组织,但也增加了命令行的冗长。这是一个权衡。就我所知,目前为止,旧的命令语法仍然适用于所有 Docker 命令,但在本书的其余示例中,我打算使用新的语法。至少我会尝试,因为旧习惯难改。

我想在这里提一点,大多数命令选项都有短格式和长格式。我会尝试在我的示例中至少分享一次长格式,这样你就会知道短版本代表什么。如果你安装了 Docker 命令行完成,它将是一个有用的资源,可以记住新的基于 Docker 管理的命令和可以与之一起使用的参数。这是容器命令的顶级命令完成帮助的样子:

该命令列表让我们提前了解了一些我们将在本章中审查的命令,所以让我们开始学习 Docker 命令。在第一章中,设置 Docker 开发环境,我们使用了两个非常常见的 Docker 命令:version命令和run命令。虽然你认为你已经了解了version命令的所有内容,但你可能会惊讶地发现它还有另一个技巧。Docker 的 version 命令还有另一个版本。

version 命令

你已经使用了docker --version命令作为一个快速测试来确认 Docker 是否已安装。现在尝试一下没有破折号的命令:

docker version

这个版本的命令可以更详细地了解安装在系统上的 Docker 的版本。值得注意的是,docker-compose 命令,我们稍后会谈到,也有两个版本的 version 命令——一个带有破折号提供单行响应,另一个没有破折号,提供更多细节。

请记住,所有 Docker 命令都有一个丰富的帮助系统。尝试输入 Docker 命令的任何部分并使用--help参数来查看。例如,docker container run --help

Docker run 命令

由于我们将经常使用run命令,我们现在应该看一下。你已经以其最基本的形式使用了run命令:

# new syntax
# Usage: docker container run [OPTIONS] IMAGE [COMMAND] [ARG...]
docker container run hello-world

# old syntax
docker run hello-world

这个命令告诉 Docker,您想要基于描述为 hello-world 的镜像运行一个容器。您可能会问自己,当我安装 Docker 时,hello-world 容器镜像是否已安装?答案是否定的。docker run命令将查看本地容器镜像缓存,以查看是否有与所请求容器描述匹配的容器镜像。如果有,Docker 将从缓存的镜像中运行容器。如果在缓存中找不到所需的容器镜像,Docker 将访问 Docker 注册表,尝试下载容器镜像,并在此过程中将其存储在本地缓存中。然后 Docker 将从缓存中运行新下载的容器。

Docker 注册表只是一个集中存储和检索 Docker 镜像的地方。我们稍后会更多地讨论注册表和 Docker 注册表。现在,只需了解有本地镜像缓存和远程镜像存储这一点。当我们在第一章中运行 hello-world 容器时,您看到了本地未找到容器的过程,设置 Docker 开发环境。当 Docker 在本地缓存中找不到容器镜像并且必须从注册表中下载时,情况是这样的:

您可以使用 docker pull命令预先填充本地 docker 缓存,以便运行您计划运行的容器镜像;例如:

# new syntax
# Usage: docker image pull [OPTIONS] NAME[:TAG|@DIGEST]
docker image pull hello-world

# old syntax
docker pull hello-world

如果您使用pull命令预取容器镜像,当您执行 docker run命令时,它将在本地缓存中找到镜像,而无需再次下载。

您可能已经注意到在前面的屏幕截图中,您请求了 hello-world 容器镜像,Docker 未能在本地缓存中找到,然后从存储库中下载了hello-world:latest容器镜像。每个容器镜像描述由三个部分组成:

  • Docker 注册表主机名

  • 斜杠分隔的名称

  • 标签名称

第一部分,注册表主机名,我们还没有看到或使用过,但它是通过公共 Docker 注册表的默认值包含的。每当您不指定注册表主机名时,Docker 将隐式使用公共 Docker 注册表。此注册表主机名是docker.io。Docker 注册表的内容可以在hub.docker.com/explore上浏览。这是 Docker 镜像的主要公共存储库。可以设置和使用其他公共或私有镜像注册表,并且许多公司将这样做,建立自己的私有 Docker 镜像注册表。我们将在第八章“Docker 和 Jenkins”中再谈一些相关内容。现在,只需了解 Docker 镜像描述的第一部分是托管容器镜像的注册表主机名。值得注意的是,注册表主机名可以包括端口号。这可以用于配置为在非默认端口值上提供数据的注册表。

容器镜像描述的第二部分是斜杠分隔的名称。这部分就像是容器镜像的路径和名称。有一些官方容器镜像不需要指定路径。对于这些镜像,您可以简单地指定斜杠分隔名称的名称部分。在我们的示例中,这是描述的 hello-world 部分。

容器镜像描述的第三部分是标签名称。这部分被认为是镜像的版本标签,但它不需要仅由数字组成。标签名称可以是任何一组 ASCII 字符,包括大写和小写字母,数字,破折号,下划线或句点。关于标签名称的唯一限制是它们不能以句点或破折号开头,并且必须少于 128 个字符。标签名称与斜杠分隔的名称之间用冒号分隔。这让我们回到之前看到的hello-world:latest镜像描述。与注册表主机名一样,标签名称有一个默认值。默认值是latest。在我们的示例中,使用的标签名称是默认值,并且在搜索和下载中显示为hello-world:latest。您可以在以下示例中看到所有这些内容:

我们确认了我们的本地镜像缓存是空的,使用docker images命令,然后拉取了完全限定的 hello-world 镜像以预取到我们的本地缓存中。然后我们使用了与之前所有的 hello-world 示例中相同的简短描述,Docker 运行容器而不再次下载,显示使用了默认值并且它们与完全限定的值匹配。

好的,现在我们已经了解了 Docker run命令的所有基础知识,让我们深入一点,检查一些你可以与run命令一起使用的可选参数。如果你查看完整的run命令语法,你会看到这样的内容:

# Usage:  docker container run [OPTIONS] IMAGE [COMMAND] [ARG...]

请注意命令的最后部分是[COMMAND] [ARG...]。这告诉我们container run命令有一个可选的命令参数,也可以包括自己的可选参数。Docker 容器镜像是使用默认命令构建的,当你基于该镜像运行容器时,会执行该默认命令。对于 hello-world 容器,默认命令是/hello。对于完整的 Ubuntu OS 容器,默认命令是bash。每当你运行一个 Ubuntu 容器并且没有指定在容器中运行的命令时,将使用默认命令。如果现在这些还不太清楚,不要担心——我们将在本章的回到 Docker 运行命令部分稍后讨论默认命令和在运行时覆盖它。现在,知道当你运行一个容器时,它将执行一个命令,要么是默认命令,要么是提供给container run命令的覆盖命令来在运行的容器中执行。最后一点注意:当运行容器的命令(默认或覆盖)终止时,容器将退出。在我们使用 hello-world 容器的示例中,一旦容器内的/hello命令终止,hello-world 容器就会退出。一会儿,你将了解更多关于运行中容器和已退出容器之间的区别。

现在,我们将继续讨论run命令的一个我最喜欢的可选参数,即--rm参数。这里需要一些背景信息。您可能还记得来自第一章的设置 Docker 开发环境,Docker 镜像由多个层组成。每当您运行一个 Docker 容器时,实际上只是使用本地缓存的 Docker 镜像(这是一堆层),并在其顶部创建一个新的读/写层。在容器运行期间发生的所有执行和更改都存储在其自己的读/写层中。

列出容器的命令

可以使用以下命令显示运行中的容器:

# Usage: docker container ls [OPTIONS]
docker container ls

这是列出容器的命令,如果没有任何额外的参数,它将列出当前正在运行的容器。我所说的当前运行是什么意思?容器是在系统上运行的特殊进程,就像系统上的其他进程一样,容器可以停止或退出。然而,与系统上其他类型的进程不同,容器的默认行为是在停止时保留其读/写层。这是因为如果需要,您可以重新启动容器,保持其退出时的状态数据。举个例子,假设您运行一个作为操作系统的容器,比如 Ubuntu,在该容器中安装了wget。容器退出后,您可以重新启动它,它仍然安装了wget。请记住,每个运行的容器都有自己的读/写层,因此,如果您运行一个 Ubuntu 容器并安装了wget,然后运行另一个 Ubuntu 容器,它将不会有wget。读/写层在容器之间不共享。但是,如果重新启动安装了wget的容器,它仍然会安装。

因此,运行中的容器和停止的容器之间的区别在于进程是正在运行还是已退出,留下了自己的读/写层。有一个参数可以让您列出所有容器,包括正在运行和已退出的容器。您可能已经猜到了,它是--all参数,它看起来像这样:

# short form of the parameter is -a
docker container ls -a
# long form is --all
docker container ls --all

# old syntax
docker ps -a

现在,让我们回到我最喜欢的可选运行命令参数之一,即--rm参数:

# there is no short form of the --rm parameter
docker container run --rm hello-world

此参数指示 Docker 在容器退出时自动删除容器的读/写层。当您运行一个 docker 容器而没有--rm参数时,容器数据在容器退出时会被留下,以便稍后可以重新启动容器。然而,如果在运行容器时包括--rm参数,那么在容器退出时所有容器的读/写数据都会被删除。这个参数提供了一个在exit时进行简单清理的功能,这在很多情况下都会非常有用。让我们通过一个快速示例来看一下,使用我们刚刚讨论过的 run 和container ls命令:

首先,我们确认我们的本地缓存中有 hello-world 镜像。接下来,我们列出了系统上所有的容器,包括正在运行和已退出的。请注意镜像和容器之间的区别。如果您熟悉 VMware,类似于模板和虚拟机的类比。接下来,我们使用--rm参数运行了 hello-world 容器。hello-world 容器打印其消息,然后立即退出(我们将输出重定向到/dev/null,以使示例输出变短)。接下来,我们再次列出了容器,因为我们看到 hello-world 容器的读/写数据在容器退出时被自动删除了。之后,我们再次运行了 hello-world 容器,但这次没有使用--rm参数。当我们这次列出容器时,我们看到了(已退出)容器的指示。通常,您会运行一个容器,知道您以后永远不需要重新启动它,并使用--rm参数自动清理它非常方便。但如果您不使用--rm参数会怎么样呢?您会被困在一个不断增长的容器列表中吗?当然不会。Docker 有一个命令来处理这个问题。那就是container rm命令。

删除容器命令

删除容器命令看起来像这样:

# the new syntax
# Usage: docker container rm [OPTIONS] CONTAINER [CONTAINER...]
docker container rm cd828234194a

# the old syntax
docker rm cd828234194a

该命令需要一个唯一标识容器的值;在本例中,我使用了刚刚运行的 hello-world 容器的完整容器 ID。您可以使用容器 ID 的前几个字符,只要它在系统上提供了唯一标识符。另一种唯一标识容器的方法是通过分配给它的name。当您运行容器时,Docker 将为其提供一个唯一的随机生成的名称。在上面的示例中,分配的随机名称是competent_payne。因此,我们可以像这样使用删除命令:

# using the randomly generated name docker container rm competent_payne

虽然 Docker 提供的随机生成的名称比其分配的容器 ID 更易读,但它们可能仍然不如您希望的那样相关。这就是为什么 Docker 为run命令提供了一个可选参数来为您的容器命名。以下是使用--name参数的示例:

# using our own name docker container run --name hi-earl hello-world

现在,当我们列出所有容器时,我们可以看到我们的容器名称为hi-earl。当然,您可能希望使用更好的容器名称,也许是描述容器执行功能的名称,例如db-for-earls-app

注意:与容器 ID 一样,容器名称在主机上必须是唯一的。您不能有两个具有相同名称的容器(即使其中一个已退出)。如果将有多个运行相同镜像的容器,例如 Web 服务器镜像,请为它们分配唯一的名称,例如 web01 和 web02。

您可以通过在命令行上提供每个容器的唯一标识符来同时删除多个容器:

# removing more than one docker container rm hi-earl hi-earl2

通常,您只会在容器退出后删除容器,例如我们一直在使用的 hello-world 容器。但是,有时您可能希望删除当前正在运行的容器。您可以使用--force参数来处理这种情况。以下是使用 force 参数删除运行中容器的示例:

# removing even if it is running docker container rm --force web-server

以下是它的样子:

请注意,在第一个container ls命令中,我们没有使用--all参数。这提醒我们 Web 服务器容器正在运行。当我们尝试删除它时,我们被告知容器仍在运行,不会被删除。这是一个很好的保障,有助于防止删除运行中的容器。接下来,我们使用了强制命令,运行中的容器被删除而没有任何警告。最后,我们进行了另一个container ls命令,包括--all参数,以显示这次实际上删除了我们容器的读/写数据。

如果您已经设置了 Docker 命令完成,您可以输入命令,直到需要输入容器的唯一标识符,然后使用Tab键获取容器列表,切换到您想要删除的容器。一旦您突出显示要删除的容器,使用空格键或Enter键进行选择。您可以再次按Tab键选择另一个要一次删除多个容器。选择所有容器后,按Enter执行命令。请记住,除非包括强制参数rm -f,否则在为rm命令切换时,您只会看到已停止的容器。

有时,您可能希望删除系统上的所有容器,无论是否正在运行。有一种有用的方法来处理这种情况。您可以结合container ls命令和容器删除命令来完成任务。您将使用container ls命令的新参数来完成这个任务——--quiet参数。此命令指示 Docker 仅返回容器 ID,而不是带有标题的完整列表。以下是命令:

# list just the container IDs docker container ls --all --quiet

现在我们可以将container ls命令返回的值作为输入参数提供给容器删除命令。它看起来像这样:

# using full parameter names
docker container rm --force $(docker container ls --all --quiet)
# using short parameter names
docker container rm -f $(docker container ls -aq)

# using the old syntax
docker rm -f $(docker ps -aq)

这将从您的系统中删除所有容器运行和退出,所以要小心!

您可能经常使用这个快捷方式,所以为它创建一个系统别名非常方便。

您可以将以下内容添加到您的~/.bash_profile~/zshrc文件中:`alias RMAC='docker container rm --force $(docker container ls --all --quiet)'。

许多容器被设计为运行并立即退出,例如我们已经多次使用的 hello-world 示例。其他容器的镜像被创建为,当您使用它运行容器时,容器将继续运行,提供一些持续有用的功能,例如提供网页服务。当您运行一个持久的容器时,它将保持前台进程直到退出,并附加到进程:标准输入、标准输出和标准错误。这对于一些测试和开发用例来说是可以的,但通常情况下,这不适用于生产容器。相反,最好将container run作为后台进程运行,一旦启动就将控制权交还给您的终端会话。当然,有一个参数可以实现这一点。那就是--detach参数。使用该参数的效果如下:

# using the full form of the parameter
docker container run --detach --name web-server --rm nginx
# using the short form of the parameter
docker container run -d --name web-server --rm nginx

使用此参数将进程从前台会话中分离,并在容器启动后立即将控制权返回给您。您可能的下一个问题是,如何停止一个分离的容器?好吧,我很高兴您问了。您可以使用container stop命令。

停止容器命令

停止命令很容易使用。以下是命令的语法和示例:

# Usage: docker container stop [OPTIONS] CONTAINER [CONTAINER...]
docker container stop web-server

在我们的情况下,运行容器时我们使用了--rm参数,因此一旦容器停止,读/写层将被自动删除。与许多 Docker 命令一样,您可以提供多个唯一的容器标识符作为参数,以一条命令停止多个容器。

现在您可能会想知道,如果我使用--detach参数,我如何查看容器的运行情况?有几种方法可以从容器中获取信息。让我们在继续运行参数探索之前先看看其中一些。

容器日志命令

当您在前台运行容器时,容器发送到标准输出和标准错误的所有输出都会显示在运行容器的会话控制台中。然而,当您使用--detach参数时,容器一旦启动,会立即返回会话控制,因此您看不到发送到stdoutstderr的数据。如果您想查看这些数据,可以使用container logs命令。该命令如下:

# the long form of the command
# Usage: docker container logs [OPTIONS] CONTAINER
docker container logs --follow --timestamps web-server
# the short form of the command
docker container logs -f -t web-server

# get just the last 5 lines (there is no short form for the "--tail" parameter)
docker container logs --tail 5 web-server

# the old syntax
docker logs web-server

--details--follow--timestamps--tail参数都是可选的,但我在这里包括了它们以供参考。当您使用container logs命令而没有可选参数时,它将只是将容器日志的所有内容转储到控制台。您可以使用--tail参数加上一个数字来仅转储最后几行。您可以组合这些参数(除了--tail--follow)以获得您想要的结果。--follow参数就像在查看不断写入的日志时使用tail -f命令,并将每行写入日志时显示出来。您可以使用*Ctrl *+ C 退出正在跟踪的日志。--timestamps参数非常适合评估写入容器日志的频率。

容器顶部命令

您可能并不总是只想查看容器的日志;有时您想知道容器内运行着哪些进程。这就是container top命令的用处。理想情况下,每个容器都运行一个进程,但世界并不总是理想的,因此您可以使用这样的命令来查看目标容器中运行的所有进程:

# using the new syntax
# Usage: docker container top CONTAINER [ps OPTIONS]
docker container top web-server

# using the old syntax
docker top web-server

正如您可能期望的那样,container top命令只用于一次查看单个容器的进程。

容器检查命令

当您运行容器时,会有大量与容器关联的元数据。有许多时候您会想要查看那些元数据。用于执行此操作的命令是:

# using the new syntax
# Usage: docker container inspect [OPTIONS] CONTAINER [CONTAINER...]
docker container inspect web-server

# using the old syntax
docker inspect web-server

如前所述,此命令返回大量数据。您可能只对元数据的子集感兴趣。您可以使用--format参数来缩小返回的数据。查看这些示例:

  • 获取一些状态数据:
# if you want to see the state of a container you can use this command
docker container inspect --format '{{json .State}}' web-server1 | jq

# if you want to narrow the state data to just when the container started, use this command
docker container inspect --format '{{json .State}}' web-server1 | jq '.StartedAt'
  • 获取一些NetworkSettings数据:
# if you are interested in the container's network settings, use this command
docker container inspect --format '{{json .NetworkSettings}}' web-server1 | jq

# or maybe you just want to see the ports used by the container, here is a command for that
docker container inspect --format '{{json .NetworkSettings}}' web-server1 | jq '.Ports'

# maybe you just want the IP address used by the container, this is the command you could use.
docker container inspect -f '{{json .NetworkSettings}}' web-server1 | jq '.IPAddress'
  • 使用单个命令获取多个容器的数据:
# maybe you want the IP Addresses for a couple containers
docker container inspect -f '{{json .NetworkSettings}}' web-server1 web-server2 | jq '.IPAddress'

# since the output for each container is a single line, this one can be done without using jq
docker container inspect -f '{{ .NetworkSettings.IPAddress }}' web-server1 web-server2 web-server3

这些示例大多使用 json 处理器jq。如果您尚未在系统上安装它,现在是一个很好的时机。以下是在本书中使用的每个操作系统上安装jq的命令:

# install jq on Mac OS
brew install jq

# install jq on ubuntu
sudo apt-get install jq

# install jq on RHEL/CentOS
yum install -y epel-release
yum install -y jq

# install jq on Windows using Chocolatey NuGet package manager
chocolatey install jq

inspect 命令的--format参数使用 go 模板。您可以在 Docker 文档页面上找到有关它们的更多信息,用于格式化输出:docs.docker.com/config/formatting

容器统计命令

另一个非常有用的 Docker 命令是 stats 命令。它为一个或多个正在运行的容器提供实时、持续更新的使用统计信息。这有点像使用 Linux 的top命令。您可以不带参数运行该命令,以查看所有正在运行的容器的统计信息,或者您可以提供一个或多个唯一的容器标识符,以查看一个或多个容器的特定容器的统计信息。以下是使用该命令的一些示例:

# using the new syntax, view the stats for all running containers
# Usage: docker container stats [OPTIONS] [CONTAINER...]
docker container stats

# view the stats for just two web server containers
docker container stats web-server1 web-server2

# using the old syntax, view stats for all running containers
docker stats

当您查看了足够的统计信息后,您可以使用 Ctrl + C退出视图。

回到run命令参数,接下来,我们将讨论通常一起使用的run命令的两个参数。有时候你运行一个容器,你想与它进行交互式会话。例如,您可能运行一个在更多或更少完整的操作系统(如 Ubuntu)内执行某些应用程序的容器,并且您希望在该容器内部进行访问以更改配置或调试一些问题,类似于使用 SSH 连接到服务器。与大多数 Docker 相关的事情一样,有多种方法可以实现这一点。一种常见的方法是使用run命令的两个可选参数:--interactive--tty。现在让我们看看它是如何工作的。您已经看到我们如何使用--detach参数启动与我们正在运行的容器断开连接:

# running detached docker container run --detach --name web-server1 nginx

当我们运行此命令启动我们的 nginx web 服务器并浏览http://localhost时,我们发现它没有提供我们期望的欢迎页面。因此,我们决定进行一些调试,而不是从容器中分离出来,我们决定使用两个--interactive--tty参数进行交互式运行。现在,由于这是一个 nginx 容器,它在容器启动时执行一个默认命令。该命令是nginx -g 'daemon off;'。由于这是默认命令,与容器进行交互对我们没有任何好处。因此,我们将通过在运行命令中提供一个参数来覆盖默认命令。它看起来会像这样:

# using the long form of the parameters
docker container run --interactive --tty --name web-server2 nginx bash

# using the short form of the parameters (joined as one), which is much more common usage
docker container run -it --name web-server2 nginx bash

这个命令将像以前一样运行容器,但是不会执行默认命令,而是执行bash命令。它还会打开一个与容器交互的终端会话。根据需要,我们可以以root用户的身份在容器内执行命令。我们可以查看文件夹和文件,编辑配置设置,安装软件包等等。我们甚至可以运行镜像的默认命令,以查看是否解决了任何问题。这里有一个有点牵强的例子:

您可能已经注意到了-p 80:80参数。这是发布参数的简写形式,我们将在回到 Docker 运行命令部分讨论。使用container ls命令,您可以看到使用默认命令运行容器与使用覆盖命令运行容器之间的区别:

Web 服务器运行使用了默认的 CMD,而 web-server2 使用了覆盖的 CMD bash。这是一个牵强的例子,帮助您理解这些概念。一个真实的例子可能是当您想要与基于操作系统的容器进行交互连接时,比如 Ubuntu。您可能还记得在第一章的开头,设置 Docker 开发环境中提到,默认在 Ubuntu 容器中运行的命令是bash。既然如此,您就不必提供一个命令来覆盖默认值。您可以使用这样的运行命令:

# running interactively with default CMD docker container run -it --name earls-dev ubuntu

使用这个container run命令,您可以连接到正在运行的 Ubuntu 容器的交互式终端会话。您可以做几乎任何您通常在连接到 Ubuntu 服务器时会做的事情。您可以使用apt-get安装软件,查看运行中的进程,执行top命令等等。可能会像这样:

还有一些其他容器命令可以帮助您与已经运行并分离的容器进行交互。现在让我们快速看一下这些命令。

容器附加命令

假设您有一个正在运行的容器。它当前与您的终端会话分离。您可以使用container attach命令将该容器的执行进程带到您的终端会话的前台进程。让我们使用之前使用过的 web 服务器示例:

# run a container detached
docker container run --detach -it --name web-server1 -p 80:80 nginx

# show that the container is running
docker container ps

# attach to the container
# Usage: docker container attach [OPTIONS] CONTAINER
docker container attach web-server1

# issue a *Ctrl* + *PQ* keystroke to detach (except for Docker on Mac, see below for special Mac instructions)

# again, show that the container is running detached.
docker container ps

当你附加到运行的容器时,它的执行命令将成为你的终端会话的前台进程。要从容器中分离,你需要发出Ctrl + PQ按键。如果你发出CtrlC按键,容器的执行进程将接收到 sig-term 信号并终止,这将导致容器退出。这通常是不希望的。所以记住要使用Ctrl + PQ按键来分离。

然而,在 macOS 上存在一个已知问题:对于 Mac 上的 Docker,Ctrl + PQ按键组合不起作用,除非你在attach命令上使用另一个参数,--sig-proxy=false参数,否则你将无法在不使用*Ctrl *+ C按键的情况下从容器中分离出来:

# when you are using Docker for Mac, remember to always add the "--sig-proxy=false" parameter
docker attach --sig-proxy=false web-server1

当你向attach命令提供--sig-proxy=false参数时,你可以向附加的容器发出*Ctrl *+ C按键,它将分离而不向容器进程发送 sig-term 信号,从而使容器再次以分离状态运行,脱离你的终端会话:

容器 exec 命令

有时,当你有一个以分离状态运行的容器时,你可能想要访问它,但不想附加到执行命令。你可以通过使用容器 exec 命令来实现这一点。这个命令允许你在运行的容器中执行另一个命令,而不附加或干扰已经运行的命令。这个命令经常用于创建与已经运行的容器的交互会话,或者在容器内执行单个命令。命令看起来像这样:

# start an nginx container detached
docker container run --detach --name web-server1 -p 80:80 nginx

# see that the container is currently running
docker container ls

# execute other commands in the running container
# Usage: docker container exec [OPTIONS] CONTAINER COMMAND [ARG...] docker container exec -it web-server1 bash
docker container exec web-server1 cat /etc/debian_version

# confirm that the container is still running 
docker container ls

exec命令完成时,你退出 bash shell,或者文件内容已经被替换,然后它会退出到终端会话,让容器以分离状态运行:

让我们在继续讨论许多可选的container run参数之前,先看看另一个 Docker 命令。

容器 commit 命令

重要的是要知道,当您连接到正在运行的容器并对其进行更改,比如安装新的软件包或更改配置文件时,这些更改只适用于该正在运行的容器。例如,如果您使用 Ubuntu 镜像运行一个容器,然后在该容器中安装curl,那么这个更改不会应用到您从中运行容器的镜像,例如 Ubuntu。如果您要从相同的 Ubuntu 镜像启动另一个容器,您需要再次安装curl。但是,如果您希望在运行新容器时保留并使用在运行容器内进行的更改,您可以使用container commit命令。container commit命令允许您保存容器的当前读/写层以及原始镜像的层,从而创建一个全新的镜像。当您使用新镜像运行容器时,它将包括您使用container commit命令保存的更改。container commit命令的样子如下:

# Usage: docker container commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
docker container commit ubuntu new-ubuntu

这里有一个使用container commit命令将curl安装到正在运行的容器中,并创建一个包含安装的curl命令的新容器的示例:

有了这个例子,我现在可以从ubuntu-curl镜像运行新的容器,它们都将已经安装了curl命令。

回到 Docker 运行命令

现在,让我们回到讨论container run命令。之前,您看到了使用run命令和--publish参数的示例。使用可选的发布参数允许您指定与运行容器相关的将要打开的端口。--publish参数包括用冒号分隔的端口号对。例如:

# create an nginx web-server that redirects host traffic from port 8080 to port 80 in the container
docker container run --detach --name web-server1 --publish 8080:80 nginx

第一个端口号与运行容器的主机相关联。在 nginx 示例中,8080在主机上暴露;在我们的情况下,那将是http://localhost:8080。第二个端口号是运行容器上打开的端口。在这种情况下,它将是80。描述--publish 8080:80参数时,您可以说类似于,发送到主机上端口8080的流量被重定向到运行容器上的端口80

重要的区别在于主机端口和容器端口。我可以在同一系统上运行多个暴露端口80的容器,但是每个端口在主机上只能有一个容器的流量。看下面的例子更好地理解:

# all of these can be running at the same time
docker container run --detach --name web-server1 --publish 80:80 nginx
docker container run --detach --name web-server2 --publish 8000:80 nginx
docker container run --detach --name web-server3 --publish 8080:80 nginx
docker container run --detach --name web-server4 --publish 8888:80 nginx # however if you tried to run this one too, it would fail to run 
# because the host already has port 80 assigned to web-server1
docker container run --detach --name web-server5 --publish 80:80 nginx

要知道这是网络的一般限制,而不是 Docker 或容器的限制。在这里我们可以看到这些命令及其输出。注意端口和名称,以及已经使用的端口作为端点的使用失败:

这是关于container run命令的各种选项参数的大量数据。这并不是所有的选项参数,但应该足够让你有一个很好的开始。如果你想了解更多我们探讨的可选参数,或者找出我们没有涵盖的内容,一定要访问 docker 文档页面上的container run命令,网址是docs.docker.com/engine/reference/run/

总结

在本章中,我们学习了关于 Docker 镜像描述和 Docker 注册表的知识。然后我们看到了版本命令的另一种形式。之后,我们探索了许多 Docker 容器命令,包括runstoplslogstopstatsattachexeccommit命令。最后,我们了解了如何通过从主机到容器打开端口来暴露您的容器。你应该对 Docker 已经能做的事情感到很满意,但是请稍等,在第三章 创建 Docker 镜像中,我们将向您展示如何使用Dockerfile和镜像构建命令创建自己的 Docker 镜像。如果你准备好了,翻页吧。

参考

第三章:创建 Docker 镜像

在本章中,我们将学习如何创建企业级的 Docker 镜像。我们将首先学习 Docker 镜像的主要构建块,具体来说是 Dockerfile。然后,我们将探索 Dockerfile 中可用的所有指令。有一些指令在表面上看起来非常相似。我们将揭示COPYADD指令之间的区别,ENVARG指令之间的区别,以及最重要的是CMDENTRYPOINT指令之间的区别。接下来,我们将了解构建上下文是什么以及为什么它很重要。最后,我们将介绍实际的镜像构建命令。

如果得到良好维护,普通的集装箱的平均寿命约为 20 年,而 Docker 容器的平均寿命为 2.5 天。- www.tintri.com/blog/2017/03/tintri-supports-containers-advanced-storage-features

在本章中,我们将涵盖以下主题:

  • 什么是 Dockerfile?

  • Dockerfile 中可以使用的所有指令

  • 何时使用COPYADD指令

  • ENVARG变量之间的区别

  • 为什么要使用CMDENTRYPOINT指令

  • 构建上下文的重要性

  • 使用 Dockerfile 构建 Docker 镜像

技术要求

您将从 Docker 的公共存储库中拉取 Docker 镜像,因此需要基本的互联网访问权限来执行本章中的示例。

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Docker-Quick-Start-Guide/tree/master/Chapter03

查看以下视频以查看代码的实际操作:bit.ly/2rbHvwC

什么是 Dockerfile?

您在第二章中学到,您可以运行 Docker 容器,对正在运行的容器进行修改,然后使用docker commit命令保存这些更改,从而有效地创建一个新的 Docker 镜像。尽管这种方法有效,但不是创建 Docker 容器的首选方式。创建 Docker 镜像的最佳方式是使用具有描述所需镜像的 Dockerfile 的 Docker 镜像构建命令。

Dockerfile(是的,正确的拼写是一个词,首字母大写D)是一个文本文件,其中包含 Docker 守护程序用来创建 Docker 镜像的指令。指令使用一种键值对语法进行定义。每个指令都在 Dockerfile 中占据一行。虽然 Dockerfile 指令不区分大小写,但有一个常用的约定,即指令单词始终大写。

Dockerfile 中指令的顺序很重要。指令按顺序评估,从 Dockerfile 的顶部开始,直到文件的底部结束。如果您还记得第一章中的内容,Docker 镜像由层组成。Dockerfile 中的所有指令都会导致生成一个新的层,因此在构建 Docker 镜像时,但是,某些指令只会向创建的镜像添加一个大小为零的元数据层。由于最佳实践是尽可能保持 Docker 镜像尽可能小,因此您将希望尽可能高效地使用创建非零字节大小层的指令。在接下来的部分中,我们将注意到使用指令创建非零字节大小层的地方,以及如何最好地使用该指令来最小化层数量和大小。另一个重要的考虑因素是指令的顺序。某些指令必须在其他指令之前使用,但除了这些例外情况,您可以按任何顺序放置其他指令。最佳实践是在 Dockerfile 的早期使用变化最小的指令,在 Dockerfile 的后期使用变化更频繁的指令。原因是当您需要重新构建镜像时,只有在 Dockerfile 中第一行更改的位置或之后的层才会被重新构建。如果您还不理解这一点,不用担心,一旦我们看到一些例子,它就会更有意义。

我们将在本节末尾回顾构建命令,但我们将从 Dockerfile 可用的指令开始,首先是必须是 Dockerfile 中的第一个指令的指令:FROM指令。

FROM 指令

每个 Dockerfile 必须有一个FROM指令,并且它必须是文件中的第一个指令。(实际上,FROM指令之前可以使用 ARG 指令,但这不是必需的指令。我们将在 ARG 指令部分更多地讨论这个。)

FROM指令设置正在创建的镜像的基础,并指示 Docker 守护程序新镜像的基础应该是指定为参数的现有 Docker 镜像。指定的镜像可以使用与我们在第二章中看到的 Docker container run命令相同的语法来描述。在这里,它是一个FROM指令,指定使用官方的nginx镜像,版本为 1.15.2:

# Dockerfile
FROM nginx:1.15.2

请注意,在这个例子中,没有指定指示指定的镜像是官方 nginx 镜像的存储库。如果没有指定标签,将假定为latest标签。

FROM指令将创建我们新镜像中的第一层。该层将是指令参数中指定的镜像大小,因此最好指定满足新镜像所需条件的最小镜像。一个特定于应用程序的镜像,比如nginx,会比一个操作系统镜像,比如 ubuntu,要小。而alpine的操作系统镜像会比其他操作系统的镜像,比如 Ubuntu、CentOS 或 RHEL,要小得多。FROM指令可以使用一个特殊的关键字作为参数。它是scratch。Scratch 不是一个可以拉取或运行的镜像,它只是向 Docker 守护程序发出信号,表明你想要构建一个带有空基础镜像层的镜像。FROM scratch指令被用作许多其他基础镜像的基础层,或者用于专门的应用程序特定镜像。你已经看到了这样一个专门的应用程序镜像的例子:hello-world。hello-world 镜像的完整 Dockerfile 如下:

# hello-world Dockerfile
FROM scratch
COPY hello /
CMD ["/hello"]

我们将很快讨论COPYCMD指令,但是你应该根据它的 Dockerfile 来感受一下 hello-world 镜像有多小。在 Docker 镜像的世界中,越小越好。参考一下一些镜像的大小:

标签指令

LABEL 指令是向 Docker 镜像添加元数据的一种方法。当创建镜像时,此指令会向镜像添加嵌入式键值对。一个镜像可以有多个 LABEL,并且每个 LABEL 指令可以提供一个或多个标签。LABEL 指令最常见的用途是提供有关镜像维护者的信息。这些数据以前有自己的指令。请参阅有关现在已弃用的 MAINTAINER 指令的下面提示框。以下是一些有效的 LABEL 指令示例:

# LABEL instruction syntax
# LABEL <key>=<value> <key>=<value> <key>=<value> ...
LABEL maintainer="Earl Waud <earlwaud@mycompany.com>"
LABEL "description"="My development Ubuntu image"
LABEL version="1.0"
LABEL label1="value1" \
 label2="value2" \
 lable3="value3"
LABEL my-multi-line-label="Labels can span \
more than one line in a Dockerfile."
LABEL support-email="support@mycompany.com" support-phone="(123) 456-7890"

LABEL 指令是 Dockerfile 中可以多次使用的指令之一。你将会在后面学到,一些可以多次使用的指令只会保留最后一次使用的内容,忽略之前的所有使用。但是 LABEL 指令不同。每次使用 LABEL 指令都会向生成的镜像添加一个额外的标签。然而,如果两次或更多次使用 LABEL 具有相同的键,标签将获得最后一个匹配的 LABEL 指令中提供的值。就像这样:

# earlier in the Dockerfile
LABEL version="1.0"
# later in the Dockerfile...
LABEL version="2.0"
# The Docker image metadata will show version="2.0"

重要的是要知道,在你的 FROM 指令中指定的基础镜像可能包含使用 LABEL 指令创建的标签,并且它们将自动包含在你正在构建的镜像的元数据中。如果你的 Dockerfile 中的 LABEL 指令使用与 FROM 镜像的 Dockerfile 中使用的 LABEL 指令相同的键,你(后来的)值将覆盖 FROM 镜像中的值。你可以使用 inspect 命令查看镜像的所有标签:

MAINTAINER 指令有一个专门用于提供有关镜像维护者信息的 Dockerfile 指令,但是这个指令已经被弃用。不过,你可能会在某个时候看到它在 Dockerfile 中被使用。语法如下:"maintainer": "Earl Waud <earlwaud@mycompany.com>"

COPY 指令

你已经在“FROM 指令”部分的 hello-world Dockerfile 中看到了使用 COPY 指令的示例。COPY 指令用于将文件和文件夹复制到正在构建的 Docker 镜像中。COPY 指令的语法如下:

# COPY instruction syntax
COPY [--chown=<user>:<group>] <src>... <dest>
# Use double quotes for paths containing whitespace)
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]

请注意,--chown 参数仅适用于基于 Linux 的容器。如果没有--chown 参数,所有者 ID 和组 ID 都将设置为 0。

<src>或源是文件名或文件夹路径,并且被解释为相对于构建的上下文。我们稍后会在本章中更多地讨论构建上下文,但现在,将其视为构建命令运行的位置。源可能包括通配符。

<dest>或目标是正在创建的图像中的文件名或路径。目标是相对于图像文件系统的根目录,除非有一个前置的WORKDIR指令。我们稍后会讨论WORKDIR指令,但现在,只需将其视为设置当前工作目录的一种方式。当COPY命令在 Dockerfile 中的WORKDIR指令之后出现时,复制到图像中的文件或文件夹将被放置在相对于当前工作目录的目标中。如果目标包括一个或多个文件夹的路径,如果它们不存在,所有文件夹都将被创建。

在我们之前的 hello-world Dockerfile 示例中,您看到了一个COPY指令,它将一个名为hello的可执行文件复制到图像的文件系统根位置。它看起来像这样:COPY hello /。这是一个基本的COPY指令。以下是一些其他示例:

# COPY instruction Dockerfile for Docker Quick Start
FROM alpine:latest
LABEL maintainer="Earl Waud <earlwaud@mycompany.com>"
LABEL version=1.0
# copy multiple files, creating the path "/theqsg/files" in the process
COPY file* theqsg/files/
# copy all of the contents of folder "folder1" to "/theqsg/" 
# (but not the folder "folder1" itself)
COPY folder1 theqsg/
# change the current working directory in the image to "/theqsg"
WORKDIR theqsg
# copy the file special1 into "/theqsg/special-files/"
COPY --chown=35:35 special1 special-files/
# return the current working directory to "/"
WORKDIR /
CMD ["sh"]

通过从图像运行容器并执行ls命令,我们可以看到使用前面的 Dockerfile 得到的图像文件系统会是什么样子:

您可以看到在目标路径中指定的文件夹在复制期间被创建。您还会注意到提供--chown参数会设置目标文件的所有者和组。一个重要的区别是当源是一个文件夹时,文件夹的内容会被复制,但文件夹本身不会被复制。请注意,使用WORKDIR指令会更改图像文件系统中的路径,并且随后的COPY指令现在将相对于新的当前工作目录。在这个例子中,我们将当前工作目录返回到/,以便在容器中执行的命令将相对于/运行。

ADD 指令

ADD指令用于将文件和文件夹复制到正在构建的 Docker 图像中。ADD指令的语法如下:

# ADD instruction syntax
ADD [--chown=<user>:<group>] <src>... <dest>
# Use double quotes for paths containing whitespace)
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]

现在,您可能会认为ADD指令似乎就像我们刚刚审查的COPY指令一样。嗯,你没错。基本上,我们看到COPY指令所做的所有事情,ADD指令也可以做。它使用与COPY指令相同的语法,两者之间的WORKDIR指令的效果也是相同的。那么,为什么我们有两个执行相同操作的命令呢?

COPY 和 ADD 之间的区别

答案是ADD指令实际上可以比COPY指令做更多。更多取决于用于源输入的值。使用COPY指令时,源可以是文件或文件夹。然而,使用ADD指令时,源可以是文件、文件夹、本地.tar文件或 URL。

ADD指令的源值是.tar文件时,该 TAR 文件的内容将被提取到镜像中的相应文件夹中。

当您在ADD指令中使用.tar文件作为源并包括--chown参数时,您可能期望在从存档中提取的文件上设置图像中的所有者和组。目前情况并非如此。不幸的是,尽管使用了--chown参数,提取内容的所有者、组和权限将与存档中包含的内容相匹配。当您使用.tar文件时,您可能希望在 ADD 之后包含RUN chown -R X:X

如前所述,ADD指令可以使用 URL 作为源值。以下是一个包含使用 URL 的ADD指令的示例 Dockerfile:

# ADD instruction Dockerfile for Docker Quick Start
FROM alpine
LABEL maintainer="Earl Waud <earlwaud@mycompany.com>"
LABEL version=3.0
ADD https://github.com/docker-library/hello-world/raw/master/amd64/hello-world/hello /
RUN chmod +x /hello
CMD ["/hello"]

ADD指令中使用 URL 是有效的,将文件下载到镜像中,但是这个功能并不被 Docker 推荐。以下是 Docker 文档对使用ADD的建议:

因此,一般来说,每当您可以使用COPY指令将所需内容放入镜像时,您应该选择使用COPY而不是ADD

ENV 指令

正如您可能猜到的那样,ENV指令用于定义将在从正在构建的镜像创建的运行容器中设置的环境变量。使用典型的键值对定义变量。Dockerfile 可以有一个或多个ENV指令。以下是ENV指令的语法:

# ENV instruction syntax
# This is the form to create a single environment variable per instruction
# Everything after the space following the <key> becomes the value
ENV <key> <value>
# This is the form to use when you want to create more than one variable per instruction
ENV <key>=<value> ...

每个ENV指令将创建一个或多个环境变量(除非键名重复)。让我们看一下 Dockerfile 中的一些ENV指令:

# ENV instruction Dockerfile for Docker Quick Start
FROM alpine
LABEL maintainer="Earl Waud <earlwaud@mycompany.com>"
ENV appDescription This app is a sample of using ENV instructions
ENV appName=env-demo
ENV note1="The First Note First" note2=The\ Second\ Note\ Second \
note3="The Third Note Third"
ENV changeMe="Old Value"
CMD ["sh"]

使用此 Dockerfile 构建镜像后,您可以检查镜像元数据,并查看已创建的环境变量:

环境变量可以在运行容器时使用--env参数进行设置(或覆盖)。在这里,我们看到了这个功能的实际应用:

重要的是要知道,使用ENV指令会在生成的镜像中创建一个大小为零字节的额外层。如果要向镜像添加多个环境变量,并且可以使用支持一次设置多个变量的指令形式,那么只会创建一个额外的镜像层,因此这是一个好方法。

ARG 指令

有时在构建 Docker 镜像时,您可能需要使用变量数据来自定义构建。ARG指令是处理这种情况的工具。要使用它,您需要将ARG指令添加到 Dockerfile 中,然后在执行构建命令时,通过--build-arg参数传入变量数据。--build-arg参数使用现在熟悉的键值对格式:

# The ARG instruction syntax
ARG <varname>[=<default value>]

# The build-arg parameter syntax
docker image build --build-arg <varname>[=<value>] ...

您可以在 Dockerfile 中使用多个ARG指令,并在 docker image build 命令上使用相应的--build-arg参数。对于每个--build-arg参数的使用,都必须包括一个ARG指令。如果没有ARG指令,则在构建过程中--build-arg参数将不会被设置,并且您将收到警告消息。如果您没有提供--build-arg参数,或者没有为现有的ARG指令提供--build-arg参数的值部分,并且该ARG指令包括默认值,那么变量将被分配默认值。

请注意,在镜像构建过程中,即使--build-arg被包括为 docker image build 命令的参数,相应的变量也不会在 Dockerfile 中的ARG指令到达之前设置。换句话说,--build-arg参数的键值对的值在其对应的ARG行之后才会被设置。

ARG 指令中定义的参数不会持续到从创建的镜像运行的容器中,但是 ARG 指令会在生成的镜像中创建新的零字节大小的层。以下是使用 ARG 指令的教育示例:

# ARG instruction Dockerfile for Docker Quick Start
FROM alpine
LABEL maintainer="Earl Waud <earlwaud@mycompany.com>"

ENV key1="ENV is stronger than an ARG"
RUN echo ${key1}
ARG key1="not going to matter"
RUN echo ${key1}

RUN echo ${key2}
ARG key2="defaultValue"
RUN echo ${key2}
ENV key2="ENV value takes over"
RUN echo ${key2}
CMD ["sh"]

创建一个包含上述代码块中显示的内容的 Dockerfile,并运行以下构建命令,以查看 ENVARG 指令的范围如何发挥作用:

# Build the image and look at the output from the echo commands
 docker image build --rm \
 --build-arg key1="buildTimeValue" \
 --build-arg key2="good till env instruction" \
 --tag arg-demo:2.0 .

第一个 echo ${key1} 会让你看到,即使有一个 --build-arg 参数用于 key1,它也不会被存储为 key1,因为有一个相同键名的 ENV 指令。这对于第二个 echo ${key1} 仍然成立,这是在 ARG key1 指令之后。当 ARGEVN 指令具有相同的键名时,ENV 变量值总是获胜。

然后,你会看到第一个 echo ${key2} 是空的,即使有一个 --build-arg 参数。这是因为我们还没有达到 ARG key2 指令。第二个 echo ${key2} 将包含相应 --build-arg 参数的值,即使在 ARG key2 指令中提供了默认值。最终的 echo ${key2} 将显示在 ENV key2 指令中提供的值,尽管在 ARG 中有默认值,并且通过 --build-arg 参数传递了一个值。同样,这是因为 ENV 总是胜过 ARG。

ENV 和 ARG 之间的区别

这是一对具有类似功能的指令。它们都可以在构建镜像时使用,设置参数以便在其他 Dockerfile 指令中使用。可以使用这些参数的其他 Dockerfile 指令包括 FROMLABELCOPYADDENVUSERWORKDIRRUNVOLUMEEXPOSESTOPSIGNALONBUILD。以下是在其他 Docker 命令中使用 ARGENV 变量的示例:

# ENV vs ARG instruction Dockerfile for Docker Quick Start
FROM alpine
LABEL maintainer="Earl Waud <earlwaud@mycompany.com>"
ENV lifecycle="production"
RUN echo ${lifecycle}
ARG username="35"
RUN echo ${username}
ARG appdir
RUN echo ${appdir}
ADD hello /${appdir}/
RUN chown -R ${username}:${username} ${appdir}
WORKDIR ${appdir}
USER ${username}
CMD ["./hello"]

使用这个 Dockerfile,你会想为 appdir ARG 指令提供 --build-arg 参数,并且在构建命令中提供用户名(如果你想要覆盖默认值)。你也可以在运行时提供一个 --env 参数来覆盖生命周期变量。以下是可能使用的构建和运行命令:

# Build the arg3 demo image
docker image build --rm \
 --build-arg appdir="/opt/hello" \
 --tag arg-demo:3.0 .

# Run the arg3 demo container
docker container run --rm --env lifecycle="test" arg-demo:3.0

虽然 ENVARG 指令可能看起来相似,但它们实际上是非常不同的。以下是记住 ENVARG 指令创建的参数之间的关键区别:

  • ENV 持续存在于运行中的容器中,ARG 不会。

  • ARG 使用相应的构建参数,ENV 不使用。

  • ENV指令必须包括键和值,ARG指令有一个键,但(默认)值是可选的。

  • ENV 比 ARG 更重要。

永远不要使用ENVARG指令向构建命令或生成的容器提供秘密数据,因为这些值对于运行 docker history 命令的任何用户都是明文可见的。

USER 指令

USER 指令允许您为 Dockerfile 中接下来的所有指令和从构建图像运行的容器设置当前用户(和组)。USER指令的语法如下:

# User instruction syntax
USER <user>[:<group>] or
USER <UID>[:<GID>]

如果将命名用户(或组)作为USER指令的参数提供,则该用户(和组)必须已经存在于系统的 passwd 文件(或组文件)中,否则将发生构建错误。如果将UID(或GID)作为USER命令的参数提供,则不会执行检查用户(或组)是否存在。考虑以下 Dockerfile:

# USER instruction Dockerfile for Docker Quick Start 
FROM alpine
LABEL maintainer="Earl Waud <earl@mycompany.com>"
RUN id
USER games:games
run id
CMD ["sh"]

当图像构建开始时,当前用户是 root 或UID=0 GID=0。然后,执行USER指令将当前用户和组设置为games:games。由于这是 Dockerfile 中USER指令的最后一次使用,所有使用构建图像运行的容器将具有当前用户(和组)设置为 games。构建和运行如下所示:

请注意,步骤 3/6 的 RUN id 的输出显示当前用户为 root,然后在步骤 5/6(在USER指令之后)中显示当前用户为 games。最后,请注意,从图像运行的容器具有当前用户 games。USER指令在图像中创建了一个大小为零字节的层。

WORKDIR 指令

我们已经在一些示例中看到了WORKDIR指令的使用,用于演示其他指令。它有点像 Linux 的cdmkdir命令的组合。WORKDIR指令将把图像中的当前工作目录更改为指令中提供的值。如果参数中路径的任何部分尚不存在,则将作为执行指令的一部分创建它。WORKDIR指令的语法如下:

# WORKDIR instruction syntax
WORKDIR instruction syntax
WORKDIR /path/to/workdir

WORKDIR指令可以使用ENVARG参数值作为其参数的全部或部分。Dockerfile 可以有多个WORKDIR指令,每个后续的WORKDIR指令将相对于前一个(如果使用相对路径)。以下是演示此可能性的示例:

# WORKDIR instruction Dockerfile for Docker Quick Start
FROM alpine
# Absolute path...
WORKDIR /
# relative path, relative to previous WORKDIR instruction
# creates new folder
WORKDIR sub-folder-level-1
RUN touch file1.txt
# relative path, relative to previous WORKDIR instruction
# creates new folder
WORKDIR sub-folder-level-2
RUN touch file2.txt
# relative path, relative to previous WORKDIR instruction
# creates new folder
WORKDIR sub-folder-level-3
RUN touch file3.txt
# Absolute path, creates three sub folders...
WORKDIR /l1/l2/l3
CMD ["sh"]

从这个 Dockerfile 构建镜像将导致镜像具有三层嵌套的文件夹。从镜像运行容器并列出文件和文件夹将如下所示:

WORKDIR指令将在生成的镜像中创建一个大小为零字节的层。

VOLUME 指令

您应该记住,Docker 镜像由一系列相互叠加的只读层组成,当您从 Docker 镜像运行容器时,它会创建一个新的读写层,您可以将其视为位于只读层之上。所有对容器的更改都应用于读写层。如果对只读层中的文件进行更改,将会创建该文件的副本并将其添加到读写层。然后,所有更改都将应用于该副本。该副本隐藏了只读层中找到的版本,因此从运行的容器的角度来看,文件只有一个版本,即已更改的版本。这大致是统一文件系统的工作原理。

这实际上是一件好事。但是,它也带来了一个挑战,即当运行的容器退出并被删除时,所有更改也将被删除。这通常是可以接受的,直到您希望在容器的生命周期之后保留一些数据,或者希望在容器之间共享数据时。Docker 有一条指令可以帮助您解决这个问题,那就是VOLUME指令。

VOLUME指令将创建一个存储位置,该位置位于美国文件系统之外,并且通过这样做,允许存储在容器的生命周期之外持久存在。以下是VOLUME指令的语法:

# VOLUME instruction syntax
VOLUME ["/data"]
# or for creating multiple volumes with a single instruction
VOLUME /var/log /var/db /moreData

创建卷的其他方法是向 docker container run命令添加卷参数,或者使用 docker volume create 命令。我们将在第四章 Docker Volumes中详细介绍这些方法。

这是一个简单的示例 Dockerfile。它在/myvol创建了一个卷,其中将有一个名为greeting的文件:

# VOLUME instruction Dockerfile for Docker Quick Start
FROM alpine
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol
CMD ["sh"]

基于从此 Dockerfile 创建的镜像运行容器将在主机系统上创建一个挂载点,最初包含greeting文件。当容器退出时,挂载点将保留。在运行具有要持久保存的挂载点的容器时,使用--rm参数要小心。使用--rm,没有其他卷参数,将导致容器退出时清理挂载点。看起来是这样的:

我们开始时没有卷。然后,我们以分离模式运行了一个基于前面的 Dockerfile 创建的镜像的容器。我们再次检查卷,看到了通过运行容器创建的卷。然后,我们停止容器并再次检查卷,现在卷已经消失了。通常,使用VOLUME指令的目的是在容器消失后保留挂载点中的数据。因此,如果您要在运行容器时使用--rm,您应该包括--mount运行参数,我们将在第四章 Docker Volumes中详细介绍。

您可以使用卷的挂载点与主机上的数据进行交互。以下是一个演示这一点的示例:

在这个演示中,我们运行了一个基于前面的 Dockerfile 创建的镜像的容器。然后,我们列出了卷,并查看了 myvolsrc 卷(我们已经知道了名称,因为我们在运行命令中提供了它,但您可以使用ls命令来查找您可能不知道的卷名称)。使用卷的名称,我们检查卷以找到它在主机上的挂载点。为了验证容器中卷的内容,我们使用 exec 命令来列出文件夹。接下来,使用挂载点路径,我们使用 touch 命令创建一个新文件。最后,我们使用相同的 exec 命令,并看到容器内的卷已经改变(来自容器外的操作)。同样,如果容器更改卷的内容,这些更改将立即反映在主机挂载点上。

前面的示例在 OS X 上直接显示不起作用。它需要一些额外的工作。不过不要惊慌!我们将向您展示如何处理 OS X 所需的额外工作,在第四章 Docker Volumes中。

使用VOLUME指令既强大又危险。它之所以强大,是因为它让您拥有超出容器生命周期的数据。它之所以危险,是因为数据会立即从容器传递到主机,如果容器被攻击,可能会带来麻烦。出于安全考虑,最佳实践是在 Dockerfile 中包含基于主机的 VOLUME 挂载。我们将在第四章中介绍一些更安全的替代方法,Docker Volumes

VOLUME指令将在生成的 Docker 镜像中添加一个大小为零字节的层。

EXPOSE 指令

EXPOSE指令是记录镜像期望在使用 Dockerfile 构建的镜像运行容器时打开的网络端口的一种方式。EXPOSE指令的语法如下:

# EXPOSE instruction syntax
EXPOSE <port> [<port>/<protocol>...]

重要的是要理解,在 Dockerfile 中包含EXPOSE指令实际上并不会在容器中打开网络端口。当从具有EXPOSE指令的 Dockerfile 中的镜像运行容器时,仍然需要包括-p-P参数来实际打开网络端口到容器。

根据需要在 Dockerfile 中包含多个EXPOSE指令。在运行时包括-P参数是一种快捷方式,可以自动打开 Dockerfile 中包含的所有EXPOSE指令的端口。在运行命令时使用-P参数时,相应的主机端口将被随机分配。

EXPOSE指令视为镜像开发者向您传达的信息,告诉您在运行容器时,镜像中的应用程序期望您打开指定的端口。EXPOSE指令在生成的镜像中创建一个大小为零字节的层。

RUN 指令

RUN指令是 Dockerfile 的真正工作马。这是您对生成的 Docker 镜像产生最大变化的工具。基本上,它允许您在镜像中执行任何命令。RUN指令有两种形式。以下是语法:

# RUN instruction syntax
# Shell form to run the command in a shell
# For Linux the default is "/bin/sh -c"
# For Windows the default is "cmd /S /C"
RUN <command>

# Exec form
RUN ["executable", "param1", "param2"]

每个RUN指令在镜像中创建一个新的层,随后的每个指令的层都将建立在RUN指令的层的结果之上。除非使用SHELL指令覆盖,默认情况下,shell 形式的指令将使用默认 shell。如果您正在构建一个不包含 shell 的容器,您将需要使用RUN指令的 exec 形式。您还可以使用 exec 形式的指令来使用不同的 shell。例如,要使用 bash shell 运行命令,您可以添加一个RUN指令,如下所示:

# Exec form of RUN instruction using bash
RUN ["/bin/bash", "-c", "echo hello world > /myvol/greeting"]

RUN命令的用途仅受想象力的限制,因此提供RUN指令示例的详尽列表是不可能的,但以下是一些使用两种形式的指令的示例,只是为了给您一些想法:

# RUN instruction Dockerfile for Docker Quick Start
FROM ubuntu
RUN useradd --create-home -m -s /bin/bash dev
RUN mkdir /myvol
RUN echo "hello DQS Guide" > /myvol/greeting
RUN ["chmod", "664", "/myvol/greeting"]
RUN ["chown", "dev:dev", "/myvol/greeting"]
VOLUME /myvol
USER dev
CMD ["/bin/bash"]

当您知道您的镜像将包含 bash 时,可以添加一个有趣且有用的RUN指令。这个想法是我在 Dockercon 16 上得知的,由我的同事Marcello de Sales与我分享。您可以使用以下代码在 shell 进入容器时创建自定义提示。如果您不喜欢鲸鱼图形,可以更改并使用任何您喜欢的东西。我包括了一些我喜欢的选项。以下是代码:

# RUN instruction Dockerfile for Docker Quick Start
FROM ubuntu
RUN useradd --create-home -m -s /bin/bash dev
# Add a fun prompt for dev user of my-app
# whale: "\xF0\x9F\x90\xB3"
# alien:"\xF0\x9F\x91\xBD"
# fish:"\xF0\x9F\x90\xA0"
# elephant:"\xF0\x9F\x91\xBD"
# moneybag:"\xF0\x9F\x92\xB0"
RUN echo 'PS1="\[$(tput bold)$(tput setaf 4)\]my-app $(echo -e "\xF0\x9F\x90\xB3") \[$(tput sgr0)\] [\\u@\\h]:\\W \\$ "' >> /home/dev/.bashrc && \
 echo 'alias ls="ls --color=auto"' >> /home/dev/.bashrc
USER dev
CMD ["/bin/bash"]

生成的提示如下:

CMD指令

CMD指令用于定义从使用其 Dockerfile 构建的镜像运行容器时采取的默认操作。虽然在 Dockerfile 中可以包含多个CMD指令,但只有最后一个才会有意义。基本上,最后一个CMD指令为镜像提供了默认操作。这允许您在 Dockerfile 的FROM指令中覆盖或使用镜像中的CMD。以下是一个示例,其中一个微不足道的 Dockerfile 不包含CMD指令,并依赖于在FROM指令中使用的 ubuntu 镜像中找到的指令:

您可以从 history 命令的输出中看到,ubuntu 镜像包括CMD ["/bin/bash"]指令。您还会看到我们的 Dockerfile 没有自己的CMD指令。当我们运行容器时,默认操作是运行"/bin/bash"

CMD指令有三种形式。第一种是 shell 形式。第二种是 exec 形式,这是最佳实践形式。第三种是特殊的 exec 形式,它有两个参数,并且与ENTRYPOINT指令一起使用,我们将在ENTRYPOINT 指令部分讨论它。以下是CMD指令的语法。

# CMD instruction syntax
CMD command param1 param2 (shell form)
CMD ["executable","param1","param2"] (exec form)
CMD ["param1","param2"] (as default parameters to ENTRYPOINT)

以下是一些CMD指令的示例供您参考:

# CMD instruction examples
CMD ["/bin/bash"]
CMD while true; do echo 'DQS Expose Demo' | nc -l -p 80; done
CMD echo "How many words are in this echo command" | wc -
CMD tail -f /dev/null
CMD ["-latr", "/var/opt"]

RUN指令一样,CMD指令的 shell 形式默认使用["/bin/sh", "-c"] shell 命令(或["cmd", "/S", "/C"]用于 Windows),除非它被SHELL指令覆盖。然而,与RUN指令不同,CMD指令在构建镜像时不执行任何操作,而是在从镜像构建的容器运行时执行。如果正在构建的容器镜像没有 shell,则可以使用指令的 exec 形式,因为它不会调用 shell。CMD指令向镜像添加了一个大小为零字节的层。

ENTRYPOINT 指令

ENTRYPOINT指令用于配置 docker 镜像以像应用程序或命令一样运行。例如,我们可以使用ENTRYPOINT指令制作一个显示curl命令帮助信息的镜像。考虑这个 Dockerfile:

# ENTRYPOINT instruction Dockerfile for Docker Quick Start
FROM alpine
RUN apk add curl
ENTRYPOINT ["curl"]
CMD ["--help"]

我们可以运行容器镜像,不覆盖CMD参数,它将显示curl命令的帮助信息。然而,当我们用CMD覆盖参数运行容器时,在这种情况下是一个 URL,响应将是curl该 URL。看一下:

当为具有ENTRYPOINT指令的 exec 形式的容器提供运行参数时,这些参数将附加到ENTRYPOINT指令,覆盖CMD指令中提供的任何内容。在这个例子中,--helpgoogle.com运行参数覆盖,所以结果指令是curl google.com。以下是ENTRYPOINT指令的实际语法:

# ENTRYPOINT instruction syntax
ENTRYPOINT command param1 param2 (shell form)
ENTRYPOINT ["executable", "param1", "param2"] (exec form, best practice)

CMD指令一样,只有最后一个ENTRYPOINT指令是重要的。同样,这允许您在使用FROM镜像时使用或覆盖ENTRYPOINT指令。与RUNCMD指令一样,使用 shell 形式将调用["/bin/sh", "-c"](或在 Windows 上为["cmd", "/S", "/C"])。当使用指令的 exec 形式时,情况并非如此。这对于没有 shell 或 shell 不可用于活动用户上下文的镜像非常重要。但是,您将不会获得 shell 处理,因此在使用指令的 exec 形式时,任何 shell 环境变量都不会被替换。通常最好尽可能使用ENTRYPOINT指令的 exec 形式。

CMD 和 ENTRYPOINT 之间的区别

在这里,我们再次有两个表面上看起来非常相似的指令。事实上,它们之间确实有一些功能重叠。这两个指令都提供了一种定义在运行容器时执行的默认应用程序的方法。然而,它们各自有其独特的目的,并且在某些情况下共同工作,以提供比任何一条指令单独提供的更大的功能。

最佳实践是在希望容器作为应用程序执行、提供特定(开发者)定义的功能时使用ENTRYPOINT指令,并在希望为用户提供更多灵活性以确定容器将提供的功能时使用CMD

这两个指令都有两种形式:shell 形式和 exec 形式。最佳实践是尽可能使用任何一种的 exec 形式。原因是,shell 形式将运行["/bin/sh", "-c"](或在 Windows 上为["cmd", "/S", "/C"])来启动指令参数中的应用程序。由于这个原因,运行在容器中的主要进程不是应用程序,而是 shell。这会影响容器的退出方式,影响信号的处理方式,并且可能会对不包括"/bin/sh"的镜像造成问题。您可能需要使用 shell 形式的一个用例是如果您需要 shell 环境变量替换。

在 Dockerfile 中还有一个使用两个指令的用例。当您同时使用两者时,可以定义在运行容器时执行的特定应用程序,并允许用户轻松提供与定义的应用程序一起使用的参数。在这种情况下,您将使用ENTRYPOINT指令设置要执行的应用程序,并使用CMD指令为应用程序提供一组默认参数。通过这种配置,容器的用户可以从CMD指令中提供的默认参数中受益,或者他们可以通过在container run命令中提供参数作为参数轻松覆盖应用程序中使用的这些参数。强烈建议在同时使用两个指令时使用它们的 exec 形式。

HEALTHCHECK指令

HEALTHCHECK指令是 Dockerfile 中相对较新的添加,用于定义在容器内运行的命令,以测试容器的应用程序健康状况。当容器具有HEALTHCHECK时,它会获得一个特殊的状态变量。最初,该变量将被设置为starting。每当成功执行HEALTHCHECK时,状态将被设置为healthy。当执行HEALTHCHECK并失败时,失败计数值将被递增,然后与重试值进行比较。如果失败计数等于或超过重试值,则状态将被设置为unhealthyHEALTHCHECK指令的语法如下:

# HEALTHCHECK instruction syntax
HEALTHCHECK [OPTIONS] CMD command (check container health by running a command inside the container)
HEALTHCHECK NONE (disable any HEALTHCHECK inherited from the base image)

在设置HEALTHCHECK时有四个选项可用,这些选项如下:

# HEALTHCHECK CMD options
--interval=DURATION (default: 30s)
--timeout=DURATION (default: 30s)
--start-period=DURATION (default: 0s)
--retries=N (default: 3)

--interval选项允许您定义HEALTHCHECK测试之间的时间间隔。--timeout选项允许您定义被视为HEALTHCHECK测试时间过长的时间量。如果超过超时时间,测试将自动视为失败。--start-period选项允许在容器启动期间定义一个无失败时间段。最后,--retries选项允许您定义多少连续失败才能将HEALTHCHECK状态更新为unhealthy

HEALTHCHECK指令的CMD部分遵循与CMD指令相同的规则。有关CMD指令的完整详情,请参阅前面的部分。使用的CMD在退出时将提供一个状态,该状态要么是成功的 0,要么是失败的 1。以下是使用HEALTHCHECK指令的 Dockerfile 示例:

# HEALTHCHECK instruction Dockerfile for Docker Quick Start
FROM alpine
RUN apk add curl
EXPOSE 80/tcp
HEALTHCHECK --interval=30s --timeout=3s \
 CMD curl -f http://localhost/ || exit 1
CMD while true; do echo 'DQS Expose Demo' | nc -l -p 80; done

使用上述 Dockerfile 构建的镜像运行容器如下:

您可以看到HEALTHCHECK最初报告状态为starting,但一旦HEALTHCHECK CMD报告成功,状态就会更新为healthy

ONBUILD 指令

ONBUILD指令是在创建将成为另一个 Dockerfile 中FROM指令参数的镜像时使用的工具。ONBUILD指令只是向您的镜像添加元数据,具体来说是存储在镜像中而不被其他方式使用的触发器。然而,当您的镜像作为另一个 Dockerfile 中FROM命令的参数提供时,该元数据触发器会被使用。以下是ONBUILD指令的语法:

# ONBUILD instruction syntax
ONBUILD [INSTRUCTION]

ONBUILD指令有点像 Docker 时间机器,用于将指令发送到未来。(如果您知道我刚刚输入Doctor time machine多少次,您可能会笑!)让我们用一个简单的例子来演示ONBUILD指令的使用。首先,我们将使用以下 Dockerfile 构建一个名为my-base的镜像:

# my-base Dockerfile
FROM alpine
LABEL maintainer="Earl Waud <earlwaud@mycompany.com>"
ONBUILD LABEL version="1.0"
ONBUILD LABEL support-email="support@mycompany.com" support-phone="(123) 456-7890"
CMD ["sh"]

接下来,让我们构建一个名为my-app的镜像,该镜像是从my-base镜像构建的,如下所示:

# my-app Dockerfile
FROM my-base:1.0
CMD ["sh"]

检查生成的my-app镜像,我们可以看到ONBUILD指令中提供的 LABEL 命令被发送到未来,到达my-app镜像:

如果您对my-base镜像进行类似的检查,您会发现它包含版本和支持标签。还要注意,ONBUILD指令是一次性使用的时间机器。如果您使用FROM指令中的my-app构建一个新的镜像,新的镜像将获得my-base镜像的 ONBUILD 指令中提供的标签。

STOPSIGNAL 指令

STOPSIGNAL指令用于设置系统调用信号,该信号将被发送到容器,告诉它退出。指令中使用的参数可以是无符号数字,等于内核系统调用表中的位置,也可以是大写的实际信号名称。以下是该指令的语法:

# STOPSIGNAL instruction syntax
STOPSIGNAL signal

STOPSIGNAL指令的示例包括以下内容:

# Sample STOPSIGNAL instruction using a position number in the syscall table
STOPSIGNAL 9
# or using a signal name
STOPSIGNAL SIGQUIT

STOPSIGNAL指令提供的参数在发出docker container stop命令时使用。请记住,使用ENTRYPOINT和/或CMD指令的执行形式非常重要,以便应用程序成为 PID 1,并直接接收信号。以下是有关在 Docker 中使用信号的出色博客文章链接:medium.com/@gchudnov/trapping-signals-in-docker-containers-7a57fdda7d86。该文章提供了使用 node.js 应用程序处理信号的出色示例,包括代码和 Dockerfile。

SHELL指令

正如您在本章的许多部分中所阅读的,有几个指令有两种形式,即执行形式或 shell 形式。如前所述,所有 shell 形式默认使用["/bin/sh", "-c"]用于 Linux 容器,以及["cmd", "/S", "/C"]用于 Windows 容器。SHELL指令允许您更改该默认设置。以下是SHELL指令的语法:

# SHELL instruction syntax
SHELL ["executable", "parameters"]

SHELL指令可以在 Dockerfile 中使用多次。所有使用 shell 的指令,并且在SHELL指令之后,将使用新的 shell。因此,根据需要可以在单个 Dockerfile 中多次更改 shell。在创建 Windows 容器时,这可能特别有用,因为它允许您在cmd.exepowershell.exe之间来回切换。

Docker 镜像构建命令

好的,镜像构建命令不是 Dockerfile 指令。相反,它是用于将 Dockerfile 转换为 docker 镜像的 docker 命令。Docker 镜像构建命令将 docker 构建上下文,包括 Dockerfile,发送到 docker 守护程序,它解析 Dockerfile 并逐层构建镜像。我们将很快讨论构建上下文,但现在可以将其视为根据 Dockerfile 中的内容构建 Docker 镜像所需的一切。构建命令的语法如下:

# Docker image build command syntax
Usage: docker image build [OPTIONS] PATH | URL | -

图像构建命令有许多选项。我们现在不会涵盖所有选项,但让我们看一下一些最常见的选项:

# Common options used with the image build command
--rm         Remove intermediate containers after a successful build
--build-arg  Set build-time variables
--tag        Name and optionally a tag in the 'name:tag' format
--file       Name of the Dockerfile (Default is 'PATH/Dockerfile')

Docker 守护程序通过从 Dockerfile 中的每个命令创建新的图像来构建图像。每个新图像都是在前一个图像的基础上构建的。使用可选的--rm参数将指示守护程序在构建成功完成时删除所有中间图像。当重新构建成功构建的图像时,使用此选项将减慢构建过程,但会保持本地图像缓存的清洁。

当我们讨论ARG指令时,我们已经谈到了构建参数。请记住,--build-arg选项是您如何为 Dockerfile 中的ARG指令提供值。

--tag选项允许您为图像指定一个更易读的名称和版本。我们在之前的几个示例中也看到了这个选项的使用。

--file选项允许您使用文件名而不是 Dockerfile,并将 Dockerfile 保留在构建上下文文件夹之外的路径中。

以下是一些图像构建命令供参考:

# build command samples
docker image build --rm --build-arg username=35 --tag arg-demo:2.0 .
docker image build --rm --tag user-demo:1.0 .
docker image build --rm --tag workdir-demo:1.0 .

您会注意到前面每个示例中都有一个尾随的。这个句号表示当前工作目录是图像构建的构建上下文的根目录。

解析指令

解析指令是 Dockerfile 中可选注释行的一个特殊子集。任何解析指令必须出现在第一个正常注释行之前。它们还必须出现在任何空行或其他构建指令之前,包括FROM指令。基本上,所有解析指令必须位于 Dockerfile 的顶部。顺便说一句,如果你还没有弄清楚,你可以通过以#字符开头来创建一个普通的注释行。解析指令的语法如下:

# directive=value
# The line above shows the syntax for a parser directive

那么,您可以使用解析器指令做什么呢?目前,唯一支持的是escapeescape解析器指令用于更改用于指示下一个字符在指令中被视为字符而不是表示的特殊字符的字符。如果不使用解析器指令,则默认值为\。在本章的几个示例中,您已经看到了它用于转义换行符,允许在 Dockerfile 中将指令继续到下一行。如果需要使用不同的escape字符,可以使用escape解析器指令来处理。您可以将escape字符设置为两种选择之一:

# escape=\ (backslash)
Or
# escape=` (backtick)

一个例子是当您在 Windows 系统上创建 Dockerfile 时可能需要更改用作escape字符的字符。如您所知,\用于区分路径字符串中的文件夹级别,例如c:\windows\system32

\drivers。切换到使用escape字符的反引号将避免需要转义此类字符串,例如:c:\windows\system32\drivers`。

构建上下文

构建上下文是在使用构建镜像命令时发送到 Docker 守护程序的所有内容。这包括 Dockerfile 和发出构建命令时当前工作目录的内容,包括当前工作目录可能包含的所有子目录。可以使用-f--file选项将 Dockerfile 放在当前工作目录以外的目录中,但 Dockerfile 仍然会随构建上下文一起发送。使用.dockerignore文件,可以在发送到 Docker 守护程序的构建上下文中排除文件和文件夹。

构建 Docker 镜像时,非常重要的是尽可能保持构建上下文的大小。这是因为整个构建上下文都会发送到 Docker 守护程序以构建镜像。如果构建上下文中有不必要的文件和文件夹,那么它将减慢构建过程,并且根据 Dockerfile 的内容,可能会导致膨胀的镜像。这是一个如此重要的考虑因素,以至于每个镜像构建命令都会在命令输出的第一行显示构建上下文的大小。它看起来像这样:

构建上下文成为 Dockerfile 中命令的文件系统根。例如,考虑使用以下COPY指令:

# build context Dockerfile for Docker Quick Start guide
FROM scratch
COPY hello /
CMD ["/hello"]

这告诉 Docker 守护程序将hello文件从构建上下文的根目录复制到容器镜像的根目录。

如果命令成功完成,将显示镜像 ID,如果提供了--tag选项,则还将显示新的标签和版本:

保持构建上下文小的关键之一是使用.dockerignore文件。

.dockerignore 文件

如果您熟悉使用.gitignore文件,那么您已经基本了解了.dockerignore文件的目的。.dockerignore文件用于排除在 docker 镜像构建过程中不想包含在构建上下文中的文件。使用它有助于防止敏感和其他不需要的文件被包含在构建上下文中,可能最终出现在 docker 镜像中。这是一个帮助保持 Docker 镜像小的绝佳工具。

.dockerignore文件需要位于构建上下文的根文件夹中。与.gitignore文件类似,它使用一个以换行符分隔的模式列表。.dockerignore文件中的注释以#作为行的第一个字符。您可以通过包含一个例外行来覆盖模式。例外行以!作为行的第一个字符。所有其他行都被视为用于排除文件和/或文件夹的模式。

.dockerignore文件中的行顺序很重要。文件后面的匹配模式将覆盖文件前面的匹配模式。如果您添加一个与.dockerignore文件或 Dockerfile 文件匹配的模式,它们仍将与构建上下文一起发送到 docker 守护程序,但它们将不可用于任何ADDCOPY指令,因此不能出现在生成的镜像中。这是一个例子:

# Example of a .dockerignore file
# Exclude unwanted files
/*~
/*.log
/.DS_Store

总结

好了!那是一次冒险。现在您应该能够构建任何类型的 Docker 镜像。您知道何时使用COPY而不是ADD,何时使用ENV而不是ARG,也许最重要的是何时使用CMD而不是ENTERYPOINT。您甚至学会了如何穿越时间!这些信息对于开始使用 Docker 来说真的是一个很好的基础,并且在您开发更复杂的 Docker 镜像时将作为一个很好的参考。

希望你从这一章学到了很多,但我们还有更多要学习,所以让我们把注意力转向下一个主题。在第四章 Docker Volumes中,我们将学习更多关于 Docker 卷的知识。翻页,让我们继续我们的快速入门之旅。

参考资料

查看以下链接,获取本章讨论的主题信息:

第四章:Docker 卷

在本章中,我们将学习 Docker 卷的秘密。我们将学习如何在 Docker 容器内部使用工作站上的文件夹,以及如何创建和使用持久卷,允许多个容器共享数据。我们将学习如何清理未使用的卷。最后,我们将学习如何创建数据卷容器,成为其他容器的卷的来源。

每年大约有 675 个集装箱在海上丢失。1992 年,一个装满玩具的 40 英尺集装箱实际上掉进了太平洋,10 个月后,其中一些玩具漂到了阿拉斯加海岸 - www.clevelandcontainers.co.uk/blog/16-fun-facts-about-containers

在本章中,我们将涵盖以下主题:

  • 什么是 Docker 卷?

  • 创建 Docker 卷

  • 删除 Docker 卷的两种方法

  • 使用数据卷容器在容器之间共享数据

技术要求

您将从 Docker 的公共存储库中拉取 Docker 镜像,因此需要基本的互联网访问权限来执行本章中的示例。

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Docker-Quick-Start-Guide/tree/master/Chapter04

查看以下视频以查看代码的运行情况:bit.ly/2QqK78a

什么是 Docker 卷?

正如我们在第三章中学到的,Docker 使用一种称为联合文件系统的特殊文件系统。这是 Docker 分层镜像模型的关键,也允许许多使用 Docker 变得如此令人向往的功能。然而,联合文件系统无法提供数据的持久存储。回想一下,Docker 镜像的层是只读的。当你从 Docker 镜像运行一个容器时,Docker 守护进程会创建一个新的读写层,其中包含代表容器的所有实时数据。当容器对其文件系统进行更改时,这些更改会进入该读写层。因此,当容器消失时,带着读写层一起消失,容器对该层内数据所做的任何更改都将被删除并永远消失。这等同于非持久存储。然而,请记住,一般来说这是一件好事。事实上,是一件很好的事情。大多数情况下,这正是我们希望发生的。容器是临时的,它们的状态数据也是如此。然而,持久数据有很多用例,比如购物网站的客户订单数据。如果一个容器崩溃或需要重新堆叠,如果所有订单数据都消失了,那将是一个相当糟糕的设计。

这就是 Docker 卷的作用。Docker 卷是一个完全独立于联合文件系统之外的存储位置。因此,它不受镜像的只读层或容器的读写层所施加的相同规则的约束。Docker 卷是一个存储位置,默认情况下位于运行使用该卷的容器的主机上。当容器消失时,无论是出于设计还是因为灾难性事件,Docker 卷都会留下并可供其他容器使用。Docker 卷可以同时被多个容器使用。

描述 Docker 卷最简单的方式是:Docker 卷是一个存在于 Docker 主机上并在运行的 Docker 容器内部挂载和访问的文件夹。这种可访问性是双向的,允许从容器内部修改该文件夹的内容,或者在文件夹所在的 Docker 主机上进行修改。

现在,这个描述有点泛化。使用不同的卷驱动程序,作为卷被挂载的文件夹的实际位置可能不在 Docker 主机上。使用卷驱动程序,您可以在远程主机或云提供商上创建您的卷。例如,您可以使用 NFS 驱动程序允许在远程 NFS 服务器上创建 Docker 卷。

与 Docker 镜像和 Docker 容器一样,卷命令代表它们自己的管理类别。正如您所期望的那样,卷的顶级管理命令如下:

# Docker volume managment command
docker volume

卷管理组中可用的子命令包括以下内容:

# Docker volume management subcommands
docker volume create # Create a volume
docker volume inspect # Display information on one or more volumes
docker volume ls # List volumes
docker volume rm # Remove one or more volumes
docker volume prune          # Remove all unused local volumes

有几种不同的方法可以创建 Docker 卷,所以让我们继续通过创建一些来调查 Docker 卷。

参考

查看以下链接以获取更多信息:

创建 Docker 卷

有几种方法可以创建 Docker 卷。一种方法是使用volume create命令。该命令的语法如下:

# Syntax for the volume create command
Usage:  docker volume create [OPTIONS] [VOLUME]

除了可选的卷名称参数外,create命令还允许使用以下选项:

# The options available to the volume create command:
-d, --driver string         # Specify volume driver name (default "local")
--label list                # Set metadata for a volume
-o, --opt map               # Set driver specific options (default map[])

让我们从最简单的例子开始:

# Using the volume create command with no optional parameters
docker volume create

执行上述命令将创建一个新的 Docker 卷并分配一个随机名称。该卷将使用内置的本地驱动程序(默认情况下)。使用volume ls命令,您可以看到 Docker 守护程序分配给我们新卷的随机名称。它看起来会像这样:

再上一层,让我们创建另一个卷,这次使用命令提供一个可选的卷名称。命令看起来会像这样:

# Create a volume with a fancy name
docker volume create my-vol-02

这次,卷已创建,并被命名为my-vol-02,如所请求的:

这个卷仍然使用默认的本地驱动程序。使用本地驱动程序只意味着这个卷所代表的文件夹的实际位置可以在 Docker 主机上本地找到。我们可以使用卷检查子命令来查看该文件夹实际上可以找到的位置:

正如您在前面的屏幕截图中所看到的,该卷的挂载点位于 Docker 主机的文件系统上,路径为/var/lib/docker/volumes/my-vol-02/_data。请注意,文件夹路径由 root 所有,这意味着您需要提升的权限才能从主机访问该位置。还要注意,这个示例是在 Linux 主机上运行的。

如果您使用的是 OS X,您需要记住,您的 Docker 安装实际上是在使用一个几乎无缝的虚拟机。其中一个无缝显示的领域是使用 Docker 卷。在 OS X 主机上创建 Docker 卷时创建的挂载点存储在虚拟机的文件系统中,而不是在您的 OS X 文件系统中。当您使用 docker volume inspect 命令并查看卷的挂载点路径时,它不是您的 OS X 文件系统上的路径,而是隐藏虚拟机文件系统上的路径。

有一种方法可以查看隐藏虚拟机的文件系统(和其他功能)。通过一个命令,通常称为魔术屏幕命令,您可以访问正在运行的 Docker VM。该命令如下:

# The Magic Screen command
screen ~/Library/Containers/com.docker.docker/Data
/com.docker.driver.amd64-linux/tty
# or if you are using Mac OS High Sierra
screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty

使用Ctrl + AK来终止屏幕会话。

您可以使用Ctrl + A Ctrl + D分离,然后使用screen -r重新连接,但不要分离然后启动新的屏幕会话。在 VM 上运行多个屏幕会给您 tty 垃圾。

这是一个在 OS X 主机上创建的卷的挂载点访问示例。这是设置:

# Start by creating a new volume
docker volume create my-osx-volume
# Now find the Mountpoint
docker volume inspect my-osx-volume -f "{{json .Mountpoint}}"
# Try to view the contents of the Mountpoint's folder
sudo ls -l /var/lib/docker/volumes/my-osx-volume
# "No such file or directory" because the directory does not exist on the OS X host

这就是设置的样子:

现在,这是如何使用魔术屏幕命令来实现我们想要的,即访问卷的挂载点:

# Now issue the Magic Screen command and hit <enter> to get a prompt
screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty
# You are now root in the VM, and can issue the following command
ls -l /var/lib/docker/volumes/my-osx**-volume** # The directory exists and you will see the actual Mountpoint sub folder "_data"
# Now hit control-a followed by lower case k to kill the screen session
<CTRL-a>k 

然后...

现在是一个很好的时机指出,我们创建了这些卷,而从未创建或使用 Docker 容器。这表明 Docker 卷是在正常容器联合文件系统之外的领域。

我们在第三章中看到,创建 Docker 镜像,我们还可以使用容器运行命令上的参数或在 Dockerfile 中添加VOLUME指令来创建卷。并且,正如您所期望的那样,您可以使用 Docker volume create命令预先创建的卷通过使用容器运行参数,即--mount参数,将卷挂载到容器中,例如,如下所示:

# mount a pre-created volume with --mount parameter
docker container run --rm -d \
--mount source=my-vol-02,target=/myvol \
--name vol-demo2 \
volume-demo2:1.0 tail -f /dev/null

这个例子将运行一个新的容器,它将挂载现有的名为my-vol-02的卷。它将在容器中的/myvol处挂载该卷。请注意,前面的例子也可以在不预先创建my-vol-02:volume的情况下运行,使用--mount参数运行容器的行为将在启动容器的过程中创建卷。请注意,当挂载卷时,图像挂载点文件夹中定义的任何内容都将添加到卷中。但是,如果图像挂载点文件夹中存在文件,则它也存在于主机的挂载点,并且主机文件的内容将最终成为文件中的内容。使用此 Dockerfile 中的图像,看起来是这样的:

# VOLUME instruction Dockerfile for Docker Quick Start
FROM alpine
RUN mkdir /myvol
RUN echo "Data from image" > /myvol/both-places.txt
CMD ["sh"]

请注意Data from image行。现在,使用一个包含与both-places.txt匹配名称的文件的预先创建的卷,但文件中包含Data from volume内容,我们将基于该图像运行一个容器。发生了什么:

正如您所看到的,尽管 Dockerfile 创建了一个带有Data from image内容的文件,但当我们从该图像运行一个容器并挂载一个具有相同文件的卷时,卷中的内容(Data from volume)占优势,并且是在运行容器中找到的内容。

请记住,无法通过 Dockerfile 中的VOLUME指令挂载预先创建的卷。不存在名为 volume 的 Dockerfile VOLUME指令。原因是 Dockerfile 无法指定卷从主机挂载的位置。允许这样做会有几个原因。首先,由于 Dockerfile 创建了一个镜像,从该镜像运行的每个容器都将尝试挂载相同的主机位置。这可能会很快变得非常糟糕。其次,由于容器镜像可以在不同的主机操作系统上运行,很可能一个操作系统的主机路径定义在另一个操作系统上甚至无法工作。再次,很糟糕。第三,定义卷主机路径将打开各种安全漏洞。糟糕,糟糕,糟糕!因此,使用 Dockerfile 构建的图像运行具有VOLUME指令的容器将始终在主机上创建一个新的,具有唯一名称的挂载点。在 Dockerfile 中使用VOLUME指令的用途有些有限,例如当容器将运行始终需要读取或写入预期在文件系统中特定位置的数据的应用程序,但不应该是联合文件系统的一部分。

还可以在主机上的文件与容器中的文件之间创建一对一的映射。要实现这一点,需要在容器运行命令中添加一个-v参数。您需要提供要从主机共享的文件的路径和文件名,以及容器中文件的完全限定路径。容器运行命令可能如下所示:

# Map a single file from the host to a container
echo "important data" > /tmp/data-file.txt
docker container run --rm -d \
 -v /tmp/data-file.txt:/myvol/data-file.txt \
 --name vol-demo \
 volume-demo2:1.0 tail -f /dev/null
# Prove it
docker exec vol-demo cat /myvol/data-file.txt

可能如下所示:

有几种不同的方法可以在容器运行命令中定义卷。为了说明这一点,看看以下运行命令,每个都将完成相同的事情:

# Using --mount with source and target
docker container run --rm -d \
 --mount source=my-volume,target=/myvol,readonly \
 --name vol-demo1 \
 volume-demo:latest tail -f /dev/null
 # Using --mount with source and destination
docker container run --rm -d \
 --mount source=my-volume,destination=/myvol,readonly \
 --name vol-demo2 \
 volume-demo:latest tail -f /dev/null
 # Using -v 
docker container run --rm -d \
 -v my-volume:/myvol:ro \
 --name vol-demo3 \
 volume-demo:latest tail -f /dev/null

前面三个容器运行命令都将创建一个已挂载相同卷的容器,以只读模式。可以使用以下命令进行验证:

# Check which container have mounted a volume by name
docker ps -a --filter volume=in-use-volume

参考资料

查看以下链接获取更多信息:

删除卷

我们已经看到并使用了卷列表命令volume ls和检查命令volume inspect,我认为您应该对这些命令的功能有很好的理解。卷管理组中还有另外两个命令,都用于卷的移除。第一个是volume rm命令,您可以使用它按名称移除一个或多个卷。然后,还有volume prune命令;使用清理命令,您可以移除所有未使用的卷。在使用此命令时要特别小心。以下是删除和清理命令的语法:

# Remove volumes command syntax
Usage: docker volume rm [OPTIONS] VOLUME [VOLUME...]
# Prune volumes command syntax
Usage: docker volume prune [OPTIONS]

以下是使用删除和清理命令的一些示例:

由于in-use-volume卷被挂载在vol-demo容器中,它没有被清理命令移除。您可以在卷列表命令上使用过滤器,查看哪些卷与容器不相关,因此将在清理命令中被移除。以下是过滤后的 ls 命令:

# Using a filter on the volume ls command
docker volume ls --filter dangling=true

参考资料

查看以下链接以获取更多信息:

使用数据卷容器在容器之间共享数据

Docker 卷的另一个功能允许您将一个 Docker 容器中挂载的卷与其他容器共享。这被称为数据卷容器。使用数据卷容器基本上是一个两步过程。在第一步中,您运行一个容器,该容器创建或挂载 Docker 卷(或两者),在第二步中,当运行其他容器时,您使用特殊的卷参数--volumes-from来配置它们挂载在第一个容器中的所有卷。以下是一个例子:

# Step 1
docker container run \
 --rm -d \
 -v data-vol-01:/data/vol1 -v data-vol-02:/data/vol2 \
 --name data-container \
 vol-demo2:1.0 tail -f /dev/null
# Step 2
docker container run \
 --rm -d \
 --volumes-from data-container \
 --name app-container \
 vol-demo2:1.0 tail -f /dev/null
# Prove it
docker container exec app-container ls -l /data
# Prove it more
docker container inspect -f '{{ range .Mounts }}{{ .Name }} {{ end }}' app-container

执行时的样子如下:

在这个例子中,第一个容器运行命令正在创建卷,但它们也可以很容易地在之前的容器运行命令中预先创建,或者来自volume create命令。

参考资料

这是一篇关于数据卷容器的优秀文章,包括如何使用它们进行数据备份和恢复:www.tricksofthetrades.net/2016/03/14/docker-data-volumes/

摘要

在本章中,我们深入探讨了 Docker 卷。我们了解了 Docker 卷的实际含义,以及创建它们的几种方法。我们学习了使用volume create命令、容器运行命令和 Dockerfile 的VOLUME指令创建 Docker 卷的区别。我们还看了一些删除卷的方法,以及如何使用数据容器与其他容器共享卷。总的来说,你现在应该对自己的 Docker 卷技能感到非常自信。到目前为止,我们已经建立了扎实的 Docker 知识基础。

在第五章中,Docker Swarm,我们将通过学习 Docker Swarm 来扩展基础知识。这将是真正开始变得令人兴奋的地方。如果你准备好学习更多,请翻页!

第五章:Docker Swarm

在本章中,我们将学习什么是 Docker swarm,以及如何设置 Docker swarm 集群。我们将了解所有的集群管理命令,然后我们将更多地了解集群管理者和集群工作者。接下来,我们将发现集群服务。最后,我们将发现在集群中任何节点上运行的容器应用程序是多么容易访问。

目前全球有超过 17,000,000 个集装箱,其中 5 或 6,000,000 个正在船舶、卡车和火车上运输。总共,它们每年大约进行 200,000,000 次旅行。- www.billiebox.co.uk/facts-about-shipping-containers

在本章中,我们将涵盖以下主题:

  • 什么是 Docker swarm?

  • 建立 Docker swarm 集群

  • 管理者和工作者

  • 集群服务

  • 访问集群中的容器应用程序

技术要求

您将从 Docker 的公共存储库中拉取 Docker 镜像,因此需要基本的互联网访问权限来执行本章中的示例。您将设置一个多节点的集群,因此需要多个节点来完成本章的示例。您可以使用物理服务器、EC2 实例、vSphere 或 Workstation 上的虚拟机,甚至是 Virtual Box 上的虚拟机。我在 Vmware Workstation 上使用了 6 个虚拟机作为我的节点。每个虚拟机配置为 1GB 内存、1 个 CPU 和 20GB 硬盘。所使用的客户操作系统是 Xubuntu 18.04,因为它体积小且具有完整的 Ubuntu 功能集。Xubuntu 可以从xubuntu.org/download/下载。任何现代的 Linux 操作系统都可以作为节点的选择。

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Docker-Quick-Start-Guide/tree/master/Chapter05

查看以下视频以查看代码的运行情况:bit.ly/2KENJOD

什么是 Docker swarm?

你可能没有注意到,但到目前为止,我们在示例中使用的所有 Docker 工作站部署或节点都是以单引擎模式运行的。这是什么意思?这告诉我们,Docker 安装是直接管理的,作为一个独立的 Docker 环境。虽然这是有效的,但它不太高效,也不具有良好的扩展性。当然,Docker 了解到这些限制,并为这个问题提供了一个强大的解决方案。它被称为 Docker 蜂群。Docker 蜂群是将 Docker 节点连接在一起,并有效地管理这些节点和在其上运行的 docker 化应用程序的一种方式。简而言之,Docker 蜂群是一组 Docker 节点连接并作为集群或蜂群进行管理。Docker 蜂群内置在 Docker 引擎中,因此无需额外安装即可使用。当 Docker 节点是蜂群的一部分时,它运行在蜂群模式下。如果有任何疑问,您可以使用docker system info命令轻松检查运行 Docker 的系统是否是蜂群的一部分或者是以单引擎模式运行:

提供蜂群模式的功能是 Docker SwarmKit 的一部分,这是一个用于在规模上编排分布式系统的工具,即 Docker 蜂群集群。一旦 Docker 节点加入蜂群,它就成为蜂群节点,成为管理节点或工作节点。我们很快会谈到管理节点和工作节点之间的区别。现在,知道加入新蜂群的第一个 Docker 节点成为第一个管理节点,也被称为领导者。当第一个节点加入蜂群并成为领导者时,会发生很多技术上的魔法(实际上,它创建并初始化了蜂群,然后加入了蜂群)。以下是发生的一些巫术(没有特定顺序):

  • 创建了基于 Swarm-ETCD 的配置数据库或集群存储,并进行了加密

  • 为所有节点间通信设置了双向 TLS(mTLS)认证和加密

  • 启用了容器编排,负责管理容器在哪些节点上运行

  • 集群存储被配置为自动复制到所有管理节点

  • 该节点被分配了一个加密 ID

  • 启用了基于 Raft 的分布式共识管理系统

  • 节点成为管理节点并被选举为蜂群领导者

  • 蜂群管理器被配置为高可用

  • 创建了一个公钥基础设施系统

  • 节点成为证书颁发机构,允许其向加入集群的任何节点颁发客户端证书

  • 证书颁发机构上配置了默认的 90 天证书轮换策略

  • 节点获得其客户端证书,其中包括其名称、ID、集群 ID 和节点在集群中的角色

  • 为添加新的 swarm 管理者创建一个新的加密加入令牌

  • 为添加新的 swarm 工作节点创建一个新的加密加入令牌

该列表代表了通过将第一个节点加入到 swarm 中获得的许多强大功能。伴随着强大的功能而来的是巨大的责任,这意味着您确实需要准备好做大量工作来创建您的 Docker swarm,正如您可能想象的那样。因此,让我们继续下一节,我们将讨论在设置 swarm 集群时如何启用所有这些功能。

参考资料

查看以下链接获取更多信息:

如何设置 Docker swarm 集群

您刚刚了解了创建 Docker swarm 集群时启用和设置的所有令人难以置信的功能。现在我将向您展示设置 Docker swarm 集群所需的所有步骤。准备好了吗?以下是它们:

# Set up your Docker swarm cluster
docker swarm init

什么?等等?剩下的在哪里?没有。没有遗漏任何内容。在上一节描述的所有设置和功能都可以通过一个简单的命令实现。通过单个的swarm init命令,集群被创建,节点从单实例节点转变为 swarm 模式节点,节点被分配为管理者角色并被选举为集群的领导者,集群存储被创建,节点成为集群的证书颁发机构并为自己分配一个包含加密 ID 的新证书,为管理者创建一个新的加密加入令牌,为工作节点创建另一个令牌,依此类推。这就是简化的复杂性。

swarm 命令组成了另一个 Docker 管理组。以下是 swarm 管理命令:

我们将在片刻后审查每个命令的目的,但在此之前,我想让您了解一些重要的网络配置。我们将在第六章 Docker Networking中更多地讨论 Docker 网络,但现在请注意,您可能需要在 Docker 节点上打开一些协议和端口的访问权限,以使 Docker swarm 正常运行。以下是来自 Docker 的Getting started with swarm mode维基的信息:

您可能需要为 REST API 打开的另外两个端口如下:

  • TCP 2375 用于 Docker REST API(纯文本)

  • TCP 2376 用于 Docker REST API(ssl)

好了,让我们继续审查 swarm 命令。

docker swarm init

您已经看到了 init 命令的用途,即创建 swarm 集群,将第一个 Docker 节点添加到其中,然后设置和启用我们刚刚介绍的所有 swarm 功能。init 命令可以简单地使用它而不带任何参数,但有许多可用的可选参数可用于微调初始化过程。您可以通过使用--help获得所有可选参数的完整列表,但现在让我们考虑一些可用的参数:

  • --autolock:使用此参数启用管理器自动锁定。

  • --cert-expiry duration:使用此参数更改节点证书的默认有效期(90 天)。

  • --external-ca external-ca:使用此参数指定一个或多个证书签名端点,即外部 CA。

docker swarm join-token

当您在第一个节点上运行swarm init命令初始化 swarm 时,执行的功能之一是创建唯一的加密加入令牌,一个加入额外的管理节点,一个加入工作节点。使用join-token命令,您可以获取这两个加入令牌。实际上,使用join-token命令将为您提供指定角色的完整加入命令。角色参数是必需的。以下是命令的示例:

# Get the join token for adding managers
docker swarm join-token manager
# Get the join token for adding workers
docker swarm join-token worker

以下是它的样子:

# Rotate the worker join token
docker swarm join-token --rotate worker

请注意,这不会使已使用旧的、现在无效的加入令牌的现有工作节点失效。它们仍然是 swarm 的一部分,并且不受加入令牌更改的影响。只有您希望加入 swarm 的新节点需要使用新令牌。

docker swarm join

您已经在前面的 docker swarm join-token 部分看到了 join 命令的使用。join 命令与加密的 join token 结合使用,用于将 Docker 节点添加到 swarm 中。除了第一个节点之外,所有节点都将使用 join 命令加入到 swarm 中(第一个节点当然使用 "init" 命令)。join 命令有一些参数,其中最重要的是 --token 参数。这是必需的 join token,可通过 join-token 命令获取。以下是一个示例:

# Join this node to an existing swarm
docker swarm join --token SWMTKN-1-3ovu7fbnqfqlw66csvvfw5xgljl26mdv0dudcdssjdcltk2sen-a830tv7e8bajxu1k5dc0045zn 192.168.159.156:2377

您会注意到,此命令不需要角色。这是因为 token 本身与其创建的角色相关联。当您执行 join 时,输出会提供一个信息消息,告诉您节点加入的角色是管理节点还是工作节点。如果您无意中使用了管理节点 token 加入工作节点,或反之,您可以使用 leave 命令将节点从 swarm 中移除,然后使用实际所需角色的 token,重新将节点加入到 swarm。

docker swarm ca

当您想要查看 swarm 的当前证书或需要旋转当前的 swarm 证书时,可以使用 swarm ca 命令。要旋转证书,您需要包括 --rotate 参数:

# View the current swarm certificate
docker swarm ca
# Rotate the swarm certificate
docker swarm ca --rotate

swarm ca 命令只能在 swarm 管理节点上成功执行。您可能使用旋转 swarm 证书功能的一个原因是,如果您正在从内部根 CA 切换到外部 CA,或者反之。另一个可能需要旋转 swarm 证书的原因是,如果一个或多个管理节点受到了威胁。在这种情况下,旋转 swarm 证书将阻止所有其他管理节点能够使用旧证书与旋转证书的管理节点或彼此进行通信。旋转证书时,命令将保持活动状态,直到所有 swarm 节点(管理节点和工作节点)都已更新。以下是在一个非常小的集群上旋转证书的示例:

由于命令将保持活动状态,直到所有节点都更新了 TLS 证书和 CA 证书,如果集群中有离线的节点,可能会出现问题。当这是一个潜在的问题时,您可以包括--detach参数,命令将启动证书旋转并立即返回会话控制。请注意,当您使用--detach可选参数时,您将不会得到有关证书旋转进度、成功或失败的任何状态。您可以使用 node ls 命令查询集群中证书的状态以检查进度。以下是您可以使用的完整命令:

# Query the state of the certificate rotation in a swarm cluster
docker node ls --format '{{.ID}} {{.Hostname}} {{.Status}} {{.TLSStatus}}'

ca rotate命令将继续尝试完成,无论是在前台还是在后台(如果分离)。如果在旋转启动时节点离线,然后重新上线,证书旋转将完成。这里有一个示例,node04在执行旋转命令时处于离线状态,然后过了一会儿,它重新上线;检查状态发现它成功旋转了:

另一个重要的要点要记住的是,旋转证书将立即使当前的加入令牌无效。

docker swarm unlock

您可能还记得关于docker swarm init命令的讨论,其中一个可选参数是--autolock。使用此参数将在集群中启用自动锁定功能。这是什么意思?嗯,当一个集群配置为使用自动锁定时,任何时候管理节点的 docker 守护程序离线,然后重新上线(即重新启动),都需要输入解锁密钥才能允许节点重新加入集群。为什么要使用自动锁定功能来锁定您的集群?自动锁定功能有助于保护集群的相互 TLS 加密密钥,以及用于集群的 raft 日志的加密和解密密钥。这是一个旨在补充 Docker Secrets 的额外安全功能。当锁定的集群的管理节点上的 docker 守护程序重新启动时,您必须输入解锁密钥。以下是使用解锁密钥的样子:

顺便说一句,对于其余的群集,尚未解锁的管理节点将报告为已关闭,即使 Docker 守护程序正在运行。Swarm 自动锁定功能可以使用swarm update命令在现有的 Swarm 集群上启用或禁用,我们很快将看一下。解锁密钥是在 Swarm 初始化期间生成的,并将在那时在命令行上呈现。如果您丢失了解锁密钥,可以使用swarm unlock-key命令在未锁定的管理节点上检索它。

docker swarm unlock-key

swarm unlock-key命令很像swarm ca命令。解锁密钥命令可用于检索当前的 Swarm 解锁密钥,或者可以用于将解锁密钥更改为新的:

# Retrieve the current unlock key
docker swarm unlock-key
# Rotate to a new unlock key
docker swarm unlock-key --rotate

根据 Swarm 集群的大小,解锁密钥轮换可能需要一段时间才能更新所有管理节点。

当您轮换解锁密钥时,最好在更新密钥之前将当前(旧)密钥随手放在一边,以防万一管理节点在获取更新的密钥之前离线。这样,您仍然可以使用旧密钥解锁节点。一旦节点解锁并接收到轮换(新)解锁密钥,旧密钥就可以丢弃了。

正如您可能期望的那样,swarm unlock-key命令只在启用了自动锁定功能的集群的管理节点上使用时才有用。如果您的集群未启用自动锁定功能,可以使用swarm update命令启用它。

docker swarm update

在第一个管理节点上通过docker swarm init命令初始化集群时,将启用或配置几个 Swarm 集群功能。在集群初始化后,可能会有时候您想要更改哪些功能已启用、已禁用或已配置。要实现这一点,您需要使用swarm update命令。例如,您可能想要为 Swarm 集群启用自动锁定功能。或者,您可能想要更改证书有效期。这些都是您可以使用swarm update命令执行的更改类型。这样做可能看起来像这样:

# Enable autolock on your swarm cluster
docker swarm update --autolock=true
# Adjust certificate expiry to 30 days
docker swarm update --cert-expiry 720h

以下是swarm update命令可能影响的设置列表:

docker swarm leave

这基本上是你所期望的。您可以使用leave命令从 swarm 中移除 docker 节点。以下是需要使用leave命令来纠正用户错误的示例:

Node03 原本是一个管理节点。我不小心将该节点添加为工作者。意识到我的错误后,我使用swarm leave命令将节点从 swarm 中移除,将其放回单实例模式。然后,使用manager加入令牌,我将节点重新添加到 swarm 作为管理者。哦!危机已解除。

参考资料

查看以下链接获取更多信息:

管理者和工作者

我们在前面的章节中已经讨论了集群管理节点,但让我们更仔细地看看管理节点的工作。管理节点确切地做了你所期望的事情。它们管理和维护集群的状态。它们调度集群服务,我们将在本章的集群服务部分中讨论,但现在,把集群服务想象成运行的容器。管理节点还提供集群的 API 端点,允许通过 REST 进行编程访问。管理节点还将流量引导到正在运行的服务,以便任何容器都可以通过任何管理节点访问,而无需知道实际运行容器的节点。作为维护集群状态的一部分,管理节点将处理系统中节点的丢失,在管理节点丢失时选举新的领导节点,并在容器或节点宕机时保持所需数量的服务容器运行。

集群中管理节点的最佳实践是三个、五个或七个。你会注意到所有这些选项都代表管理节点的奇数数量。这是为了在领导节点丢失时,raft 一致性算法可以更容易地为集群选择新的领导者。你可以运行一个只有一个管理节点的集群,这实际上比有两个管理节点更好。但是,对于一个更高可用的集群,建议至少有三个管理节点。对于更大的集群,有五个或七个管理节点是不错的选择,但不建议超过七个。一旦在同一集群中有超过七个管理节点,你实际上会遇到性能下降的问题。

对于管理节点来说,另一个重要考虑因素是它们之间的网络性能。管理节点需要低延迟的网络连接以实现最佳性能。例如,如果你在 AWS 上运行你的集群,你可能不希望管理节点分布在不同的地区。如果这样做,你可能会遇到集群的问题。如果你将管理节点放在同一地区的不同可用区内,你不应该遇到任何与网络性能相关的问题。

工作节点除了运行容器之外什么也不做。当领导节点宕机时,它们没有发言权。它们不处理 API 调用。它们不指挥流量。它们除了运行容器之外什么也不做。事实上,你不能只有一个工作节点的 swarm。另一方面,你可以只有一个管理节点的 swarm,在这种情况下,管理节点也将充当工作节点,并在其管理职责之外运行容器。

默认情况下,所有管理节点实际上也是工作节点。这意味着它们可以并且将运行容器。如果您希望您的管理节点不运行工作负载,您需要更改节点的可用性设置。将其更改为排水将小心地停止标记为排水的管理节点上的任何运行容器,并在其他(非排水)节点上启动这些容器。在排水模式下,不会在节点上启动新的容器工作负载,例如如下所示:

# Set node03's availability to drain
docker node update --availability drain ubuntu-node03

也许有时候您想要或需要改变 swarm 中 docker 节点的角色。您可以将工作节点提升为管理节点,或者将管理节点降级为工作节点。以下是这些活动的一些示例:

# Promote worker nodes 04 and 05 to manager status
docker node promote ubuntu-node04 ubuntu-node05
# Demote manager nodes 01 and 02 to worker status
docker node demote ubuntu-node01 ubuntu-node02

参考

请查看有关节点如何工作的官方文档[https://docs.docker.com/engine/swarm/how-swarm-mode-works/nodes/]。

Swarm 服务

好了。现在你已经了解了如何设置 Docker swarm 集群,以及它的节点如何从单引擎模式转换为 swarm 模式。你也知道这样做的意义是为了让你摆脱直接管理单个运行的容器。因此,你可能开始想知道,如果我现在不直接管理我的容器,我该如何管理它们?你来对地方了!这就是 swarm 服务发挥作用的地方。swarm 服务允许您根据容器应用程序的并发运行副本数量来定义所需的状态。让我们快速看一下在 swarm 服务的管理组中有哪些可用的命令,然后我们将讨论这些命令:

您可能想要做的第一件事情是创建一个新的服务,因此我们将从service create命令开始讨论我们的 swarm 服务。以下是service create命令的语法和基本示例:

# Syntax for the service create command
# Usage: docker service create [OPTIONS] IMAGE [COMMAND] [ARG...]
# Create a service
docker service create --replicas 1 --name submarine alpine ping google.com

好的。让我们分解一下这里显示的service create命令示例。首先,你有管理组服务,然后是create命令。然后,我们开始进入参数;第一个是--replicas。这定义了应同时运行的容器副本数量。接下来是--name参数。这个很明显,是我们要创建的服务的名称,在这种情况下是submarine。我们将能够在其他服务命令中使用所述名称。在名称参数之后,我们有完全合格的 Docker 镜像名称。在这种情况下,它只是alpine。它可以是诸如alpine:3.8alpine:latest之类的东西,或者更合格的东西,比如tenstartups/alpine:latest。在用于服务的图像名称之后是运行容器时要使用的命令和传递给该命令的参数——分别是pinggoogle.com。因此,前面的service create命令示例将从alpine镜像启动一个单独的容器,该容器将使用ping命令和 google.com 参数运行,并将服务命名为submarine。看起来是这样的:

你现在知道了创建 docker 服务的基础知识。但在你过于兴奋之前,service create命令还有很多内容要涵盖。事实上,这个命令有很多选项,列出它们将占据本书两页的篇幅。所以,我希望你现在使用--help功能并输入以下命令:

# Get help with the service create command
docker service create --help

我知道,对吧?有很多可选参数可以使用。别担心。我不会丢下你不管的。我会给你一些指导,帮助你建立创建服务的坚实基础,然后你可以扩展并尝试一些你在--help中看到的其他参数。

只是让你知道,到目前为止我们使用的两个参数,--replicas--name,都是可选的。如果你不提供要使用的副本数量,服务将以默认值 1 创建。此外,如果你不为服务提供名称,将会编造一个奇特的名称并赋予服务。这与我们在第二章中使用docker container run命令时看到的默认命名类型相同,学习 Docker 命令。通常最好为每个发出的service create命令提供这两个选项。

另外,要知道,一般来说,在前面的示例中提供的镜像的命令和命令参数也是可选的。在这种特定情况下,它们是必需的,因为单独从 alpine 镜像运行的容器,如果没有提供其他命令或参数,将会立即退出。在示例中,这将显示为无法收敛服务,Docker 将永远尝试重新启动服务。换句话说,如果使用的镜像内置了命令和参数(比如在 Dockerfile 的CMDENTRYPOINT指令中),则可以省略命令及其参数。

现在让我们继续讨论一些创建参数。你应该还记得第二章中提到的--publish参数,你可以在docker container run命令上使用,它定义了在 docker 主机上暴露的端口以及主机端口映射到的容器中的端口。它看起来像这样:

# Create a nginx web-server that redirects host traffic from port 8080 to port 80 in the container docker container run --detach --name web-server1 --publish 8080:80 nginx

好吧,你需要为一个集群服务使用相同的功能,在他们的智慧中,Docker 使container run命令和service create命令使用相同的参数:--publish。你可以使用我们之前看到的相同的缩写格式,--publish 8080:80,或者你可以使用更详细的格式:--publish published=8080target=80。这仍然意味着将主机流量从端口8080重定向到容器中的端口 80。让我们尝试另一个例子,这次使用--publish参数。我们将再次运行nginx镜像:

# Create a nginx web-server service using the publish parameter
docker service create --name web-service --replicas 3 --publish published=8080,target=80 nginx

这个例子将创建一个新的服务,运行三个容器副本,使用nginx镜像,在容器上暴露端口80,在主机上暴露端口8080。看一下:

现在,您已经接近成功了。让我们快速介绍另外三个参数,然后您就可以准备好应对世界(至少是集群服务的世界)。首先是 --restart-window。此参数用于告诉 Docker 守护程序在测试容器是否健康之前等待多长时间启动其应用程序。默认值为五秒。如果您在容器中创建了一个需要超过五秒才能启动并报告为健康的应用程序,您将需要在 service create 中包含 --restart-window 参数。接下来是 --restart-max-attempts。此参数告诉 Docker 守护程序在放弃之前尝试启动未报告为健康的容器副本的次数。默认值是永不放弃永不投降!最后,让我们谈谈 --mode 参数。集群服务的默认模式是replicated。这意味着 Docker 守护程序将继续为您的服务创建容器,直到同时运行的容器数量等于您在 --replicas 参数中提供的值(如果您没有提供该参数,则为 1)。例如,使用 --replicas 3 参数,您将在集群中获得三个运行中的容器。还有另一种模式,称为global。如果您在创建服务时提供 --mode global 参数,Docker 守护程序将在集群中的每个节点上精确地创建一个容器。如果您有一个六节点的集群,您将得到六个运行中的容器,每个节点一个。对于一个 12 节点的集群,您将得到 12 个容器,依此类推。当您有为每个主机提供功能的服务时,例如监控应用程序或日志转发器,这是一个非常方便的选项。

让我们回顾一些其他您需要了解和使用的服务命令。一旦您创建了一些服务,您可能想要列出这些服务。这可以通过 service list 命令来实现。如下所示:

# List services in the swarm
# Usage: docker service ls [OPTIONS]
docker service list

一旦您查看了运行中服务的列表,您可能想要了解一个或多个服务的更多详细信息。为了实现这一点,您将使用 service ps 命令。看一下:

# List the tasks associated with a service
# Usage: docker service ps [OPTIONS] SERVICE [SERVICE...]
docker service ps

一旦一个服务已经没有用处,您可能想要终止它。执行此操作的命令是 service remove 命令。如下所示:

# Remove one or more services from the swarm
# Usage: docker service rm SERVICE [SERVICE...]
docker service remove sleepy_snyder

如果您想要删除在集群中运行的所有服务,您可以组合其中一些命令并执行类似以下的命令:

# Remove ALL the services from the swarm
docker service remove $(docker service list -q)

最后,如果您意识到当前配置的副本数量未设置为所需数量,您可以使用service scale命令进行调整。以下是您可以这样做的方法:

# Adjust the configured number of replicas for a service
# Usage: docker service scale SERVICE=REPLICAS [SERVICE=REPLICAS...]
docker service scale web-service=4

这应该足够让您忙一段时间了。在我们继续第六章之前,Docker 网络,让我们在本章中再涵盖一个主题:访问在集群中运行的容器应用程序。

参考

阅读有关 Docker 服务创建参考的更多信息docs.docker.com/engine/reference/commandline/service_create/

在集群中访问容器应用程序

所以,现在您有一个运行着奇数个管理节点和若干个工作节点的集群。您已经部署了一些集群服务来运行您喜爱的容器化应用程序。接下来呢?嗯,您可能想要访问在您的集群中运行的一个或多个应用程序。也许您已经部署了一个 web 服务器应用程序。能够访问该 web 服务器共享的网页将是很好的,对吧?让我们快速看一下,看看这是多么容易。

集群管理器为我们处理的功能之一是将流量引导到我们的服务。在之前的示例中,我们设置了一个在集群中运行三个副本的 web 服务。我目前使用的集群恰好有三个管理节点和三个工作节点。所有六个节点都有资格运行工作负载,因此当服务启动时,六个节点中的三个将最终运行一个容器。如果我们使用service ps命令查看服务的任务的详细信息,您可以看到六个节点中哪些正在运行 web 服务容器:

在这个例子中,您可以看到 web 服务容器正在节点 01、02 和 04 上运行。美妙的是,您不需要知道哪些节点正在运行您的服务容器。您可以通过集群中的任何节点访问该服务。当然,您期望能够访问节点 01、02 或 04 上的容器,但是看看这个:

拥有在集群中的任何节点上访问服务的能力会带来一个不幸的副作用。你能想到可能是什么吗?我不会让你悬念太久。副作用是你只能将(主机)端口分配给集群中的一个服务。在我们的例子中,我们正在为我们的 web 服务使用端口8080。这意味着我们不能将端口8080用于我们想要在这个集群中运行的任何其他服务的主机端口:

参考资料

查看以下链接以获取更多信息:

总结

在本章中,我们最终开始整合一些要点,并实现一些有趣的事情。我们了解了通过启用集群模式和创建集群集群可以获得多少功能。而且,我们发现了使用一个swarm init命令设置一切有多么容易。然后,我们学会了如何扩展和管理我们的集群集群,最后,我们学会了如何在我们的新集群集群中将我们的容器作为服务运行。很有趣,对吧?!

现在,让我们把事情提升到下一个级别。在第六章中,Docker 网络,我们将学习关于 Docker 网络的知识。如果你准备好了解更多好东西,就翻页吧。

第六章:Docker 网络

在本章中,我们将学习关于 Docker 网络的知识。我们将深入研究 Docker 网络,学习容器如何被隔离,它们如何相互通信,以及它们如何与外部世界通信。我们将探索 Docker 在开箱即用安装中提供的本地网络驱动程序。然后,我们将通过部署 Weave 驱动程序的示例来研究远程网络驱动程序的使用。之后,我们将学习如何创建 Docker 网络。我们将通过查看我们的 Docker 网络所获得的免费服务来结束讨论。

“大约 97%的集装箱都是在中国制造的。在装运时,生产集装箱比在世界各地重新定位集装箱要容易得多。” - www.billiebox.co.uk/

在本章中,我们将涵盖以下主题:

  • 什么是 Docker 网络?

  • 内置(也称为本地)Docker 网络的全部内容

  • 第三方(也称为远程)Docker 网络如何?

  • 如何创建 Docker 网络

  • 免费的服务发现和负载平衡功能

  • 选择适合您需求的正确 Docker 网络驱动程序

技术要求

您将从 Docker 的公共存储库中拉取 Docker 镜像,并从 Weave 安装网络驱动程序,因此在执行本章示例时需要基本的互联网访问。此外,我们将使用jq 软件包,因此如果您尚未安装,请参阅如何执行此操作的说明-可以在第二章的容器检查命令部分找到,学习 Docker 命令

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Docker-Quick-Start-Guide/tree/master/Chapter06

查看以下视频以查看代码的实际操作:bit.ly/2FJ2iBK

什么是 Docker 网络?

正如你已经知道的,网络是一个允许计算机和其他硬件设备进行通信的连接系统。Docker 网络也是一样的。它是一个连接系统,允许 Docker 容器在同一台 Docker 主机上相互通信,或者与容器、计算机和容器主机之外的硬件进行通信,包括在其他 Docker 主机上运行的容器。

如果你熟悉宠物与牛群的云计算类比,你就明白了能够在规模上管理资源的必要性。Docker 网络允许你做到这一点。它们抽象了大部分网络的复杂性,为你的容器化应用程序提供了易于理解、易于文档化和易于使用的网络。Docker 网络基于一个由 Docker 创建的标准,称为容器网络模型CNM)。还有一个由 CoreOS 创建的竞争性网络标准,称为容器网络接口CNI)。CNI 标准已被一些项目采用,尤其是 Kubernetes,可以提出支持其使用的论点。然而,在本章中,我们将把注意力集中在 Docker 的 CNM 标准上。

CNM 已经被 libnetwork 项目实现,你可以通过本节参考中的链接了解更多关于该项目的信息。用 Go 编写的 CNM 实现由三个构造组成:沙盒、端点和网络。沙盒是一个网络命名空间。每个容器都有自己的沙盒。它保存了容器的网络堆栈配置。这包括其路由表、接口和 IP 和 MAC 地址的 DNS 设置。沙盒还包含容器的网络端点。接下来,端点是连接沙盒到网络的东西。端点本质上是网络接口,比如eth0。一个容器的沙盒可能有多个端点,但每个端点只能连接到一个网络。最后,网络是一组连接的端点,允许连接之间进行通信。每个网络都有一个名称、地址空间、ID 和网络类型。

Libnetwork 是一个可插拔的架构,允许网络驱动程序实现我们刚刚描述的组件的具体内容。每种网络类型都有自己的网络驱动程序。Docker 提供了内置驱动程序。这些默认或本地驱动程序包括桥接驱动程序和覆盖驱动程序。除了内置驱动程序,libnetwork 还支持第三方创建的驱动程序。这些驱动程序被称为远程驱动程序。一些远程驱动程序的例子包括 Calico、Contiv 和 Weave。

现在你已经了解了 Docker 网络是什么,阅读了这些细节之后,你可能会想,他说的“简单”在哪里?坚持住。现在我们将开始讨论你如何轻松地创建和使用 Docker 网络。与 Docker 卷一样,网络命令代表它们自己的管理类别。正如你所期望的,网络的顶级管理命令如下:

# Docker network managment command
docker network 

网络管理组中可用的子命令包括以下内容:

# Docker network management subcommands
docker network connect # Connect a container to a network
docker network create            # Create a network
docker network disconnect        # Disconnect a container from a network
docker network inspect # Display network details
docker network ls # List networks
docker network rm # Remove one or more networks
docker network prune # Remove all unused networks

现在让我们来看看内置或本地网络驱动程序。

参考

查看以下链接以获取更多信息:

内置(本地)Docker 网络

Docker 的开箱即用安装包括一些内置网络驱动程序。这些也被称为本地驱动程序。最常用的两个驱动程序是桥接网络驱动程序和覆盖网络驱动程序。其他内置驱动程序包括 none、host 和 MACVLAN。此外,没有创建网络的情况下,你的新安装将会有一些预先创建并准备好使用的网络。使用network ls命令,我们可以轻松地查看新安装中可用的预先创建的网络列表:

在这个列表中,您会注意到每个网络都有其独特的 ID、名称、用于创建它(并控制它)的驱动程序以及网络范围。不要将本地范围与驱动程序的类别混淆,驱动程序的类别也是本地。本地类别用于区分驱动程序的来源,而不是具有远程类别的第三方驱动程序。本地范围值表示网络的通信限制仅限于本地 Docker 主机内。为了澄清,如果两个 Docker 主机 H1 和 H2 都包含具有本地范围的网络,即使它们使用相同的驱动程序并且网络具有相同的名称,H1 上的容器也永远无法直接与 H2 上的容器通信。另一个范围值是 swarm,我们稍后会更多地谈论它。

在所有 Docker 部署中找到的预创建网络是特殊的,因为它们无法被移除。不需要将容器附加到其中任何一个,但是尝试使用 docker network rm 命令移除它们将始终导致错误。

有三个内置的网络驱动程序,其范围为本地:桥接、主机和无。主机网络驱动程序利用 Docker 主机的网络堆栈,基本上绕过了 Docker 的网络。主机网络上的所有容器都能够通过主机的接口相互通信。使用主机网络驱动程序的一个重要限制是每个端口只能被单个容器使用。也就是说,例如,您不能运行两个绑定到端口 80 的 nginx 容器。正如您可能已经猜到的那样,因为主机驱动程序利用了其所在主机的网络,每个 Docker 主机只能有一个使用主机驱动程序的网络:

接下来是空或无网络。使用空网络驱动程序创建一个网络,当容器连接到它时,会提供一个完整的网络堆栈,但不会在容器内配置任何接口。这使得容器完全隔离。这个驱动程序主要是为了向后兼容而提供的,就像主机驱动程序一样,Docker 主机上只能创建一个空类型的网络:

第三个具有本地范围的网络驱动程序是桥接驱动程序。桥接网络是最常见的类型。连接到同一桥接网络的任何容器都能够彼此通信。Docker 主机可以使用桥接驱动程序创建多个网络。但是,连接到一个桥接网络的容器无法与不同桥接网络上的容器通信,即使这些网络位于同一个 Docker 主机上。请注意,内置桥接网络和任何用户创建的桥接网络之间存在轻微的功能差异。最佳实践是创建自己的桥接网络并利用它们,而不是使用内置的桥接网络。以下是使用桥接网络运行容器的示例:

除了创建具有本地范围的网络的驱动程序之外,还有内置网络驱动程序创建具有集群范围的网络。这些网络将跨越集群中的所有主机,并允许连接到它们的容器进行通信,尽管它们在不同的 Docker 主机上运行。您可能已经猜到,使用具有集群范围的网络需要 Docker 集群模式。实际上,当您将 Docker 主机初始化为集群模式时,将为您创建一个具有集群范围的特殊新网络。这个集群范围网络被命名为ingress,并使用内置的覆盖驱动程序创建。这个网络对于集群模式的负载平衡功能至关重要,该功能在第五章的访问集群中的容器应用部分中使用了Docker Swarm。在swarm init中还创建了一个名为 docker_gwbridge 的新桥接网络。这个网络被集群用于向外通信,有点像默认网关。以下是在新的 Docker 集群中找到的默认内置网络:

使用覆盖驱动程序允许您创建跨 Docker 主机的网络。这些是第 2 层网络。在创建覆盖网络时,幕后会铺设大量网络管道。集群中的每个主机都会获得一个带有网络堆栈的网络沙盒。在该沙盒中,会创建一个名为 br0 的桥接。然后,会创建一个 VXLAN 隧道端点并将其附加到桥接 br0 上。一旦所有集群主机都创建了隧道端点,就会创建一个连接所有端点的 VXLAN 隧道。实际上,这个隧道就是我们看到的覆盖网络。当容器连接到覆盖网络时,它们会从覆盖子网中分配一个 IP 地址,并且该网络上的容器之间的所有通信都通过覆盖网络进行。当然,在幕后,通信流量通过 VXLAN 端点传递,穿过 Docker 主机网络,并且通过连接主机与其他 Docker 主机网络的任何路由器。但是,您永远不必担心所有幕后的事情。只需创建一个覆盖网络,将您的容器连接到它,您就大功告成了。

我们将讨论的下一个本地网络驱动程序称为 MACVLAN。该驱动程序创建的网络允许每个容器都有自己的 IP 和 MAC 地址,并连接到非 Docker 网络。这意味着除了使用桥接和覆盖网络进行容器间通信外,使用 MACVLAN 网络还可以连接到 VLAN、虚拟机和其他物理服务器。换句话说,MACVLAN 驱动程序允许您将容器连接到现有网络和 VLAN。必须在每个要运行需要连接到现有网络的容器的 Docker 主机上创建 MACVLAN 网络。而且,您需要为要连接的每个 VLAN 创建一个不同的 MACVLAN 网络。虽然使用 MACVLAN 网络听起来是一个好方法,但使用它有两个重要的挑战。首先,您必须非常小心地分配给 MACVLAN 网络的子网范围。容器将从您的范围中分配 IP,而不考虑其他地方使用的 IP。如果您有一个分配 IP 的 DHCP 系统与您给 MACVLAN 驱动程序的范围重叠,很容易导致重复的 IP 情况。第二个挑战是 MACVLAN 网络需要将您的网络卡配置为混杂模式。这在企业网络中通常是不被赞成的,但在云提供商网络中几乎是被禁止的,例如 AWS 和 Azure,因此 MACVLAN 驱动程序的使用情况非常有限。

本节涵盖了大量关于本地或内置网络驱动程序的信息。不要绝望!它们比这些丰富的信息所表明的要容易得多。我们将在创建 Docker 网络部分很快讨论创建和使用信息,但接下来,让我们快速讨论一下远程(也称为第三方)网络驱动程序。

参考资料

查看以下链接以获取更多信息:

第三方(远程)网络驱动程序

如前所述,在什么是 Docker 网络?部分中,除了 Docker 提供的内置或本地网络驱动程序外,CNM 还支持社区和供应商创建的网络驱动程序。其中一些第三方驱动程序的例子包括 Contiv、Weave、Kuryr 和 Calico。使用这些第三方驱动程序的好处之一是它们完全支持在云托管环境中部署,例如 AWS。为了使用这些驱动程序,它们需要在每个 Docker 主机的单独安装步骤中安装。每个第三方网络驱动程序都带来了自己的一套功能。以下是 Docker 在参考架构文档中分享的这些驱动程序的摘要描述:

尽管这些第三方驱动程序各自具有独特的安装、设置和执行方法,但一般步骤是相似的。首先,您下载驱动程序,然后处理任何配置设置,最后运行驱动程序。这些远程驱动程序通常不需要群集模式,并且可以在有或没有群集模式的情况下使用。例如,让我们深入了解如何使用织物驱动程序。要安装织物网络驱动程序,请在每个 Docker 主机上发出以下命令:

# Install the weave network driver plug-in
sudo curl -L git.io/weave -o /usr/local/bin/weave
sudo chmod a+x /usr/local/bin/weave
# Disable checking for new versions
export CHECKPOINT_DISABLE=1
# Start up the weave network
weave launch [for 2nd, 3rd, etc. optional hostname or IP of 1st Docker host running weave]
# Set up the environment to use weave
eval $(weave env)

上述步骤需要在将用于在织物网络上相互通信的容器的每个 Docker 主机上完成。启动命令可以提供第一个 Docker 主机的主机名或 IP 地址,该主机已设置并已运行织物网络,以便与其对等,以便它们的容器可以通信。例如,如果您已经在node01上设置了织物网络,当您在node02上启动织物时,您将使用以下命令:

# Start up weave on the 2nd node
weave launch node01

或者,您可以使用连接命令连接新的(Docker 主机)对等体,从已配置的第一个主机执行。要添加node02(在安装和运行织物后),请使用以下命令:

# Peer host node02 with the weave network by connecting from node01
weave connect node02

您可以在主机上不启用群集模式的情况下使用织物网络驱动程序。一旦织物被安装和启动,并且对等体(其他 Docker 主机)已连接,您的容器将自动利用织物网络,并能够相互通信,无论它们是在同一台 Docker 主机上还是在不同的主机上。

织物网络显示在您的网络列表中,就像您的其他任何网络一样:

让我们测试一下我们闪亮的新网络。首先确保你已经按照之前描述的步骤在所有你想要连接的主机上安装了 weave 驱动。确保你要么使用node01作为参数启动命令,要么从node01开始为你配置的每个额外节点使用 connect 命令。在这个例子中,我的实验服务器名为 ubuntu-node01 和 ubuntu-node02。让我们从node02开始:

请注意,在ubuntu-node01上:

# Install and setup the weave driver
sudo curl -L git.io/weave -o /usr/local/bin/weave
sudo chmod a+x /usr/local/bin/weave
export CHECKPOINT_DISABLE=1
weave launch
eval $(weave env)

并且,请注意,在ubuntu-node02上:

# Install and setup the weave driver
sudo curl -L git.io/weave -o /usr/local/bin/weave
sudo chmod a+x /usr/local/bin/weave
export CHECKPOINT_DISABLE=1
weave launch
eval $(weave env)

现在,回到ubuntu-node01,请注意以下内容:

# Bring node02 in as a peer on node01's weave network
weave connect ubuntu-node02

现在,让我们在每个节点上启动一个容器。确保给它们命名以便易于识别,从ubuntu-node01开始:

# Run a container detached on node01
docker container run -d --name app01 alpine tail -f /dev/null

现在,在ubuntu-node02上启动一个容器:

# Run a container detached on node02
docker container run -d --name app02 alpine tail -f /dev/null

很好。现在,我们在两个节点上都有容器在运行。让我们看看它们是否可以通信。因为我们在node02上,我们首先检查那里:

# From inside the app02 container running on node02,
# let's ping the app01 container running on node01
docker container exec -it app02 ping -c 4 app01

是的!成功了。让我们试试反过来:

# Similarly, from inside the app01 container running on node01,
# let's ping the app02 container running on node02
docker container exec -it app01 ping -c 4 app02

太棒了!我们有双向通信。你注意到了什么其他的吗?我们的应用容器有名称解析(我们不仅仅需要通过 IP 来 ping)。非常好,对吧?

参考资料

查看这些链接以获取更多信息:

创建 Docker 网络

好的,现在你已经对本地和远程网络驱动有了很多了解,你已经看到了在安装 Docker 和/或初始化 swarm 模式(或安装远程驱动)时,有几个驱动是为你创建的。但是,如果你想使用其中一些驱动创建自己的网络怎么办?这其实非常简单。让我们来看看。network create命令的内置帮助如下:

# Docker network create command syntax
# Usage: docker network create [OPTIONS] NETWORK

检查这个,我们看到这个命令基本上有两个部分需要处理,OPTIONS 后面跟着我们想要创建的网络的 NETWORK 名称。我们有哪些选项?嗯,有相当多,但让我们挑选一些让你快速上手的。

可能最重要的选项是--driver选项。这是我们告诉 Docker 在创建此网络时要使用哪个可插拔网络驱动程序的方式。正如您所见,驱动程序的选择决定了网络的特性。您提供给驱动程序选项的值将类似于从docker network ls命令的输出中显示的 DRIVER 列中显示的值。一些可能的值是 bridge、overlay 和 macvlan。请记住,您不能创建额外的主机或空网络,因为它们限制为每个 Docker 主机一个。到目前为止,这可能是什么样子?以下是使用大部分默认选项创建新覆盖网络的示例:

# Create a new overlay network, with all default options
docker network create -d overlay defaults-over

这很好。您可以运行新服务并将它们附加到您的新网络。但是我们可能还想控制网络中的其他内容吗?嗯,IP 空间怎么样?是的,Docker 提供了控制网络 IP 设置的选项。这是使用--subnet--gateway--ip-range可选参数来完成的。所以,让我们看看如何使用这些选项创建一个新网络。如果您还没有安装 jq,请参阅第二章,学习 Docker 命令,了解如何安装它:

# Create a new overlay network with specific IP settings
docker network create -d overlay \
--subnet=172.30.0.0/24 \
--ip-range=172.30.0.0/28 \
--gateway=172.30.0.254 \
specifics-over
# Initial validation
docker network inspect specifics-over --format '{{json .IPAM.Config}}' | jq

在我的实验室中执行上述代码看起来是这样的:

通过查看这个例子,我们看到我们使用特定的 IP 参数为子网、IP 范围和网关创建了一个新的覆盖网络。然后,我们验证了网络是否使用了请求的选项进行创建。接下来,我们使用我们的新网络创建了一个服务。然后,我们找到了属于该服务的容器的容器 ID,并用它来检查容器的网络设置。我们可以看到,容器是使用我们配置网络的 IP 范围中的 IP 地址(在这种情况下是172.30.0.7)运行的。看起来我们成功了!

如前所述,在创建 Docker 网络时还有许多其他选项可用,我将把它作为一个练习留给您,让您使用docker network create --help命令来发现它们,并尝试一些选项以查看它们的功能。

参考资料

您可以在docs.docker.com/engine/reference/commandline/network_create/找到network create命令的文档。

免费网络功能

有两个网络功能或服务是您在 Docker 群集网络中免费获得的。第一个是服务发现,第二个是负载均衡。当您创建 Docker 服务时,您会自动获得这些功能。我们在本章和第五章《Docker Swarm》中体验了这些功能,但并没有真正以名称的方式提到它们。所以,在这里我们来具体提一下。

首先是服务发现。当您创建一个服务时,它会得到一个唯一的名称。该名称会在群集 DNS 中注册。而且,每个服务都使用群集 DNS 进行名称解析。这里有一个例子。我们将利用之前在创建 Docker 网络部分创建的specifics-over叠加网络。我们将创建两个服务(tester1tester2)并连接到该网络,然后我们将连接到tester1服务中的一个容器,并通过名称 pingtester2服务。看一下:

# Create service tester1
docker service create --detach --replicas 3 --name tester1 \
 --network specifics-over alpine tail -f /dev/null
# Create service tester2
docker service create --detach --replicas 3 --name tester2 \
 --network specifics-over alpine tail -f /dev/null
# From a container in the tester1 service ping the tester2 service by name
docker container exec -it tester1.3.5hj309poppj8jo272ks9n4k6a ping -c 3 tester2

以下是执行前述命令时的样子:

请注意,我输入了服务名称的第一部分(tester1)并使用命令行补全,通过按下Tab键来填写 exec 命令的容器名称。但是,正如您所看到的,我能够在tester1容器内通过名称引用tester2服务。

免费!

我们得到的第二个免费功能是负载均衡。这个强大的功能非常容易理解。它允许将发送到服务的流量发送到群集中的任何主机,而不管该主机是否正在运行服务的副本。

想象一下这样的情景:您有一个六节点的群集集群,以及一个只部署了一个副本的服务。您可以通过群集中的任何主机发送流量到该服务,并知道无论容器实际在哪个主机上运行,流量都会到达服务的一个容器。事实上,您可以使用负载均衡器将流量发送到群集中的所有主机,比如采用轮询模式,每次将流量发送到负载均衡器时,该流量都会无误地传递到应用程序容器。

相当方便,对吧?再次强调,这是免费的!

参考资料

想要尝试服务发现吗?那就查看training.play-with-docker.com/swarm-service-discovery/

你可以在docs.docker.com/engine/swarm/key-concepts/#load-balancing阅读有关 swarm 服务负载平衡的信息。

我应该使用哪个 Docker 网络驱动程序?

对于这个问题的简短答案就是适合工作的正确驱动程序。这意味着没有单一的网络驱动程序适合每种情况。如果你在笔记本电脑上工作,swarm 处于非活动状态,并且只需要容器之间能够通信,那么简单的桥接模式驱动程序是理想的。

如果你有多个节点,只需要容器对容器的流量,那么覆盖驱动程序是正确的选择。如果你需要容器对 VM 或容器对物理服务器的通信(并且可以容忍混杂模式),那么 MACVLAN 驱动程序是最佳选择。或者,如果你有更复杂的需求,许多远程驱动程序可能正是你需要的。

我发现对于大多数多主机场景,覆盖驱动程序可以胜任,所以我建议你启用 swarm 模式,并在升级到其他多主机选项之前尝试覆盖驱动程序。

总结

你现在对 Docker 网络有什么感觉?Docker 已经将复杂的技术网络变得易于理解和使用。大部分疯狂、困难的设置都可以通过一个swarm init命令来处理。让我们回顾一下:你了解了 Docker 创建的网络设计,称为容器网络模型或 CNM。然后,你了解了 libnetwork 项目如何将该模型转化为可插拔架构。之后,你发现 Docker 创建了一组强大的驱动程序,可以插入 libnetwork 架构,以满足大部分容器通信需求的各种网络选项。由于架构是可插拔的,其他人已经创建了更多的网络驱动程序,解决了 Docker 驱动程序无法处理的任何边缘情况。Docker 网络真的已经成熟了。

我希望你已经做好准备,因为在第七章中,Docker Stacks,我们将深入探讨 Docker 堆栈。这是你迄今为止学到的所有信息真正汇聚成一种辉煌的交响乐。深呼吸,翻开下一页吧!

第七章:Docker 堆栈

在本章中,我们将汇集前六章所学,并用它来定义、部署和管理多容器应用程序。我们将通过使用 Docker 堆栈来实现这一点。我们将学习如何使用 Docker 堆栈和定义多容器应用程序所需的 YAML 文件。我们将利用我们对 Docker 服务、Docker 卷、Docker 集群和 Docker 网络的了解来创建功能齐全的基于 Docker 的多服务应用程序。

最大的货船长 400 米,可以携带 15,000 至 18,000 个集装箱!

在本章中,我们将涵盖以下主题:

  • 使用 Docker 堆栈

  • 部署多服务 Docker 应用程序

  • 创建和使用 compose(堆栈)YAML 文件

  • 扩展已部署的多服务 Docker 应用程序

技术要求

您将从 Docker 的公共存储库中拉取 Docker 镜像,并从 Weave 安装网络驱动程序,因此执行本章示例需要基本的互联网访问。此外,我们将使用 jq 软件包,因此如果您尚未安装,请参阅如何安装的说明;可以在第二章的容器检查命令部分找到。

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Docker-Quick-Start-Guide/tree/master/Chapter07

查看以下视频以查看代码的实际操作:bit.ly/2E2qc9U

了解 Docker 堆栈的使用

到目前为止,我们主要关注的是从单个 Docker 镜像中运行 Docker 容器,简化 Docker 模型,想象一个世界,每个应用程序只需要一个服务,因此只需要一个 Docker 镜像来运行。然而,正如你所知,这是一个相当不现实的模型。现实世界的应用程序由多个服务组成,并且这些服务是使用多个 Docker 镜像部署的。要运行所有必要的容器,并将它们保持在所需数量的副本,处理计划和非计划的停机时间,扩展需求以及所有其他服务管理需求是一个非常艰巨和复杂的任务。最近,这种情况是使用一个名为 Docker Compose 的工具来处理的。Docker Compose(正如你在第一章中学到的,设置 Docker 开发环境)是一个额外的工具,你可以在 Docker 环境中安装,我们已经在工作站的环境中完成了安装。虽然 Docker Compose 的许多功能与 Docker 堆栈中的功能类似,但我们将在本章中专注于 Docker 堆栈。我们这样做是因为 Docker Compose 用于管理容器,而 Docker 世界已经向服务作为通用单元的演变。Docker 堆栈管理服务,因此我认为 Docker 堆栈是 Docker Compose 的演变(它是一个名为 Fig 的项目的演变)。我们之所以没有在第一章中安装 Docker 堆栈,设置 Docker 开发环境,是因为堆栈已经作为标准 Docker 安装的一部分包含在内。

好的,所以 Docker 堆栈是新的改进版 Docker Compose,并且已包含在我们的安装中。我敢打赌你在想,太好了。但这意味着什么?Docker 堆栈的用例是什么?好问题!Docker 堆栈是利用我们在前几章中学到的所有功能的方式,比如 Docker 命令、Docker 镜像、Docker 服务、Docker 卷、Docker 集群和 Docker 网络,将所有这些功能包装在一个易于使用、易于理解的声明性文档文件中,这将代表我们实例化和维护一个复杂的多镜像应用程序。

大部分工作,仍然是简单的部分,将在创建用于 Docker 堆栈命令的 compose 文件中进行。当 Docker 创建、启动和管理所有多服务(多容器)应用程序所需的所有服务时,所有真正的艰苦工作都将由 Docker 完成。所有这些都由您的一条命令处理。就像镜像一样,容器和 swarm 堆栈是另一个 Docker 管理组。让我们来看看堆栈管理命令:

那么,我们在这里有什么?对于这个管理组所代表的所有功能,它有一组非常简单的命令。主要命令是deploy命令。它是强大的!通过此命令(和一个 compose 文件),您将启动您的应用程序,拉取任何不在本地环境中的镜像,运行镜像,根据需要创建卷,根据需要创建网络,为每个镜像部署定义的副本数量,将它们分布在您的 swarm 中以实现高可用性和负载平衡,并且更多。这个命令有点像《指环王》中的一环。除了部署应用程序,当您需要执行诸如扩展应用程序之类的操作时,您将使用相同的命令来更新正在运行的应用程序。

管理组中的下一个命令是列出堆栈的命令。顾名思义,ls 命令允许您获取当前部署到您的 swarm 的所有堆栈的列表。当您需要关于正在 swarm 中运行的特定堆栈的更详细信息时,您将使用ps命令列出特定堆栈的所有任务。当到达结束生命周期部署的堆栈时,您将使用强大的 rm 命令。最后,作为管理命令的补充,我们有 services 命令,它允许我们获取堆栈的一部分的服务列表。堆栈谜题的另一个重要部分是--orchestrator选项。通过此选项,我们可以指示 Docker 使用 Docker swarm 或 Kubernetes 进行堆栈编排。当然,要使用 Kubernetes,必须已安装,并且要使用 swarm——如果未指定该选项,则必须启用 swarm 模式。

在本章的其余部分,我们将深入研究使用示例应用程序的 Docker stacks。Docker 提供了几个这样的示例,但我们要检查的是投票应用程序示例。我将提供应用程序的 Docker 存储库的链接,以及我空间中项目的分支的链接,以防 Docker 应用程序发生重大变化或项目消失。让我们来看一下示例投票应用程序的堆栈文件。

参考资料

查看以下链接以获取更多信息:

如何创建和使用 Compose YAML 文件用于 Stacks

堆栈文件是一个 YAML 文件,基本上与 Docker Compose 文件相同。两者都是定义 Docker 基础应用程序的 YAML 文件。从技术上讲,堆栈文件是一个需要特定版本(或更高版本)的 Compose 规范的 Compose 文件。Docker stacks 仅支持版本 3.0 规范及以上。如果您有一个使用 Docker compose YAML 文件的现有项目,并且这些文件使用的是版本 2 或更旧的规范,那么您需要将 YAML 文件更新到版本 3 规范,以便能够在 Docker stacks 中使用它们。值得注意的是,相同的 YAML 文件可以用于 Docker stacks 或 Docker compose(前提是使用了版本 3 规范或更高版本)。但是,有一些指令将被其中一个工具忽略。例如,Docker stacks 会忽略构建指令。这是因为堆栈和 compose 之间最重要的区别之一是,所有使用的 Docker 映像必须预先创建以供堆栈使用,而 Docker 映像可以作为建立基于 compose 的应用程序的一部分而创建。另一个重要的区别是,堆栈文件能够定义 Docker 服务作为应用程序的一部分。

现在是克隆投票应用程序项目和可视化器镜像存储库的好时机。

# Clone the sample voting application and the visualizer repos
git clone https://github.com/EarlWaud/example-voting-app.git
git clone https://github.com/EarlWaud/docker-swarm-visualizer.git

严格来说,您不需要克隆这两个存储库,因为您真正需要的只是投票应用程序的堆栈组合文件。这是因为所有的镜像已经被创建并且可以从 hub.docker.com 公开获取,并且当您部署堆栈时,这些镜像将作为部署的一部分被获取。因此,这是获取堆栈 YAML 文件的命令:

# Use curl to get the stack YAML file
curl -o docker-stack.yml\
 https://raw.githubusercontent.com/earlwaud/example-voting-app/master/docker-stack.yml

当然,如果您想以任何方式自定义应用程序,将项目本地化可以让您构建自己的 Docker 镜像版本,然后使用您的自定义镜像部署应用程序的自定义版本。

一旦您在系统上拥有项目(或至少有docker-stack.yml文件),您就可以开始使用 Docker 堆栈命令进行操作。现在,让我们继续使用docker-stack.yml文件来部署我们的应用程序。您需要设置好 Docker 节点并启用 swarm 模式才能使其工作,所以如果您还没有这样做,请按照第五章中描述的设置您的 swarm,Docker Swarm。然后,使用以下命令来部署您的示例投票应用程序:

# Deploy the example voting application 
# using the downloaded stack YAML file
docker stack deploy -c docker-stack.yml voteapp

这是它可能看起来的样子:

让我快速解释一下这个命令:我们正在使用deploy命令与docker-stack.yml组合文件,并将我们的堆栈命名为voteapp。这个命令将处理我们新应用程序的所有配置、部署和管理。根据docker-stack.yml文件中定义的内容,需要一些时间来使一切都正常运行,所以在这段时间里,让我们开始深入了解我们的堆栈组合文件。

到目前为止,您知道我们正在使用docker-stack.yml文件。因此,当我们解释堆栈组合文件的各个部分时,您可以在您喜欢的编辑器中打开该文件,并跟随我们的讲解。我们开始吧!

我们要看的第一件事是顶层键。在这种情况下,它们如下所示:

  • 版本

  • 服务

  • 网络

如前所述,版本必须至少为 3 才能与 Docker 堆栈一起使用。查看docker-stack.yml文件中的第 1 行(版本键始终在第 1 行),我们看到以下内容:

完美!我们有一个符合版本 3 规范的组合文件。在一分钟内跳过(折叠的)服务密钥部分,让我们看一下网络密钥,然后是卷密钥。在网络密钥部分,我们指示 Docker 创建两个网络,一个名为 frontend,一个名为 backend。实际上,在我们的情况下,网络将被命名为voteapp_frontendvoteapp_backend。这是因为我们将我们的堆栈命名为voteapp,Docker 将在部署堆栈的一部分时将堆栈的名称前置到各个组件的名称之前。通过在堆栈文件的网络密钥中包含我们所需网络的名称,Docker 将在部署堆栈时创建我们的网络。我们可以为每个网络提供特定的细节(正如我们在第六章中学到的,Docker 网络),但如果我们不提供任何细节,那么将使用某些默认值。我们的堆栈可能已经足够长时间来部署我们的网络了,所以让我们使用网络列表命令来看看我们现在有哪些网络:

它们在这里:voteapp_frontendvoteapp_backend。您可能想知道voteapp_default网络是什么。当您部署一个堆栈时,您将始终获得一个默认的 Swarm 网络,如果它们在堆栈组合文件中没有为它们定义任何其他网络连接,那么所有容器都将连接到它。这非常酷,对吧?!您不必执行任何 docker 网络创建命令,您的所需网络已经在应用程序中创建并准备好使用。

卷密钥部分基本上与网络密钥部分做了相同的事情,只是它是为卷而不是网络。当您部署堆栈时,您的定义卷将自动创建。如果在堆栈文件中没有提供额外的配置,卷将以默认设置创建。在我们的示例中,我们要求 Docker 创建一个名为db-data的卷。正如您可能已经猜到的那样,实际创建的卷的名称实际上是voteapp_db-data,因为 Docker 将我们堆栈的名称前置到卷名称之前。在我们的情况下,它看起来像这样:

因此,部署我们的堆栈创建了我们期望的网络和我们期望的卷。所有这些都是通过我们堆栈组合文件中易于创建、易于阅读和理解的内容实现的。好的,现在我们对堆栈组合文件中的四个顶级键部分中的三个有了很好的理解。现在,让我们返回到服务键部分。如果我们展开这个键部分,我们将看到我们希望作为应用程序的一部分部署的每个服务的定义。在docker-stack.yml文件的情况下,我们定义了六个服务。这些是 redis、db、vote、result、worker 和 visualizer。在堆栈组合文件中,它们看起来像这样:

让我们扩展第一个 redis,并仔细看一下为我们的应用程序定义的 redis 服务:

如果您回忆一下来自第五章的 Docker 服务的讨论,Docker Swarm,那么这里显示的许多键对您来说应该是很熟悉的。现在让我们来检查 redis 服务中的键。首先,我们有image键。图像键是服务定义所必需的。这个键告诉 docker 要拉取和运行这个服务的 Docker 镜像是redis:alpine。正如您现在应该理解的那样,这意味着我们正在使用来自 hub.docker.com 的官方 redis 镜像,请求标记为alpine的版本。接下来使用的键是ports。它定义了容器将从主机暴露的端口以及主机的端口。在这种情况下,要映射到容器的暴露端口(6379)的主机端口由 Docker 分配。您可以使用docker container ls命令找到分配的端口。在我的情况下,redis 服务将主机的端口30000映射到容器的端口6379。接下来使用的键是networks。我们已经看到部署堆栈将为我们创建网络。这个指令告诉 Docker 应该将 redis 副本容器连接到哪些网络;在这种情况下是frontend网络。如果我们检查 redis 副本容器,检查网络部分,我们将看到这是准确的。您可以使用这样的命令查看您的部署(请注意,容器名称在您的系统上可能略有不同):

# Inspect a redis replica container looking at the networks
docker container inspect voteapp_redis.1.nwy14um7ik0t7ul0j5t3aztu5  \
 --format '{{json .NetworkSettings.Networks}}' | jq

在我们的示例中,您应该看到容器连接到两个网络:入口网络和我们的voteapp_frontend网络。

我们 redis 服务定义中的下一个键是 deploy 键。这是在 compose 文件规范的 3 版本中添加的一个键类别。它定义了基于此服务中的镜像运行容器的具体信息:在这种情况下,是 redis 镜像。这实质上是编排指令。replicas标签告诉 docker 在应用程序完全部署时应该运行多少副本或容器。在我们的示例中,我们声明我们只需要一个 redis 容器的实例运行。update_config键提供了两个子键,parallelismdelay,告诉 Docker 应该以多少容器replicas并行启动,并且在启动每个parallel容器replicas之间等待多长时间。当然,对于一个副本,parallelism 和 delay 的细节几乎没有用处。如果replicas的值更大,比如10,我们的 update_config 键将导致两个副本同时启动,并且在启动之间等待 10 秒。最后的 deploy 键是restart_policy,它定义了在部署的堆栈中新副本将被创建的条件。在这种情况下,如果一个 redis 容器失败,将启动一个新的 redis 容器来替代它。让我们来看看我们应用程序中的下一个服务,db服务:

db 服务与 redis 服务将有几个相同的键,但值不同。首先,我们有 image 键。这次我们指定要使用带有版本 9.4 标签的官方 postgres 镜像。我们的下一个键是 volumes 键。我们指定我们正在使用名为 db-data 的卷,并且在 DB 容器中,卷应该挂载在/var/lib/postgresql/data。让我们来看看我们环境中的卷信息:

使用 volume inspect 命令,我们可以获取卷的挂载点,然后比较容器内文件夹的内容与主机上挂载点的内容:

哇!正如预期的那样,它们匹配。在 Mac 上,情况并非如此简单。有关如何在 OS X 上处理此问题的详细信息,请参阅 Docker Volumes 第四章,有关 Docker 卷的详细信息。接下来是网络密钥,在这里我们指示 Docker 将后端网络连接到我们的 db 容器。接下来是部署密钥。在这里,我们看到一个名为placement的新子密钥。这是一个指令,告诉 Docker 我们只希望 db 容器在管理节点上运行,也就是说,在具有manager角色的节点上。

您可能已经注意到,部署密钥的一些子密钥存在于 redis 服务中,但在我们的 db 服务中不存在,最显著的是replicas密钥。默认情况下,如果您没有指定要维护的副本数量,Docker 将默认为一个副本。总的来说,db 服务配置的描述与 redis 服务几乎相同。您将看到所有服务的配置之间的相似性。这是因为 Docker 已经非常容易地定义了我们服务的期望状态,以及我们的应用程序。为了验证这一点,让我们来看一下堆栈组合文件中的下一个服务,即vote服务:

您应该开始熟悉这些密钥及其值。在投票服务中,我们看到定义的镜像不是官方容器镜像之一,而是在名为dockersamples的公共存储库中。在该存储库中,我们使用了名为examplevotingapp_vote的图像,版本标签为before。我们的端口密钥告诉 Docker 和我们,我们要在 swarm 主机上打开端口5000,并且将该端口上的流量映射到正在运行的投票服务容器中的端口 80。事实证明,投票服务是我们应用程序的face,我们将通过端口5000访问它。由于它是一个服务,我们可以通过在 swarm 中的任何主机上的端口5000访问它,即使特定主机没有运行其中一个副本。

看着下一个关键点,我们看到我们正在将frontend网络连接到我们的投票服务容器。在那里没有什么新的,然而,因为我们下一个关键点是我们以前没有见过的:depends_on关键点。这个关键点告诉 Docker 我们的投票服务需要 redis 服务才能运行。对于我们的deploy命令来说,这意味着被依赖的服务需要在启动这个服务之前启动。具体来说,redis 服务需要在投票服务之前启动。这里的一个关键区别是我说的是启动。这并不意味着在启动这个服务之前必须运行依赖的服务;依赖的服务只需要在之前启动。再次强调,具体来说,redis 服务在启动投票服务之前不必处于运行状态,它只需要在投票服务之前启动。在投票服务的部署关键点中,我们还没有看到任何新的东西,唯一的区别是我们要求投票服务有两个副本。你开始理解堆栈组合文件中服务定义的简单性和强大性了吗?

在我们的堆栈组合文件中定义的下一个服务是结果服务。然而,由于在该服务定义中没有我们之前没有见过的关键点,我将跳过对结果服务的讨论,转而讨论工作人员服务,我们将看到一些新东西。以下是工作人员服务的定义:

你知道图像密钥及其含义。你也知道网络密钥及其含义。你知道部署密钥,但是我们在这里有一些新的子密钥,所以让我们谈谈它们,从mode密钥开始。你可能还记得我们在第五章中讨论服务时,Docker Swarm,有一个--mode参数,可以有两个值:globalreplicated。这个密钥与我们在第五章中看到的参数完全相同,Docker Swarm。默认值是 replicated,所以如果你不指定 mode 密钥,你将得到 replicated 行为,即确切地有定义的副本数量(或者如果没有指定副本数量,则为一个副本)。使用 global 的其他值选项将忽略 replicas 密钥,并在集群中的每个主机上部署一个容器。

我们在这个堆栈组合文件中以前没有见过的下一个密钥是labels密钥。这个密钥的位置很重要,因为它可以作为自己的上层密钥出现,也可以作为 deploy 密钥的子密钥出现。有什么区别?当你将labels密钥作为 deploy 密钥的子密钥使用时,标签将仅设置在服务上。当你将labels密钥作为自己的上层密钥使用时,标签将被添加到作为服务的一部分部署的每个副本或容器中。在我们的例子中,APP=VOTING标签将被应用到服务,因为labels密钥是 deploy 密钥的子密钥。再次,在我们的环境中看看这个:

# Inspect the worker service to see its labels
docker service inspect voteapp_worker \
 --format '{{json .Spec.Labels}}' | jq

在我的系统上看起来是这样的:

在工作容器上执行 inspect 命令以查看其标签,将显示APP=VOTING标签不存在。如果你想在你的系统上确认这一点,命令将如下(使用不同的容器名称):

# Inspect the labels on a worker container
docker container inspect voteapp_worker.1.rotx91qw12d6x8643z6iqhuoj \
 -f '{{json .Config.Labels}}' | jq

在我的系统上看起来是这样的:

重启策略键的两个新子键是max_attemptswindow键。你可能能猜到它们的目的;max_attempts键告诉 Docker 在放弃之前尝试启动工作容器的次数,最多三次。window键告诉 Docker 在之前多久等待重新尝试启动工作容器,如果之前启动失败。相当简单,对吧?同样,这些定义很容易设置,易于理解,并且对于编排我们应用程序的服务非常强大。

好的。我们还有一个服务定义需要审查新内容,那就是可视化服务。在我们的堆栈组合文件中,它看起来是这样的:

唯一真正新的键是stop_grace_period键。这个键告诉 Docker 在它告诉一个容器停止之后等待多长时间才会强制停止容器。如果没有使用stop_grace_period键,默认时间段是 10 秒。当你需要更新一个堆栈,本质上是重新堆叠,一个服务的容器将被告知优雅地关闭。Docker 将等待在stop_grace_period键中指定的时间量,或者如果没有提供键,则等待 10 秒。如果容器在那段时间内关闭,容器将被移除,并且一个新的容器将被启动来取代它。如果容器在那段时间内没有关闭,它将被强制停止,杀死它,然后移除它,然后启动一个新的容器来取代它。这个键的重要性在于它允许运行需要更长时间才能优雅停止的进程的容器有必要的时间来实际优雅停止。

我想指出这项服务的最后一个方面,那就是关于列出的有点奇怪的卷。这不是一个典型的卷,并且在卷键定义中没有条目。/var/run/docker.sock:/var/run/docker.sock卷是一种访问主机的 Docker 守护程序正在侦听的 Unix 套接字的方式。在这种情况下,它允许容器与其主机通信。可视化器容器正在收集关于哪些容器在哪些主机上运行的信息,并且能够以图形方式呈现这些数据。你会注意到它将 8080 主机端口映射到 8080 容器端口,所以我们可以通过浏览到任何我们的 swarm 节点上的 8080 端口来查看它共享的数据。这是我(当前)三节点 swarm 上的样子:

堆栈其余命令

现在,让我们通过我们部署了voteapp堆栈的 swarm 的视角快速看一下我们的其他与堆栈相关的命令。首先,我们有列出堆栈的命令:docker stack ls。试一下看起来像这样:

# List the stacks deployed in a swarm
docker stack ls

这是示例环境中的样子:

这表明我们当前部署了一个名为 voteapp 的堆栈,它由六个服务组成,并且正在使用 swarm 模式进行编排。知道部署堆栈的名称可以让我们使用其他堆栈命令来收集更多关于它的信息。接下来是列出堆栈任务的命令。让我们在示例环境中尝试一下这个命令:

# List the tasks for our voteapp stack filtered by desried state
docker stack ps voteapp --filter desired-state=running

这是我当前环境中的结果;你的应该看起来非常相似:

现在,让我们来看看堆栈服务命令。这个命令将为我们提供一个关于作为堆栈应用程序一部分部署的服务的简要摘要。命令看起来像这样:

# Look at the services associated with a deployed stack
docker stack services voteapp

这是我们在示例环境中看到的:

这个命令提供了一些非常有用的信息。我们可以快速看到我们服务的名称,所需副本的数量,以及每个服务的实际副本数量。我们可以看到用于部署每个服务的镜像,并且我们可以看到每个服务使用的端口映射。在这里,我们可以看到可视化服务正在使用端口8080,就像我们之前提到的那样。我们还可以看到我们的投票服务暴露在我们集群主机的端口5000上。让我们通过浏览到端口5000(在集群中的任何节点上)来看看我们在我们的 voteapp 中展示了什么:

你是狗派还是猫派?你可以通过在你自己的 voteapp 中投票来表达自己!投票然后使用堆栈服务命令中的数据来查看投票结果,浏览到端口5001

是的,我是一个狗派。还有一个最终的堆栈命令:删除命令。我们可以通过发出rm命令来快速轻松地关闭使用堆栈部署的应用程序。看起来是这样的:

# Remove a deploy stack using the rm command
docker stack rm voteapp

现在你看到它了,现在你看不到了:

你应该注意到这里没有任何“你确定吗?”的提示,所以在按下Enter键之前一定要非常确定和非常小心。让我们通过快速查看作为 Docker 堆栈部署的应用程序的扩展或重新堆叠的最佳实践来结束对 Docker 堆栈的讨论。

扩展堆栈应用程序的最佳实践

与大多数 Docker 相关的事物一样,有几种不同的方法可以实现应用程序的期望状态。当您使用 Docker 堆栈时,应始终使用与部署应用程序相同的方法来更新应用程序。在堆栈 compose 文件中进行任何期望的状态更改,然后运行与部署堆栈时使用的完全相同的命令。这允许您使用标准源代码控制功能来正确处理您的 compose 文件,例如跟踪和审查更改。而且,它允许 Docker 正确地为您的应用程序进行编排。如果您需要在应用程序中缩放服务,您应该在堆栈 compose 文件中更新 replicas 键,然后再次运行部署命令。在我们的示例中,我们的投票服务有两个副本。如果投票需求激增,我们可以通过将 replica 值从 2 更改为 16 来轻松扩展我们的应用程序,方法是编辑docker-stack.yml文件,然后发出与最初用于部署应用程序相同的命令:

# After updating the docker-stack.yml file, scale the app using the same deploy command
docker stack deploy -c docker-stack.yml voteapp

现在,当我们检查服务时,我们可以看到我们正在扩展我们的应用程序:

就是这样,一个易于使用、易于理解且非常强大的 Docker 应用程序编排!

参考资料

查看以下链接获取更多信息:

摘要

现在你对 Docker 堆栈有了很多了解。你可以使用 compose 文件轻松创建应用程序定义,然后使用 stack deploy 命令部署这些应用程序。你可以使用 ls、ps 和 services 命令探索已部署堆栈的细节。你可以通过对 compose 文件进行简单修改并执行与部署应用程序相同的命令来扩展你的应用程序。最后,你可以使用 stack rm 命令移除已经到达生命周期终点的应用程序。伴随着强大的能力而来的是巨大的责任,所以在使用移除命令时要非常小心。现在你已经有足够的信息来创建和编排世界级的企业级应用程序了,所以开始忙碌起来吧!然而,如果你想学习如何将 Docker 与 Jenkins 一起使用,你会很高兴地知道这就是第八章《Docker 和 Jenkins》的主题,所以翻开书页开始阅读吧!

第八章:Docker 和 Jenkins

在本章中,我们将学习如何利用 Jenkins 来构建我们的 Docker 镜像并部署我们的 Docker 容器。接下来,我们将学习如何将我们的 Jenkins 服务器部署为 Docker 容器。然后,我们将学习如何在 Docker 化的 Jenkins 服务器中构建 Docker 镜像。这通常被称为 Docker 中的 Docker。最后,我们将看到如何利用 Docker 容器作为 Jenkins 构建代理,允许每个构建在一个原始的、短暂的 Docker 容器中运行。当然,我们将展示如何在我们的 Docker 化的 Jenkins 构建代理中构建 Docker 镜像、测试应用程序,并将经过测试的镜像推送到 Docker 注册表中。这将为您提供设置 CI/CD 系统所需的所有工具。

如果世界上所有的集装箱都被排成一排,它们将绕地球超过两次。- www.bigboxcontainers.co.za/

在本章中,我们将涵盖以下主题:

  • 使用 Jenkins 构建 Docker 镜像

  • 设置 Docker 化的 Jenkins 服务器

  • 在 Docker 化的 Jenkins 服务器中构建 Docker 镜像

  • 使用 Docker 容器作为您的 Jenkins 构建节点

  • 在 Docker 化的构建节点中构建、测试和推送 Docker 镜像

技术要求

您将从 Docker 的公共仓库中拉取 Docker 镜像,并安装 Jenkins 服务器软件,因此执行本章示例需要基本的互联网访问。还要注意,这些示例的系统要求比前几章中介绍的要高。本章示例中使用的服务器具有 8GB 的内存、2 个 CPU 和 20GB 的硬盘。

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Docker-Quick-Start-Guide/tree/master/Chapter08

查看以下视频以查看代码的实际操作:bit.ly/2AyRz7k

使用 Jenkins 构建 Docker 镜像

您可能已经知道 Jenkins 是一个广泛使用的持续集成/持续交付(CI/CD)系统工具。几乎每家公司,无论大小,都在某种程度上使用它。它非常有效,高度可配置,特别是可以与之一起使用的各种插件。因此,将其用于创建 Docker 镜像是非常自然的。使用 Jenkins 与 Docker 的第一步相当容易完成。如果您今天正在使用现有的 Jenkins 服务器,要使用它来构建 Docker 镜像,您只需要在 Jenkins 服务器上安装 Docker。您可以使用我们在第一章“设置 Docker 开发环境”中看到和使用的完全相同的安装技术。根据运行 Jenkins 服务器的系统的操作系统,您可以按照第一章中学到的安装步骤,设置 Docker 开发环境;完成后,您可以使用 Jenkins 构建 Docker 镜像。

如果您还没有运行的 Jenkins 服务器,您可以按照以下“参考”部分中的安装 Jenkins网页链接中找到的指南进行操作,并在您正在使用的任何操作系统上安装 Jenkins。例如,我们将使用该页面的信息在 Ubuntu 系统上设置 Jenkins 服务器。首先打开一个终端窗口。现在获取 Jenkins 软件包的 apt-key。接下来,您将向 apt 源列表中添加 Debian Jenkins 源。然后,您将更新系统上的软件包,最后,您将使用 apt-get 安装 Jenkins。命令看起来像下面这样:

# If Java has not yet been installed, install it now
sudo apt install openjdk-8-jre-headless

# Install Jenkins on an Ubuntu system
wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -
sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
sudo apt-get update
sudo apt-get install jenkins

在我的系统上运行这些命令看起来像下面这样:

安装完成后,您将要打开浏览器,并浏览到系统上的端口8080,完成 Jenkins 系统的设置和配置。这将包括输入管理员密码,然后决定在 Jenkins 服务器的初始部署中安装哪些插件。我建议使用 Jenkins 建议的设置,因为这是一个很好的起点:

既然你有了一个 Jenkins 服务器,你可以开始创建工作来执行确认它是否按预期工作。让我们从一个微不足道的 Hello world!工作开始,以确认 Jenkins 正在工作。登录到你的 Jenkins 服务器,点击“新建项目”链接。在新项目页面中,输入我们的工作名称。我使用hello-test。选择我们要创建的工作类型为 pipeline。接下来,点击页面左下角附近的“确定”按钮。这将带你到我们新工作的配置屏幕。这个将会非常简单。我们将创建一个 pipeline 脚本,所以向下滚动直到看到 Pipeline 脚本输入框,并输入以下脚本(注意 pipeline 脚本是用 groovy 编写的,它使用 Java(和 C)形式的注释):

// Our hello world pipeline script, named "hello-test"
node {
  stage('Say Hello') {
      echo 'Hello Docker Quick Start Guide Readers!'
   }
}

现在就这样吧,点击“保存”按钮保存我们 Jenkins 工作的更新配置。一旦配置保存了,让我们通过点击“立即构建”链接来测试工作。如果一切都按预期运行,我们应该看到工作成功完成。它会看起来像下面这样:

现在让我们创建另一个工作。点击链接返回仪表板,然后再次点击“新建项目”链接。这次,让我们把工作命名为hello-docker-test。同样,选择 pipeline 作为你想要创建的工作类型,然后点击“确定”按钮。再次向下滚动到 Pipeline 脚本输入框,并输入以下内容:

// Our Docker hello world pipeline script, named "hello-docker-test"
node {
   stage('Hello via Alpine') {
      docker.image('alpine:latest').inside {
         sh 'echo Hello DQS Readers - from inside an alpine container!'
      }
   }
}

点击“保存”按钮保存新工作的配置,然后点击“立即构建”链接启动 Jenkins 工作。以下是这次可能看起来的样子:

这次发生了什么?这次没有成功完成。显然失败了,因为我们的 Jenkins 服务器上还没有安装 Docker。所以让我们继续按照第一章“设置 Docker 开发环境”中找到的指令,安装 Docker,并将其安装在我们的 Jenkins 服务器上。一旦安装好了,还有一个额外的步骤你会想要做,那就是将 Jenkins 用户添加到 Docker 组中。命令如下:

# Add the jenkins user to the docker group
sudo usermod -aG docker jenkins
# Then restart the jenkins service
sudo service jenkins restart

这与我们用来将我们的 Docker 服务器的当前用户添加到 docker 组的命令非常相似,因此在 Docker 命令中不需要使用sudo。好的,现在让我们回到我们的 Jenkins 服务器 UI 和我们的hello-docker-test作业,再次点击“立即构建”按钮。

恭喜!您有一个全新的 Jenkins 服务器,已正确配置为构建(测试、推送和部署)Docker 映像。干得好。尽管这是一个伟大的成就,但工作量还是有点大。您难道不希望有更简单的方法来设置新的 Jenkins 服务器吗?所以,您知道您已经有一组运行 Docker 的服务器?您认为您可以使用该环境以更简单的方式建立起您的 Jenkins 服务器吗?当然可以!让我们来看看。

参考资料

以下是安装 Jenkins 的网页:jenkins.io/doc/book/installing/

设置 Docker 化的 Jenkins 服务器

您刚刚看到了设置新的 Jenkins 服务器有多少工作。虽然这并不是一个艰巨的工作,但至少有五个步骤您必须完成,然后才能选择您的插件并登录开始工作。并且在游戏节目猜猜这首歌的精神下,我可以在三个步骤内部署一个 Jenkins 服务器,前两个步骤只是为了让我们的 Jenkins 数据在托管 Jenkins 服务器的 Docker 容器的生命周期之外持久存在。假设您已经按照第一章“设置 Docker 开发环境”的说明设置并运行了 Docker 主机,我们希望创建一个位置,让 Jenkins 服务器存储其数据。我们将创建一个文件夹并分配所有权。它将如下所示:

# Setup volume location to store Jenkins configuration
mkdir $HOME/jenkins_home
chown 1000 $HOME/jenkins_home

所有者1000是将在 Docker 容器内用于 jenkins 用户的用户 ID。

第三步是部署我们的容器。在我向您展示命令之前,让我稍微谈一下要使用哪个容器映像。我包含了一个链接,可以在 Docker hub 上搜索 Jenkins 映像。如果您使用该链接或自行搜索,您会发现有很多选择。最初,您可能会考虑使用官方的 Jenkins 映像。然而,如果您浏览该存储库,您会发现我觉得有点奇怪的是,官方映像已经被弃用。它已经停止更新到 LTS 2.60.x 版本:

它建议使用在 jenkins/jenkins:lts Jenkins 镜像库中找到的镜像,目前的版本是 2.149.x。这是我们将在下面的示例中使用的镜像。以下是我们将使用的命令来部署我们的 Jenkins 服务器容器:

# Deploy a Jenkins server that is configured to build Docker images
docker container run -d -p 8080:8080 -p 50000:50000 \
-v $HOME/jenkins_home:/var/jenkins_home \
--name jenkins --rm jenkins/jenkins:lts

仔细看这个命令,我们可以看到我们正在将容器作为守护进程(非交互式)启动。我们看到我们在主机上打开了两个端口,它们映射到容器上的相同端口号,具体是808050000。接下来,我们看到我们正在使用一个卷,并且它映射到我们之前创建的文件夹。这是 Jenkins 将存储其数据的地方,比如我们创建的作业和它们执行的状态。然后您会注意到我们给容器命名为jenkins。之后,我们告诉 Docker 在退出时删除容器,使用--rm标志。最后,我们告诉 Docker 我们要运行哪个镜像。

当您运行此容器时,请给它一两分钟来启动,并浏览到 Docker 主机上的端口8080,您将看到与在部署 Jenkins 作为独立应用程序时看到的密码提示相同。然后会出现创建第一个用户的屏幕和默认插件配置屏幕。试试看吧。

由于我们为 Jenkins 数据创建了一个卷(写入/var/jenkins_home),我们的 Jenkins 配置数据被保存到主机上,并且将超出容器本身的生命周期。当然,您可以使用存储驱动程序,并将这些数据保存在比 Docker 主机更持久的地方,但您明白我的意思,对吧?

唯一的问题是,官方的 Jenkins 镜像和jenkins/jenkins镜像都不支持创建将构建 Docker 镜像的作业。而且由于本书都是关于 Docker 的,我们需要做的不仅仅是使用上述镜像运行我们的 Jenkins 服务器。别担心,我有个计划……继续阅读。

参考资料

在 Docker 化的 Jenkins 服务器内构建 Docker 镜像

好了。现在你知道如何将 Jenkins 部署为 Docker 容器,但我们真的希望能够使用 Jenkins 来构建 Docker 镜像,就像我们在独立部署 Jenkins 时所做的那样。为了做到这一点,我们可以部署相同的 Jenkins 镜像,并在其中安装 Docker,可能可以让它工作,但我们不需要那么麻烦。我们不是第一个走上这条路的先驱。已经创建了几个 Docker 镜像,可以做我们想做的事情。其中一个镜像是h1kkan/jenkins-docker:lts。您可以通过以下参考部分中的链接阅读有关它的信息,但现在只需知道它是一个已设置为 Jenkins 服务器的镜像,并且其中已经安装了 Docker。实际上,它还预先安装了 Ansible 和 AWSCLI,因此您可以使用它来构建 Docker 镜像以外的其他操作。

首先,我们将在 Docker 主机上创建一个位置,以挂载 Docker 卷来存储和保留 Jenkins 配置。如果您正在使用与上一节相同的 Docker 主机,您应该已经创建了文件夹并将其分配给 ID1000。如果没有,以下是您可以使用的命令:

# Setup volume location to store Jenkins configuration
mkdir $HOME/jenkins_home
chown 1000 $HOME/jenkins_home

另外,如果您还没有这样做,您可以使用docker container stop jenkins命令来停止(并删除)我们在上一节中创建的 Jenkins 容器,以为我们的新的、改进的 Jenkins 服务器腾出空间。当您准备创建新的容器时,您可以使用以下命令:

# Deploy a Jenkins server that is configured to build Docker images
docker container run -d -p 8080:8080 -p 50000:50000 \
-v $HOME/jenkins_home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
--name jenkins --rm h1kkan/jenkins-docker:lts

# Start the Docker service in the Jenkins docker container
docker container exec -it -u root jenkins service docker start

您可能已经注意到了这个代码块中的一些不同之处。第一个是使用了第二个卷。这是一种众所周知的技巧,允许容器向其主机发出 Docker 命令。这本质上允许了所谓的 Docker-in-Docker。下一个不同之处是额外的 Docker 命令,它将在运行的容器内启动 Docker 服务。因为每个容器都会启动一个单一进程,所以同时运行 Jenkins 服务器进程和 Docker 守护程序需要这一额外步骤。

一旦在 Jenkins 容器内启动了 Docker 服务,您就可以创建使用和构建 Docker 镜像的新 Jenkins 作业。您可以通过在新的 Jenkins 服务器中重新创建上面的第二个示例hello-docker-test来自行测试。由于我们使用的是挂载在主机上的 Docker 卷$HOME/jenkins_home来存储我们的 Jenkins 数据,这应该是您需要创建此作业的最后一次。

这一切都运作得很好,但您可能还记得第七章Docker Stacks中我们有一个比使用docker container run命令更好的部署应用程序的方法,即使用 Docker 堆栈。那么,您想看到我们的示例重新构想为 Docker 堆栈吗?我也是!好的,那么,让我们来做吧。

首先,使用容器停止命令停止当前的 Jenkins 容器。它将保留我们的 Jenkins 服务器数据的jenkins_home文件夹,但如果由于某种原因您跳到本章的这一部分并且还没有创建它,以下是要使用的命令:

# Setup volume location to store Jenkins configuration
mkdir $HOME/jenkins_home
chown 1000 $HOME/jenkins_home

再次强调,如果您对先前的示例中的这两个命令进行了操作,并且您正在使用相同的 Docker 主机,您就不必再次执行这些操作,因为该文件夹已经存在并且具有正确的所有权。

接下来,您需要为我们的 Jenkins 堆栈创建一个 compose 文件。我把我的命名为jenkins-stack.yml,并输入以下 YML 代码:

# jenkins-stack.yml
version: "3"
services:
  jenkins:
    image: h1kkan/jenkins-docker:lts
    ports:
       - 8080:8080
       - 50000:50000
    volumes:
       - $HOME/jenkins_home:/var/jenkins_home
       - /var/run/docker.sock:/var/run/docker.sock
    deploy:
       replicas: 1
       restart_policy:
         condition: on-failure
    placement:
      constraints: [node.role == manager]

  registry:
    image: registry
    ports:
       - 5000:5000
 deploy:
    replicas: 1
    restart_policy:
      condition: on-failure

您将注意到我们正在创建两个服务;一个是我们的 Jenkins 服务器,另一个是 Docker 注册表。我们将在即将到来的示例中使用注册表服务,所以现在把它放在心里。查看 Jenkins 服务描述时,我们没有看到在第七章Docker Stacks中学到的任何内容。您将注意到我们的两个端口映射和上一个示例中使用的两个卷。我们将把单个 Jenkins 副本限制在我们的管理节点上。

记住,要使用 Docker 堆栈,我们必须在集群模式下运行,因此,如果您还没有这样做,请使用我们在第五章学到的docker swarm init命令创建您的集群,Docker Swarm

请注意,如果您的集群有多个管理节点,您需要进一步将 Jenkins 副本限制在只有您的jenkins_home卷挂载点的单个管理节点上。这可以通过角色和标签的组合来实现。或者,您可以使用存储驱动程序并挂载一个可以在集群管理节点之间共享的卷。为了简单起见,我们假设我们的示例中有一个单独的管理节点。

现在使用堆栈部署命令来设置 Jenkins 应用程序。以下是要使用的命令的示例:

# Deploy our Jenkins application via a Docker stack
docker stack deploy -c jenkins-stack.yml jenkins

一旦堆栈部署并且服务正在运行,您可以浏览到您集群中的任何节点,端口为 8080,并访问您的 Jenkins 服务器。更重要的是,如果您正在重用我们之前示例中的jenkins_home文件夹,您将不必提供管理员密码,创建新用户和选择插件,因为所有与这些任务相关的数据都存储在jenkins_home文件夹中,并且现在由基于堆栈的 Jenkins 服务重用。另一个有趣的事实是,当您在堆栈应用程序中使用此镜像时,您无需启动 Docker 服务。奖励!

好了,现在我们有一个甜蜜的基于堆栈的 Jenkins 服务,可以使用和构建 Docker 镜像。一切看起来都很好。但有一件事可以让这更好。通过更好,我指的是更加 Docker 化:而不是使用普通的 Jenkins 代理进行我们的构建作业,如果我们想要为每次执行 Jenkins 作业都启动一个新的原始 Docker 容器呢?这将确保每个构建都是在一个干净、一致的环境中从头构建的。此外,这真的可以提升 Docker 的内在水平,所以我非常喜欢。如果您想看看如何做到这一点,请继续阅读。

参考资料

使用 Docker 容器作为 Jenkins 构建节点

要将 Docker 容器用于 Jenkins 构建代理,您需要对 Jenkins 配置进行一些操作:

  • 构建一个新的 Docker 镜像,可以作为 Jenkins 构建代理,并能够构建 Docker 镜像(当然)

  • 将新镜像推送到 Docker 注册表

  • 关闭默认的 Jenkins 构建代理

  • 安装 Jenkins 的 Docker 插件

  • 配置一个新的云以启用 Docker 化的构建代理

构建 Docker 镜像

让我们开始吧。我们要做的第一件事是构建我们专门的 Docker 镜像,用于我们的 Jenkins 代理。为此,我们将使用第三章“创建 Docker 镜像”中学到的技能来创建 Docker 镜像。首先在您的开发系统上创建一个新文件夹,然后将工作目录更改为该文件夹。我把我的命名为jenkins-agent

# Make a new folder to use for the build context of your new Docker image, and cd into it
mkdir jenkins-agent
cd jenkins-agent

现在创建一个新文件,命名为Dockerfile,使用您喜欢的编辑器,输入以下代码,然后保存:

# jenkins-agent Dockerfile
FROM h1kkan/jenkins-docker:lts-alpine
USER root
ARG user=jenkins

ENV HOME /home/${user}
ARG VERSION=3.26
ARG AGENT_WORKDIR=/home/${user}/agent

RUN apk add --update --no-cache curl bash git openssh-client openssl procps \
 && curl --create-dirs -sSLo /usr/share/jenkins/slave.jar https://repo.jenkins-ci.org/public/org/jenkins-ci/main/remoting/${VERSION}/remoting-${VERSION}.jar \
 && chmod 755 /usr/share/jenkins \
 && chmod 644 /usr/share/jenkins/slave.jar \
 && apk del curl

ENV AGENT_WORKDIR=${AGENT_WORKDIR}
RUN mkdir -p /home/${user}/.jenkins && mkdir -p ${AGENT_WORKDIR}
USER ${user}

VOLUME /home/${user}/.jenkins
VOLUME ${AGENT_WORKDIR}
WORKDIR /home/${user}

我们的新 Dockerfile 正在做什么:在我们的FROM指令中,我们使用了与上面的 Docker-in-Docker 示例中相同的 Docker 镜像,以便我们有一个基础镜像,可以让我们构建 Docker 镜像。接下来,我们使用USER命令将当前用户设置为 root。然后,我们创建一个名为用户的ARG并将其设置为jenkins的值。之后,我们设置了一个名为HOME的环境变量,该变量具有 Jenkins 用户的主目录的值。然后,我们设置了另外两个ARGs,一个用于版本,一个用于 Jenkins 代理的工作目录。接下来是魔术发生的地方。我们使用RUN命令来设置并获取 Jenkins 的slave.jar文件。这是作为 Jenkins 代理所需的部分。我们还在文件夹和文件上设置了一些权限,然后通过删除 curl 来进行一些清理。之后,我们设置了另一个环境变量,这个环境变量是AGENT_WORKDIR。接下来,我们在容器中创建了一些文件夹。然后,我们再次使用USER指令,这次将当前用户设置为我们的 Jenkins 用户。最后,我们通过创建一些VOLUME实例来完成 Dockerfile,并将当前工作目录设置为我们的 Jenkins 用户的主目录。哦!这似乎很多,但实际上并不那么糟糕,您只需将上述代码复制粘贴到您的 Dockerfile 中并保存即可。

现在我们的 Dockerfile 准备好使用了,现在是创建一个 git 仓库并将代码保存到其中的好时机。一旦您确认您的项目已经正确地使用 git 设置好,我们就可以构建我们的新 Docker 镜像。以下是您将用于此目的的命令:

# Build our new Jenkins agent image
docker image build -t jenkins-agent:latest .

它应该成功构建并创建一个本地缓存的镜像,标记为jenkins-agent:latest

将新镜像推送到 Docker 注册表。

接下来,我们需要将我们的新镜像推送到 Docker 注册表。当然,我们可以将其推送到 hub.docker.com 中的我们的仓库,但由于我们恰好部署了一个 Docker 注册表的应用程序堆栈,为什么不利用它来存储我们的 Jenkins 代理镜像呢?首先,我们需要使用注册表为我们的新镜像打标签。基于您的 Docker Swarm 的域名,您的标签命令将与我的不同,但对于我的示例,以下是我的标签命令的样子:

# Tag the image with our swarm service registry
docker image tag jenkins-agent:latest ubuntu-node01:5000/jenkins-agent:latest

现在镜像已经在本地标记,我们可以使用以下命令将其推送到注册表;同样,基于您的 Swarm 的域名,您的命令将有所不同:

# Push the Jenkins agent image to the registry
docker image push ubuntu-node01:5000/jenkins-agent:latest

所有这些命令可能会使用比latest标签更好的版本方案,但您应该能够自行解决这个问题。随着我们的镜像构建、标记和推送到 Docker 注册表,我们准备好更新 Jenkins 配置以使用它。

关闭默认的 Jenkins 构建代理

现在我们准备更新 Jenkins 配置以支持我们的 Docker 化构建代理。我们要做的第一个配置更改是关闭默认的构建代理。要做到这一点,登录到您的 Jenkins 服务器,然后单击“管理 Jenkins”菜单链接。这将带您进入各种配置组,例如系统、插件和 CLI 设置。现在,我们需要进入“配置系统”管理组:

一旦您进入“配置系统”管理组,您将更改“执行器数量”的值为0。它应该看起来像下面这样:

当您将“执行器数量”更改为0后,您可以点击屏幕左下角的保存按钮保存设置。在这一点上,由于没有配置 Jenkins 代理来运行作业,您的 Jenkins 服务器将无法运行任何作业。因此,让我们快速进行下一步,即安装 Docker 插件。

安装 Jenkins 的 Docker 插件

现在我们需要为 Jenkins 安装 Docker 插件。您可以像安装其他插件一样完成此操作。单击“管理 Jenkins”菜单链接,然后从配置组列表中,单击“管理插件”组的链接:

一旦您进入“管理插件”配置组,选择“可用插件”选项卡,然后在筛选框中输入docker以缩小可用插件的列表,以便找到与 Docker 相关的插件:

即使有一个经过筛选的列表,仍然有很多插件可供选择。找到并勾选 Docker 插件。它看起来像下面这样:

勾选 Docker 插件复选框,向下滚动并单击“无需重新启动安装”按钮。这将为您下载并安装插件,然后在 Jenkins 重新启动时启用它。在安装屏幕上,您可以选择在插件安装完成后立即执行重新启动的选项。要执行此操作,请勾选“安装完成后重新启动 Jenkins 并且没有作业正在运行”复选框:

由于我们几分钟前将执行器数量设置为0,现在不会有任何作业正在运行,因此一旦安装插件,Jenkins 将重新启动。Jenkins 一旦恢复在线,插件将被安装。我们需要重新登录 Jenkins 并设置我们的云。

创建一个新的云,以启用我们的 Docker 化构建代理

现在,我们将告诉 Jenkins 使用我们的自定义 Docker 镜像来作为 Jenkins 构建代理运行容器。再次,单击“管理 Jenkins”菜单链接。从配置组列表中,您将再次单击“配置系统”组的链接。您将在配置选项的底部附近找到云配置。单击“添加新云”下拉菜单,并选择Docker

屏幕将更新,您将看到两个新的配置组:Docker Cloud 详细信息...和 Docker 代理模板...:

让我们先处理 Docker Cloud 的细节。现在点击该按钮。您可以将名称值保留为docker的默认值。在 Docker 主机 URI 字段中,输入unix:///var/run/docker.sock。您可以通过单击问号帮助图标并将其复制粘贴到输入字段中来找到此值。接下来,单击“测试连接”按钮,您应该会看到一个版本行显示出来,类似于您将在以下屏幕截图中看到的内容。记下 API 版本号,因为您将需要它进行高级设置。单击“高级”按钮,并在 Docker API 版本字段中输入 API 版本号。您需要勾选“已启用”复选框以启用此功能,所以一定要这样做。最后,您可能需要更改系统可以同时运行的容器数量。默认值为 100。例如,我将该值减少到10。完成后,您的配置应该看起来类似于以下内容:

接下来,点击 Docker 代理模板...按钮,然后点击出现的添加 Docker 模板按钮,以便配置 Jenkins 代理设置。在这里,您将要点击代理的已启用复选框,以启用我们的新代理模板。您可以给一个名称,用作由 Jenkins 作为构建代理运行的容器的前缀,或者您可以将名称留空,将使用docker前缀。接下来,输入您要用于构建代理容器的镜像的存储库和名称标签。我们创建了我们的自定义镜像,标记了它,并将其推送到我们的 Jenkins 堆栈应用程序存储库,使用ubuntu-node01:5000/jenkins-agent:latest镜像名称,因此将该值输入到 Docker 镜像字段中。将实例容量值设置为1,将远程文件系统根值设置为/home/jenkins/agent。确保使用值设置为尽可能多地使用此节点,并使用连接方法附加 Docker 容器值。将用户设置为root。将拉取策略值更改为拉取一次并更新最新

最后,我们需要配置一些容器设置..., 所以点击展开该部分。我们需要在这里输入的值是容器运行时要使用的命令。Docker 命令字段中需要的值是 java -jar /usr/share/jenkins/slave.jar。卷字段中需要的值是 /var/run/docker.sock:/var/run/docker.sock

最后,勾选分配伪 TTY 的复选框:

滚动到配置屏幕底部,然后单击保存按钮以保存所有云设置。这是一些严肃的配置功夫 - 做得好!但是,以防万一您想要所有输入值的快速参考,这里是我们示例中用于配置 Docker 云的所有自定义(或非默认)值:

字段名称 使用的值
Docker 主机 URI unix:///var/run/docker.sock
Docker API 版本 1.38(与连接测试中显示的版本匹配)
Docker 云已启用 已勾选
容器容量 10
Docker 代理已启用 已勾选
Docker 代理模板名称 agent
Docker 镜像 ubuntu-node01:5000/jenkins-agent:latest
实例容量 1
远程文件系统根 /home/jenkins/agent
用途 尽可能多地使用此节点
连接方法 附加 Docker 容器
用户 root
拉取策略 拉取一次并更新最新
Docker 命令 java -jar /usr/share/jenkins/slave.jar
/var/run/docker.sock:/var/run/docker.sock
分配伪 TTY 已选中

现在一切都配置好了,让我们测试一下我们新定义的 Jenkins 代理。

测试我们的新构建代理

返回 Jenkins 仪表板,点击“计划构建”按钮,为我们的hello-docker-test作业。这将为我们的作业启动一个新的构建,然后将创建一个新的 Docker 化构建代理。它使用我们设置的配置来执行docker container run命令,以运行一个基于我们指定的镜像的新容器。最初,执行器将处于离线状态,因为容器正在启动:

注意,执行器名称具有我们指定的代理前缀。一旦容器运行起来,Jenkins 作业将在其中启动,基本上使用docker container exec命令。当 Jenkins 作业启动时,正常的作业进度图形将显示,并且执行器将不再显示为离线状态。状态然后会看起来像这样:

如果您点击正在执行的作业的进度条,您可以查看作业的控制台输出,不久后,作业将显示已完成:成功状态,如下所示:

工作完成得很好!让我们检查最后一个例子 Jenkins 作业,展示一个具有更多阶段的流水线脚本,代表了一个真实世界的 Docker 作业的例子。你准备好了吗?继续阅读。

在 Docker 化构建节点内构建、测试和推送 Docker 镜像

在 Docker 和 Jenkins 的这一章结束之前,让我们走一遍为真实世界的 Docker 化节点应用程序创建模板的步骤。以下是我们将要做的:

准备我们的应用程序:

  • 在 GitHub 上创建一个新的存储库

  • 克隆存储库到我们的开发工作站

  • 创建我们的应用程序文件

  • 将我们的应用程序文件上传到 GitHub

创建并测试将构建我们的 Docker 化节点应用程序的 Jenkins 作业:

  • 创建一个利用 GitHub 存储库的新 Jenkins 作业

  • 测试我们的 Jenkins 作业,将拉取存储库,构建应用程序,测试它,并发布镜像

  • 庆祝我们的成功!

让我们开始准备我们的应用程序。

我们要做的第一件事是在 GitHub 上创建我们的应用程序存储库。浏览并登录github.com,转到你的存储库页面,然后点击创建新存储库按钮。输入新存储库的名称。在我们的示例中,我使用了dqs-example-app。输入一个合适的描述。你可以将你的存储库设置为公开或私有。在这个示例中,我将其设置为公开,以简化后续不需要身份验证即可拉取存储库的过程。勾选初始化存储库复选框,这样你就可以立即在你的工作站上克隆空的存储库。你可以选择创建.gitignore文件时要使用的项目类型。我选择了Node。当你输入并选择了所有这些内容后,它会看起来像下面这样:

点击创建存储库按钮来创建你的新应用程序存储库。现在它在 GitHub 上创建好了,你会想要将它克隆到你的工作站上。使用克隆或下载按钮,然后使用复制按钮来复制存储库的 URL 以进行克隆步骤:

现在,回到你的工作站,在你保存本地存储库的位置,克隆这个新的(大部分是)空的存储库。然后切换到新存储库的文件夹中。对我来说,看起来像下面这样:

现在我们要创建应用程序的脚手架。这将包括创建一个Dockerfile,一个Jenkinsfilemain.jstest.js文件,以及package.json文件。使用你喜欢的编辑器在你的应用程序文件夹中创建这些文件。以下是这些文件的内容:

以下是Dockerfile文件的内容:

FROM node:10-alpine
COPY . .
RUN npm install
EXPOSE 8000
CMD npm start

以下是Jenkinsfile文件的内容:

node {
   def app
   stage('Clone repository') {
      /* Clone the repository to our workspace */
      checkout scm
   }
   stage('Build image') {
      /* Builds the image; synonymous to docker image build on the command line */
      /* Use a registry name if pushing into docker hub or your company registry, like this */
      /* app = docker.build("earlwaud/jenkins-example-app") */
      app = docker.build("jenkins-example-app")
   }
   stage('Test image') {
      /* Execute the defined tests */
      app.inside {
         sh 'npm test'
      }
   }
   stage('Push image') {
      /* Now, push the image into the registry */
      /* This would probably be docker hub or your company registry, like this */
      /* docker.withRegistry('https://registry.hub.docker.com', 'docker-hub-credentials') */

      /* For this example, We are using our jenkins-stack service registry */
      docker.withRegistry('https://ubuntu-node01:5000') {
         app.push("latest")
      }
   }
}

以下是main.js文件的内容:

// load the http module
var http = require('http');

// configure our HTTP server
var server = http.createServer(function (request, response) {
   response.writeHead(200, {"Content-Type": "text/plain"});
   response.end("Hello Docker Quick Start\n");
});

// listen on localhost:8000
server.listen(8000);
console.log("Server listening at http://127.0.0.1:8000/");

以下是package.json文件的内容:

{
   "name": "dqs-example-app",
   "version": "1.0.0",
   "description": "A Docker Quick Start Example HTTP server",
   "main": "main.js",
   "scripts": {
      "test": "node test.js",
      "start": "node main.js"
   },
   "repository": {
      "type": "git",
      "url": "https://github.com/earlwaud/dqs-example-app/"
   },
   "keywords": [
      "node",
      "docker",
      "dockerfile",
      "jenkinsfile"
   ],
   "author": "earlwaud@hotmail.com",
   "license": "ISC",
   "devDependencies": { "test": ">=0.6.0" }
}

最后,以下是test.js文件的内容:

var assert = require('assert')

function test() {
   assert.equal(1 + 1, 2);
}

if (module == require.main) require('test').run(test);

当你完成所有操作后,你的存储库文件夹应该看起来像下面这样:

现在,让我们将我们的工作推送到 GitHub 存储库。你将使用标准的 git 命令来添加文件,提交文件,然后将文件推送到存储库。以下是我使用的命令:

# Initial commit of our application files to the new repo
git add Dockerfile Jenkinsfile main.js package.json test.js
git commit -m "Initial commit"
git push origin master

对我来说,情况是这样的:

现在,我们的应用程序的初始版本已经创建并推送到我们的 GitHub 仓库,我们准备创建 Jenkins 作业来拉取我们的仓库代码,构建我们的应用程序镜像,对其进行测试,然后发布我们应用程序的 Docker 镜像。首先,通过登录到 Jenkins 服务器并单击“新项目”链接来创建一个新的 Jenkins 作业。接下来,在“输入项目名称”输入框中输入要用于作业的名称。我正在使用dqs-example-app。选择“流水线”作为我们正在创建的作业类型,然后单击“确定”按钮。

您可以并且可能应该为我们正在创建的构建作业提供有意义的描述。只需将其输入到配置屏幕顶部的“描述:”输入框中。对于我们的示例,我输入了略显简洁的描述“使用来自 SCM 的管道脚本构建 dqs-example-app”。您可能可以做得更好。

我们将设置 Jenkins 作业,每五分钟轮询 GitHub 仓库,以查找主分支的更改。有更好的选项,可以在仓库更改时触发构建作业,而不是定期轮询,但为了简单起见,我们将只使用轮询方法。因此,请滚动到作业配置的“构建触发器”部分,并选中“轮询 SCM”。然后在计划中输入值H/5 * * * *

接下来,我们要设置我们的流水线。与以前的示例不同,这次我们将选择“来自 SCM 的管道脚本”选项。我们将为我们的 SCM 选择Git,然后输入 GitHub 上我们应用程序仓库的存储库 URL。对于此示例,该 URL 为https://github.com/EarlWaud/dqs-example-app.git。确保“要构建的分支”值设置为*/master,这是默认值。您的流水线定义应该看起来很像以下内容:

流水线的另一个关键设置是脚本路径。这是 Jenkins 脚本文件的(路径和)文件名。在我们的情况下,这实际上只是Jenkinsfile,因为我们给文件的名称是Jenkinsfile,它位于我们仓库的根目录。这是我们示例的输入样子:

这是目前所需的所有配置。其他一切都已经在我们的源文件中设置好了,它们将从我们的应用程序存储库中拉取。配置所需做的就是点击保存按钮。回到作业页面,我们已经准备好执行我们的第一个构建。在我们的示例中,新创建的作业屏幕看起来像这样:

现在,只需等待。在五分钟或更短的时间内,作业的第一个构建将自动启动,因为我们已经设置了每五分钟轮询存储库。当作业完成后,我们将查看控制台日志,但首先让我们看一下作业完成后的 Jenkins 作业视图(当然是成功的):

以下是控制台日志输出的编辑视图,供参考(完整的日志输出可以在源代码包中找到):

Started by an SCM change
Started by user Earl Waud
Obtained Jenkinsfile from git https://github.com/EarlWaud/dqs-example-app.git
[Pipeline] node
Running on agent-00042y2g983xq on docker in /home/jenkins/agent/workspace/dqs-example-app
[Pipeline] { (Clone repository)
Cloning repository https://github.com/EarlWaud/dqs-example-app.git
> git init /home/jenkins/agent/workspace/dqs-example-app # timeout=10
[Pipeline] { (Build image)
+ docker build -t jenkins-example-app .
Successfully built b228cd7c0013
Successfully tagged jenkins-example-app:latest
[Pipeline] { (Test image)
+ docker inspect -f . jenkins-example-app
+ npm test
> node test.js
Passed:1 Failed:0 Errors:0
[Pipeline] { (Push image)
+ docker tag jenkins-example-app ubuntu-node01:5000/jenkins-example-app:latest
+ docker push ubuntu-node01:5000/jenkins-example-app:latest
Finished: SUCCESS

现在剩下的就是庆祝我们的成功:

说真的,这是创建自己的 Docker 应用程序并使用 Jenkins 构建、测试和发布它们的一个很好的基础。把它看作一个模板,你可以重复使用并构建。现在你已经准备好以任何你想要的方式在 Jenkins 中使用 Docker 了。

摘要

好了,我们到了本章的结尾。我希望你阅读本章的乐趣和我写作时一样多。我们有机会运用我们在之前章节学到的许多技能。不仅如此,本章还包含一些非常有用的 Jenkins 知识。以至于你可以认真考虑跳过任何计划中的 Jenkins 培训或书籍阅读,因为你几乎可以在这里找到关于使用 Jenkins 的一切知识。

让我们回顾一下:首先,我们学习了如何设置独立的 Jenkins 服务器。我们很快过渡到将 Jenkins 服务器部署为 Docker 容器。这就是你阅读这本书的目的,对吧?然后我们学会了如何在 Docker 化的 Jenkins 服务器中构建 Docker 镜像。接下来,我们找出了如何用超酷的 Docker 容器替换无聊的 Jenkins 代理,这些容器可以构建我们的 Docker 镜像。你可能会考虑这个以及 Docker 中的 Docker 中的 Docker。你看过电影《盗梦空间》吗?嗯,你刚刚经历了它。最后,在本章的总结中,我们创建了一个示例的 Docker 化应用程序和构建、测试和发布该应用程序镜像的 Jenkins 作业。这是一个示例,你可以将其用作未来创建的真实应用程序的模板和基础。

现在,我们来到了书的结尾。我再说一遍……我希望你阅读这本书和我写这本书一样开心。我也希望你从中学到的和我写这本书一样多。在这些章节中,我们涵盖了大量关于 Docker 的信息。在第一章中,设置 Docker 化的开发环境,我们成功搭建了 Docker 工作站,无论你喜欢的操作系统类型是什么。在第二章中,学习 Docker 命令,我们学到了几乎所有关于 Docker 命令集的知识。在第三章中,创建 Docker 镜像,我们深入研究了Dockerfile指令集,并学会了如何创建几乎任何你想构建的 Docker 镜像。第四章,Docker 卷,向我们展示了 Docker 卷的强大和实用性。在第五章中,Docker Swarm,我们开始运用前几章的几个教训,练习了几乎神奇的 Docker swarm 的功能。然后,在第六章中,Docker 网络,我们继续学习 Docker 知识,这次学习了 Docker 如何为我们简化了复杂的网络主题。在第七章中,Docker 堆栈,我们看到了更多 Docker 的魔力和力量,当我们了解了 Docker 堆栈。最后,在第八章中,Docker 和 Jenkins,我们将所有学到的知识应用起来,利用 Docker 和 Jenkins 为我们准备好创建真实世界的应用程序。

我所能做的就是说声谢谢,并祝愿你在 Docker 之旅中取得成功。

posted @ 2024-05-06 18:33  绝不原创的飞龙  阅读(149)  评论(0编辑  收藏  举报