精通-Docker-第四版-全-
精通 Docker 第四版(全)
原文:
annas-archive.org/md5/6094a042b5ec05de7ad0830d3491c927译者:飞龙
序言
在现代应用架构和部署方面,Docker 是一个颠覆性的工具。它已经成长为推动创新的关键力量,不仅在系统管理领域,而且在 Web 开发等领域产生了深远影响。但如何确保你跟得上 Docker 推动的创新呢?如何确保你充分利用它的潜力?
本书将向你展示如何使用 Docker,它不仅展示了如何更有效地使用 Docker,还帮助你重新思考和重新构想 Docker 所能实现的可能性。
你将学习基本的主题,如构建、管理和存储镜像,以及最佳实践,然后再深入探讨 Docker 安全性。你将了解如何以新颖创新的方式扩展和集成 Docker。Docker Compose、Docker Swarm 和 Kubernetes 将帮助你以高效的方式管理容器。
本书结束时,你将全面而详细地理解 Docker 的功能,并了解它如何无缝融入你的本地工作流程、高可用的公共云平台以及其他工具中。
本书适合谁阅读
如果你是 IT 专业人士,并且认识到 Docker 在从系统管理到 Web 开发各个领域创新中的重要性,但不确定如何充分利用它,本书适合你。
本书的内容
第一章**, Docker 概述,讨论了 Docker 的起源以及它对开发人员、运维人员和企业的意义。
第二章**, 构建容器镜像,介绍了你可以用来构建自己容器镜像的各种方法。
第三章**, 存储和分发镜像,介绍了在我们了解如何构建镜像之后,如何共享和分发镜像。
第四章**, 管理容器,深入探讨了如何管理容器。
第五章**, Docker Compose,介绍了 Docker Compose——一个允许我们共享包含多个容器的应用程序的工具。
第六章**, Docker Machine、Vagrant 和 Multipass,介绍了 Docker Machine 和其他工具,它们使你能够在各种平台上启动和管理 Docker 主机。
第七章**, 从 Linux 到 Windows 容器,解释了传统上容器是基于 Linux 的工具。通过与 Docker 合作,微软现在引入了 Windows 容器。在这一章中,我们将探讨这两种类型容器之间的差异。
第八章**, 使用 Docker Swarm 集群, 本章讨论了到目前为止我们一直针对单个 Docker 主机的情况。Docker Swarm 是 Docker 提供的一种集群技术,允许你在多个主机上运行容器。
第九章**, Portainer – 一个 Docker 的图形界面, 解释了我们与 Docker 的大部分交互都是通过命令行进行的。在这里,我们将介绍 Portainer,这是一个允许你通过 Web 界面管理 Docker 资源的工具。
第十章**, 在公共云中运行 Docker, 本章我们将探讨如何在公共云服务中运行容器的不同方式。
第十一章**, Docker 和 Kubernetes, 本章介绍了 Kubernetes。像 Docker Swarm 一样,你可以使用 Kubernetes 创建和管理集群,运行基于容器的应用程序。
第十二章**, 探索其他 Kubernetes 选项, 本章在本地使用 Docker 运行 Kubernetes 后,探讨了在本地机器上使用 Kubernetes 的其他选项。
第十三章**, 在公共云中运行 Kubernetes, 本章探讨了来自“四大”云服务提供商的各种 Kubernetes 服务:Azure、Google Cloud、Amazon Web Services 和 DigitalOcean。
第十四章**, Docker 安全性, 探讨 Docker 安全性。本章将涵盖从 Docker 主机到如何启动镜像、镜像的来源以及镜像内容的各个方面。
第十五章**, Docker 工作流, 本章开始将所有部分整合起来,以便你能够在生产环境中开始使用 Docker,并且能够自如地操作。
第十六章**, Docker 下一步, 本章不仅探讨了你如何为 Docker 作出贡献,还讨论了为支持基于容器的应用程序和部署而兴起的更大生态系统。
为了充分利用本书
为了充分利用本书,你需要一台能够运行 Docker 的机器。该机器应至少具备 8 GB 的内存和 30 GB 的可用硬盘空间,且搭载 Intel i3 或更高版本的处理器,运行以下操作系统之一:
-
macOS High Sierra 或更高版本
-
Windows 10 专业版
-
Ubuntu 18.04 或更高版本
此外,你需要访问以下一个或多个公共云服务提供商:DigitalOcean、Amazon Web Services、Azure 和 Google Cloud。
如果你使用的是本书的数字版,建议你亲自输入代码或通过 GitHub 仓库访问代码(链接将在下一节提供)。这样可以避免因复制和粘贴代码而可能导致的错误。
下载示例代码文件
您可以从您的账户下载本书的示例代码文件,网址为 www.packt.com。如果您是在其他地方购买的此书,可以访问 www.packtpub.com/support 注册,以便直接将文件发送至您的邮箱。
您可以按照以下步骤下载代码文件:
-
登录或注册 www.packt.com。
-
选择支持标签。
-
点击代码下载。
-
在搜索框中输入书名并按照屏幕上的指示操作。
文件下载完成后,请确保使用最新版本的工具解压文件:
-
Windows 版 WinRAR/7-Zip
-
Mac 版 Zipeg/iZip/UnRarX
-
Linux 版 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Mastering-Docker-Fourth-Edition。如果代码有更新,将会在现有的 GitHub 仓库中更新。
我们还提供了其他代码包,来自我们丰富的书籍和视频目录,网址为 github.com/PacktPublishing/。快去看看吧!
代码示例
本书的《代码示例》视频可以在 https://bit.ly/35aQnry 观看。
下载彩色图片
我们还提供了一份包含本书中截图/图表的彩色图片的 PDF 文件,您可以在此下载:www.packtpub.com/sites/default/files/downloads/9781839213519_ColorImages.pdf。
使用的约定
本书中使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。示例:‘您可以使用以下 docker inspect 命令查看容器的标签。’
代码块如下所示:
ENTRYPOINT ['nginx']
CMD ['-g', 'daemon off;']
当我们希望您注意代码块的某一部分时,相关行或项目会以粗体显示:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
任何命令行输入或输出如下所示:
$ curl -sSL https://get.docker.com/ | sh
$ sudo systemctl start docker
Yes 将启动 Docker 安装程序,显示以下提示。
提示或重要说明
显示效果如下。
联系我们
我们非常欢迎读者的反馈。
customercare@packtpub.com。
勘误:尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果您在本书中发现错误,欢迎向我们报告。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接并输入详细信息。
copyright@packt.com 并附有相关资料链接。
如果您有兴趣成为作者:如果您在某个领域有专长,并且有兴趣撰写或为书籍做贡献,请访问authors.packtpub.com。
评价
请留下您的评价。当您阅读并使用本书后,为什么不在您购买该书的网站上留下评价呢?潜在的读者可以看到并参考您的公正意见来做出购买决策,我们在 Packt 也能了解您对我们产品的看法,而我们的作者也可以看到您对他们书籍的反馈。谢谢!
欲了解更多 Packt 信息,请访问packt.com。
第一部分:开始使用 Docker
在本节中,您将学习如何安装、配置并使用 Docker 在本地机器上启动简单和复杂的容器化应用程序。
本节包含以下章节:
第一章,Docker 概述
第二章,构建容器镜像
第三章,存储与分发镜像
第四章,容器管理
第五章,使用 Docker Compose 启动多个容器
第六章,使用 Docker Machine、Vagrant 和 Multipass
第一章:Docker 概述
欢迎来到 《精通 Docker,第四版》!本章将涵盖你应该已经掌握的 Docker 基础知识。如果你现在还没有掌握必要的知识,本章将帮助你尽快跟上,以免后续章节显得太难。
到书籍结束时,你将成为一个 Docker 大师,能够在你的环境中实现 Docker,构建和支持基于 Docker 的应用程序。
本章我们将回顾以下内容:
-
理解 Docker
-
专用主机、虚拟机和 Docker 安装器/安装之间的区别
-
Docker 命令
-
Docker 和容器生态系统
技术要求
本章我们将讨论如何在本地安装 Docker。为此,你需要一台运行以下三种操作系统之一的主机:
-
macOS High Sierra 及以上版本
-
Windows 10 专业版
-
Ubuntu 18.04 及以上版本
-
查看以下视频,看看代码实际效果:
bit.ly/35fytE3
理解 Docker
在我们开始安装 Docker 之前,让我们先了解一下 Docker 技术旨在解决的问题。
开发者
Docker 背后的公司,也叫做 Docker,一直将该程序描述为解决“它在我的机器上能运行”问题。这个问题可以通过一张基于“灾难女孩”模因的图片来总结,图片上简单写着“在开发中运行正常,现在是运维的问题”的标语,几年前开始在各种演示、论坛和 Slack 渠道中出现。虽然它很有趣,但不幸的是,它是一个过于真实的问题,我个人也曾深受其害,下面我们来看一个例子,理解这个问题是什么意思。
问题
即使在遵循DevOps最佳实践的世界中,开发者的工作环境与最终的生产环境不匹配的情况仍然屡见不鲜。
例如,使用 macOS 版本的开发者,假设他们使用的是 PHP,可能不会运行与托管生产代码的 Linux 服务器 相同的版本。即使版本相同,你还必须应对配置和运行该版本的整体环境的差异,比如不同操作系统版本之间处理文件权限的方式差异,仅举一个潜在问题。
当开发者将代码部署到主机时,一切问题往往爆发出来,这时候该怎么办?是让生产环境配置与开发者的机器一致,还是开发者只在与生产环境匹配的环境中进行工作?
在理想的世界里,一切应该保持一致,从开发者的笔记本电脑到生产服务器;然而,这种乌托邦理想传统上一直很难实现。每个人都有自己的工作方式和个人偏好——在一个工程师负责系统的情况下,强制在多个平台之间保持一致就已经足够困难,更别提一个由数百个开发人员组成的团队在一起合作了。
Docker 解决方案
使用 Docker for Mac 或 Docker for Windows,开发者可以快速将他们的代码封装在一个容器中,该容器是他们自己定义的或在与系统管理员或运维团队合作时创建的Dockerfile。我们将在第二章《构建容器镜像》中介绍这一点,同时也会详细讲解Docker Compose文件,相关内容将在第五章《Docker Compose》中进一步探讨。
程序员可以继续使用他们选择的集成开发环境(IDE),并在处理代码时保持他们的工作流。正如我们将在本章的后续部分看到的那样,安装和使用 Docker 并不难;考虑到过去即使在有自动化的情况下维持一致的环境也是一项繁琐的工作,Docker 现在感觉有点太简单——几乎像是在作弊。
操作符
我在运维工作中已经多年,尽管不愿意承认,但以下问题经常出现。
问题
假设你负责管理五台服务器:三台负载均衡的 Web 服务器和两台数据库服务器,它们以主从配置专门运行应用程序 1。你使用一个工具,例如Puppet或Chef,来自动管理这五台服务器上的软件栈和配置。
一切进展顺利,直到你被告知需要在与应用程序 1 运行的同一台服务器上部署应用程序 2。乍一看,这似乎不是问题——你可以调整 Puppet 或 Chef 配置来添加新用户、添加虚拟主机、拉取最新的代码等等。然而,你注意到应用程序 2 需要比应用程序 1 运行的版本更新的软件。
更糟糕的是,你已经知道应用程序 1 完全拒绝与新软件栈兼容,而应用程序 2 又不支持向后兼容。
传统上,这让你面临几个选择,每个选择都以不同的方式加剧问题:
-
请求更多的服务器?虽然这个传统方案可能是最安全的技术解决方案,但它并不自动意味着会有足够的预算来增加资源。
-
重新架构解决方案?从负载均衡器或复制中移除一个 Web 服务器和数据库服务器,并使用应用程序 2 的软件栈重新部署它们,从技术角度来看,似乎是下一个最简单的选择。然而,你正在为应用程序 2 引入单点故障,并减少了应用程序 1 的冗余:你最初之所以运行三台 Web 服务器和两台数据库服务器,可能是有原因的。
-
尝试在你的服务器上并行安装新的软件栈?这当然是可行的,可能看起来是一个不错的短期计划,可以让项目迅速推出,但这可能会让你陷入如同纸牌屋般的困境,当需要为任一软件栈打上第一个关键的安全补丁时,一切都可能崩塌。
Docker 解决方案
这就是 Docker 展现其优势的地方。如果你的应用程序 1 正在三个 Web 服务器上的容器中运行,你可能已经不止在运行三个容器;实际上,你可能已经在运行六个容器,容器的数量翻倍,允许你进行应用程序的滚动部署,而不会减少应用程序 1 的可用性。
在这个环境中部署应用程序 2 就像只是简单地在你的三个主机上启动更多容器,然后通过负载均衡器将流量路由到新部署的应用程序一样。因为你只是部署容器,所以不需要担心在同一服务器上部署、配置和管理两个版本的相同软件栈的物流问题。
我们将在第五章中通过一个具体示例讲解这一场景,Docker Compose。
企业
企业面临与开发者和运维人员相同的问题,因为它们同时拥有这两种职业;然而,它们在更大规模上拥有这两个实体,并且涉及的风险也更大。
问题
由于风险以及任何停机可能导致销售损失或影响声誉,企业在发布之前需要测试每个部署。这意味着新功能和修复将被搁置,直到以下事项发生:
-
测试环境被启动并配置好。
-
应用程序已部署到新启动的环境中。
-
执行测试计划,并调整应用程序和配置,直到测试通过。
-
提出变更请求,提交并讨论,最终将更新后的应用程序部署到生产环境中。
这个过程可能需要几天、几周,甚至几个月的时间,具体取决于应用程序的复杂性和更改所引入的风险。尽管这个过程对于确保企业在技术层面上的连续性和可用性是必需的,但它可能会在业务层面上增加风险。如果你的新功能被卡在这一持有状态,而竞争对手提前发布了类似的功能,甚至是完全相同的功能,怎么办?
这种情况可能对销售和声誉造成的损害,与该过程最初被引入以保护你免受的停机损失一样严重。
Docker 解决方案
Docker 并没有消除像刚才描述的那样的流程存在或被遵循的必要性。然而,正如我们已经提到的,它确实使事情变得更加轻松,因为你已经在持续工作。这意味着开发人员一直在使用与生产环境中运行的相同容器配置进行工作。这意味着将该方法应用于你的测试就不会是太大的步骤。
例如,当开发人员检查他们在本地开发环境中知道可以正常运行的代码时(因为他们一直在该环境中进行工作),你的测试工具可以启动相同的容器来运行自动化测试。使用过的容器可以被删除,以便为下一轮测试释放资源。这意味着,突然之间,你的测试过程和流程变得更加灵活,你可以继续重用相同的环境,而不是为下一轮测试重新部署或重新映像服务器。
这个流程的简化可以延伸到让你的新应用容器推送到生产环境中。
这个过程完成得越快,你就越能自信地推出新功能或修复,保持在竞争前沿。
所以,我们知道 Docker 是为了解决什么问题而开发的。接下来我们需要讨论的是 Docker 到底是什么,它做了什么。
专用主机、虚拟机和 Docker 之间的差异
Docker 是一种容器管理系统,帮助我们更轻松、普遍地高效管理Linux 容器(LXC)。这让你可以在笔记本上的虚拟环境中创建镜像,并对其运行命令。你在这些本地运行的环境中对容器执行的操作,将与它们在生产环境中运行时所执行的命令或操作相同。
这有助于你在从开发环境(例如本地机器上的环境)转到生产环境(服务器上的环境)时,不必做出不同的操作。现在,让我们来看看 Docker 容器和典型虚拟机环境之间的区别:

图 1.1 – 在虚拟机环境中运行的应用程序
如你所见,在一台专用机器上,我们有三个应用程序,它们共享相同的橙色软件栈。运行虚拟机使我们能够运行三个应用程序,运行两种完全不同的软件栈。以下图表展示了在 Docker 容器中运行的同三个应用程序:

图 1.2 – 运行在 Docker 上的应用程序
这个图表为我们提供了 Docker 最重要的关键好处的深刻见解,即每次我们需要启动一个新容器时,不需要一个完整的操作系统,这大大减少了容器的整体大小。由于几乎所有版本的 Linux 都使用标准内核模型,Docker 依赖于使用主机操作系统的 Linux 内核作为其构建的操作系统,例如 Red Hat、CentOS 和 Ubuntu。
出于这个原因,你几乎可以将任何 Linux 操作系统作为主机操作系统,并能够在主机上叠加其他基于 Linux 的操作系统。也就是说,你的应用程序会误以为一个完整的操作系统已经安装——但实际上,我们只安装了二进制文件,例如一个包管理器,以及比如 Apache/PHP 和运行应用程序所需的库,这样就足够让你的应用程序运行。
例如,在之前的图示中,我们可以让 Red Hat 为橙色应用程序提供支持,而 Debian 为绿色应用程序提供支持,但实际上并不需要在主机上安装 Red Hat 或 Debian。因此,Docker 的另一个好处是它创建镜像时的大小。它们的构建没有包含最重要的部分:内核或操作系统。这使得它们非常小、紧凑,且易于传输。
Docker 安装
安装程序是你在本地机器和服务器环境中开始使用 Docker 时需要的第一批软件之一。首先,我们来看看你可以在哪些环境中安装 Docker:
-
Linux(各种 Linux 发行版)
-
macOS
-
Windows 10 专业版
此外,你还可以在公共云上运行它们,比如 Amazon Web Services、Microsoft Azure 和 DigitalOcean 等。通过之前列出的每个安装程序,Docker 在操作系统上的工作方式有所不同。例如,Docker 在 Linux 上本地运行。然而,如果你使用的是 macOS 或 Windows 10,它的运行方式会有所不同,因为它依赖于 Linux。
让我们快速看看如何在运行 Ubuntu 18.04 的 Linux 桌面上安装 Docker,然后在 macOS 和 Windows 10 上进行安装。
在 Linux 上安装 Docker
如前所述,这是我们将要讨论的三个系统中最简单的安装方法。我们将在 Ubuntu 18.04 上安装 Docker;然而,不同的 Linux 发行版有各自的包管理器,它们的安装方式会稍有不同。有关其他 Linux 发行版安装的详细信息,请参阅 进一步阅读 部分。要安装 Docker,只需在终端会话中运行以下命令:
$ curl -sSL https://get.docker.com/ | sh
$ sudo systemctl start docker
系统会要求你将当前用户添加到 Docker 组中。为此,请运行以下命令,确保将用户名替换为你自己的用户名:
$ sudo usermod -aG docker username
这些命令将从 Docker 官方下载、安装并配置最新版本的 Docker。编写本文时,官方安装脚本安装的 Linux 操作系统版本是 19.03。
运行以下命令应确认 Docker 已安装并正在运行:
$ docker version
你应该会看到类似如下的输出:

图 1.3 – 显示系统上安装的 Docker 版本的 docker version 命令输出
我们将在后续章节中使用一个支持工具,它作为 Docker for macOS 或 Windows 10 安装程序的一部分进行安装。
为了确保我们在后续章节中能顺利使用该工具,我们应该现在就安装它。这个工具叫做1.25.4——在以下代码块中的命令中将版本号替换为你安装时的最新版本:
$ COMPOSEVERSION=1.25.4
$ curl -L https://github.com/docker/compose/releases/
download/$COMPOSEVERSION/docker-compose-`uname -s`-`uname -m`
>/tmp/docker-compose
$ chmod +x /tmp/docker-compose
$ sudo mv /tmp/docker-compose /usr/local/bin/docker-compose
安装完成后,你应该能够运行以下两个命令来确认软件版本是否正确:
$ docker-compose version
现在我们知道如何在 Linux 上安装它,接下来让我们看看如何在 macOS 上安装它。
在 macOS 上安装 Docker
与命令行的 Linux 安装不同,Docker for Mac 提供了一个图形化安装程序。
提示
在下载之前,你应该确保你正在运行至少是 Apple macOS X Yosemite 10.10.3,因为这是运行我们将在本书中讨论的 Docker 版本的最低操作系统要求。如果你正在使用更旧的版本,情况也并非全无希望;你仍然可以运行 Docker。请参阅本章的 较旧操作系统 部分。
让我们在 macOS 上安装 Docker:
-
访问 Docker 商店:
hub.docker.com/editions/community/docker-ce-desktop-mac。 -
点击 获取 Docker 链接。
-
下载完成后,你应该会得到一个
DMG文件。双击它会挂载映像,打开桌面上挂载的映像后,你应该会看到类似如下的界面:![图 1.4 – macOS 上 Docker 安装程序的拖拽界面]()
](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/ms-dkr-4e/img/image004.jpg)
图 1.4 – macOS 上 Docker 安装程序的拖拽界面
-
将 Docker 图标拖动到 应用程序 文件夹后,双击图标,系统会询问您是否要打开已下载的应用程序。
-
点击 是 将打开 Docker 安装程序,并显示以下提示:
![图 1.5 – Docker 安装程序提示]()
图 1.5 – Docker 安装程序提示
-
点击 确定 后,将弹出一个对话框,询问您的密码。输入密码后,您应该会在屏幕左上角的图标栏中看到 Docker 图标。
-
点击图标并选择 关于 Docker,您应该会看到类似以下内容:
![图 1.6 – 关于 Docker 界面]()
图 1.6 – 关于 Docker 界面
-
您还可以运行以下命令,检查与 Docker 引擎一起安装的 Docker Compose 版本:
$ docker-compose version
现在我们已经了解了如何在 macOS 上安装 Docker,让我们继续进行在最后一个操作系统上的安装:Windows 10 专业版。
在 Windows 10 专业版上安装 Docker
和 Docker for Mac 一样,Docker for Windows 使用图形化安装程序。
重要提示
在下载之前,您应该确保自己运行的是 Microsoft Windows 10 专业版或企业版 64 位版本。如果您运行的是较旧版本或不受支持的 Windows 10 版本,仍然可以运行 Docker;有关更多信息,请参阅本章中的 旧版操作系统 部分。Docker for Windows 之所以有此要求,是因为它依赖于 Hyper-V。Hyper-V 是 Windows 的本地虚拟机管理程序,允许您在 Windows 机器上运行 x86-64 虚拟机,无论是 Windows 10 专业版还是 Windows Server。它甚至是 Xbox One 操作系统的一部分。
让我们为 Windows 安装 Docker:
-
从 Docker 商店下载适用于 Windows 的 Docker 安装程序:
hub.docker.com/editions/community/docker-ce-desktop-windows。 -
点击 获取 Docker 按钮下载安装程序。
-
下载完成后,运行安装包,您将看到以下界面:
![图 1.7 – Docker for Windows 安装程序配置界面]()
图 1.7 – Docker for Windows 安装程序配置界面
-
保持默认配置,然后点击 确定。这将触发安装所有运行 Docker 所需的组件:
![图 1.8 – Docker 安装进度]()
图 1.8 – Docker 安装进度
-
安装完成后,您将被提示重启。为此,只需点击 关闭并重启 按钮:
![图 1.9 – Docker 安装完成确认界面]()
图 1.9 – Docker 安装完成确认界面
-
一旦你的机器重启,你应该会在屏幕右下角的图标托盘中看到 Docker 图标。点击它并从菜单中选择关于 Docker,将显示以下内容:
![图 1.10 – Docker 关于我页面]()
图 1.10 – Docker 关于我页面
-
打开 PowerShell 窗口,输入以下命令:
$ docker version这也应该会显示与 Mac 和 Linux 版本相似的输出:

](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/ms-dkr-4e/img/image010.jpg)
图 1.11 – docker version 命令的输出
同样,你也可以运行以下命令来检查与 Docker Engine 一起安装的 Docker Compose 和 Docker Machine 的版本:
$ docker-compose version
你应该会看到与 macOS 和 Linux 版本类似的输出。正如你可能已经开始发现的那样,一旦安装了这些包,它们的使用方法会非常相似。当我们进入本章的使用 Docker 命令部分时,你会看到这一点。
较旧的操作系统
如果你在 Mac 或 Windows 上运行的操作系统版本不够新,那么你需要使用 Docker Toolbox。可以参考运行以下命令时输出的内容:
$ docker version
在我们至今所做的所有三个安装中,它显示了两个不同的版本,一个是客户端版本,另一个是服务器版本。可以预见,Linux 版本显示客户端和服务器的架构都是 Linux;然而,你可能会注意到 Mac 版本显示客户端运行在 Darwin 上,这是 Apple 的类 Unix 内核,而 Windows 版本显示的是 Windows。不过,两个服务器版本显示的架构都是 Linux,那这又是怎么回事呢?
这是因为无论是 Mac 版本还是 Windows 版本的 Docker,都会下载并在后台运行一个虚拟机,而这个虚拟机运行的是基于 Alpine Linux 的一个小型轻量级操作系统。这个虚拟机使用 Docker 的库,连接到你选择的环境的内置虚拟化管理程序。
对于 macOS,这是内置的Hypervisor.framework,而对于 Windows,如我们之前提到的,它是Hyper-V。
为了确保没有人错过 Docker 体验,对于较旧版本的 macOS 和不受支持的 Windows 版本,有一种不使用这些内置虚拟化管理程序的 Docker 版本。这些版本使用VirtualBox作为虚拟化管理程序,来运行 Linux 服务器,供本地客户端连接。
重要提示
VirtualBox 是由 Oracle 开发的开源 x86 和 AMD64/Intel64 虚拟化产品。它支持在 Windows、Linux、Macintosh 和 Solaris 主机上运行,并支持多种 Linux、Unix 和 Windows 客户操作系统。
欲了解有关 Docker Toolbox 的更多信息,请访问项目官网:github.com/docker/toolbox/,你还可以从发布页面下载适用于 macOS 和 Windows 的安装程序。
重要提示
本书假设你已经在 Linux 上安装了最新版本的 Docker,或者使用了 Docker for Mac 或 Docker for Windows。虽然使用 Docker Toolbox 安装的 Docker 应该也能支持本书中的命令,但在将本地机器的数据挂载到容器时,可能会遇到文件权限和所有权方面的问题。
现在你已经在选择的系统上启动并运行 Docker,让我们开始探索一些必要的命令,以便高效使用 Docker。
使用 Docker 命令
你应该已经熟悉这些 Docker 命令。然而,值得一遍遍地过一遍,确保你了解所有的命令。我们将从一些常用命令开始,然后看一下用于 Docker 镜像的命令。接着,我们将深入了解用于容器的命令。
提示
不久前,Docker 对其命令行客户端进行了重构,将命令分为更合逻辑的组别,因为客户端提供的功能数量激增,导致一些命令开始交叉重叠。本书中我们将使用这种结构,而不是客户端中仍然存在的一些简写命令。
我们将要查看的第一个命令是最有用的命令之一,既适用于 Docker,也适用于你使用的任何命令行工具——help命令。它只需这样运行:
$ docker help
这个命令会给你列出所有 Docker 命令的完整列表,并简要描述每个命令的功能。我们将在第四章《管理容器》中更详细地探讨这一点。对于某个特定命令的进一步帮助,你可以运行以下命令:
$ docker <COMMAND> --help
接下来,让我们运行hello-world容器。只需运行以下命令即可:
$ docker container run hello-world
无论你在哪个主机上运行 Docker,Linux、macOS 还是 Windows 上都会发生相同的事情。Docker 会下载hello-world容器镜像并执行它,执行完毕后,容器会停止。
你的终端会话应该如下所示:

图 1.12 – docker container run hello-world 的输出
让我们尝试一些更具冒险性的操作——通过运行以下两个命令来下载并运行一个 NGINX 容器:
$ docker image pull nginx
$ docker container run -d --name nginx-test -p 8080:80 nginx
重要提示
NGINX 是一个开源的 Web 服务器,可以用作负载均衡器、邮件代理、反向代理,甚至是 HTTP 缓存。
第一个命令下载了 NGINX 容器镜像,第二个命令在后台启动了一个名为nginx-test的容器,使用我们拉取的nginx镜像。它还将主机上的8080端口映射到容器中的80端口,使得我们可以通过本地浏览器访问localhost:8080/。
正如你从以下截图中看到的,命令和结果在所有三种操作系统中完全相同。这里我们有 Linux:

图 1.13 – 在 Linux 上运行 docker image pull nginx 的输出
重要提示
你可能会注意到,Linux 和 macOS 的屏幕乍一看似乎很相似。那是因为我正在使用一台远程的 Linux 服务器,稍后我们会更详细地探讨如何实现这一点。
这是在 macOS 上的结果:

图 1.14 – 在 macOS 上运行 docker image pull nginx 的输出
而这就是它在 Windows 上的样子:

图 1.15 – 在 Windows 上运行 docker image pull nginx 的输出
在接下来的三章中,我们将更详细地了解如何使用 Docker 命令行客户端。目前,让我们暂停并通过运行以下命令停止并移除我们的 nginx-test 容器:
$ docker container stop nginx-test
$ docker container rm nginx-test
如你所见,在我们安装了 Docker 的三个主机上运行一个简单的 NGINX 容器的体验是完全相同的。正如我相信你可以想象的那样,如果没有像 Docker 这样的工具,在这三种平台上实现这一点将是一个挑战,并且在每个平台上的体验也会大不相同。传统上,这一直是导致本地开发环境差异的原因之一,因为人们需要下载特定平台的安装程序并为其配置服务。而且,在某些情况下,不同平台之间可能存在功能差异。
现在我们已经掌握了 Docker 命令的基础,让我们拓宽视野,看看它的容器生态系统。
Docker 和容器生态系统
如果你一直在关注 Docker 和容器的发展,你会注意到,过去几年里,Docker 网站上的信息逐渐从关于容器是什么的头条,转向了更多关注 Docker 作为一家公司所提供的服务。
这一变化的核心驱动力之一是,传统上,所有东西都被统称为“Docker”,这会让人感到困惑。现在,由于人们不再需要太多地了解容器是什么或 Docker 可以解决什么问题,公司需要开始尝试与涌现出来的各种支持容器技术的竞争者区分开来。
所以,让我们尝试解开 Docker 的所有内容,这包括以下几点:
-
开源项目:Docker 启动了几个开源项目,现在由一个庞大的开发者社区维护。
-
Docker, Inc:这是支持和开发核心 Docker 工具的公司。
-
Docker CE 和 Docker EE:这是建立在开源组件之上的核心 Docker 工具集。
我们将在后面的章节中进一步讨论一些第三方服务。同时,让我们更详细地了解每一个,首先从开源项目开始。
开源项目
Docker 公司在过去几年中将其核心项目开源并捐赠给了各种开源基金会和社区。这些项目包括以下内容:
-
Moby 项目是 Docker 引擎的上游项目,它提供了组建一个功能齐全的容器系统所需的所有组件。
-
Runc是一个命令行接口,用于创建和配置容器,并已构建为 OCI 规范。
-
Containerd是一个易于嵌入的容器运行时。它也是 Moby 项目的核心组件。
-
LibNetwork是一个 Go 库,为容器提供网络功能。Notary 是一个客户端和服务器,旨在为签名的容器镜像提供信任系统。
-
HyperKit是一个工具包,允许你将虚拟机管理程序功能嵌入到自己的应用程序中;目前,它仅支持 macOS 和 Hypervisor 框架。
-
VPNKit为 HyperKit 提供 VPN 功能。
-
DataKit允许你使用类似 Git 的工作流程来编排应用程序数据。
-
SwarmKit是一个工具包,使你能够使用与 Docker Swarm 相同的 Raft 共识算法构建分布式系统。
-
LinuxKit是一个框架,允许你开发和编译一个小型的可移植 Linux 操作系统,用于运行容器。
-
InfraKit是一组工具,你可以使用它来定义运行LinuxKit生成的发行版的基础设施。
单独使用这些组件,你可能永远不会使用它们;然而,上述每个项目都是由 Docker 公司维护的工具的一部分。我们将在最后一章中详细介绍这些项目。
Docker 公司。
Docker 公司成立之初开发了Docker 社区版(Docker CE)和Docker 企业版(Docker EE)。它还曾为 Docker EE 提供基于 SLA 的支持服务,并向希望将现有应用程序容器化并将其作为 Docker 的现代化传统应用程序(MTA)计划的一部分的公司提供咨询服务。
你会注意到,我在前一句话中使用了很多过去时态。这是因为在 2019 年 11 月,Docker 公司进行了重组,并将其平台业务出售给了一家名为 Mirantis Inc.的公司。它们从 Docker 公司收购了以下资产:
-
Docker 企业版,包括 Docker EE
-
Docker 受信注册表
-
Docker 统一控制平面
-
Docker CLI
Mirantis Inc.是一家总部位于加利福尼亚的公司,专注于开发和支持OpenStack和Kubernetes基础的解决方案。它是非营利性企业实体 OpenStack 基金会的创始人之一,并且在为企业级客户提供支持方面拥有丰富的经验。
前 Docker 公司首席执行官 Rob Bearden,在宣布辞职后不久被引用说:
“在与管理团队和董事会进行了彻底分析后,我们确定 Docker 拥有两个非常不同的业务:一个是活跃的开发者业务,另一个是不断发展的企业业务。我们还发现,产品和财务模式大不相同。这导致了我们决定重组公司,将两者分开,这是对客户最有利的做法,也有助于 Docker 行业内领先的技术蓬勃发展。”
现在,企业业务由 Mirantis Inc. 负责,Docker, Inc. 专注于通过 Docker Desktop 和 Docker Hub 提供更好的开发者工作流程,这使得用户能够避免供应商锁定的威胁。
Docker CE 和 Docker EE
Docker, Inc. 提供并支持了许多工具。我们已经提到了一些,其他的我们将在后续章节中讲解。在结束本章之前,我们应该对将要使用的工具有一个大致了解。其中最重要的工具是核心 Docker 引擎。
这是 Docker 的核心,所有我们将要介绍的其他工具都依赖于它。我们在本章的 Docker 安装和 Docker 命令部分中已经使用了它。目前有两个版本的 Docker 引擎;其中 Docker EE 由 Mirantis Inc. 维护,Docker CE 由我们使用。在本书中,我们将使用 Docker CE。
除了稳定版的 Docker CE,Docker 还将通过夜间仓库(正式为 Docker CE Edge)提供 Docker 引擎的夜间构建版本,并通过 Edge 渠道提供 Docker for Mac 和 Docker for Windows 的每月构建版本。
还有以下工具:
-
Docker Compose:一个允许您定义和共享多容器定义的工具;在 第五章 中详细介绍了 Docker Compose。
-
Docker Machine:一个在多个平台上启动 Docker 主机的工具;我们将在 第六章 中介绍 管理容器。
-
Docker Hub:一个用于存储您的 Docker 镜像的仓库,接下来的三章将详细介绍。
-
Docker Desktop(Mac):我们在本章中已经涵盖了 Docker for Mac。
-
Docker 桌面版(Windows):我们在本章中已经涵盖了 Docker for Windows。
-
Docker Swarm:一个多主机感知的编排工具,在 第八章 中详细介绍 Docker Swarm。现在由 Mirantis Inc. 负责维护。
总结
在本章中,我们介绍了一些您应该已经知道(或现在已经知道)的基本信息,为接下来的章节做准备。我们讲解了 Docker 的基础知识,以及与其他主机类型的对比。我们讲解了安装程序、它们在不同操作系统中的运行方式以及如何通过命令行控制它们。请务必查看安装程序的要求,确保您使用适合您操作系统的版本。
然后,我们简单介绍了如何使用 Docker,并执行了一些基本命令来帮助你入门。我们将在未来的章节中详细讨论所有管理命令,深入了解它们是什么,以及如何和何时使用它们。最后,我们讨论了 Docker 生态系统以及不同工具的职责。
在下一章,我们将探讨如何构建基础容器,并深入研究 Dockerfile 以及存储镜像的位置,还会介绍如何使用环境变量和 Docker 卷。
问题
-
你可以从哪里下载 Docker Desktop(Mac)和 Docker Desktop(Windows)?
-
我们使用了什么命令来下载 NGINX 镜像?
-
哪个开源项目是核心 Docker 引擎的上游项目?
-
现在哪个公司在维护 Docker Enterprise?
-
你会运行哪个命令来获取更多关于 Docker 容器子集命令的信息?
进一步阅读
这些是参与维护 Docker 的公司:
-
Docker, Inc.:
docker.com -
Mirantis Inc.:
www.mirantis.com
在本章中,我们提到了以下虚拟化管理程序:
-
macOS 虚拟化框架:
developer.apple.com/documentation/hypervisor -
Hyper-V:
docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v
有关如何在其他 Linux 发行版上安装的详细信息,请查看 Docker 文档中的安装 Docker 引擎页面:docs.docker.com/engine/install/。
我们引用了以下 Docker 的博客文章:
-
Docker CLI 重组博客文章:
www.docker.com/blog/whats-new-in-docker-1-13/ -
Docker 扩展支持公告:
www.docker.com/blog/extending-support-cycle-docker-community-edition/
接下来,我们讨论了以下开源项目:
-
Moby 项目:
mobyproject.org -
Containerd:
containerd.io -
LibNetwork:
github.com/moby/libnetwork -
HyperKit:
github.com/moby/hyperkit -
VPNKit:
github.com/moby/vpnkit -
DataKit:
github.com/moby/datakit
第二章:构建容器镜像
在本章中,你将开始构建容器镜像。我们将介绍使用原生 Docker 工具定义和构建镜像的五种不同方式。
我们将讨论你可以用来定义和构建自己镜像的推荐方法,以及一种虽然不是最佳实践,但也有其用处的方法。
我们将覆盖以下主题:
-
介绍 Dockerfile
-
构建 Docker 镜像
让我们开始吧!
技术要求
在本章中,我们将使用 Docker 安装来构建镜像。一些支持命令,虽然较少且间隔较远,可能仅适用于 macOS 和基于 Linux 的操作系统。
查看以下视频,查看实际代码:bit.ly/3h7oDX5
提示
虽然本章中的截图将来自我偏好的操作系统 macOS,但我们将运行的 Docker 命令将在我们已经安装 Docker 的所有三个操作系统上运行。
介绍 Dockerfile
在本节中,我们将深入讨论 Dockerfile,并介绍其使用中的最佳实践。那么,什么是 Dockerfile 呢?
docker image build 命令,我们将接下来讨论,它用于组装容器镜像。
一个 Dockerfile 看起来如下所示:
FROM alpine:latest
LABEL maintainer=”Russ McKendrick <russ@mckendrick.io>”
LABEL description=”This example Dockerfile installs NGINX.”
RUN apk add --update nginx && \
rm -rf /var/cache/apk/* && \
mkdir -p /tmp/nginx/
COPY files/nginx.conf /etc/nginx/nginx.conf
COPY files/default.conf /etc/nginx/conf.d/default.conf
ADD files/html.tar.gz /usr/share/nginx/
EXPOSE 80/tcp
ENTRYPOINT [“nginx”]
CMD [“-g”, “daemon off;”]
如你所见,即使没有解释,也很容易理解 Dockerfile 中每个步骤指示构建命令做什么。在我们继续并逐步解析前面的文件之前,我们应该简要了解一下 Alpine Linux。
grsecurity/PaX,它已经被补丁修复到内核中。这个版本提供了对多种潜在零日漏洞和其他漏洞的主动保护。
由于其体积小巧且功能强大,Alpine Linux 已成为 Docker 官方容器镜像的默认基础镜像。因此,我们将在本书中使用它。为了让你了解官方的 Alpine Linux 镜像有多小,我们将其与写作时可用的其他一些发行版进行比较:

图 2.1 – 比较流行基础镜像的大小
如前面的终端输出所示,Alpine Linux 仅重 5.59 MB,相比之下,最大的镜像 CentOS 重 237 MB。Alpine Linux 的裸机安装大约为 130 MB,仍然几乎是 CentOS 容器镜像大小的一半。
深入审查 Dockerfile
让我们来看一下我们在前面的 Dockerfile 示例中使用的指令。我们将按照它们出现的顺序进行查看:
-
FROM -
LABEL -
RUN -
COPY和ADD -
EXPOSE -
ENTRYPOINT和CMD -
其他 Dockerfile 指令
FROM
FROM指令告诉 Docker 你希望使用哪一个基础镜像。正如我们之前提到的,我们使用的是 Alpine Linux,所以我们只需要声明镜像的名称以及我们希望使用的发布标签。在我们的例子中,要使用最新的官方 Alpine Linux 镜像,我们只需添加alpine:latest。
LABEL
LABEL指令可以用来为镜像添加额外的信息。这些信息可以是版本号,也可以是描述。建议你限制使用标签的数量。一个良好的标签结构可以帮助以后使用我们镜像的其他人。
然而,使用过多的标签也会导致镜像效率降低,因此我建议使用在label-schema.org中详细介绍的标签结构。你可以通过以下docker inspect命令查看容器的标签:
$ docker image inspect <IMAGE_ID>
或者,你可以使用以下命令来仅过滤标签:
$ docker image inspect -f {{.Config.Labels}} <IMAGE_ID>
在下面的截图中,你可以看到 CentOS 镜像的标签:

图 2.2 – 检查镜像标签
在我们的示例 Dockerfile 中,我们添加了两个标签:
maintainer=”Russ McKendrick <russ@mckendrick.io>”:添加一个标签,帮助镜像的最终用户识别谁在维护它。
description=”This example Dockerfile installs NGINX.”:添加一个简短的描述,说明镜像的功能。
通常来说,最好在你从镜像创建容器时再定义标签,而不是在构建时定义,所以最好将标签限制为仅关于镜像的元数据,其他内容不需要。
RUN
RUN指令是我们与镜像互动的地方,用来安装软件、运行脚本、命令和其他任务。正如下面的RUN指令所示,我们实际上运行了三条命令:
RUN apk add --update nginx && \
rm -rf /var/cache/apk/* && \
mkdir -p /tmp/nginx/
我们三条命令中的第一条,相当于如果我们在 Alpine Linux 主机上有一个 shell 时运行以下命令:
$ apk add --update nginx
这个命令使用 Alpine Linux 的包管理器来安装 NGINX。
小贴士
我们使用&&运算符,确保前一条命令成功执行后再执行下一条命令。这样能让我们在 Dockerfile 中更清晰地知道每一条命令的执行情况。我们还使用了****,允许我们将命令分割成多行,这使得命令更加易读。
我们链中的下一条命令删除了所有临时文件,以保持镜像的最小体积:
$ rm -rf /var/cache/apk/*
我们链中的最后一条命令会创建一个路径为/tmp/nginx/的文件夹,这样 NGINX 在我们运行容器时就能正确启动:
$ mkdir -p /tmp/nginx/
我们也可以在我们的 Dockerfile 中使用以下内容来达到相同的效果:
RUN apk add --update nginx
RUN rm -rf /var/cache/apk/*
RUN mkdir -p /tmp/nginx/
然而,就像添加多个标签一样,这被认为是低效的,因为它可能会增加镜像的整体大小,我们应该尽量避免这种情况。虽然有一些有效的使用场景,某些命令在通过&&连接时效果不好,但大部分情况下,在构建镜像时应该避免这种运行命令的方式。
COPY 和 ADD
初看之下,COPY和ADD似乎在执行相同的任务,都是用于将文件传输到镜像中。然而,它们之间存在一些重要的差异,我们将在这里讨论。
COPY指令是两者中更直接的一种:
COPY files/nginx.conf /etc/nginx/nginx.conf
COPY files/default.conf /etc/nginx/conf.d/default.conf
正如你可能已经猜到的,我们正在从我们构建镜像的主机上的files文件夹中复制两个文件。第一个文件是nginx.conf,这是一个最小化的 NGINX 配置文件:
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main ‘$remote_addr - $remote_user [$time_
local] “$request” ‘
‘$status $body_bytes_sent “$http_referer”
‘
‘”$http_user_agent” “$http_x_forwarded_
for”’;
access_log /var/log/nginx/access.log main;
sendfile off;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
}
这将覆盖在RUN指令中通过 APK 安装部分安装的 NGINX 配置。
接下来的文件default.conf是我们可以配置的最简单的虚拟主机,包含以下内容:
server {
location / {
root /usr/share/nginx/html;
}
}
同样,这将覆盖任何现有的文件。到目前为止,一切都很好!那么,为什么我们会使用ADD指令呢?
在我们的示例 Dockerfile 中,ADD指令如下所示:
ADD files/html.tar.gz /usr/share/nginx/
如你所见,我们添加了一个名为html.tar.gz的文件,但实际上我们在 Dockerfile 中并没有对归档文件进行任何解压操作。这是因为ADD会自动上传、解压并将解压后的文件夹和文件添加到我们请求的路径中,在我们这个案例中是/usr/share/nginx/。这为我们提供了/usr/share/nginx/html/作为我们定义的 Web 根目录,该目录也在我们复制到镜像中的default.conf文件的虚拟主机块中进行了定义。
ADD指令也可以用来从远程源添加内容。例如,考虑以下内容:
ADD https://raw.githubusercontent.com/PacktPublishing/Mastering-Docker-Fourth-Edition/master/chapter02/dockerfile-example/files/html.tar.gz /usr/share/nginx/
上述命令行会从https://raw.githubusercontent.com/PacktPublishing/Mastering-Docker-Fourth-Edition/master/chapter02/dockerfile-example/files/下载html.tar.gz并将文件放置在镜像中的/usr/share/nginx/文件夹中。
来自远程源的归档文件被视为文件,并不会被解压,这一点在使用时需要考虑到。这意味着文件必须在RUN指令之前被添加,以便我们手动解压文件夹,并且还需要删除html.tar.gz文件。
EXPOSE:EXPOSE指令让 Docker 知道当镜像运行时,定义的端口和协议将在运行时暴露。这个指令不会将端口映射到主机机器,而是打开端口以允许对容器网络中的服务进行访问。
例如,在我们的 Dockerfile 中,我们告诉 Docker 每次运行镜像时都打开80端口:
EXPOSE 80/tcp
使用ENTRYPOINT而不是CMD的好处是,它们可以配合使用。ENTRYPOINT可以单独使用,但请记住,只有在你希望容器可执行时,才应单独使用ENTRYPOINT。
作为参考,如果你考虑一些可能使用的CLI命令,你必须指定的不仅仅是CLI命令。你可能还需要添加一些额外的参数,让命令进行解释。这就是仅使用ENTRYPOINT的使用场景。
例如,如果你想在容器内部执行一个默认命令,你可以像下面的示例一样做。请确保使用一个可以保持容器运行的命令。
在这里,我们使用的是以下内容:
ENTRYPOINT [“nginx”]
CMD [“-g”, “daemon off;”]
这意味着,每当我们从镜像启动容器时,NGINX 二进制文件就会执行,这正是我们定义的入口点。然后,我们定义的CMD将被执行,相当于运行以下命令:
$ nginx -g daemon off;
ENTRYPOINT的另一个使用示例如下:
$ docker container run --name nginx-version dockerfile-example
-v
这相当于在我们的主机上运行以下命令:
$ nginx -v
注意到我们并不需要告诉 Docker 使用 NGINX。由于我们将 NGINX 二进制文件作为入口点,因此任何传递的命令都会覆盖在 Dockerfile 中定义的CMD指令。
这将显示我们安装的 NGINX 版本,且容器会停止运行,因为 NGINX 二进制文件仅执行以显示版本信息。我们将在构建并启动容器后查看此效果。我们继续之前,应该先了解一下我们没有在 Dockerfile 中包含的一些指令。
其他 Dockerfile 指令
我们的示例 Dockerfile 中没有包含一些指令。让我们在这里看看它们:
-
USER:USER指令让你指定在运行命令时使用的用户名。USER指令可以用于RUN、CMD或ENTRYPOINT指令中。此外,在USER指令中定义的用户必须存在,否则你的镜像构建将失败。使用USER指令还可能引入权限问题,不仅仅是容器本身,甚至在挂载卷时也会出现问题。 -
WORKDIR:WORKDIR指令设置工作目录,用于USER指令可以使用的相同指令集(RUN、CMD和ENTRYPOINT)。它还允许你使用CMD和ADD指令。 -
ONBUILD:ONBUILD指令允许你将一组命令暂存起来,在将该镜像作为另一个容器镜像的基础镜像使用时执行。例如,如果你想将镜像提供给开发人员,并且他们都有不同的代码库需要测试,你可以使用ONBUILD指令在需要实际代码之前先为其打好基础。然后,开发人员只需将他们的代码添加到你指定的目录,当他们运行新的 Docker 构建命令时,代码就会被添加到运行中的镜像中。ONBUILD指令可以与ADD和RUN指令结合使用,如以下示例所示:
ONBUILD RUN apk update && apk upgrade && rm -rf /var/cache/
apk/*
这将在每次我们的镜像作为基础镜像用于另一个容器镜像时,执行更新和软件包升级。
ENV:ENV指令在镜像构建和执行时设置环境变量。这些变量可以在启动镜像时被覆盖。
Dockerfile – 最佳实践
现在我们已经介绍了 Dockerfile 指令,让我们来看一下编写 Dockerfile 的一些最佳实践。遵循这些最佳实践可以确保你的镜像精简、一致,并且便于他人使用:
-
你应该养成使用
.dockerignore文件的习惯。在本章的 构建 Docker 镜像 部分,我们将介绍.dockerignore文件;如果你已经习惯使用.gitignore文件,它看起来会非常熟悉。它本质上会在构建过程中忽略你在文件中指定的项。 -
记得每个文件夹中只保留一个 Dockerfile,以帮助你组织容器。
-
使用版本控制系统,如 Git,来管理你的 Dockerfile;就像其他文本文件一样,版本控制将帮助你在必要时不仅向前推进,也能向后回溯。
-
最小化每个镜像中需要安装的包数量。构建镜像时你要实现的最大目标之一就是保持镜像尽可能小且安全。不安装不必要的包将大大帮助你实现这个目标。
-
确保每个容器只运行一个应用进程。每当你需要一个新的应用进程时,最好创建一个新的容器来运行该应用。
-
保持简洁;过于复杂的 Dockerfile 会增加冗余,可能会在后期造成问题。
-
通过示例学习!Docker 官方提供了一个非常详细的风格指南,用于发布他们在 Docker Hub 上托管的官方镜像。你可以在本章末尾的 进一步阅读 部分找到该链接。
构建 Docker 镜像
在本节中,我们将介绍 docker image build 命令。正如人们所说,这就是实现目标的关键时刻。现在是我们构建基础的时刻,未来我们将基于此开始构建我们的镜像。我们将查看完成这个目标的不同方法。可以将其视为你之前使用虚拟机创建的模板。这将帮助你节省时间,因为它会为你完成繁重的工作;你只需创建需要添加到新镜像中的应用程序。
在使用 docker build 命令时,你可以使用许多开关。所以,让我们使用一个总是方便的开关。在这里,我们将使用 docker image build 命令的 --help 开关来查看我们可以做什么:
$ docker image build --help
在构建镜像时,你可以传递很多不同的标志。现在,虽然看起来有很多内容需要消化,但在所有这些选项中,我们只需要使用 --tag 或其简写 -t 来命名我们的镜像。
你可以使用其他选项来限制构建过程中 CPU 和内存的使用量。在某些情况下,你可能不希望 build 命令占用尽可能多的 CPU 或内存。这个过程可能会稍微慢一些,但如果你在本地机器或生产服务器上运行它,并且这是一个长时间的构建过程,你可能想要设置一个限制。还有一些选项会影响为构建镜像而启动的容器的网络配置。
通常,你不需要使用 --file 或 -f 开关,因为你会在与 Dockerfile 相同的文件夹中运行 docker build 命令。将 Dockerfile 保存在单独的文件夹中有助于整理文件,并保持文件命名的一致性。
还值得一提的是,虽然你可以在构建时将额外的环境变量(ENVs)作为参数传递,但它们仅在构建时使用,你的容器镜像不会继承它们。这对于传递诸如代理设置等信息非常有用,这些设置可能只适用于你的初始构建/测试环境。
.dockerignore 文件,正如我们之前讨论的,用于排除那些我们不希望包含在 Docker 构建中的文件或文件夹,因为默认情况下,和 Dockerfile 同一文件夹中的所有文件都会被上传。我们还讨论了将 Dockerfile 放在一个单独的文件夹中的做法,同样的做法也适用于 .dockerignore 文件。它应该放在与 Dockerfile 相同的位置。
将你想要在镜像中使用的所有项目保存在同一个文件夹中,有助于将 .dockerignore 文件中的项目数量保持到最小。
由于我们在本章的最后几节已经讨论了 Dockerfile,接下来让我们使用我们在这里讨论的示例文件来开始构建镜像。
使用 Dockerfile
我们将要查看的第一种构建基础容器镜像的方法是创建一个 Dockerfile。事实上,我们将使用上一节的 Dockerfile,然后执行 docker image build 命令来获取一个 NGINX 镜像。
所以,让我们再次查看 Dockerfile:
FROM alpine:latest
LABEL maintainer=”Russ McKendrick <russ@mckendrick.io>”
LABEL description=”This example Dockerfile installs NGINX.”
RUN apk add --update nginx && \
rm -rf /var/cache/apk/* && \
mkdir -p /tmp/nginx/
COPY files/nginx.conf /etc/nginx/nginx.conf
COPY files/default.conf /etc/nginx/conf.d/default.conf ADD files/html.tar.gz /usr/share/nginx/
EXPOSE 80/tcp
ENTRYPOINT [“nginx”]
CMD [“-g”, “daemon off;”]
别忘了你还需要 default.conf、html.tar.gz 和 nginx.conf 文件,这些文件位于 files 文件夹中。你可以在附带的 GitHub 仓库中找到这些文件。
所以,我们可以通过两种方式构建我们的镜像。第一种方式是通过在使用 docker image build 命令时指定 --file 开关。我们还将使用 --tag 开关为新镜像指定一个唯一名称:
$ docker image build --file <path_to_Dockerfile> --tag
<REPOSITORY>:<TAG> .
现在,<REPOSITORY> 通常是你在 Docker Hub 上注册的用户名。我们将在 第三章中详细讨论 存储和分发镜像,但现在我们将使用 local。<TAG> 是一个唯一值,允许你识别一个容器。通常,这将是一个版本号或其他描述符。
由于我们有一个名为 Dockerfile 的文件,我们也可以跳过使用 --file 开关。这是构建镜像的第二种方式。以下是代码:
$ docker image build --tag local:dockerfile-example .
最重要的是要记住最后的点(或句号)。这是告诉 docker image build 命令在当前文件夹中构建镜像。当你构建镜像时,你应该会看到类似以下的终端输出:

图 2.3 – 从我们的 Dockerfile 构建镜像
一旦构建完成,你应该能够运行以下命令来检查镜像是否可用,以及镜像的大小:
$ docker image ls
从以下终端输出中可以看到,我的镜像大小是 7.15 MB:

图 2.4 – 检查容器镜像的大小
你可以通过运行以下命令来启动一个带有新构建镜像的容器:
$ docker container run -d --name dockerfile-example -p 8080:80
local:dockerfile-example
这将启动一个名为 dockerfile-example 的容器。你可以通过以下命令检查它是否正在运行:
$ docker container ls
打开浏览器并访问 http://localhost:8080/ 应该会显示一个非常简单的网页,页面内容如下:

图 2.5 – 在浏览器中检查容器
接下来,我们将快速运行一些我们在本章的 介绍 Dockerfiles 部分中讲解过的命令,首先是以下命令:
$ docker container run --name nginx-version local:dockerfile-
example -v
从以下终端输出中可以看到,我们当前运行的是 NGINX 版本 1.16.1:

图 2.6 – 检查 NGINX 版本
我们将要运行的下一个命令会显示我们在构建时嵌入的标签。
要查看此信息,运行以下命令:
$ docker image inspect -f {{.Config.Labels}} local:dockerfile-
example
从以下输出中,你可以看到我们输入的信息:

图 2.7 – 检查我们新构建镜像的标签
在我们继续之前,你可以使用以下命令停止并删除我们启动的容器:
$ docker container stop dockerfile-example
$ docker container rm dockerfile-example nginx-version
我们将在第四章,管理容器中详细讨论 Docker 容器命令。
使用现有容器
构建基础镜像的最简单方法是首先使用 Docker Hub 上的官方镜像之一。Docker 还将这些官方构建的 Dockerfile 保存在它们的 GitHub 仓库中。因此,你至少有两种选择可以使用别人已经创建的现有镜像。通过使用 Dockerfile,你可以清楚地看到构建中包含了哪些内容,并添加你需要的内容。如果你想稍后修改或共享,你还可以对 Dockerfile 进行版本控制。
还有一种方法可以实现这一目标;然而,这种方法不推荐使用,也不被认为是良好的实践,我强烈不建议你使用它。
提示
我只会在原型阶段使用这种方法,检查你运行的命令是否在交互式 shell 中按预期工作,然后再将它们放入 Dockerfile 中。你应该始终使用 Dockerfile。
首先,我们应该下载我们想要作为基础使用的镜像;正如我们之前所做的,我们将使用 Alpine Linux:
$ docker image pull alpine:latest
接下来,我们需要在前台运行一个容器,以便与之交互:
$ docker container run -it --name alpine-test alpine /bin/sh
一旦容器运行,你可以根据需要使用apk命令,或根据你的 Linux 版本使用相应的包管理命令来添加包。
例如,以下命令将安装 NGINX:
$ apk update
$ apk upgrade
$ apk add --update nginx
$ rm -rf /var/cache/apk/*
$ mkdir -p /tmp/nginx/
$ exit
安装完你需要的包后,你需要保存容器。前面一组命令中的exit命令将停止正在运行的容器,因为我们要脱离的 shell 进程恰好是保持容器在前台运行的进程。
你可以在以下终端输出中看到这个:

图 2.8 – 检查终端输出
提示
就在这个时候,你应该真正停止;除了我们稍后讨论的一个使用案例外,我不建议你使用前面的命令来创建和分发镜像。
所以,为了将我们停止的容器保存为镜像,你需要执行类似以下的操作:
$ docker container commit <container_name> <REPOSITORY>:<TAG>
例如,我运行了以下命令来保存我们启动并定制的容器副本:
$ docker container commit alpine-test local:broken-container
注意到我将我的镜像命名为broken-container了吗?因为采取这种方法的一个使用场景是,如果由于某种原因,你的容器出现问题,那么将失败的容器保存为镜像,甚至将其导出为TAR文件,以便与他人共享,帮助你找出问题的根本原因,这非常有用。
要保存镜像文件,只需运行以下命令:
$ docker image save -o <name_of_file.tar> <REPOSITORY>:<TAG>
所以,在我们的示例中,我运行了以下命令:
$ docker image save -o broken-container.tar local:broken-
container
这给了我一个 7.9 MB 的文件,名为broken-container.tar。既然我们有了这个文件,我们可以解压它并查看其结构。它将具有以下结构:

图 2.9 – JSON 文件、文件夹和 TAR 文件的集合
该镜像由一组 JSON 文件、文件夹和其他 TAR 文件组成。所有镜像都遵循这种结构,所以你可能会在想,为什么这种方法如此糟糕?
最大的原因是信任(正如我们之前提到的)。你的最终用户将无法轻松地查看他们正在运行的镜像中包含什么内容。你会从一个未知来源随机下载一个预打包的镜像来运行你的工作负载,而不检查该镜像是如何构建的吗?谁知道它是如何配置的,以及安装了哪些软件包!
使用 Dockerfile,你可以清楚地看到创建镜像时执行了哪些操作,但按照这里描述的方法,你根本无法看到这些内容。
另一个原因是很难为你构建一套良好的默认设置。例如,如果你按这种方式构建镜像,那么你将无法真正利用诸如ENTRYPOINT和CMD这样的功能,甚至是最基本的指令,比如EXPOSE。相反,用户在运行docker container run命令时必须定义所有所需的内容。
在 Docker 的早期,将以这种方式准备的镜像进行分发是常见做法。事实上,我自己也犯过这个错误,因为我来自运维背景,启动一个机器,引导它,然后创建一个金牌主镜像,这在当时是非常有意义的。幸运的是,过去几年,Docker 扩展了构建功能,现在这种选项已经不再被考虑了。
使用 scratch 作为基础
到目前为止,我们一直在使用来自 Docker Hub 的准备好镜像作为我们的基础镜像。然而,最好完全避免这种做法(有点),而是从头开始构建自己的镜像。
现在,当你通常听到“从零开始”这个短语时,它的字面意思是你从无开始。我们现在就是这种情况——你得到的完全是什么都没有,必须在此基础上构建。这可能是一个好处,因为它会保持镜像文件非常小,但如果你对 Docker 比较陌生,这可能会变得很复杂。
Docker 已经为我们做了一些艰苦的工作,并在 Docker Hub 上创建了一个名为scratch的空TAR文件;你可以在 Dockerfile 的FROM部分使用它。你可以以此为基础进行整个 Docker 构建,然后根据需要添加各个部分。
再次说明,我们将使用 Alpine Linux 作为镜像的基础操作系统。这样做的原因不仅是它作为 ISO、Docker 镜像和各种虚拟机镜像发布,而且整个操作系统都可以作为压缩的TAR文件提供。你可以在本书的 GitHub 仓库中找到下载,或者在 Alpine Linux 的下载页面找到。
若要下载副本,只需从下载页面选择合适的下载,下载页面地址为www.alpinelinux.org/downloads/。我使用的是MINI ROOT FILESYSTEM部分的x86_64版本。
一旦下载完成,你需要创建一个使用scratch的 Dockerfile,然后添加tar.gz文件,确保使用正确的文件,如下所示:
FROM scratch
ADD files/alpine-minirootfs-3.11.3-x86_64.tar.gz /
CMD [“/bin/sh”]
你可能在想,为什么我刚刚下载了alpine-minirootfs-3.11.3-x86_64.tar.gz文件?难道我不应该使用http://dl-cdn.alpinelinux.org/alpine/v3.11/releases/x86_64/alpine-minirootfs-3.11.3-x86_64.tar.gz吗?
记住,远程归档文件被视为文件并只是被下载。通常,这不是问题,因为我们可以通过添加一个RUN命令来解压文件,但由于我们使用的是scratch,操作系统尚未安装,这意味着没有可供RUN执行任何操作的命令。
现在我们有了 Dockerfile,我们可以像构建任何其他 Docker 镜像一样构建我们的镜像——通过运行以下命令:
$ docker image build --tag local:fromscratch .
这应该会给你以下输出:

图 2.10 – 从零开始构建
你可以通过运行以下命令,将我们构建的镜像大小与其他容器镜像进行对比:
$ docker image ls
如下截图所示,我构建的镜像与我们一直在 Docker Hub 上使用的 Alpine Linux 镜像大小完全相同:

图 2.11 – 审查镜像大小
现在我们自己的镜像已经构建完成,我们可以通过运行以下命令来测试它:
$ docker container run -it --name alpine-test local:fromscratch
/bin/sh
如果你遇到错误,可能已经有一个名为alpine-test的容器正在运行或已创建。通过运行docker container stop alpine-test,然后运行docker container rm alpine-test来删除它。
这应该会让我们进入 Alpine Linux 镜像的 Shell。你可以通过运行以下命令来检查:
$ cat /etc/*release
这将显示容器运行的版本信息。为了了解整个过程的样子,请参见以下终端输出:

图 2.12 – 从零开始运行镜像
尽管一切看起来很简单,但这仅仅是因为 Alpine Linux 以其独特的方式打包操作系统。当你选择使用其他以不同方式打包操作系统的发行版时,情况可能会变得更加复杂。
有几种工具可以用来生成操作系统的捆绑包。我们在这里不会详细讨论如何使用这些工具,因为如果你需要考虑这种方法,你可能有一些非常具体的要求。你可以在本章末尾的进一步阅读部分查看工具的列表,了解更多细节。
那么,这些要求可能是什么呢?对大多数人来说,它们是遗留应用程序;例如,如果你有一个要求操作系统的应用程序,而这个操作系统不再受支持或无法从 Docker Hub 获取,但你需要一个更现代的平台来支持这个应用程序,那么会发生什么呢?好吧,你应该能够构建自己的镜像并在那里安装应用程序,从而使你能够在现代且可支持的操作系统/架构上托管你的旧遗留应用程序。
使用 ENVs
在本节中,我们将介绍一组非常强大的变量,称为 ENVs (ENVs),因为你将会看到它们很多。你可以在 Dockerfile 中使用 ENVs 做很多事情。如果你熟悉编程,它们可能对你来说并不陌生。
对于像我这样的人,起初,它们看起来令人畏惧,但不要气馁。一旦你掌握了它们,它们将成为一个很好的资源。它们可以在运行容器时设置信息,这意味着你不必更新 Dockerfile 中的很多命令或你在服务器上运行的脚本。
要在 Dockerfile 中使用 ENVs,你可以使用 ENV 指令。ENV 指令的结构如下:
ENV <key> <value>
ENV username admin
或者,你也可以在键和值之间放置等号:
ENV <key>=<value>
ENV username=admin
现在,问题是,为什么有两种定义它们的方式,它们之间有什么区别?
-
在第一个示例中,你每行只能设置一个 ENV;然而,它易于阅读和跟随。
-
在第二个 ENV 示例中,你可以在同一行上设置多个环境变量,如下所示:
ENV username=admin database=wordpress tableprefix=wp
你可以使用 docker inspect 命令查看在镜像中设置了哪些 ENVs:
$ docker image inspect <IMAGE_ID>
现在我们知道如何在 Dockerfile 中设置它们了,让我们来看看它们的实际应用。到目前为止,我们一直在使用 Dockerfile 构建一个只安装了 NGINX 的简单镜像。现在,让我们来看看如何构建一些更具动态性的内容。
使用 Alpine Linux,我们将执行以下操作:
-
设置一个 ENV 来定义我们希望安装的 PHP 版本。
-
安装 Apache2 和我们选择的 PHP 版本。
-
设置镜像,使 Apache2 能够正常启动。
-
移除默认的
index.html文件,并添加一个index.php文件,显示phpinfo命令的结果。 -
在容器上暴露端口 80。
-
设置 Apache,使其成为默认进程:
信息
请注意,PHP5 已经不再受到支持。因此,我们不得不使用较旧版本的 Alpine Linux(3.8),因为这是最后一个支持 PHP5 包的版本。
FROM alpine:3.8
LABEL maintainer=”Russ McKendrick <russ@mckendrick.io>”
LABEL description=”This example Dockerfile installs Apache & PHP.”
ENV PHPVERSION 7
RUN apk add --update apache2 php${PHPVERSION}-apache2 php${PHPVERSION} && \
rm -rf /var/cache/apk/* && \
mkdir /run/apache2/ && \
rm -rf /var/www/localhost/htdocs/index.html && \
echo “<?php phpinfo(); ?>” > /var/www/localhost/htdocs/index.php && \
chmod 755 /var/www/localhost/htdocs/index.php
EXPOSE 80/tcp
ENTRYPOINT [“httpd”]
CMD [“-D”, “FOREGROUND”]
如你所见,我们选择了安装 PHP7;我们可以通过运行以下命令构建镜像:
$ docker build --tag local/apache-php:7 .
注意我们稍微修改了命令。这次,我们调用的是local/apache-php镜像,并将版本标记为7。通过运行之前的命令得到的完整输出如下:
Sending build context to Docker daemon 2.56kB
Step 1/8 : FROM alpine:3.8
---> c8bccc0af957
Step 2/8 : LABEL maintainer=”Russ McKendrick <russ@mckendrick.
io>”
---> Running in 7746dd8cabd0
Removing intermediate container 7746dd8cabd0
---> 9173a415ed21
Step 3/8 : LABEL description=”This example Dockerfile installs Apache & PHP.”
---> Running in 322e98b9c2e0
Removing intermediate container 322e98b9c2e0
---> aefb9450e664
Step 4/8 : ENV PHPVERSION 7
从前面的输出可以看到,PHPVERSION ENV 已经设置为数字7:
---> Running in 0b9e9a5d8956
Removing intermediate container 0b9e9a5d8956
---> 219afdf8eeb8
Step 5/8 : RUN apk add --update apache2 php${PHPVERSION}-
apache2 php${PHPVERSION} && rm -rf /var/cache/apk/* &&
mkdir /run/apache2/ && rm -rf /var/www/
localhost/htdocs/index.html && echo “<?php phpinfo();
?>” > /var/www/localhost/htdocs/index.php && chmod 755
/var/www/localhost/htdocs/index.php
---> Running in 36823df46b29
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/main/x86_64/
APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/community/
x86_64/APKINDEX.tar.gz
(1/14) Installing libuuid (2.32-r0)
(2/14) Installing apr (1.6.3-r1)
(3/14) Installing expat (2.2.8-r0)
(4/14) Installing apr-util (1.6.1-r3)
(5/14) Installing pcre (8.42-r0)
(6/14) Installing apache2 (2.4.41-r0)
Executing apache2-2.4.41-r0.pre-install
到目前为止,我们只提到过 ENV。如下所示,必要的php7包将开始安装:
(7/14) Installing php7-common (7.2.26-r0)
(8/14) Installing ncurses-terminfo-base (6.1_p20180818-r1)
(9/14) Installing ncurses-terminfo (6.1_p20180818-r1)
(10/14) Installing ncurses-libs (6.1_p20180818-r1)
(11/14) Installing libedit (20170329.3.1-r3)
(12/14) Installing libxml2 (2.9.8-r2)
(13/14) Installing php7 (7.2.26-r0)
(14/14) Installing php7-apache2 (7.2.26-r0)
Executing busybox-1.28.4-r3.trigger
OK: 26 MiB in 27 packages
Removing intermediate container 36823df46b29
现在所有包已安装,构建可以进行一些清理工作,然后完成:
---> 842eebf1d363
Step 6/8 : EXPOSE 80/tcp
---> Running in 40494d3b357f
Removing intermediate container 40494d3b357f
---> 074e10ff8526
Step 7/8 : ENTRYPOINT [“httpd”]
---> Running in a56700cae985
Removing intermediate container a56700cae985
---> 25b63b51f243
Step 8/8 : CMD [“-D”, “FOREGROUND”]
---> Running in d2c478e67c0c
Removing intermediate container d2c478e67c0c
---> 966dcf5cafdf
Successfully built 966dcf5cafdf
Successfully tagged local/apache-php:7
我们可以通过运行以下命令来检查是否一切按预期运行,启动一个使用该镜像的容器:
$ docker container run -d -p 8080:80 --name apache-php7 local/
apache-php:7
一旦启动,打开浏览器并访问http://localhost:8080/。你应该看到一页显示正在使用 PHP7:

图 2.13 – 检查 PHP 版本
提示
不要被接下来的部分搞混了;没有 PHP6。你可以通过以下 RFC 和关于跳过 PHP6 投票的结果了解更多,网址是 https://wiki.php.net/rfc/php6。
现在,在你的 Dockerfile 中,将PHPVERSION从7改为5,然后运行以下命令构建一个新的镜像:
$ docker image build --tag local/apache-php:5 .
从以下终端输出可以看到,大部分输出是相同的,除了正在安装的包:
Sending build context to Docker daemon 2.56kB
Step 1/8 : FROM alpine:3.8
---> c8bccc0af957
Step 2/8 : LABEL maintainer=”Russ McKendrick <russ@mckendrick.
io>”
---> Using cache
---> 9173a415ed21
Step 3/8 : LABEL description=”This example Dockerfile installs
Apache & PHP.”
---> Using cache
---> aefb9450e664
Step 4/8 : ENV PHPVERSION 5
在这里,我们可以看到5已被设置为PHPVERSION ENV 的值。从这里开始,构建将像之前一样继续:
---> Running in d6e8dc8b70ce
Removing intermediate container d6e8dc8b70ce
---> 71896c898e35
Step 5/8 : RUN apk add --update apache2 php${PHPVERSION}-
apache2 php${PHPVERSION} && rm -rf /var/cache/apk/* &&
mkdir /run/apache2/ && rm -rf /var/www/
localhost/htdocs/index.html && echo “<?php phpinfo();
?>” > /var/www/localhost/htdocs/index.php && chmod 755
/var/www/localhost/htdocs/index.php
---> Running in fb946c0684e4
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/main/x86_64/
APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/community/
x86_64/APKINDEX.tar.gz
(1/15) Installing libuuid (2.32-r0)
(2/15) Installing apr (1.6.3-r1)
(3/15) Installing expat (2.2.8-r0)
(4/15) Installing apr-util (1.6.1-r3)
(5/15) Installing pcre (8.42-r0)
(6/15) Installing apache2 (2.4.41-r0)
Executing apache2-2.4.41-r0.pre-install
这里是安装 PHP5 包的地方。这是我们两个构建之间唯一的区别:
(7/15) Installing php5-common (5.6.40-r0)
(8/15) Installing ncurses-terminfo-base (6.1_p20180818-r1)
(9/15) Installing ncurses-terminfo (6.1_p20180818-r1)
(10/15) Installing ncurses-libs (6.1_p20180818-r1)
(11/15) Installing readline (7.0.003-r0)
(12/15) Installing libxml2 (2.9.8-r2)
(13/15) Installing php5-cli (5.6.40-r0)
(14/15) Installing php5 (5.6.40-r0)
(15/15) Installing php5-apache2 (5.6.40-r0)
Executing busybox-1.28.4-r3.trigger
OK: 48 MiB in 28 packages
Removing intermediate container fb946c0684e4
同样,既然包已经安装,构建将像之前一样继续,直到我们得到完整的镜像:
---> 54cbb6ef4724
Step 6/8 : EXPOSE 80/tcp
---> Running in 59776669f08a
Removing intermediate container 59776669f08a
---> e34c5c34658d
Step 7/8 : ENTRYPOINT [“httpd”]
---> Running in 037ecfed197c
Removing intermediate container 037ecfed197c
---> c50bdf3e4b02
Step 8/8 : CMD [“-D”, “FOREGROUND”]
---> Running in 9eccc9131ef9
Removing intermediate container 9eccc9131ef9
---> 7471b75e789e
Successfully built 7471b75e789e
Successfully tagged local/apache-php:5
我们可以通过运行以下命令启动一个容器,这次使用 9090 端口:
$ docker container run -d -p 9090:80 --name apache-php5 local/apache-php:5
再次打开浏览器,但这次访问http://localhost:9090/,应该会显示我们正在运行 PHP5:

图 2.14 – 运行 PHP5
最后,你可以通过运行以下命令来比较镜像的大小:
$ docker image ls
你应该看到以下终端输出:

图 2.15 – 比较镜像大小
这显示 PHP7 镜像比 PHP5 镜像要小得多。我们来讨论一下在构建这两个不同的容器镜像时到底发生了什么。
那么,发生了什么呢?实际上,当 Docker 启动 Alpine Linux 镜像以创建我们的镜像时,首先做的就是设置我们定义的 ENVs,使其对容器内的所有 shell 可用。
幸运的是,Alpine Linux 中 PHP 的命名方案只需替换版本号,并保持我们需要安装的包名不变,这意味着我们运行以下命令:
RUN apk add --update apache2 php${PHPVERSION}-apache2 php${PHPVERSION}
但它实际上会被解释为以下内容:
RUN apk add --update apache2 php7-apache2 php7
对于 PHP5,它会被解释为以下内容:
RUN apk add --update apache2 php5-apache2 php5
这意味着我们不需要逐个查看整个 Dockerfile,手动替换版本号。这种方法在从远程 URL 安装软件包时尤其有用,比如软件发布页面。
以下是一个更高级的示例:一个安装和配置由 HashiCorp 提供的 Consul 的 Dockerfile。在这个 Dockerfile 中,我们使用环境变量(ENVs)来定义版本号和我们下载的文件的 SHA256 哈希值:
FROM alpine:latest
LABEL maintainer=”Russ McKendrick <russ@mckendrick.io>”
LABEL description=”An image with the latest version on Consul.”
ENV CONSUL_VERSION 1.7.1
ENV CONSUL_SHA256 09f3583c6cd7b1f748c0c012ce9b3d96de95
a6c0d2334327b74f7d72b1fa5054
RUN apk add --update ca-certificates wget && \
wget -O consul.zip https://releases.hashicorp.com/consul/
${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip && \
echo “$CONSUL_SHA256 *consul.zip” | sha256sum -c - && \
unzip consul.zip && \
mv consul /bin/ && \
rm -rf consul.zip && \
rm -rf /tmp/* /var/cache/apk/*
EXPOSE 8300 8301 8301/udp 8302 8302/udp 8400 8500 8600 8600/udp
VOLUME [ “/data” ]
ENTRYPOINT [ “/bin/consul” ]
CMD [ “agent”, “-data-dir”, “/data”, “-server”, “
-bootstrap-expect”, “1”, “-client=0.0.0.0”]
如你所见,Dockerfile 可能会变得非常复杂,使用环境变量(ENVs)有助于维护。每当 Consul 发布新版本时,我只需更新 ENV 行并提交到 GitHub,这会触发构建一个新镜像。如果我们配置好了,它本应如此。我们将在下一章中查看这一点。
你可能已经注意到我们在 Dockerfile 中使用了一个我们尚未讨论过的指令。别担心——我们将在第四章,管理容器 中查看 VOLUME 指令。
使用多阶段构建
在本节中,作为我们使用 Dockerfile 和构建容器镜像旅程的最后一部分,我们将研究一种相对较新的构建镜像的方法。在之前的章节中,我们介绍了通过包管理器(如 Alpine Linux 的 APK)直接将二进制文件添加到镜像中,或者在之前的示例中,通过从软件供应商下载预编译的二进制文件。
如果我们想在构建过程中编译自己的软件呢?历史上,我们不得不使用一个包含完整构建环境的容器镜像,这可能非常大。这意味着我们很可能需要拼凑出一个脚本,按照类似以下的流程运行:
-
下载构建环境容器镜像并启动一个
build容器。 -
将源代码复制到
build容器中。 -
在
build容器上编译源代码。 -
将编译后的二进制文件从
build容器中复制到外部。 -
删除
build容器。 -
使用预编写的 Dockerfile 来构建一个镜像,并将二进制文件复制到其中。
这有很多逻辑——在理想的世界里,它应该是 Docker 的一部分。幸运的是,Docker 社区也这么认为,名为多阶段构建的功能在 Docker 17.05 中被引入。
Dockerfile 包含两个不同的构建阶段:
- 第一个阶段,名为
builder,使用的是 Docker Hub 上的官方 Go 容器镜像。在这里,我们安装了一个先决条件,从 GitHub 直接下载源代码,然后将其编译成静态二进制文件:
FROM golang:latest as builder
WORKDIR /go-http-hello-world/
RUN go get -d -v golang.org/x/net/html
ADD https://raw.githubusercontent.com/geetarista/go-http-hello-world/master/hello_world/hello_world.go ./hello_world.go
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o
app .
FROM scratch
COPY --from=builder /go-http-hello-world/app .
CMD [“./app”]
提示
注意这里我们使用ADD与一个 URL,因为我们想下载源代码的未压缩版本,而不是一个压缩的存档。
由于我们的静态二进制文件具有内置的 Web 服务器,从操作系统的角度来看,我们实际上不需要任何其他东西。因此,我们能够将scratch用作基础镜像,这意味着我们的整个镜像只包含我们从构建镜像复制的静态二进制文件,并且完全不包含任何builder环境。
要构建镜像,我们只需要运行以下命令:
$ docker image build --tag local:go-hello-world .
上述命令的输出可在以下代码块中找到。有趣的部分发生在步骤 5和步骤 6之间:
Sending build context to Docker daemon 2.048kB
Step 1/8 : FROM golang:latest as builder
latest: Pulling from library/golang
50e431f79093: Pull complete
dd8c6d374ea5: Pull complete
c85513200d84: Pull complete
55769680e827: Pull complete
15357f5e50c4: Pull complete
9edb2e455d9d: Pull complete
ed2acfe844ed: Pull complete
Digest: sha256:d27017d27f9c9a58b361aa36126a29587ffd3b1b274af0
d583fe4954365a4a59
Status: Downloaded newer image for golang:latest
---> 25c4671a1478
现在已经拉取了构建环境容器镜像,我们可以准备环境来构建我们的代码:
Step 2/8 : WORKDIR /go-http-hello-world/
---> Running in 9c23e012e016
Removing intermediate container 9c23e012e016
---> ea0d7e26799e
Step 3/8 : RUN go get -d -v golang.org/x/net/html
---> Running in 17f173992763
get “golang.org/x/net/html”: found meta tag get.metaImport
{Prefix:”golang.org/x/net”, VCS:”git”,
RepoRoot:”https://go.googlesource.com/net”} at
//golang.org/x/net/html?go-get=1
get “golang.org/x/net/html”: verifying non-authoritative meta
tag
golang.org/x/net (download)
Removing intermediate container 17f173992763
环境准备就绪后,我们可以从 GitHub 下载源代码并进行编译:
---> 68f07e01b0cf
Step 4/8 : ADD https://raw.githubusercontent.com/geetarista/go-http-hello-world/master/hello_world/hello_world.go ./hello_world.go
Downloading 393B
---> 4fb92adacdb0
Step 5/8 : RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
---> Running in 61a82b417f60
Removing intermediate container 61a82b417f60
---> 502d219e6869
现在,我们已经将我们的编译代码作为单个可执行二进制文件,这意味着我们可以使用scratch创建一个新的构建镜像,并将二进制文件从先前的构建镜像复制到新的构建镜像中:
Step 6/8 : FROM scratch
--->
Step 7/8 : COPY --from=builder /go-http-hello-world/app .
---> 2b4a6e6066e5
Step 8/8 : CMD [“./app”]
---> Running in c82089ea8a6b
Removing intermediate container c82089ea8a6b
---> 5e196ed7f699
Successfully built 5e196ed7f699
Successfully tagged local:go-hello-world
如您所见,我们的二进制文件已经被编译,并且包含构建环境的容器已被删除,留下了存储我们二进制文件的镜像。如果您运行以下命令,您将了解为什么不将应用程序与其构建环境一起发布是一个好主意:
$ docker image ls
下面的输出显示,golang镜像大小为809MB;加上我们的源代码和先决条件后,大小增加到862MB:

图 2.16 – 检查图像大小
但最终的镜像大小仅为7.41MB。我相信您会同意这节省了相当多的空间。它还遵循最佳实践,只将与我们应用程序相关的内容包含在内,并且非常非常小。
您可以通过使用以下命令启动一个容器来测试应用程序:
$ docker container run -d -p 8000:80 --name go-hello-world
local:go-hello-world
应用程序可通过浏览器访问,并在每次加载页面时简单地递增计数器。要在 macOS 和 Linux 上测试它,您可以使用curl命令,如下所示:
$ curl http://localhost:8000/
这应该会给您类似以下的结果:

图 2.17 – 运行容器并使用 curl 调用页面
Windows 用户只需在浏览器中访问http://localhost:8000/。要停止并删除运行中的容器,请使用以下命令:
$ docker container stop go-hello-world
$ docker container rm go-hello-world
如您所见,使用多阶段构建是一个相对简单的过程,并且符合您应该已经开始感到熟悉的指令。
摘要
在本章中,我们看了 Dockerfiles,我相信您会同意它们是定义您自己的 Docker 镜像的一种简单直接的方式。
当我们深入探讨完 Dockerfile 后,我们接着看了构建镜像的五种方法。我们首先了解了使用 Dockerfile,因为这是构建镜像最常见的方式,并且我们将在本书的其余部分中使用它。
然后我们讨论了使用现有容器,因为在 Docker 刚出现时,这就是大多数人构建镜像的方式。现在它不再被视为最佳实践,只有在你需要为调试目的创建一个正在运行或崩溃容器的快照时,才应该使用。
接下来我们讨论了使用 scratch 作为基础。这可能是创建镜像的最简化方式,因为你实际上是从零开始。
我们接着讨论了使用环境变量。在这里,我们查看了如何将版本号等变量引入 Dockerfile,这样我们就不需要在多个地方更新文件。
最后,我们讨论了scratch容器,提供了可能最小的可用镜像。
在下一章中,现在我们知道如何使用 Dockerfile 构建镜像,我们将深入了解 Docker Hub 以及使用注册表服务的所有优点。
我们还将了解 Docker 注册表,它是开源的,这样你就可以创建和配置自己的存储镜像的地方,以及第三方托管的注册表服务,所有这些都可以用来分发你自己的容器镜像。
问题
-
判断对错:
LABEL指令在镜像构建完成后标记你的镜像。 -
ENTRYPOINT和CMD指令有什么区别? -
判断对错:使用
ADD指令时,不能下载并自动解压外部托管的压缩包。 -
使用现有容器作为镜像基础的有效用途是什么?
-
EXPOSE指令暴露什么?
进一步阅读
-
官方 Docker 容器镜像的指南可以在
github.com/docker-library/official-images/找到。 -
一些帮助你从现有安装创建容器的工具如下:
debootstrap:
wiki.debian.org/Debootstrapyumbootstrap:
dozzie.jarowit.net/trac/wiki/yumbootstraprinse:
packages.debian.org/sid/admin/rinseDocker contrib 脚本:
github.com/moby/moby/tree/master/contrib -
Go HTTP Hello World 应用的完整 GitHub 仓库可以在
github.com/geetarista/go-http-hello-world找到。
第三章:存储与分发镜像
本章将介绍多个服务,如 Docker Hub,它允许你存储镜像,以及 Docker Registry,你可以用来运行本地的 Docker 容器存储。我们将回顾这些服务之间的区别,以及何时和如何使用它们。本章还将介绍如何使用 Web 钩子设置自动构建,以及设置这些自动构建所需的所有组件。
让我们快速浏览一下本章将涵盖的主题:
-
了解 Docker Hub
-
部署你自己的 Docker 注册中心
-
审查第三方注册中心
-
查看 Microbadger
让我们开始吧!
技术要求
在本章中,我们将使用我们的 Docker 安装来构建镜像。如前一章所提,尽管本章中的截图来自我偏好的操作系统——macOS,但我们将运行的命令适用于前几章中涵盖的所有三种操作系统。
查看以下视频,了解代码的实际应用:bit.ly/3iaKo9I
了解 Docker Hub
尽管我们在前两章中介绍了 Docker Hub,但除了使用docker image pull命令下载远程镜像外,我们并没有与它有过太多交互。
本节将重点介绍 Docker Hub,它提供了一个免费选项,允许你只托管公开访问的镜像,还有一个订阅选项,允许你托管自己的私有镜像。我们将重点讲解 Docker Hub 的网页部分以及你可以在那里进行的管理操作。
主页可以在hub.docker.com找到,其中包含一个注册表单,并且在右上角有一个登录选项。如果你曾经接触过 Docker,那么你很可能已经拥有一个 Docker ID。如果没有,使用主页上的注册表单来创建一个。如果你已经有 Docker ID,只需点击登录即可。
登录后,你将看到主仪表板。
Docker Hub 仪表板
登录 Docker Hub 后,你将被带到以下着陆页。这个页面被称为 Docker Hub 的仪表板:

图 3.1 – 初始的 Docker Hub 仪表板
从这里,你可以访问 Docker Hub 的所有其他子页面。然而,在查看这些部分之前,我们应该先谈谈仪表板。从这里,你可以查看所有的镜像,包括公共和私有镜像。它们首先按星标数排序,然后按拉取次数排序;这个顺序无法更改。
在接下来的几节中,我们将逐一介绍你在仪表板上可以看到的所有内容,从页面顶部的浅蓝色菜单开始。
探索
探索选项将带你查看官方 Docker 镜像列表;就像你的仪表盘一样,它们按星级和下载量排序。如下图所示,我选择了基础镜像。每个最受欢迎的官方镜像的下载量都超过了 1000 万,并且拥有成千上万的星级评分:

图 3.2 – 探索 Docker Hub
Docker Hub 现在已整合 Docker Store,让你可以在一个地方购买与 Docker 相关的所有内容,而不是从多个不同的来源访问。
仓库
我们将在本章的 创建自动化构建 部分详细讨论创建仓库的过程,因此此处不再展开细节。在这一部分,你可以管理自己的仓库。如以下截图所示,你可以快速查看有多少人已经开始使用你的仓库,以及你的镜像的下载量,同时也能查看仓库是公开还是私有:

图 3.3 – 我的 Docker 仓库列表
如你所见,这里有一个 创建仓库 按钮。由于我们将在 创建自动化构建 部分更详细地讨论这一过程,接下来我们将进入下一个选项。
组织
组织是你创建或被添加到的组织。组织允许你为多个协作的项目分配权限控制。每个组织都有自己的设置,例如是否默认将仓库存储为公开或私密,或者更改计划以允许不同数量的私有仓库,甚至将仓库与自己或他人拥有的仓库分开:

图 3.4 – 查看我的组织列表
你还可以通过仪表盘中的 Docker 标志下方切换或访问帐户或组织,登录时通常会看到你的用户名:

图 3.5 – 切换组织
在管理多个项目/应用的容器镜像分发时,组织非常有用。
获取帮助
这个下拉菜单是通向 Docker 管理的各种帮助和支持站点的跳板。我们快速看看这些链接将带你到哪里:
-
文档:此项将带你到 Docker Hub 的官方文档页面。
-
Docker Hub:此项将直接带你进入 Docker 社区论坛中的 Docker Hub 分类。
-
新功能:此项将带你查看一系列以“Docker Hub”标签标记的 Docker 博客文章。
-
支持:这是一个关于 Docker Hub 的常见问题解答,并提供联系支持的选项。
个人资料和设置
顶部菜单中的最后一个选项是关于管理您的 个人资料、内容 和 设置:

图 3.6 – 查看您的个人资料
设置页面允许您设置公共个人资料,包括以下选项:
-
常规:您可以将电子邮件地址添加到您的账户、修改密码,并配置在个人资料中显示的信息,如姓名、公司位置和链接。
-
关联账户:在这里,您可以关联您的 GitHub 和 Bitbucket 账户(有关此内容的更多信息将在本章的 创建自动化构建 部分讨论)。
-
安全:在这里,您可以管理个人访问令牌和最近引入的双重身份验证。
-
默认隐私:您希望新创建的仓库默认是公开还是私密的?您可以在这里选择。
-
通知:在这里,您可以注册有关您的构建和账户活动的通知。您可以提供电子邮件地址或连接到 Slack 安装。
-
转换账户:在这里,您可以将账户转换为组织。您可能不希望这样做;在继续执行此选项之前,请阅读页面上的警告。
-
停用账户:这正如您所想的那样,它的作用就是停用账户。再次提醒,在进行任何操作之前,请查看页面上的警告,因为此操作是不可撤销的。
-
我的个人资料:此菜单项将您带到您的公开个人资料页面;我的个人资料可以在
hub.docker.com/u/russmckendrick/找到。 -
我的内容:此链接将带您查看您可能已在 Docker Hub 上订阅的容器列表。
创建自动化构建
在本节中,我们将讨论自动化构建。自动化构建是指您可以将其链接到您的 GitHub 或 Bitbucket 帐户,随着您在代码仓库中更新代码,Docker Hub 会自动构建镜像。我们将查看执行此操作所需的所有组件。到本节结束时,您将能够自动化您的构建。
设置您的代码
创建自动化构建的第一步是设置您的 GitHub 或 Bitbucket 仓库。这是您选择存储代码时的两种选项之一。例如,我将使用 GitHub,但如果您使用的是 GitHub 或 Bitbucket,设置过程是相同的。
实际上,我将使用本书随附的仓库。由于该仓库是公开的,您可以分叉它并使用自己的 GitHub 帐户跟随我所做的操作,正如以下截图所示:

图 3.7 – 分叉随附的 GitHub 仓库
在 第二章,构建容器镜像 中,我们处理了几个不同的 Dockerfile。我们将使用这些文件进行自动构建。你可能还记得,我们安装了 NGINX 并添加了一个简单的页面,页面显示 Hello world! This is being served from Docker。我们也有一个多阶段构建。
现在我们知道要使用哪个 Dockerfile,让我们在 Docker Hub 中设置一个仓库。
设置 Docker Hub
在 Docker Hub 中,我们将使用 创建仓库 按钮,该按钮可以在 Repositories(仓库)下找到。点击后,我们将进入一个页面,需要提供一些关于构建的信息。我们还需要选择一个来源:

图 3.8 – 在 Docker Hub 中创建仓库
如前所示截图所示,我已经将我的 GitHub 帐户与 Docker Hub 帐户关联起来。将这两个工具连接的过程很简单——我只需按照屏幕上的说明,允许 Docker Hub 访问我的 GitHub 帐户。
当连接 Docker Hub 和 GitHub 时,有两个选项:
-
公开:这限制了 Docker Hub 对公开可用的仓库和组织的访问。如果你使用此选项链接帐户,Docker Hub 将无法配置自动构建所需的 Web 钩子。然后,你需要从你想要创建自动构建的任何位置搜索并选择仓库。这实际上会创建一个 Web 钩子,每次对选定的 GitHub 仓库进行提交时触发。这样,就会在 Docker Hub 上创建一个新的构建。
-
私有:这是两个选项中推荐的一个,因为 Docker Hub 将可以访问所有你的公共和私有仓库以及组织。Docker Hub 还能够配置在设置自动构建时所需的 Web 钩子。
在上面的截图中,我选择了 masteringdockerfourthedition,并访问了自动构建的设置页面。在这里,我们可以选择将镜像附加到哪个 Docker Hub 配置文件,命名镜像,将其从公共镜像转换为私有镜像,描述构建,并通过点击 点击这里自定义 来进行自定义。
我们可以通过以下方式告知 Docker Hub 我们 Dockerfile 的位置:

图 3.9 – 这是填写完成后的表单样式
如果你正在跟随操作,我输入了以下信息:
-
dockerfile-example -
测试自动构建 -
Public
然后,在 构建设置 下,选择 GitHub:
-
组织:选择托管你代码的 GitHub 组织。
-
Master-Docker-Fourth-Edition。
点击构建规则旁的 + 图标,然后输入以下内容:
-
Branch -
master -
latest -
Dockerfile -
构建缓存:保持此选项被选中
点击 创建 后,你将进入一个类似于以下的页面:

图 3.10 – 创建的仓库
现在我们定义了构建,我们可以通过点击 构建 来添加一些额外的配置。由于我们在 Dockerfile 中使用了官方的 Alpine Linux 镜像,我们可以将其链接到我们自己的构建中。在进行此操作时,我们还需要配置一个额外的路径。请按照以下步骤操作:
-
点击 配置自动构建 按钮。然后,在配置的 仓库链接 部分,点击 为基础镜像启用 单选框,再点击 保存 按钮。
-
每当发布新的官方 Alpine Linux 镜像时,这将启动一个无人值守的构建。
-
接下来,滚动到
./chapter02/dockerfile-example/。这将确保 Docker 的构建服务器能够找到我们添加到 Dockerfile 中的任何文件:

图 3.11 – 将我们的仓库与 Dockerfile 连接
所以,现在,只要我们更新 GitHub 仓库,或者发布新的官方镜像,我们的镜像就会自动重建并发布。
由于这两者都不太可能立即发生,点击 触发 按钮,在 构建 页面上手动启动构建:

图 3.12 – 触发构建
你会注意到 触发 按钮变成了旋转图标,并且 最新构建状态 变为 待处理,如下图所示。这确认了一个构建已在后台排队:

图 3.13 – 构建正在进行
一旦你触发了构建,滚动到 最近的构建。这将列出该镜像的所有构建——成功的、失败的以及正在进行的。你应该看到一个构建正在进行;点击该构建将显示其日志:

图 3.14 – 查看构建进度
一旦构建完成,你应该能够通过运行以下命令切换到本地的 Docker 安装,确保如果你一直在跟随教程,请拉取你自己的镜像:
$ docker image pull masteringdockerfouthedition/dockerfile-example:latest
$ docker image ls
这些命令显示在下图中:

图 3.15 – 拉取我们新构建的镜像
你也可以使用以下命令运行 Docker Hub 创建的镜像,再次确保如果你有自己的镜像,请使用你自己的镜像:
$ docker container run -d -p8080:80 --name example masteringdockerthirdedition/dockerfiles-example
我还以完全相同的方式添加了多阶段构建。Docker Hub 对构建没有问题,如以下日志所示,日志开始提供有关 Docker 构建环境的一些信息:
Cloning into '.'...
Warning: Permanently added the RSA host key for IP address '140.82.114.3' to the list of known hosts.
Reset branch 'master'
Your branch is up-to-date with 'origin/master'.
KernelVersion: 4.4.0-1060-aws
Components: [{u'Version': u'18.03.1-ee-3', u'Name': u'Engine', u'Details': {u'KernelVersion': u'4.4.0-1060-aws', u'Os': u'linux', u'BuildTime': u'2018-08-30T18:42:30.000000000+00:00', u'ApiVersion': u'1.37', u'MinAPIVersion': u'1.12', u'GitCommit': u'b9a5c95', u'Arch': u'amd64', u'Experimental': u'false', u'GoVersion': u'go1.10.2'}}]
Arch: amd64
BuildTime: 2018-08-30T18:42:30.000000000+00:00
ApiVersion: 1.37
Platform: {u'Name': u''}
Version: 18.03.1-ee-3
MinAPIVersion: 1.12
GitCommit: b9a5c95
Os: linux
GoVersion: go1.10.2
然后,构建开始编译我们的代码,如下所示:
Starting build of index.docker.io/masteringdockerfouthedition/multi-stage:latest...
Step 1/8 : FROM golang:latest as builder
---> 374d57ff6662
Step 2/8 : WORKDIR /go-http-hello-world/
Removing intermediate container 63fc21e72f2b
---> 25ed949838cf
Step 3/8 : RUN go get -d -v golang.org/x/net/html
---> Running in 57072354b296
get "golang.org/x/net/html": found meta tag get.metaImport{Prefix:"golang.org/x/net", VCS:"git", RepoRoot:"https://go.googlesource.com/net"} at //golang.org/x/net/html?go-get=1
get "golang.org/x/net/html": verifying non-authoritative meta tag
golang.org/x/net (download)
Removing intermediate container 57072354b296
---> 6731fc3ade79
Step 4/8 : ADD https://raw.githubusercontent.com/geetarista/go-http-hello-world/master/hello_world/hello_world.go ./hello_world.go
---> 2129f7e7cbab
Step 5/8 : RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
---> Running in 9d5646bf1b92
Removing intermediate container 9d5646bf1b92
---> 997b92d1a701
现在我们的代码已经编译完成,它将继续将应用程序二进制文件复制到最终镜像中,使用的是 scratch:
Step 6/8 : FROM scratch
--->
Step 7/8 : COPY --from=builder /go-http-hello-world/app .
---> 70eb0af7f82c
Step 8/8 : CMD ["./app"]
---> Running in 41cc8b47f714
Removing intermediate container 41cc8b47f714
---> 71fc328a30c4
Successfully built 71fc328a30c4
Successfully tagged masteringdockerfouthedition/multi-stage:latest
Pushing index.docker.io/masteringdockerfouthedition/multi-stage:latest...
Done!
Build finished
You can pull and launch a container using the image with the following commands:
$ docker image pull masteringdockerfouthedition/multi-stage
$ docker image ls
$ docker container run -d -p 8000:80 --name go-hello-world masteringdockerfouthedition/multi-stage
$ curl http://localhost:8000/
如下截图所示,镜像的行为与我们在本地创建时完全相同:

图 3.16 – 拉取我们的多阶段环境并启动构建
如果你启动了容器,可以使用以下命令将其删除:
$ docker container stop example go-hello-world
$ docker container rm example go-hello-world
现在我们已经了解了自动构建,接下来我们将讨论其他将镜像推送到 Docker Hub 的方法。
推送你自己的镜像
在 第二章 构建容器镜像 中,我们讨论了如何在不使用 Dockerfile 的情况下创建镜像。虽然这仍然不是一个好主意,应该仅在真正需要时进行,但你可以将自己的镜像推送到 Docker Hub。
小贴士
在以这种方式将镜像推送到 Docker Hub 时,请确保不要包含任何不希望公开访问的代码、文件或环境变量。
为此,我们首先需要通过运行以下命令将本地 Docker 客户端与 Docker Hub 链接起来:
$ docker login
然后,你将被提示输入你的 Docker ID 和密码。然而,如果你启用了多因素身份验证,那么你需要使用个人访问令牌而不是密码。要创建个人访问令牌,请转到 Docker Hub 中的 设置,点击左侧菜单中的 安全性,然后点击 新建访问令牌 按钮。根据屏幕上的说明,访问令牌只会显示一次,所以请确保记下它。将个人访问令牌视为密码的替代品,并妥善存储:

图 3.17 – 使用 Docker 客户端登录 Docker Hub
此外,如果你正在使用 Docker for Mac 或 Docker for Windows,现在会通过应用程序登录,并且应该可以从菜单中访问 Docker Hub:

图 3.18 – 在 Docker Desktop 中查看你的 Docker Hub 详情
现在我们的客户已被授权与 Docker Hub 进行交互,我们需要一个镜像来进行构建。
让我们来看一下推送我们在 第二章 中构建的 scratch 镜像,构建容器镜像。首先,我们需要构建镜像。为此,我使用以下命令:
$ docker build --tag masteringdockerfouthedition/scratch-example:latest .
如果你跟随此步骤进行操作,请将 masteringdockerfouthedition 替换为你自己的用户名或组织:

图 3.19 – 本地构建镜像
一旦镜像构建完成,我们可以通过运行以下命令将其推送到 Docker Hub:
$ docker image push masteringdockerfouthedition/scratch-example:latest
以下截图显示了输出结果:

图 3.20 – 推送镜像到 Docker Hub
如你所见,由于我们在构建镜像时定义了 masteringdockerfouthedition/scratchexample:latest,Docker 会自动将镜像上传到该位置,从而将一个新镜像添加到 masteringdockerfouthedition 组织中:

图 3.21 – 在 Docker Hub 中查看我们本地构建的镜像
你会注意到,在 Docker Hub 中的构建功能有限。这是因为镜像并不是由 Docker Hub 构建的,因此,它并不真正知道构建镜像时包含了哪些内容,这正是为什么这种分发镜像的方法不被推荐的原因。
现在我们已经讨论了如何分发镜像,接下来我们来看完全相反的情况,讨论认证镜像和发布者。
Docker 认证镜像和验证发布者
你可能还记得,在第一章中,Docker 概览,我们下载了 Docker for macOS 和 Docker for Windows,以及 Docker Hub。除了作为一个单一位置来下载各种平台的 Docker CE 和 Docker EE 版本外,它现在也是查找 Docker 插件、Docker 认证镜像和来自验证发布者的镜像的首选位置:

图 3.22 – 在 Docker Hub 中探索 Docker 认证镜像
仔细查看 Docker Hub 中的Splunk Enterprise镜像,你可以获取关于镜像的责任人信息。它还显示这是一个认证镜像,如下图所示:

图 3.23 – 查看 Splunk Enterprise Docker Hub 镜像
正如你可能注意到的,镜像上有一个价格(免费版为 $0.00,但有一些限制),这意味着你可以通过 Docker Hub 购买商业软件,因为它内置了支付和许可功能。如果你是软件发布者,你可以通过 Docker Hub 签署并分发你自己的软件。
部署你自己的 Docker 注册表
在本节中,我们将介绍 Docker 注册表。Docker 注册表是一个开源应用程序,你可以在任何地方运行它并存储你的 Docker 镜像。我们将提供 Docker 注册表与 Docker Hub 的比较,以及如何在两者之间进行选择。
到本节结束时,你将学习如何运行自己的 Docker 注册表,并检查它是否适合你。
Docker 注册表概述
如前所述,Docker 注册表是一个开源应用程序,你可以用它在你选择的平台上存储你的 Docker 镜像。这让你可以选择将它们保持 100% 私密,或者根据需要共享它们。
如果您想部署自己的 Registry,而不需要支付 Docker Hub 的所有私人功能,那么 Docker Registry 是非常有意义的。让我们来看一些 Docker Hub 和 Docker Registry 之间的对比,帮助您做出明智的决策,选择适合您的镜像存储方式。
Docker Registry 具有以下功能:
-
您可以托管并管理自己的 Registry,从中可以提供所有仓库作为私有、公共或两者的混合。
-
根据您托管的镜像数量或服务的拉取请求数量,您可以按需扩展 Registry。
-
一切都基于命令行。
使用 Docker Hub,您将能够执行以下操作:
-
获取一个基于 GUI 的界面,您可以用来管理您的镜像
-
在云中已经设置好一个位置,准备处理公共和/或私有镜像
-
不用担心管理托管所有镜像的服务器
现在我们知道了部署自己 Registry 和 Docker Hub 之间的区别,让我们来看一下部署自己 Registry 的步骤。
部署您自己的 Registry
正如您可能已经猜到的,Docker Registry 是作为 Docker Hub 上的镜像分发的,这使得部署它变得像运行以下命令一样简单:
$ docker image pull registry:2
$ docker container run -d -p 5000:5000 --name registry registry:2
运行这些命令应该会给您类似以下的终端输出:

图 3.24 – 部署您的 Docker Registry
这些命令将为您提供 Docker Registry 的最基本安装。让我们快速看看如何将镜像推送到它。首先,我们需要一个镜像,所以让我们重新获取 Alpine 镜像:
$ docker image pull alpine
现在我们已经获得了 Alpine Linux 镜像的副本,我们需要将其推送到本地 Docker Registry,地址为localhost:5000。为此,我们需要为 Alpine Linux 镜像打上本地 Docker Registry 的 URL,并且使用不同的镜像名称:
$ docker image tag alpine localhost:5000/localalpine
现在我们已经标记了镜像,可以通过运行以下命令将其推送到本地托管的 Docker Registry:
$ docker image push localhost:5000/localalpine
以下截图展示了之前命令的输出:

图 3.25 – 将镜像推送到您自己的 Docker Registry
尝试运行以下命令:
$ docker image ls
输出应该显示您有两个具有相同IMAGE ID的镜像:

图 3.26 – 列出镜像
在我们从本地 Docker Registry 拉取镜像之前,我们应该删除这两个本地镜像副本。我们需要使用REPOSITORY名称来执行此操作,而不是IMAGE ID,因为我们有两个来自两个位置的镜像,具有相同的 ID,而 Docker 会抛出错误:
$ docker image rm alpine localhost:5000/localalpine
现在原始镜像和已标记镜像已被删除,我们可以通过运行以下命令从本地 Docker Registry 拉取镜像:
$ docker image pull localhost:5000/localalpine
$ docker image ls
如你所见,我们现在有一个从运行在 localhost:5000 的 Docker Registry 中拉取的镜像副本:

图 3.27 – 从你自己的 Docker Registry 拉取
你可以通过运行以下命令停止并移除 Docker Registry:
$ docker container stop registry
$ docker container rm -v registry
现在,在启动 Docker Registry 时有很多选项和考虑因素。正如你所想,最重要的考虑因素是存储。
由于注册中心的唯一目的是存储和分发镜像,因此使用某种级别的持久性操作系统存储非常重要。Docker Registry 当前支持以下存储选项:
-
/var/lib/registry。 -
Azure:这使用 Microsoft Azure Blob 存储。
-
GCS:这使用 Google Cloud 存储。
-
S3:这使用亚马逊简单存储服务 (Amazon S3)。
-
Swift:这是使用 OpenStack Swift。
如你所见,除了文件系统,所有支持的存储引擎都是高度可用、分布式的对象级存储形式。
Docker 受信任注册中心
商业版Docker 企业版 (Docker EE)随附的一个组件是Docker 受信任注册中心 (DTR),这两个组件现在由 Mirantis 开发和支持。可以将它看作是一个你可以在自己基础设施上托管的 Docker Hub 版本。DTR 在免费 Docker Hub 和 Docker Registry 提供的功能基础上,添加了以下特性:
-
集成到你的身份验证服务中,如 Active Directory 或 LDAP
-
在你自己的基础设施(或云)中部署,并置于防火墙之后
-
镜像签名,以确保你的镜像是受信任的
-
内置的安全扫描
-
直接从 Mirantis 获取优先支持
审查第三方注册中心
不仅 Docker 提供镜像注册服务;像 Red Hat 这样的公司也提供自己的注册中心,在那里你可以找到 Red Hat 容器目录,其中包含所有 Red Hat 产品的容器化版本,以及支持其 OpenShift 服务的容器。像 JFrog 的 Artifactory 这样的服务,也将私人 Docker 注册中心作为其构建服务的一部分提供。
还有其他 Registry-as-a-Service 提供商,例如 Quay(也是 Red Hat 提供的),以及来自 GitHub、亚马逊 Web 服务、微软 Azure 和 Google Cloud 的服务。
让我们快速看一下这些服务。
GitHub Packages 和 Actions
我们要查看的第一个服务是 GitHub Packages。在这里,我们将看一下如何将容器上传到我为本书的 GitHub 仓库所做的 fork 中。首先,我们需要一个个人访问令牌。获取这个令牌,请登录到你的 GitHub 账户,进入设置,然后选择开发者设置,接着点击个人访问令牌。
生成一个访问令牌,命名为 cli-package-access,并赋予以下权限:
-
repo -
write:packages -
read:packages -
delete:packages -
workflow
在显示时记下 token,因为你将无法再次查看它。完成此操作后,我将我的 token 存放在用户根目录下的一个名为 .githubpackage 的文件中。将其放在那里意味着我每次登录时都不需要输入密码。我可以通过使用以下命令来实现:
$ cat ~/.githubpackage | docker login docker.pkg.github.com -u russmckendrick --password-stdin
登录后,我们可以构建镜像。对于这个例子,我使用了 dockerfile-example:
$ docker image build --tag docker.pkg.github.com/russmckendrick/mastering-docker-fourth-edition/dockerfile-example:latest .
请注意,我使用的是 mastering-docker-fourth-edition 仓库名称,并且全部采用小写字母。如果你尝试使用任何大写字母,Docker 会报错。
一旦构建并标记,你可以使用以下命令推送你的镜像:
$ docker image push docker.pkg.github.com/russmckendrick/
mastering-docker-fourth-edition/dockerfile-example:latest
推送后,你应该能看到现在仓库中有一个包:

图 3.28 – 查看推送到 GitHub 的包
深入查看该包将显示以下基本统计信息和下载信息:

图 3.29 – 查看更多关于该包的信息
你也可以通过运行以下命令下载镜像:
$ docker image pull docker.pkg.github.com/russmckendrick/
mastering-docker-fourth-edition/dockerfile-example:latest
由于我们的 GitHub 仓库是公开的,我们的包也将是公开的,意味着任何人都可以下载它。
所以,这就涵盖了推送一个现有镜像的过程。然而,正如我们在本章中已经提到过几次的,这其实并不推荐。幸运的是,GitHub 引入了 GitHub Actions,允许你设置自动化工作流,每当发生某个事件(比如推送到仓库)时,就会执行相应的 操作。
要创建 GitHub Action,请进入你的仓库并点击仓库中的 .github/workflows/main.yml。
在提供的空间中输入以下内容:
name: Create Multi Stage Docker Image CI
on: [push]
jobs:
build_docker_image:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- name: Build and tag image
run: docker build -t "docker.pkg.github.com/$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')/multi-stage:$GITHUB_RUN_NUMBER" -f ./chapter02/multi-stage/Dockerfile ./chapter02/multi-stage/
- name: Docker login
run: docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $GITHUB_TOKEN
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Publish to GPR
run: docker push "docker.pkg.github.com/$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')/multi-stage:$GITHUB_RUN_NUMBER"
如你所见,这与我们构建和标记镜像、在 GitHub Packages 进行身份验证以及推送镜像的步骤非常相似。有一些与 GitHub Actions 相关的内容,例如 $GITHUB_REPOSITORY 和 $GITHUB_RUN_NUMBER 变量,以及 ${{secrets.GITHUB_TOKEN}},所有这些都确保我的 Action 中没有硬编码的内容。这意味着你可以在你自己的仓库中安全地运行它。
输入后,点击 Start commit 按钮,输入一些细节,然后点击 Commit new file。一旦提交,工作流将自动开始。你可以通过返回 Actions,然后选择新创建的工作流和任务来查看输出:

图 3.30 – 查看我们的 GitHub Actions 构建结果
完成后,进入仓库中的 Packages,应该能看到一个如下所示的包:

图 3.31 – 检查我们的 GitHub Action 创建的包
如你所见,虽然这与自动化的 Docker Hub 构建几乎相同,但你对发生的事情以及构建过程本身有更多的控制权。
Azure 容器注册表
在我们第三方容器注册表教程的下一个环节中,我们将介绍微软的 Azure 容器注册表。要创建一个,登录到你的 Microsoft Azure 账户。我们将在第十章中更详细地讨论 Microsoft Azure,在公有云中运行 Docker。
登录后,在屏幕顶部的搜索栏中输入 容器注册表,并从结果中选择该选项。加载 容器注册表 页面后,点击 + 添加 按钮。你将看到如下页面:

图 3.32 – 创建我们的 Azure 容器注册表
如果你在跟着做,请输入以下信息:
-
订阅:选择你希望使用的订阅。
-
masteringdocker-acr-rg。 -
masteringdocker。 -
英国南部。 -
启用。 -
基础。这对于测试应该足够了。
我们现在将忽略加密选项,因为它们仅在使用高级 SKU 和标签时可用,所以点击 审核 + 创建。验证部署后,点击 创建 按钮。几分钟后,部署将完成。
与 GitHub Packages 一样,我们将构建并推送一个容器。为此,我们需要一些凭据。要找到这些凭据,点击 访问密钥,并记录下 登录服务器、用户名 和其中一个 密码 的详细信息:

图 3.33 – 获取我们的 Azure 容器注册表的访问密钥
与 GitHub Packages 类似,将密码保存在文本文件中。我使用的是~/.azureacrpassword。然后,通过运行以下命令使用 Docker 命令行客户端进行登录:
$ cat ~/.azureacrpassword | docker login masteringdocker.azurecr.io -u masteringdocker --password-stdin
现在我们已经通过身份验证,切换到 dockerfile-example 文件夹,该文件夹可以在本书 GitHub 仓库的 chapter02 文件夹中找到,然后构建、标记并推送我们的镜像:
$ docker image build --tag masteringdocker.azurecr.io/dockerfile-example:latest .
$ docker image push masteringdocker.azurecr.io/dockerfile-example:latest
推送完成后,你应该能够在主菜单的 服务 部分点击 注册表 来查看它在 Azure 容器注册表页面上的列表。选择镜像和版本。完成此操作后,你将看到类似以下内容:

图 3.34 – 在 Azure 容器注册表中查看我们的容器
当拉取镜像时,你需要确保已经对你的容器注册表进行了身份验证,因为它是一个私有服务。如果没有登录尝试拉取镜像,将会导致错误。
你还可以基于将 Dockerfile 提交到 GitHub 仓库来自动化这些构建。然而,这需要更多的操作,因为目前只能通过 Azure 的命令行工具进行配置。有关如何配置 Azure 容器注册任务的更多信息,请参阅进一步阅读部分。
查看 MicroBadger
MicroBadger 是一个很棒的工具,当你准备发布容器或移动图像时,它会非常有帮助。它会考虑到特定 Docker 图像中每一层的所有情况,并给出关于它的实际大小或它所占磁盘空间的输出结果。
当你访问 MicroBadger 网站 microbadger.com/ 时,将会看到以下页面:

图 3.35 – MicroBadger 首页
你可以在 Docker Hub 上搜索图像,让 MicroBadger 提供该图像的信息。或者,如果你想提供一些示例集,或者查看更复杂的设置,你也可以加载一个示例图像集。
在这个例子中,我们将搜索我的一张图像,russmckendrick/ab,并选择最新的标签。默认情况下,它会始终加载最新的标签,但你也可以通过从版本下拉菜单中选择你想查看的标签来更改标签。如果你有例如一个预发布标签,并考虑将这个新图像推送到你的最新标签,但又想查看它对图像大小的影响,这个功能会非常有用。
如以下截图所示,MicroBadger 展示了你的图像包含多少层的信息:

图 3.36 – 在 MicroBadger 中查看我们的容器详情
通过显示每一层的大小和在图像构建过程中执行的 Dockerfile 命令,你可以看到在图像构建的哪个阶段添加了臃肿,这在减少图像大小时非常有用。
MicroBadger 的另一个伟大功能是,它让你可以将图像的基本统计数据嵌入到你的 GitHub 仓库或 Docker Hub 中。例如,以下截图展示了 russmckendrick/ab 的 Docker Hub 页面:

图 3.37 – 将 MicroBadger 的统计数据添加到图像的 README 文件中
如你所见,MicroBadger 显示了图像的整体大小,在这个例子中是5.1MB,以及图像包含的总层数,这里是7。MicroBadger 服务仍处于测试阶段,新的功能正在不断添加。我建议你关注它的更新。
总结
在本章中,我们研究了使用 Docker Hub 构建容器镜像的几种手动和自动方法。我们讨论了除了 Docker Hub 之外的各种注册中心,例如 GitHub Packages 和微软的 Azure Container Registry。
我们还研究了如何部署我们自己的本地 Docker 注册表,并讨论了部署时需要考虑的存储问题。最后,我们了解了 MicroBadger,这是一项允许我们展示远程托管容器镜像信息的服务。
所有这些意味着你现在有了一个安全分发自己容器镜像的方式,并且这种方式让你可以轻松地保持容器镜像的最新。
这很重要,因为它意味着,如果你愿意,可以通过一次构建触发所有镜像的更新,而不必手动构建和推送每个单独的镜像。
在下一章,我们将研究如何通过命令行管理我们的容器。
问题
-
对错题:Docker Hub 是你唯一可以下载官方 Docker 镜像的来源。
-
说明为什么你想将自动构建与官方 Docker Hub 镜像链接。
-
Docker Hub 支持多阶段构建吗?
-
对错题:在命令行登录 Docker 也会登录到桌面应用程序。
-
如何删除共享相同
IMAGE ID的两个镜像? -
Docker 注册表默认运行在哪个端口?
进一步阅读
关于 Docker Store、受信注册表和注册表的更多信息可以通过以下链接找到:
-
Docker Hub 发布者注册:
store.docker.com/publisher/signup/ -
Docker 注册表文档:
docs.docker.com/registry/ -
Docker 受信注册表(DTR):
www.mirantis.com/software/docker/image-registry/
你可以通过以下链接找到更多关于用于 Docker 注册表的不同类型云存储的详细信息:
-
Azure Blob 存储:
azure.microsoft.com/en-gb/services/storage/blobs/ -
Google Cloud 存储:
cloud.google.com/storage/ -
亚马逊简单存储服务(Amazon S3):
aws.amazon.com/s3/
一些第三方注册服务可以在这里找到:
-
GitHub Actions:
github.com/features/actions -
Azure 容器注册表:
azure.microsoft.com/en-gb/services/container-registry/ -
Azure 容器注册任务:
docs.microsoft.com/zh-cn/azure/container-registry/container-registry-tutorial-quick-task -
Amazon 弹性容器注册:
aws.amazon.com/ecr/ -
Google Cloud 容器注册:
cloud.google.com/container-registry -
Red Hat 容器目录:
catalog.redhat.com/software/containers/explore -
OpenShift:
www.openshift.com/ -
Red Hat 的 Quay:
quay.io/ -
JFrog 的 Artifactory:
www.jfrog.com/artifactory/
最后,您可以在这里找到我的 Apache Bench 镜像的 Docker Hub 和 Microbadger 链接:
-
Apache Bench 镜像 (Docker Hub):
hub.docker.com/r/russmckendrick/ab/ -
Apache Bench 镜像 (Microbadger):
microbadger.com/images/russmckendrick/ab
第四章:管理容器
到目前为止,我们一直在关注如何构建、存储和分发 Docker 镜像。现在,我们将看看如何启动容器,以及如何使用 Docker 命令行客户端来管理和与其交互。
我们将在第一章《Docker 概述》中使用的命令上,进行更多的细节讲解,然后深入了解可用的其他命令。一旦我们熟悉了容器命令,就会进一步探讨 Docker 网络和 Docker 卷。
本章将涵盖以下主题:
-
理解 Docker 容器命令
-
Docker 网络和卷
-
Docker Desktop 仪表盘
技术要求
在本章中,我们将继续使用本地的 Docker 安装。本章中的截图将来自我偏好的操作系统 macOS,但我们将运行的 Docker 命令将在我们迄今已安装 Docker 的三种操作系统上都能运行;不过,一些辅助命令(虽然极少)可能仅适用于基于 macOS 和 Linux 的操作系统。
查看以下视频,观看代码演示:bit.ly/3m1Wtk4
理解 Docker 容器命令
在我们深入学习更复杂的 Docker 命令之前,让我们回顾一下并更详细地了解我们在前几章中使用的命令。
基础知识
在第一章,《Docker 概述》中,我们启动了最基本的容器——hello-world容器,使用以下命令:
$ docker container run hello-world
正如你可能记得的,这个命令从 Docker Hub 拉取了一个1.84 KB 的镜像。你可以在hub.docker.com/images/hello-world/找到该镜像的 Docker Hub 页面,正如以下Dockerfile所示,它运行一个名为hello的可执行文件:
FROM scratch
COPY hello /
CMD ["/hello"]
hello可执行文件将Hello from Docker!文本打印到终端,然后进程退出。正如从以下终端输出的完整信息中可以看到的,hello二进制文件还会准确告诉你刚刚发生了哪些步骤:

图 4.1 – 运行 hello-world
当进程退出时,我们的容器也会停止。可以通过运行以下命令来查看:
$ docker container ls -a

图 4.2 – 列出我们的容器
你可能会注意到,在终端输出中,我首先运行了带有和不带-a标志的docker container ls命令。-a是--all的简写,因为不带此标志运行时不会显示任何已退出的容器。
你可能已经注意到,我们不需要给容器命名。这是因为它不会存在得足够长,让我们在意它叫什么。不过,Docker 会自动为容器分配名称,在我的情况下,你可以看到它被命名为awesome_jackson。
在使用 Docker 时,你会注意到,如果你选择让它为容器自动生成名称,它会为你起一些非常有趣的名字。它为左边的词从一个单词列表中选取,而右边的词则来自著名科学家和黑客的名字。虽然这有点偏题,但生成这些名称的代码可以在names-generator.go中找到。在源代码的最后,它有如下的if语句:
if name == "boring_wozniak" /* Steve Wozniak is not boring */ { goto begin }
这意味着永远不会有一个名为 boring_wozniak 的容器(而且这也完全合理)。
信息:
史蒂夫·沃兹尼亚克(Steve Wozniak)是发明家、电子工程师、程序员和企业家,他与史蒂夫·乔布斯共同创办了苹果公司。他被誉为 70 年代和 80 年代个人计算机革命的先驱,绝对不无聊!
我们可以通过运行以下命令来删除一个状态为exited的容器,确保你将容器名称替换为你自己的容器名称:
$ docker container rm awesome_jackson
同样,在第一章,“Docker 概述”的结尾,我们通过以下命令使用官方的 NGINX 镜像启动了一个容器:
$ docker container run -d --name nginx-test -p 8080:80 nginx
如你所记得,这将下载镜像并运行它,将主机上的端口8080映射到容器中的端口80,并将其命名为nginx-test:

图 4.3 – 运行一个 NGINX 容器
如你所见,运行 Docker 镜像的ls命令显示我们现在有两个已下载并运行的镜像。以下命令向我们展示了一个正在运行的容器:
$ docker container ls
以下终端输出显示,当我运行命令时,我的容器已经运行了 5 分钟:

图 4.4 – 查看正在运行的容器
从我们的docker container run命令可以看出,我们引入了三个标志。其中一个是-d,它是--detach的简写。如果我们没有添加这个标志,容器将会在前台执行,这意味着我们的终端将会被锁住,直到我们按下Ctrl + C来发送中断命令。
我们可以通过运行以下命令来启动第二个 NGINX 容器,使其与我们已经启动的容器并行运行,从而看到其实际效果:
$ docker container run --name nginx-foreground -p 9090:80 nginx
启动后,打开浏览器并输入http://localhost:9090/。当你加载页面时,你会注意到页面访问的记录会打印到屏幕上。在浏览器中点击刷新会显示更多的访问记录,直到你在终端中按下Ctrl + C:

图 4.5 – 在前台运行容器时查看输出
运行docker container ls -a显示你有两个容器,其中一个已经退出:

图 4.6 – 列出正在运行的容器
那么,发生了什么呢?当我们移除detach标志时,Docker 直接将我们连接到容器中的 NGINX 进程,这意味着我们可以查看该进程的stdin、stdout和stderr。当我们按下Ctrl + C时,我们实际上是向 NGINX 进程发送了一个终止指令。由于该进程是保持容器运行的进程,一旦没有了正在运行的进程,容器也就立即退出了。
重要提示
标准输入 (stdin) 是我们的进程读取以获取来自最终用户信息的句柄。标准输出 (stdout) 是进程写入正常信息的地方。标准错误 (stderr) 是进程写入错误信息的地方。
你可能还注意到,在启动nginx-foreground容器时,我们使用--name标志给它起了一个不同的名字。
这是因为不能有两个容器使用相同的名称,Docker 提供了通过CONTAINER ID 或 NAME值与容器交互的选项。这就是名称生成器功能存在的原因:为你不想自己命名的容器分配一个随机名称,并且确保我们永远不会称 Steve Wozniak 为无聊。
最后需要提到的是,当我们启动nginx-foreground时,我们要求 Docker 将容器上的端口9090映射到主机的端口80。这是因为我们不能为主机上的一个端口分配多个进程,所以如果我们试图启动第二个容器并使用与第一个容器相同的端口,就会收到错误消息:
docker: Error response from daemon: driver failed programming external connectivity on endpoint nginx-foreground (3f5b355607f24e03f09a60ee688645f223bafe4492f807459e4a 2b83571f23f4): Bind for 0.0.0.0:8080 failed: port is already allocated.
此外,由于我们在前台运行容器,NGINX 进程可能会因启动失败而返回错误。
ERRO[0003] error getting events from daemon: net/http: request cancelled
然而,你可能还会注意到我们正在将端口80映射到容器上——为什么没有错误呢?
正如在第一章中解释的那样,Docker 概述,容器本身是隔离的资源,这意味着我们可以启动任意数量的容器并重新映射端口80,它们之间不会发生冲突;只有当我们希望从 Docker 主机路由到暴露的容器端口时,才会遇到问题。
让我们让 NGINX 容器继续运行,直到下一个章节,我们将探索更多与容器交互的方法。
与容器交互
到目前为止,我们的容器只运行了一个进程。Docker 为你提供了一些工具,能够让你既能分叉更多的进程,又能与它们进行交互,我们将在接下来的章节中介绍这些工具:
attach
与正在运行的容器交互的第一种方式是附加到运行中的进程。我们仍然有nginx-test容器在运行,因此我们可以通过运行以下命令来连接到它:
$ docker container attach nginx-test
打开浏览器并访问http://localhost:8080/将会打印 NGINX 访问日志到屏幕上,就像我们启动nginx-foreground容器时一样。按下Ctrl + C将终止进程并将终端恢复到正常状态。然而,和之前一样,我们会终止保持容器运行的进程:

图 4.7 – 附加到我们的容器
我们可以通过运行以下命令重新启动我们的容器:
$ docker container start nginx-test
这将使容器重新启动为分离模式,意味着它再次在后台运行,因为这是容器最初启动时的状态。访问http://localhost:8080/将再次显示 NGINX 欢迎页面。
让我们重新附加到我们的进程,但这次添加一个额外的选项:
$ docker container attach --sig-proxy=false nginx-test
多次访问容器的 URL 然后按下Ctrl + C将使我们与 NGINX 进程断开连接,但这次,不是终止 NGINX 进程,而是将我们返回到终端,容器保持在分离状态,通过运行 docker container ls 可以查看:

图 4.8 – 从容器断开连接
这是快速附加到运行中的容器以调试问题,同时保持容器主进程运行的好方法。
exec
attach命令如果你需要连接到容器正在运行的进程很有用,但如果你需要一些更具交互性的操作呢?
你可以使用exec命令。这会在容器内启动一个第二个进程,允许你与之交互。例如,要查看/etc/debian_version文件的内容,我们可以运行以下命令:
$ docker container exec nginx-test cat /etc/debian_version
这将启动第二个进程,在这种情况下是cat命令,它将/etc/debianversion的内容打印到标准输出(stdout)。第二个进程将终止,容器将恢复到执行exec命令之前的状态:

图 4.9 – 对容器执行命令
我们可以进一步执行此操作,运行以下命令:
$ docker container exec -i -t nginx-test /bin/bash
这次,我们将分叉一个 bash 进程,并使用-i和-t标志保持对容器的控制台访问。-i标志是--interactive的简写,它指示 Docker 保持stdin打开,以便我们可以向进程发送命令。-t标志是–tty的简写,它为会话分配一个伪终端(pseudo-TTY)。
重要提示
早期连接到计算机的用户终端被称为 电传打字机。虽然这些设备今天已不再使用,但缩写 TTY 仍然被用来描述现代计算中的仅文本控制台。
这意味着你可以像使用 SSH 一样与容器进行交互,仿佛你拥有一个远程终端会话:

图 4.10 – 打开与容器的交互式会话
尽管这非常有用,因为你可以像操作虚拟机一样与容器进行交互,但我不建议在使用伪 TTY 时对容器进行任何更改。很可能这些更改不会持久化,并且在容器被删除时会丢失。我们将在 第十五章,《Docker 工作流》中更详细地讨论这一点。
现在我们已经介绍了如何连接和与容器交互的各种方法,接下来我们将了解 Docker 提供的一些工具,这些工具可以让你不必手动执行这些操作。
日志和进程信息
到目前为止,我们一直在连接到容器中的进程或容器本身,以查看信息。Docker 提供了我们将在本节中介绍的命令,这些命令允许你在不使用 attach 或 exec 命令的情况下查看容器的信息。
我们先来看一下如何在不需要将其置于前台运行的情况下,查看容器内进程生成的输出。
日志
logs 命令是相当直观的。它允许你与容器的 stdout 流进行交互,而 Docker 会在后台跟踪这些流。例如,要查看我们 nginx-test 容器写入的最后几条 stdout 条目,只需使用以下命令:
$ docker container logs --tail 5 nginx-test
命令的输出如下所示:

图 4.11 – 跟踪日志
要实时查看日志,我只需运行以下命令:
$ docker container logs -f nginx-test
-f 标志是 --follow 的简写。例如,我还可以通过运行以下命令查看从某个时间点以来记录的所有内容:
$ docker container logs --since 2020-03-28T15:52:00 nginx-test
命令的输出如下所示:

图 4.12 – 检查某个时间点之后的日志
如果你注意到访问日志中的时间戳与你搜索的时间不同,那是因为 logs 命令显示的是 Docker 记录的 stdout 时间戳,而不是容器内部的时间。一个例子是,由于 英国夏令时(BST),主机与容器之间可能存在数小时的时间差。
幸运的是,为了避免混淆,你可以在 logs 命令中添加 -t 参数:
$ docker container logs --since 2020-03-28T15:52:00 -t nginx-test
-t 标志是 --timestamp 的简写;此选项会在输出前添加 Docker 捕获时间:

图 4.13 – 查看日志以及记录条目的时间
现在,既然我们已经查看了如何查看容器中运行的进程的输出,我们来看看如何获得关于进程本身的更多细节。
top
top 命令是一个非常简单的命令;它列出了指定容器中正在运行的进程,使用方法如下:
$ docker container top nginx-test
命令的输出如下所示:

图 4.14 – 运行 top 命令
如下图所示,我们有两个进程在运行,都是 NGINX,这是预期的结果。
stats
stats 命令提供了关于指定容器的实时信息,或者如果没有传递 NAME 或 ID 参数,则提供所有正在运行的容器的信息:

图 4.15 – 查看单个容器的实时统计信息
如下图所示,我们可以看到指定容器的 CPU、RAM、NETWORK、DISK IO 和 PIDS 信息:

图 4.16 – 查看所有正在运行的容器的实时统计信息
然而,正如你从前面的输出中看到的,如果容器没有运行,那么没有资源被使用,因此它除了提供容器数量和资源使用情况的可视化表示外,并不会提供太多价值。
同时值得指出的是,stats 命令显示的信息仅为实时数据;Docker 并不会像 logs 命令那样记录资源利用情况并进行存储。我们将在后续章节中探讨更多关于资源利用的长期存储选项。
资源限制
我们运行的 stats 命令显示了容器的资源利用情况。默认情况下,当容器启动时,如果需要,它将被允许消耗主机上所有可用的资源。我们可以为容器消耗的资源设置限制。让我们从更新 nginx-test 容器的资源限制开始。
通常情况下,我们会在启动容器时使用 run 命令设置限制;例如,要将 CPU 优先级减半并设置 128M 的内存限制,我们可以使用以下命令:
$ docker container run -d --name nginx-test --cpu-shares 512 --memory 128M -p 8080:80 nginx
然而,我们并没有为 nginx-test 容器启动时设置任何资源限制,这意味着我们需要更新已经运行的容器。为此,我们可以使用 update 命令。现在,你可能会认为这只是运行以下命令:
$ docker container update --cpu-shares 512 --memory 128M nginx-test
但实际上,运行前述命令会导致错误:
Error response from daemon: Cannot update container 662b6e5153ac77685f25a1189922d7f49c2df6b2375b3635a37eea 4c8698aac2: Memory limit should be smaller than already set memoryswap limit, update the memoryswap at the same time
那么,memoryswap限制当前设置为何?为了找出这个信息,我们可以使用inspect命令来显示我们正在运行的容器的所有配置数据;只需运行以下命令:
$ docker container inspect nginx-test
如果你正在跟随操作,你会发现运行前述命令时,会显示大量的配置数据,这里无法显示所有内容。当我运行此命令时,返回了一个199行的JSON数组。我们可以使用grep命令仅过滤出包含memory的行:
$ docker container inspect nginx-test | grep -i memory
这将返回以下配置数据:
"Memory": 0,
"KernelMemory": 0,
"KernelMemoryTCP": 0,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
一切都设置为0,那怎么128M会比 0 小呢?
$ docker container update --cpu-shares 512 --memory 128M --memory-swap 256M nginx-test
在资源配置的上下文中,0实际上是默认值,表示没有限制。请注意,每个数值后面没有M,这意味着我们的update命令应当按前述命令进行操作。
重要说明
分页是一种内存管理方案,内核将数据从二级存储中存储和检索,或者交换到主内存中使用。这使得进程可以超出物理内存的大小。
默认情况下,当你在run命令中设置--memory时,Docker 会将--memory-swap size设置为--memory的两倍。如果你现在运行docker container stats nginx-test,你应该会看到我们设置的限制:

图 4.17 – 使用统计数据查看限制
此外,重新运行docker container inspect nginx-test | grep -i memory将显示以下变化:
"Memory": 134217728,
"KernelMemory": 0,
"KernelMemoryTCP": 0,
"MemoryReservation": 0,
"MemorySwap": 268435456,
"MemorySwappiness": null,
你会注意到,虽然我们定义了以 MB 为单位的数值,但在这里它们以字节显示,所以是正确的。
提示
运行docker container inspect时显示的值是以字节为单位,而不是兆字节(MB)。
容器状态和杂项命令
本节的最后部分,我们将查看容器可能的各种状态,以及我们尚未覆盖的docker container命令的一些剩余命令。
运行docker container ls -a应该会显示类似以下的终端输出:

图 4.18 – 列出所有容器,包括已退出的容器
如你所见,我们有两个容器,一个状态为Up,另一个为Exited。在继续之前,让我们再启动五个容器。要快速完成此操作,运行以下命令:
$ for i in {1..5}; do docker container run -d --name nginx$(printf "$i") nginx; done
你应该看到如下类似的输出:

图 4.19 – 快速启动五个容器
当运行docker container ls -a时,你应该会看到五个新容器,命名为nginx1至nginx5:

图 4.20 – 查看我们的五个新容器
现在,我们已经启动了额外的容器,让我们来看看如何查看它们的状态。
暂停和恢复
让我们来看一下如何暂停 nginx1。只需运行以下命令:
$ docker container pause nginx1
运行 docker container ls 会显示容器的状态为 Up,但也会显示为 Paused:

图 4.21 – 暂停容器
请注意,我们无需使用 -a 标志来查看容器的信息,因为该进程尚未终止;相反,它是通过 cgroups 冷冻机制被挂起的。使用 cgroups 冷冻机制时,进程并不知道它已被挂起,这意味着它可以被恢复。
如你所料,你可以使用 unpause 命令恢复暂停的容器,命令如下:
$ docker container unpause nginx1
这个命令很有用,特别是当你需要冻结一个容器的状态时;例如,可能有一个容器出现异常,你需要稍后进行调查,但又不希望它对其他运行中的容器造成负面影响。
现在,让我们来看看如何正确地停止和删除容器。
停止、启动、重启和终止
接下来,我们将介绍 stop、start、restart 和 kill 命令。我们已经使用过 start 命令来恢复一个状态为 Exited 的容器。stop 命令的工作方式与我们之前使用 Ctrl + C 从前台分离容器的方式完全相同。
运行以下命令:
$ docker container stop nginx2
通过此操作,向进程发送终止请求,称为 SIGTERM。如果进程在宽限期内未自行终止,则会发送一个终止信号,称为 SIGKILL。这将立即终止进程,不给它任何时间完成可能导致延迟的操作;例如,将数据库查询结果提交到磁盘。
由于这可能导致问题,Docker 允许你通过使用 -t 标志来覆盖默认的宽限期(10 秒);这代表 --time。例如,运行以下命令将在发送 SIGKILL 命令之前等待最多 60 秒,万一需要终止进程:
$ docker container stop -t 60 nginx3
如我们所见,start 命令会重新启动进程;然而,与 pause 和 unpause 命令不同,进程在这种情况下会从头开始,使用最初启动它时的标志,而不是从暂停的位置继续:
$ docker container start nginx2 nginx3
restart 命令是以下两个命令的组合;它先停止然后启动你传递给它的 ID 或 NAME 容器。此外,和 stop 命令一样,你可以使用 -t 标志:
$ docker container restart -t 60 nginx4
最后,你也可以选择通过运行 kill 命令立即向容器发送 SIGKILL 命令:
$ docker container kill nginx5
还有一件事需要讲解,那就是删除容器。
删除容器
让我们通过docker container ls -a命令检查我们使用过的容器的状态。当我运行该命令时,我看到有两个容器的状态为Exited,其他的都在运行中:

Figure 4.22 – 查看所有容器的状态
要删除这两个已退出的容器,我可以简单地运行prune命令:
$ docker container prune
这样做时,会弹出一个警告,询问你是否确定,如下图所示:

Figure 4.23 – 修剪容器
你可以使用rm命令选择要删除的容器,下面是一个示例:
$ docker container rm nginx4
另一种选择是将stop和rm命令连接在一起:
$ docker container stop nginx3 && docker container rm nginx3
然而,鉴于现在可以使用prune命令,这可能是一个过于繁琐的过程,尤其是当你正在尝试删除容器并且可能不太关心该过程是如何优雅地终止时。
随意使用你喜欢的任何方法删除剩余的容器。
在结束本章这一部分之前,我们还将查看一些不能真正归类在一起的有用命令。
杂项命令
本节的最后部分,我们将查看一些你在日常使用 Docker 时可能不会过多使用的命令。第一个是create命令。create命令与run命令非常相似,区别在于它不会启动容器,而是准备和配置容器:
$ docker container create --name nginx-test -p 8080:80 nginx
你可以通过运行docker container ls -a命令检查已创建容器的状态,然后使用docker container start nginx-test启动容器,再次检查状态:

Figure 4.24 – 创建并运行容器
接下来我们将快速查看port命令;该命令会显示容器的port号以及所有端口映射:
$ docker container port nginx-test
它应该返回以下内容:
80/tcp -> 0.0.0.0:8080
我们已经知道了这一点,因为这是我们所配置的内容。此外,端口会在docker container ls输出中列出。
接下来我们将快速查看的命令是diff命令。这个命令列出自容器启动以来所有已添加(A)或已更改(C)的文件——基本上,这是一个列出原始镜像和当前容器文件系统之间差异的文件清单。
在我们运行命令之前,先使用exec命令在nginx-test容器内创建一个空白文件:
$ docker container exec nginx-test touch /tmp/testing
现在我们在/tmp目录下有一个名为testing的文件,我们可以使用以下命令查看原始镜像和正在运行的容器之间的差异:
$ docker container diff nginx-test
这将返回一个文件列表。从以下列表中可以看到,我们的测试文件已经在那里,同时还有 NGINX 启动时创建的文件:
C /run
A /run/nginx.pid
C /tmp
A /tmp/testing
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
值得指出的是,一旦我们停止并移除容器,这些文件将会丢失。在本章的下一节中,我们将学习 Docker 卷,并了解如何持久化数据。不过,在继续之前,让我们使用cp命令获取我们刚刚创建的文件的副本。
为此,我们可以运行以下命令:
$ docker container cp nginx-test:/tmp/testing testing
从命令可以看到,我们提供了容器名称,后面跟着 : 和我们想要复制的文件的完整路径。接下来是本地路径。在这里,你可以看到我们只是简单地将文件命名为testing,并且它将被复制到当前文件夹:

图 4.25 – 将文件复制到容器
由于文件中没有数据,让我们添加一些数据,然后将其复制回容器:
$ echo "This is a test of copying a file from the host machine to the container" > testing
$ docker container cp testing nginx-test:/tmp/testing
$ docker container exec nginx-test cat /tmp/testing
请注意,在第二个命令中,我们交换了路径的位置。这次,我们提供的是本地文件的路径以及容器名称和路径:

图 4.26 – 将文件及其内容复制到容器
另一个值得注意的地方是,尽管我们正在覆盖一个现有文件,但 Docker 并没有警告我们或提供回退的选项——它直接覆盖了该文件,因此在使用docker container cp时请小心。
如果你在跟随操作,请在继续之前使用你选择的命令移除本节中启动的任何运行中的容器。
Docker 网络与卷
接下来,我们将使用默认驱动程序来了解 Docker 网络和 Docker 卷的基础知识。首先,我们来看一下网络。
Docker 网络
到目前为止,我们一直在一个单一的扁平共享网络上启动容器。虽然我们还没有讨论过这个问题,但这意味着我们启动的容器之间可以互相通信,而无需使用任何主机网络。
现在我们不打算详细讲解,而是通过一个示例来演示。我们将运行一个包含两个容器的应用程序;第一个容器运行 Redis,第二个容器运行我们的应用程序,它使用 Redis 容器来存储系统状态。
重要提示
Redis 是一个内存数据结构存储,可以用作数据库、缓存或消息代理。它支持不同级别的磁盘持久化。
在启动应用程序之前,让我们先下载我们将使用的容器镜像,并创建网络:
$ docker image pull redis:alpine
$ docker image pull russmckendrick/moby-counter
$ docker network create moby-counter
你应该会看到类似于以下的终端输出:

图 4.27 – 拉取我们需要的镜像并创建网络
现在我们已经拉取了镜像并创建了网络,可以启动我们的容器,从 Redis 容器开始:
$ docker container run -d --name redis --network moby-counter redis:alpine
如你所见,我们使用--network标志来定义我们启动容器时使用的网络。现在 Redis 容器已经启动,我们可以通过运行以下命令启动应用程序容器:
$ docker container run -d --name moby-counter --network moby-counter -p 8080:80 russmckendrick/moby-counter
再次说明,我们在moby-counter网络上启动了容器。这一次,我们将端口8080映射到容器上的端口80。请注意,我们不需要担心暴露 Redis 容器的任何端口。这是因为 Redis 镜像自带一些默认设置,暴露了默认端口,对我们来说是6379。通过运行docker container ls可以看到这一点:

图 4.28 – 列出我们应用程序所需的容器
剩下的就是访问应用程序了。为此,请打开浏览器并访问http://localhost:8080/。你应该看到一个大致空白的页面,并显示点击以添加徽标…的消息:

图 4.29 – 我们的应用程序已准备就绪
点击页面上的任何地方都会添加 Docker 徽标,所以尽管点击:

图 4.30 – 向页面添加一些徽标
那么,发生了什么?来自moby-counter容器的应用程序正在连接到redis容器,并使用该服务来存储你通过点击屏幕上每个徽标的坐标。
moby-counter应用程序是如何连接到redis容器的?嗯,在server.js文件中,设置了以下默认值:
var port = opts.redis_port || process.env.USE_REDIS_PORT || 6379
var host = opts.redis_host || process.env.USE_REDIS_HOST || 'redis'
这意味着moby-counter应用程序正在尝试连接到名为redis的主机,端口为6379。让我们尝试使用exec命令从moby-counter应用程序 ping redis容器,看看会得到什么结果:
$ docker container exec moby-counter ping -c 3 redis
你应该看到类似于以下输出的内容:

图 4.31 – 使用容器名称 ping Redis 容器
如你所见,moby-counter容器将redis解析为redis容器的 IP 地址,即172.18.0.2。你可能会想,应用程序的主机文件中是否包含了redis容器的条目;让我们通过以下命令来查看:
$ docker container exec moby-counter cat /etc/hosts
这将返回/etc/hosts的内容,在我的案例中,内容如下所示:
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.18.0.3 e7335ca1830d
除了末尾的条目,它实际上是解析到本地容器主机名的 IP 地址,e7335ca1830d是容器的 ID;没有找到redis的条目。接下来,让我们通过运行以下命令检查/etc/resolv.conf:
$ docker container exec moby-counter cat /etc/resolv.conf
这返回了我们需要的内容。如你所见,我们正在使用一个本地的nameserver:
nameserver 127.0.0.11
options ndots:0
让我们对 redis 执行 DNS 查找,目标是 127.0.0.11,使用以下命令:
$ docker container exec moby-counter nslookup redis 127.0.0.11
这将返回 redis 容器的 IP 地址:
Server: 127.0.0.11
Address 1: 127.0.0.11
Name: redis
Address 1: 172.18.0.2 redis.moby-counter
让我们创建一个第二个网络并启动另一个应用程序容器:
$ docker network create moby-counter2
$ docker run -itd --name moby-counter2 --network moby-counter2 -p 9090:80 russmckendrick/moby-counter
现在我们已经启动了第二个应用程序容器,让我们尝试从它向 redis 容器发送 ping 请求:
$ docker container exec moby-counter2 ping -c 3 redis
在我的情况下,我遇到了以下错误:

图 4.32 – 在不同网络中隔离我们的应用程序
让我们检查 resolv.conf 文件,看看是否已经在使用相同的 nameserver,如下所示:
$ docker container exec moby-counter2 cat /etc/resolv.conf
从以下输出可以看出,nameserver 确实已经在使用:
nameserver 127.0.0.11
options ndots:0
由于我们已经将 moby-counter2 容器启动在一个与 redis 容器所在的网络不同的网络中,因此我们无法解析容器的主机名:
$ docker container exec moby-counter2 nslookup redis 127.0.0.11
所以,它返回了一个错误地址:
Server: 127.0.0.11
Address 1: 127.0.0.11
nslookup: can't resolve 'redis': Name does not resolve
让我们来看一下如何在第二个网络中启动第二个 Redis 服务器。正如我们之前讨论的,我们不能有两个相同名称的容器,因此我们可以创造性地命名它为redis2。由于我们的应用程序配置为连接到一个解析为redis的容器,这是否意味着我们需要对应用程序容器做出更改?不,Docker 已经帮我们解决了这个问题。
虽然我们不能有两个相同名称的容器,正如我们之前发现的那样,我们的第二个网络与第一个网络完全隔离,这意味着我们仍然可以使用 redis 的 DNS 名称。为了做到这一点,我们需要添加 -network-alias 标志,如下所示:
$ docker container run -d --name redis2 --network moby-counter2 --network-alias redis redis:alpine
如你所见,我们将容器命名为 redis2,但将 --network-alias 设置为 redis:
$ docker container exec moby-counter2 nslookup redis 127.0.0.1
这意味着当我们进行查找时,会看到返回正确的 IP 地址:
Server: 127.0.0.1
Address 1: 127.0.0.1 localhost
Name: redis
Address 1: 172.19.0.3 redis2.moby-counter2
如你所见,redis 实际上是 redis2.moby-counter2 的别名,随后解析为 172.19.0.3。
现在,我们应该有两个应用程序在本地 Docker 主机上并排运行,分别在 http://localhost:8080/ 和 http://localhost:9090/ 可访问。运行 docker network ls 将显示所有在 Docker 主机上配置的网络,包括默认网络:

图 4.33 – 列出我们的网络
你可以通过运行以下 inspect 命令来了解更多关于网络配置的信息:
$ docker network inspect moby-counter
运行前面的命令会返回以下 JSON 数组。它首先提供网络的一些一般信息:
[
{
"Name": "moby-counter",
"Id": "c9d98376f13ccd556d84b708e132350900036fb4 cfecf275dcbd8657dc69b22c",
"Created": "2020-03-29T13:06:03.3911316Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
接下来是 IP 地址管理系统使用的配置。它显示了子网范围和网关 IP 地址:
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
接下来是剩余的通用配置:
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
然后,我们获得了关于附加到网络的容器的详细信息。这里我们可以找到每个容器的 IP 地址和 MAC 地址:
"Containers": {
"e7335ca1830da66d4bdc2915a6a35e83e 546cbde63cd97ab48bfd3ca06ae99ae": {
"Name": "moby-counter",
"EndpointID": "fb405fac3e0814e3ab7f1b8e2c4 2bbfe09d751982c502ff196ac794e382bbb2a",
"MacAddress": "02:42:ac:12:00:03",
"IPv4Address": "172.18.0.3/16",
"IPv6Address": ""
},
"f3b6a0d45f56fe2a0b54beb4b89d6094aaf 42598e11c3080ef0a21b78f0ec159": {
"Name": "redis",
"EndpointID": "817833e6bba40c73a3a349fae 53205b1c9e19d73f3a8d5e296729ed5876cf648",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
最后,我们有了配置的最后部分:
"Options": {},
"Labels": {}
}
]
如你所见,它包含了关于网络地址的信息,这些信息位于 IPAM 部分,以及关于在该网络中运行的两个容器的详细信息。
重要提示
IPAM同时提供DNS和DHCP服务,因此每个服务都能通知对方的变化。例如,DHCP会为container2分配地址,随后DNS服务会更新,以便在对container2进行查找时返回DHCP分配的 IP 地址。
在进入下一节之前,我们应该删除其中一个应用程序及其关联的网络。要执行此操作,请运行以下命令:
$ docker container stop moby-counter2 redis2
$ docker container prune
$ docker network prune
这些操作将删除容器和网络,如下图所示:

图 4.34 – 使用清理命令删除未使用的网络
正如本节开始时提到的,这仅仅是默认的网络驱动程序,这意味着我们只能在单个 Docker 主机上使用网络。 在后面的章节中,我们将学习如何在多个主机甚至提供商之间扩展我们的 Docker 网络。
现在我们已经了解了 Docker 网络的基础知识,接下来让我们看看如何为容器提供额外的存储。
Docker 卷
如果你跟随了上一节的网络示例,你应该会看到两个容器正在运行,如下图所示:

图 4.35 – 列出正在运行的容器
当你在浏览器中访问应用程序(http://localhost:8080/)时,你可能会看到屏幕上已经出现了 Docker 图标。让我们停止并删除 Redis 容器,然后看看会发生什么。为此,请运行以下命令:
$ docker container stop redis
$ docker container rm redis
如果你打开了浏览器,你可能会注意到 Docker 图标已经渐隐到背景中,屏幕中央出现了一个动画加载器。这基本上表示应用程序正在等待与 Redis 容器的连接重新建立:

图 4.36 – 应用程序无法再连接到 Redis
使用以下命令重新启动 Redis 容器:
$ docker container run -d --name redis --network moby-counter redis:alpine
这恢复了连接。然而,当你开始与应用程序交互时,之前的图标消失了,你将看到一个空白界面。快速地在屏幕上再添加一些图标,这次它们的位置与之前不同,就像我这里所做的那样:

图 4.37 – 添加更多的 logo
一旦你有了一个模式,接下来我们再通过运行以下命令删除 Redis 容器:
$ docker container stop redis
$ docker container rm redis
正如我们在本章之前讨论的,丢失容器中的数据是可以预期的。然而,由于我们使用的是官方的 Redis 镜像,实际上我们并没有丢失任何数据。
我们使用的官方 Redis 镜像的 Dockerfile 如下所示:
FROM alpine:3.11
RUN addgroup -S -g 1000 redis && adduser -S -G redis -u 999 redis
RUN apk add --no-cache \
'su-exec>=0.2' \
tzdata
ENV REDIS_VERSION 5.0.8
ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-5.0.8.tar.gz
ENV REDIS_DOWNLOAD_SHA f3c7eac42f433326a8d981b50dba0169 fdfaf46abb23fcda2f933a7552ee4ed7
前面的步骤通过添加用户组、安装一些软件包并设置环境变量来准备容器。接下来的步骤安装运行 Redis 所需的先决条件:
RUN set -eux; \
\
apk add --no-cache --virtual .build-deps \
coreutils \
gcc \
linux-headers \
make \
musl-dev \
openssl-dev \
; \
\
现在,Redis 的源代码已下载并复制到镜像中的正确位置:
wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \
echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
mkdir -p /usr/src/redis; \
tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \
rm redis.tar.gz; \
\
现在 Redis 的源代码已进入镜像,并应用了配置:
grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \
sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr/src/redis/src/server.h; \
grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \
\
现在,Redis 已经编译并测试完成:
make -C /usr/src/redis -j "$(nproc)" all; \
make -C /usr/src/redis install; \
\
serverMd5="$(md5sum /usr/local/bin/redis-server | cut -d' ' -f1)"; export serverMd5; \
find /usr/local/bin/redis* -maxdepth 0 \
-type f -not -name redis-server \
-exec sh -eux -c ' \
md5="$(md5sum "$1" | cut -d" " -f1)"; \
test "$md5" = "$serverMd5"; \
' -- '{}' ';' \
-exec ln -svfT 'redis-server' '{}' ';' \
; \
\
然后移除build目录,并删除不再需要的软件包:
rm -r /usr/src/redis; \
\
runDeps="$( \
scanelf --needed --nobanner --format '%n#p' --recursive /usr/local \
| tr ',' '\n' \
| sort -u \
| awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \
)"; \
apk add --no-network --virtual .redis-rundeps $runDeps; \
apk del --no-network .build-deps; \
\
现在 Redis 已经构建,软件包和构建产物已整理好,进行最终测试。如果在这里失败,构建也会失败:
redis-cli --version; \
redis-server --version
在所有内容安装完毕后,最后的镜像配置就可以进行:
RUN mkdir /data && chown redis:redis /data
VOLUME /data
WORKDIR /data
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 6379
CMD ["redis-server"]
如果你注意到,在文件的最后部分,声明了VOLUME和WORKDIR指令;这意味着,当我们的容器启动时,Docker 实际上创建了一个卷,并从该卷中运行了redis-server。我们可以通过运行以下命令来验证:
$ docker volume ls
这应该至少显示两个卷,如下图所示:

图 4.38 – 列出卷
如你所见,卷的名称并不友好。实际上,它是卷的唯一 ID。那么,我们如何在启动 Redis 容器时使用该卷呢?从 Dockerfile 中我们知道,卷被挂载到了容器内的/data路径,所以我们只需要告诉 Docker 在运行时使用哪个卷,并将其挂载到指定位置。
为此,运行以下命令,确保用你自己的卷 ID 替换:
$ docker container run -d --name redis -v 45c4cb295fc831c085c49963a01f8e0f79534b9 f0190af89321efec97b9d051f:/data -network moby-counter redis:alpine
如果在启动 Redis 容器后,你的应用页面仍然显示它正在尝试重新连接到 Redis 容器,你可能需要刷新浏览器。如果不行,可以通过运行docker container restart moby-counter重启应用容器,再刷新浏览器应该能解决问题。
你可以通过运行以下命令查看卷的内容,附加到容器并列出/data中的文件:
$ docker container exec redis ls -lhat /data
这将返回类似如下的内容:
total 12K
drwxr-xr-x 1 root root 4.0K Mar 29 13:51 ..
drwxr-xr-x 2 redis redis 4.0K Mar 29 13:35 .
-rw-r--r-- 1 redis redis 210 Mar 29 13:35 dump.rdb
你也可以移除正在运行的容器并重新启动它,不过这次使用第二个卷的 ID。正如你从浏览器中的应用程序所看到的,最初创建的两种不同模式仍然完好无损。
我们再次移除Redis容器:
$ docker container stop redis
$ docker container rm redis
最后,你可以用你自己的卷来覆盖默认的卷。要创建卷,我们需要使用volume命令:
$ docker volume create redis_data
一旦创建,我们将能够使用redis_data卷来存储我们的Redis,方法是移除当前正在运行的redis容器后运行以下命令:
$ docker container run -d --name redis -v redis_data:/data --network moby-counter redis:alpine
然后我们可以根据需要重新使用该卷。以下屏幕展示了卷的创建,附加到容器,然后容器被移除,最后重新附加到一个新容器:

图 4.39 – 创建卷并将其附加到容器
类似于 network 命令,我们可以使用 inspect 命令查看更多关于卷的信息,如下所示:
$ docker volume inspect redis_data
上述命令将产生如下输出:
[
{
"CreatedAt": "2020-03-29T14:01:05Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/redis_data/_data",
"Name": "redis_data",
"Options": {},
"Scope": "local"
}
]
使用本地驱动程序时,您会发现卷的体积并不大。值得注意的一点是,数据存储在 Docker 主机上的路径是/var/lib/docker/volumes/redis_data/_data。如果您使用的是 Docker for Mac 或 Docker for Windows,那么这个路径将指向您的 Docker 主机虚拟机,而不是您的本地机器,意味着您无法直接访问卷内的数据。
不用担心;我们将在后续章节中讨论 Docker 卷及如何与其中的数据进行交互。在整理容器、网络和卷之前,如果您正在运行 Docker Desktop,那么我们应该先看看 Docker Desktop 仪表盘。
Docker Desktop 仪表盘
如果您正在运行 Docker for Mac 或 Docker for Windows,那么主菜单中有一个选项可以打开仪表盘,显示正在运行的容器的信息:

图 4.40 – 打开 Docker Desktop 仪表盘
打开后,您应该会看到如下屏幕。正如您所见,我们列出了 redis 和 moby-counter 容器:

图 4.41 – 查看正在运行的容器
选择 redis 容器后,您将进入一个概览屏幕,默认显示 Logs 输出:

图 4.42 – Logs 输出的概览屏幕
我们从屏幕顶部开始。右侧可以看到四个蓝色图标,它们从左到右分别是:
-
连接到容器:这将打开您的默认终端应用程序,并连接到当前选中的容器。
-
停止当前连接的容器:停止后,图标将变为启动图标。
-
接下来是重启图标。点击它,您猜得没错?!它将重启当前选中的容器。
-
最后的垃圾桶图标将终止当前选中的容器。
接下来,我们来看一下屏幕左侧的菜单项。我们已经看过了 Logs 输出,它会实时更新,您也可以选择搜索日志输出。下面是 Inspect;它显示有关容器的一些基本信息:

图 4.43 – 使用 inspect 获取容器信息
最后一项是Stats;正如你可能已经猜到的,这会给我们与docker container stats redis命令相同的输出:

图 4.44 – 查看实时统计信息
进入moby-counter容器会在顶部菜单开始处添加一个额外的图标:

图 4.45 – 查看额外的图标
这将打开你的默认浏览器,并带你到外部暴露的端口,在这种情况下是localhost:8080。
你已经注意到,仪表板中有一些功能,比如创建容器的能力。然而,随着新版本的发布,我相信会有更多的管理功能被添加进来。
现在,我们应该整理一下。首先,移除两个容器和网络:
$ docker container stop redis moby-counter
$ docker container prune
$ docker network prune
然后,我们可以通过运行以下命令来移除卷:
$ docker volume prune
你应该看到类似以下的终端输出:

图 4.46 – 删除我们启动的所有内容
现在我们已经恢复了干净的状态,可以进入下一个章节。
概述
在本章中,我们学习了如何使用 Docker 命令行客户端管理单个容器,并启动在自己隔离的 Docker 网络中的多容器应用程序。我们还讨论了如何使用 Docker 卷在文件系统上持久化数据。到目前为止,在本章和前几章中,我们已经详细介绍了将来我们将在以下部分使用的大部分可用命令:
$ docker container [command]
$ docker network [command]
$ docker volume [command]
$ docker image [command]
现在我们已经涵盖了本地使用 Docker 的四个主要领域,我们可以开始学习如何创建更复杂的应用程序。在下一章中,我们将看看另一个核心 Docker 工具,称为Docker Compose。
问题
-
要查看所有容器(包括正在运行的和已停止的容器),你需要在
docker container ls命令后加上哪个标志? -
判断题:
-p 8080:80标志会将容器上的端口80映射到主机上的端口8080。 -
解释使用Ctrl + C退出已附加的容器与使用
attach命令并加上--sig-proxy=false时的区别。 -
判断题:
exec命令将你附加到正在运行的进程。 -
如果你已经有一个在另一个网络中运行的容器,并且该容器使用相同的 DNS 名称,应该使用哪个标志来给容器添加别名,使其响应 DNS 请求?
-
使用哪个命令可以查看 Docker 卷的详细信息?
深入阅读
你可以通过以下链接了解更多关于我们在本章中讨论的一些话题:
-
名称生成器代码:
github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go -
cgroups冻结功能:www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.tx -
Redis:
redis.io/
第五章:Docker Compose
在这一章中,我们将介绍另一个核心 Docker 工具,名为 Docker Compose,同时也会涉及当前正在开发中的 Docker App。
这两种工具都可以让你轻松地定义、构建和分发由多个容器组成的应用程序,使用的语法与我们在前几章中手动启动容器时使用的命令类似。
我们将把这一章分成以下几个部分:
-
探索 Docker Compose 的基础知识
-
创建我们的第一个 Docker Compose 应用程序
-
探索 Docker Compose 命令
-
使用 Docker App
技术要求
与前几章一样,我们将继续使用本地 Docker 安装,本章中的截图也将再次来自我偏好的操作系统 macOS。与之前一样,我们将运行的 Docker 命令将适用于我们至今已安装 Docker 的所有三种操作系统。然而,一些辅助命令,虽然很少,但可能仅适用于 macOS 和 Linux 操作系统——这些将会特别标明。
探索 Docker Compose 的基础知识
在 第一章* Docker 概述* 中,我们讨论了 Docker 设计用于解决的几个问题。我们探讨了 Docker 如何应对许多开发和运维团队所面临的挑战。其中一个解决方案是通过将每个应用栈的进程隔离到一个容器中,来并行运行两个不同的应用栈。这样,你就可以在同一主机上运行两个完全不同版本的同一软件栈——比如 PHP 5.6 和 PHP 7——正如我们在 第二章* 构建容器镜像* 中所做的那样。
在 第四章* 管理容器* 的最后,我们启动了一个由多个容器组成的应用程序,而不是在单个容器中运行所需的软件栈。我们开始的示例应用程序 Moby Counter 是用 Node.js 编写的,并使用 Redis 作为后端存储键值数据,在我们的案例中是 Docker 图标在屏幕上的坐标。
为了能够运行 Moby Counter 应用程序,我们必须启动两个不同的容器,一个用于 Node.js 应用程序,一个用于 Redis。虽然因为应用程序本身非常简单,启动这些容器非常容易,但手动启动单个容器也有一些缺点。
例如,如果我希望同事部署相同的应用程序,我必须将以下命令传给他们:
$ docker image pull redis:alpine
$ docker image pull russmckendrick/moby-counter
$ docker network create moby-counter
$ docker container run -d --name redis --network moby-counter redis:alpine
$ docker container run -d --name moby-counter --network moby-counter -p 8080:80 russmckendrick/moby-counter
毋庸置疑,如果我的同事的本地机器上没有镜像,图像将在两个docker run命令执行时被拉取,我可以忽略前两个命令,但随着应用程序变得越来越复杂,我将不得不开始传递一组不断增长的命令和指令。
我还必须明确指出,他们必须考虑命令需要执行的顺序。此外,我的笔记还必须包括任何潜在问题的详细信息,以支持他们解决任何可能出现的问题——这可能导致我们陷入“在开发中运行良好,但在运维中出现问题”的场景,我们务必要尽量避免。
尽管 Docker 的责任应该只限于创建镜像并使用这些镜像启动容器,但 Docker 的创建者预见到了这种情况,并力求克服这一问题。多亏了 Docker,人们再也不必担心他们启动应用程序的环境不一致,因为现在可以通过镜像来进行部署。
Orchard Laboratories
在我们讨论 Docker Compose 之前,让我们快速回顾一下 2014 年 7 月,当时 Docker 收购了一家名为 Orchard Laboratories 的英国初创公司,该公司提供了两种基于容器的产品。
两个产品中的第一个是基于 Docker 的托管平台。通过一个单一命令orchard,您可以启动一个主机机器,然后将您的 Docker 命令代理到新启动的主机上;例如,您将使用以下命令:
$ orchard hosts create
$ orchard docker run -p 6379:6379 -d orchardup/redis
这些命令将在 Orchard 的平台上启动一个 Docker 主机,然后是一个 Redis 容器。我说“将会”,因为当 Docker 收购 Orchard Laboratories 时,他们首先做的事情之一是停止了 Orchard 托管服务。
Orchard Laboratories 的第二个产品是一个名为 Fig 的开源命令行工具,当时 Docker 收购 Orchard Laboratories 时,他们正是看中了这一点。Fig 是用 Python 编写的,它让您可以使用一个 YAML 文件来定义多容器应用程序的结构。Fig 接收 YAML 文件并指示 Docker 按定义启动容器。
这样做的好处是,因为它是一个 YAML 文件,对于开发人员来说,在其代码库中开始使用fig.yml文件与其 Dockerfile 一起进行部署是非常简单的。一个典型的fig.yml文件如下所示:
web:
image: web
links:
- db
ports:
- "8000:8000"
- "49100:22"
db:
image: postgres
要启动在fig.yml文件中定义的两个容器,您必须在存储fig.yml文件的同一文件夹内运行以下命令:
$ fig up
您可能已经注意到,我过去一直在提到 Fig,并且这是因为在 2015 年 2 月,Fig 成为了 Docker Compose。在本章的下一部分,我们将看看如何启动我们的第一个 Docker Compose 应用程序,而您将首先注意到的是,定义应用程序的语法与原始 Fig 语法非常接近。
制作我们的第一个 Docker Compose 应用程序
在我们安装 Docker for Mac、Docker for Windows 和 Docker for Linux 的过程中,在第一章**《Docker 概述》中,我们安装了 Docker Compose,所以我们不再进一步讨论它的作用,让我们尝试仅使用 Docker Compose 来启动我们在上一章末尾手动启动的两容器应用程序。
如前所述,Docker Compose 使用 YAML 文件,通常命名为docker-compose.yml,来定义你的多容器应用程序应该是什么样子。我们在第四章**《容器管理》中启动的两容器应用程序的 Docker Compose 表示如下:
version: "3.7"
services:
redis:
image: redis:alpine
volumes:
- redis_data:/data
restart: always
mobycounter:
depends_on:
- redis
image: russmckendrick/moby-counter
ports:
- "8080:80"
restart: always
volumes:
redis_data:
即使不逐行分析文件内容,基于我们在前几章中使用的命令,你应该能够很容易地跟踪正在发生的事情——我们将在本章的下一节中查看docker-compose.yml文件的内容。
要启动我们的应用程序,我们只需切换到包含docker-compose.yml文件的文件夹,并运行以下命令:
$ docker-compose up
从以下终端输出中可以看到,Docker Compose 启动我们的应用程序时发生了不少事情:

图 5.1 – docker-compose up 的输出
你可以在前几行中看到 Docker Compose 执行了以下操作:
-
它创建了一个名为
mobycounter_redis_data的卷,使用的是我们在docker-compose.yml文件末尾定义的默认驱动程序。 -
它创建了一个名为
mobycounter_default的网络,使用的是默认网络驱动程序——我们在任何时候都没有要求 Docker Compose 执行此操作。 -
它启动了两个容器,一个名为
mobycounter_redis_1,另一个名为mobycounter_mobycounter_1。
你可能也注意到,在我们的多容器应用程序中,Docker Compose 命名空间前缀加上了mobycounter——它从存储我们 Docker Compose 文件的文件夹中获取了这个名称。
启动后,Docker Compose 附加到mobycounter_redis_1和mobycounter_mobycounter_1并将输出流式传输到我们的终端会话。在终端屏幕上,你可以看到redis_1和mobycounter_1开始相互交互。
当使用docker-compose up运行 Docker Compose 时,它将在前台运行。按下Ctrl + C将停止容器并返回对终端会话的访问。
Docker Compose YAML 文件
在进一步了解 Docker Compose 的使用之前,我们应该深入研究一下docker-compose.yml文件,因为它们是 Docker Compose 的核心。
重要说明
YAML 是一个递归首字母缩略词,代表YAML Ain't Markup Language。它被许多不同的应用程序用于配置和定义结构化数据格式,且这种格式易于人类阅读。你在示例中看到的缩进非常重要,它有助于定义数据的结构。
Moby 计数器应用
我们用来启动多容器应用的docker-compose.yml文件被分为三个独立的部分。
第一部分只是指定我们使用的是哪个版本的 Docker Compose 定义语言;在我们的例子中,由于我们正在运行的是最新版本的 Docker 和 Docker Compose,因此我们使用的是版本 3,如下所示:
version: "3.7"
如前所述,Docker Compose 自 2015 年以来一直存在,在此期间 Docker 客户端和引擎经历了多个不同版本。随着软件更新以包含新特性,现有功能也进行了简化以提高性能,同时一些功能被从核心 Docker 引擎中拆分或完全移除。Docker Compose 一直被维护以保持与旧版本 Docker 的兼容性。
如果在docker-compose.yml文件开头没有声明版本号,Docker Compose 将默认使用版本 1;这与原始的 Fig 语法非常接近,这意味着 Docker Compose 将无法读取我们的docker-compose.yml文件,因为容器是定义在services部分的,并且不支持卷、网络,甚至构建参数——这些我们将在本章后续部分详细介绍。
以下截图展示了如果我们移除版本号会发生的情况:

图 5.2 – docker-compose up 输出
如你所见,尝试在没有声明版本号的情况下运行docker-compose up会以错误结束,因为 Docker Compose 根本不知道如何解释我们定义的内容。
下一部分是我们定义容器的地方;这一部分是services部分。它的格式如下:
services:
----> container name:
--------> container options
------------> sub options
----> container name:
--------> container options
------------> sub options
如你所见,services声明没有任何缩进,然后每个容器有 4 个空格,每个选项有 8 个空格;进一步的选项则使用 12 个空格。空格的数量是个人选择,因为我认为这样有助于提高可读性——重要的是使用空格而不是制表符,并确保文件中的缩进保持一致,因为缩进用于清晰地定义信息块。
在我们的例子中,我们在services部分下定义了两个容器。在以下代码片段中,它们被分开以便于阅读:
services:
redis:
image: redis:alpine
volumes:
- redis_data:/data
restart: always
mobycounter:
depends_on:
- redis
image: russmckendrick/moby-counter
ports:
- "8080:80"
restart: always
初看之下,定义服务的语法与使用docker container run命令启动容器的方式非常相似。我说“相似”是因为,虽然当你阅读定义时它完全合理,但只有在更仔细检查时你才会发现,Docker Compose 的语法与docker container run命令之间其实有很大的区别。
例如,使用docker container run命令时,没有以下这些标志:
-
image:这告诉 Docker Compose 下载并使用哪个镜像。当使用命令行运行docker container run时,这个选项不存在,因为你只能运行一个容器;正如我们在前面的章节中看到的,镜像总是在命令的末尾定义,不需要传递标志。 -
volume:这是--volume标志的等效项,但它可以接受多个卷。它只使用在 Docker Compose YAML 文件中声明的卷;稍后我们会详细讲解。 -
depends_on:这在执行docker container run时是行不通的,因为该命令仅针对单个容器。对于 Docker Compose,depends_on用来帮助构建容器启动顺序的逻辑——例如,只有在容器 A 成功启动后才启动容器 B。 -
ports:这基本上是--publish标志,接受一个端口列表。
我们使用的命令中,唯一在执行 docker container run 时有等效标志的是 restart。这与使用 --restart 标志相同,并接受相同的输入。
我们的 Docker Compose YAML 文件的最后一部分是声明我们的卷,具体如下:
volumes:
redis_data:
这相当于使用 Docker 命令行客户端运行 docker volume create redis_data。
示例投票应用程序
如前所述,Moby 计数器应用程序的 Docker Compose 文件是一个相当简单的示例。让我们来看看一个更复杂的 Docker Compose 文件,并了解如何引入构建容器和多个网络。
重要提示
在本书的仓库中,你会在 chapter05 目录下找到一个名为 example-voting-app 的文件夹。这是官方 Docker 示例仓库中投票应用程序的一个分支。
如你所见,如果打开 docker-compose.yml 文件,应用程序由五个容器、两个网络和一个卷组成。如果我们将这个应用程序可视化,它看起来大致如下:

图 5.3 – docker-compose.yml 的容器结构
忽略其他文件,因为我们将在未来章节中讨论其中的一些内容;让我们通过 docker-compose.yml 文件,因为其中有很多内容。请查看以下代码片段:
version: "3"
services:
如你所见,它首先通过定义版本开始,然后开始列出 service。我们的第一个容器名为 vote,它是一个 Python 应用程序,允许用户提交投票。从下面的定义可以看出,我们并不是下载一个包含应用程序的镜像,而是通过使用 build 来部署我们的应用程序,取代了 image 定义:
vote:
build: ./vote
command: python app.py
volumes:
- ./vote:/app
ports:
- "5000:80"
networks:
- front-tier
- back-tier
这里的build指令告诉 Docker Compose 使用位于./vote文件夹中的 Dockerfile 构建容器。Dockerfile 本身对于一个 Python 应用程序来说相当简单。
一旦容器启动,我们将从主机机器中挂载./vote文件夹到容器中,这通过传递我们要挂载的文件夹路径以及希望它挂载到容器中的位置来实现。
我们告诉容器在启动时运行python app.py命令;将主机机器上的5000端口映射到容器内的80端口;最后,我们还将两个网络连接到容器,一个叫front-tier,另一个叫back-tier。
front-tier网络将包含需要映射端口到主机机器的容器;back-tier网络则用于那些不需要暴露端口的容器,作为一个私有的、隔离的网络。
接下来,我们有另一个与front-tier网络连接的容器。该容器显示投票结果。result容器包含一个 Node.js 应用程序,该程序连接到 PostgreSQL 数据库(稍后我们会介绍),并在投票容器中的投票被提交时实时显示结果。与vote容器一样,该镜像是使用位于./result文件夹中的 Dockerfile 在本地构建的,如下方代码片段所示:
result:
build: ./result
command: nodemon server.js
volumes:
- ./result:/app
ports:
- "5001:80"
- "5858:5858"
networks:
- front-tier
- back-tier
我们暴露了5001端口,这样就可以连接到该端口以查看结果。下一个也是最后一个应用容器叫做worker,如以下代码片段所示:
worker:
build:
context: ./worker
depends_on:
- "redis"
- "db"
networks:
- back-tier
worker容器运行一个.NET 应用程序,其唯一任务是连接到 Redis 并通过将投票转移到一个名为db的容器中的 PostgreSQL 数据库来注册每一票。该容器仍然是通过 Dockerfile 构建的,但这次我们不是传递 Dockerfile 和应用程序所在文件夹的路径,而是使用context。这会为docker build命令设置工作目录,并且还可以定义其他选项,比如标签和更改 Dockerfile 的名称。
由于此容器仅做连接redis和db容器的工作,因此不需要暴露任何端口,因为没有任何直接连接到它的东西;它也不需要与运行在front-tier网络上的容器进行通信,这意味着我们只需要添加back-tier网络。
现在,我们有了vote应用程序,它从最终用户那里注册投票并将其发送到redis容器,在那里投票会被worker容器处理。redis容器的服务定义如下所示:
redis:
image: redis:alpine
container_name: redis
ports: ["6379"]
networks:
- back-tier
这个容器使用官方的 Redis 镜像,并不是从 Dockerfile 构建的;我们确保端口 6379 可用,但仅在 back-tier 网络上可用。我们还指定了容器的名称,使用 container_name 将其设置为 redis。这样可以避免我们在代码中考虑 Docker Compose 默认生成的名称,因为,正如你所记得的,Docker Compose 使用文件夹名称来在自己的应用程序命名空间中启动容器。
下一个也是最后一个容器是 PostgreSQL 容器(我们之前已经提到过),名为 db,如下代码片段所示:
db:
image: postgres:9.4
container_name: db
volumes:
- "db-data:/var/lib/postgresql/data"
networks:
- back-tier
如你所见,它与 redis 容器非常相似,因为我们使用的是官方镜像;然而,你可能注意到我们没有暴露端口,因为这是官方镜像的默认选项。我们还指定了容器的名称。
由于这是我们存储投票的地方,我们正在创建并挂载一个卷,作为 PostgreSQL 数据库的持久存储,如下所示:
volumes:
db-data:
接下来,最后是我们在定义应用程序容器时所提到的两个网络:
networks:
front-tier:
back-tier:
运行 docker-compose up 会输出大量关于启动过程的反馈;第一次启动应用程序大约需要 5 分钟。如果你没有跟随并自行启动应用程序,接下来是简化版的启动过程。
提示
你可能会遇到一个错误,提示 npm ERR! request to https://registry.npmjs.org/nodemon failed, reason: Hostname/IP doesn't match certificate's altnames。如果遇到此错误,请以有权限写入 /etc/hosts 文件的用户身份运行命令 echo "104.16.16.35 registry.npmjs.org" >> /etc/hosts。
Docker Compose 首先创建网络,并为我们的容器准备好卷,如下所示:
Creating network "example-voting-app_front-tier" with the default driver
Creating network "example-voting-app_back-tier" with the default driver
Creating volume "example-voting-app_db-data" with default driver
然后,它构建 vote 容器镜像,如下所示:
Building vote
Step 1/7 : FROM python:2.7-alpine
2.7-alpine: Pulling from library/python
c9b1b535fdd9: Already exists
fea5c17ab132: Pull complete
5dbe995357bf: Pull complete
b6d238951af6: Pull complete
Digest: sha256:5217b150a5f7eecea55f6224440f3b5 c5f975edc32de7c0bfdf98280ed11d76c
Status: Downloaded newer image for python:2.7-alpine
现在,图像已经下载完毕,可以开始构建应用程序的第一部分,如下所示:
---> 7ec8514e7bc5
Step 2/7 : WORKDIR /app
---> Running in 7d26310faa98
Removing intermediate container 7d26310faa98
---> 8930ad501196
Step 3/7 : ADD requirements.txt /app/requirements.txt
---> 33ff980bd133
Step 4/7 : RUN pip install -r requirements.txt
---> Running in 999e575570ef
[lots of python build output here]
Removing intermediate container 999e575570ef
---> 72637119e7df
Step 5/7 : ADD . /app
---> 81adb9e92ce4
Step 6/7 : EXPOSE 80
---> Running in a5aaf5b9ed1b
Removing intermediate container a5aaf5b9ed1b
---> 366d2e32ceb4
Step 7/7 : CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--log-file", "-", "--access-logfile", "-", "--workers", "4", "--keep-alive", "0"]
---> Running in 212e82c06cf3
Removing intermediate container 212e82c06cf3
---> 4553ffa35ea4
Successfully built 4553ffa35ea4
镜像构建完成后,它将被标记,如下所示:
Successfully tagged example-voting-app_vote:latest
WARNING: Image for service vote was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
一旦 Docker Compose 构建了 vote 镜像,它将开始构建 result 镜像,如下所示:
Building result
Step 1/9 : FROM node:10-slim
10-slim: Pulling from library/node
619014d83c02: Pull complete
8c5d9aed65fb: Pull complete
ec6ca7c6739a: Pull complete
6da8fc40e075: Pull complete
6161f60894b2: Pull complete
Digest: sha256:10c4d19a2a2fa5ad416bddb3a4b208e 34b0d4263c3978df6aa06d9ba9687bbe8
Status: Downloaded newer image for node:10-slim
---> ad4ea09bf0f3
Again, now the images have been downloaded the build of the image containing the application can start:
Step 2/9 : WORKDIR /app
---> Running in 040efda3a918
Removing intermediate container 040efda3a918
---> 3d3326950331
Step 3/9 : RUN npm install -g nodemon
---> Running in a0ce3043aba5
[lots of nodejs build output here]
Removing intermediate container a0ce3043aba5
---> 925a30942e5f
Step 4/9 : COPY package*.json ./
---> 9fd59fddc0e8
Step 5/9 : RUN npm ci && npm cache clean --force && mv /app/node_modules /node_modules
---> Running in 3c0871538d04
[lots of nodejs build output here]
Removing intermediate container 3c0871538d04
---> 8db74baa1959
Step 6/9 : COPY . .
---> a47af934177b
Step 7/9 : ENV PORT 80
---> Running in 57f80f86faf0
Removing intermediate container 57f80f86faf0
---> e5a01939876b
Step 8/9 : EXPOSE 80
---> Running in 614bd7bd4ab3
Removing intermediate container 614bd7bd4ab3
---> 461355b7e66e
Step 9/9 : CMD ["node", "server.js"]
---> Running in 4c64da5f054c
Removing intermediate container 4c64da5f054c
---> 65c854a0b292
Successfully built 65c854a0b292
Successfully tagged example-voting-app_result:latest
WARNING: Image for service result was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
随后是从 Docker Hub 拉取 db 容器的 PostgreSQL 镜像,如下所示:
Pulling db (postgres:9.4)...
9.4: Pulling from library/postgres
619014d83c02: Already exists
7ec0fe6664f6: Pull complete
9ca7ba8f7764: Pull complete
9e1155d037e2: Pull complete
febcfb7f8870: Pull complete
8c78c79412b5: Pull complete
5a35744405c5: Pull complete
27717922e067: Pull complete
8e8ebde0a697: Pull complete
f6d85e336541: Pull complete
c802081bbe1e: Pull complete
f35abd4ea98b: Pull complete
50335e437328: Pull complete
a1c34d9ddebb: Pull complete
Digest: sha256:d6bc1739199cc52f038f54e1ab 671f5229d114fb667e9ad08add6cd66e8a9b28
Status: Downloaded newer image for postgres:9.4
最后,构建 worker 镜像,如下所示:
Building worker
Step 1/5 : FROM microsoft/dotnet:2.0.0-sdk
2.0.0-sdk: Pulling from microsoft/dotnet
3e17c6eae66c: Pull complete
74d44b20f851: Pull complete
a156217f3fa4: Pull complete
4a1ed13b6faa: Pull complete
18842ff6b0bf: Pull complete
e857bd06f538: Pull complete
b800e4c6f9e9: Pull complete
Digest: sha256:f4ea9cdf980bb9512523a3fb88e 30f2b83cce4b0cddd2972bc36685461081e2f
Status: Downloaded newer image for microsoft/dotnet:2.0.0-sdk
Now that the SDK images have been downloaded Docker Compose can build the application:
---> fde8197d13f4
Step 2/5 : WORKDIR /code
---> Running in ac782e4c8cb2
Removing intermediate container ac782e4c8cb2
---> 3881e09f0d22
Step 3/5 : ADD src/Worker /code/src/Worker
---> cf0468608709
Step 4/5 : RUN dotnet restore -v minimal src/Worker && dotnet publish -c Release -o "./" "src/Worker/"
---> Running in ca04867b0e86
[lots of .net build output here]
Worker -> /code/src/Worker/bin/Release/netcoreapp2.0/Worker.dll
Worker -> /code/src/Worker/
Removing intermediate container ca04867b0e86
---> 190aee9b4b98
Step 5/5 : CMD dotnet src/Worker/Worker.dll
---> Running in 069b5806b25e
Removing intermediate container 069b5806b25e
---> 56c488a158bb
Successfully built 56c488a158bb
Successfully tagged example-voting-app_worker:latest
WARNING: Image for service worker was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
你可能注意到,redis 容器使用的 Redis 镜像并没有被拉取——这是因为最新版本已经下载完毕。现在,所有镜像都已经构建或拉取完毕,网络和卷也已经就绪,Docker Compose 可以启动我们的应用程序,如下所示:
Creating redis ... done
Creating db ... done
Creating example-voting-app_vote_1 ... done
Creating example-voting-app_result_1 ... done
Creating example-voting-app_worker_1 ... done
Attaching to db, redis, example-voting-app_worker_1, example-voting-app_vote_1, example-voting-app_result_1
应用程序的 result 部分可以通过 http://localhost:5001 访问。默认情况下,没有投票,且结果是 50/50,如下图所示:
![图 5.4 – 默认投票统计]
图 5.4 – 默认投票统计
应用程序的 voting 部分可以在 http://localhost:5000 上访问,下面是该页面的截图:

图 5.5 – 投票界面
点击 CATS 或 DOGS 其中一个选项将注册一票;你应该能够在终端中看到类似的 Docker Compose 输出,如下所示:

图 5.6 – 显示投票的 docker-compute 输出
这里有一些错误,因为 Redis 表结构只有在投票应用注册第一票时才会创建;一旦投票完成,Redis 表结构将被创建,worker 容器将处理这票并写入 db 容器。投票完成后,result 容器将实时更新,如下图所示:

图 5.7 – 投票后结果页面
在接下来的章节中,我们将再次查看 Docker Compose YAML 文件,当我们讨论如何启动 Docker Swarm 堆栈和 Kubernetes 集群时。现在,让我们回到 Docker Compose,看看我们可以运行的一些命令。
探索 Docker Compose 命令
我们已经完成了本章的一半,至今为止我们执行的唯一 Docker Compose 命令是docker-compose up。如果你一直在跟着操作并运行docker container ls -a,你会看到类似以下终端画面的内容:

图 5.8 – 执行 docker container ls -a 的输出
如你所见,我们有很多容器的状态是 Exited。这是因为当我们按下 Ctrl + C 返回到终端时,Docker Compose 容器被停止了。
选择一个 Docker Compose 应用,切换到包含 docker-compose.yml 文件的文件夹,我们将继续操作更多的 Docker Compose 命令。我将使用 Example Vote 应用。
up 和 ps
第一个命令是 docker-compose up,但这次我们将添加一个标志。在你选择的应用文件夹中,运行以下命令:
$ docker-compose up -d
这将重新启动你的应用,这次是以分离模式运行,它会将你带回终端提示符,如下所示:

图 5.9 – 执行 docker-compose up -d 的输出
一旦终端控制权返回,你应该能够使用以下命令检查容器是否在运行:
$ docker-compose ps
如下终端输出所示,你会看到所有容器的状态都是 Up:

图 5.10 – 执行 docker-compose ps 的输出
在运行这些命令时,Docker Compose 只会识别在 docker-compose.yml 文件的 services 部分中定义的容器;所有其他容器将被忽略,因为它们不属于我们的服务堆栈。
config
运行以下命令将验证我们的 docker-compose.yml 文件:
$ docker-compose config
如果没有问题,它将打印出 Docker Compose 解释您文件的渲染副本。如果您不想查看这个输出,只想检查是否有错误,那么您可以运行以下命令:
$ docker-compose config -q
这是 --quiet 的简写。如果有任何错误(根据我们迄今为止的示例,应该没有),它们将如下所示:
ERROR: yaml.parser.ParserError: while parsing a block mapping in "./docker- compose.yml", line 1, column 1 expected <block end>, but found '<block mapping start>' in "./docker-compose.yml", line 27, column 3
pull、build 和 create
接下来的两个命令将帮助您准备启动 Docker Compose 应用程序。以下命令将读取 Docker Compose YAML 文件并拉取它找到的任何镜像:
$ docker-compose pull
以下命令将执行文件中找到的任何构建指令:
$ docker-compose build
当你第一次定义基于 Docker Compose 的应用程序并希望在不启动应用程序的情况下进行测试时,这些命令非常有用。docker-compose build 命令也可以用来触发构建,如果用于最初构建镜像的任何 Dockerfile 有更新的话。
pull 和 build 命令只生成/拉取我们应用程序所需的镜像;它们不会配置容器本身。为此,我们需要使用以下命令:
$ docker-compose create
这将创建容器,但不会启动它们。与 docker container create 命令一样,容器将处于 Exited 状态,直到您启动它们。create 命令有一些有用的标志可以传递,具体如下:
-
--force-recreate:即使配置中没有变化,也会重新创建容器。 -
--no-recreate:如果容器已经存在,它不会重新创建该容器;此标志不能与前面的标志一起使用。 -
--no-build:即使缺少需要构建的镜像,也不会构建镜像。 -
--build:在创建容器之前构建镜像。
start、stop、restart、pause 和 unpause
以下命令的工作方式与其 docker container 对应命令完全相同,唯一的区别是它们对所有容器产生影响:
$ docker-compose start
$ docker-compose stop
$ docker-compose restart
$ docker-compose pause
$ docker-compose unpause
可以通过传递服务名称来定位单个服务;例如,要暂停和恢复 db 服务,我们可以运行以下命令:
$ docker-compose pause db
$ docker-compose unpause db
现在我们知道如何停止和启动整个或部分的 Docker Compose 应用程序,我们可以看看如何查看应用程序的更多信息。
top、logs、events、exec 和 run
接下来的三个命令都为我们提供有关正在运行的容器和 Docker Compose 的反馈信息。
以下命令,与其 docker container 对应命令一样,显示每个 Docker Compose 启动的容器内正在运行的进程信息:
$ docker-compose top
如下所示,从终端输出中可以看到,每个容器被拆分到自己的区域:

图 5.11 – docker-compose top 的输出
如果您只想查看其中一个服务,只需在运行命令时传递其名称,如下所示:
$ docker-compose top db
以下命令会将每个正在运行的容器的日志流式传输到屏幕:
$ docker-compose logs
与 docker container 命令一样,您可以传递诸如 -f 或 --follow 的标志,保持流式传输直到按下 Ctrl + C。此外,您还可以通过将服务的名称附加到命令末尾,来流式传输单个服务的日志,如下所示:

图 5.12 – 日志流
events 命令再次类似于 docker container 命令的功能;它实时流式传输事件——例如我们之前讨论的其他命令触发的事件。比如,运行以下命令:
$ docker-compose events
在第二个终端窗口中运行 docker-compose pause,将给出以下输出:

图 5.13 – docker-compose events 的输出
这两个命令的运行方式与其 docker container 等效命令类似。运行以下命令:
$ docker-compose exec worker ping -c 3 db
这将在已经运行的 worker 容器中启动一个新进程,并向 db 容器发送三次 ping 请求,如下所示:

图 5.14 – docker-compose exec worker ping -c 3 db 的输出
run 命令非常有用,如果您需要在应用程序中作为一次性任务运行一个容器化的命令。例如,如果您使用像 composer 这样的包管理器来更新存储在卷中的项目依赖关系,您可以运行如下命令:
$ docker-compose run --volume data_volume:/app composer install
这将运行 composer 容器,并使用 install 命令,并在容器内的 /app 路径挂载 data_volume。
scale
scale 命令将接收您传递给命令的服务,并将其扩展到您定义的数量。例如,要添加更多的 worker 容器,我只需要运行以下命令:
$ docker-compose scale worker=3
但是,这实际上会给出以下警告:
WARNING: The scale command is deprecated. Use the up command with the - scale flag instead.
我们现在应该使用的是以下命令:
$ docker-compose up -d --scale worker=3
虽然 scale 命令在当前版本的 Docker Compose 中可用,但它将在未来的版本中被移除。
您会注意到,我选择了扩展 worker 容器的数量。这是有原因的,如果您尝试运行以下命令,您将亲眼看到:
$ docker-compose up -d --scale vote=3
您会注意到,尽管 Docker Compose 创建了另外两个容器,但它们未能启动,并出现以下错误:

图 5.15 – docker-compose up -d --scale vote=3 的输出
这是因为我们不能让三个独立的容器都尝试映射到主机上的相同端口;因此,你应该始终在未显式定义端口映射的容器上使用scale命令。
我们最后要介绍的三个 Docker Compose 命令是删除/终止 Docker Compose 应用的命令。第一个命令通过立即停止正在运行的容器进程来停止我们的运行中的容器。这个命令是kill,如下面所示:
$ docker-compose kill
运行此命令时请小心,因为它不会等待容器优雅地停止,例如在运行docker-compose stop时,这意味着使用docker-compose kill命令可能会导致数据丢失。
接下来是rm命令;这个命令会删除状态为exited的容器,具体如下面所示:
$ docker-compose rm
最后,我们有down命令。正如你可能已经猜到的,它与运行docker-compose up的效果相反,如下所示:
$ docker-compose down
该命令会删除在运行docker-compose up时创建的容器和网络。如果你想删除所有内容,可以通过运行以下命令来实现:
$ docker-compose down --rmi all --volumes
该命令将删除你在运行docker-compose up命令时创建的所有容器、网络、卷和镜像(包括拉取和构建的镜像);这包括可能在你的 Docker Compose 应用外部使用的镜像。
然而,如果镜像正在使用中,将会出现错误,并且这些镜像不会被删除,如下图所示:

图 5.16 – docker-compose down --rmi all --volumes的输出
从前面的输出中可以看到,有一个使用redis镜像的容器,即 Moby 计数器应用,因此它没有被删除。然而,Example Vote应用使用的所有其他镜像都被删除了,包括通过初始的docker-compose up命令构建的镜像和从 Docker Hub 下载的镜像。
使用 Docker App
在开始本节之前,我需要发出以下警告:
重要提示
我们将要讨论的这个功能非常实验性。它处于开发的初期阶段,不能被视为一个成熟的功能,而只能视为即将发布的功能的预览。
由于其实验性质,我只会介绍在 macOS 上使用 Docker App。然而,在启用它之前,我们先来讨论什么是 Docker 应用。虽然 Docker Compose 文件在与他人共享环境时非常有用,但你可能已经注意到,在本章中有一个非常关键的元素我们至今还没有提到,那就是能够像分发 Docker 镜像一样分发你的 Docker Compose 文件的能力。
Docker 已经意识到这个问题,并且目前正在开发一个名为 Docker App 的新功能,希望它能填补这个空白。
Docker App 当前是一个命令行客户端插件,可以帮助你创建可以通过 Docker Hub 或 Docker 企业注册表共享的应用程序包。该插件内置于 Docker 19.03 中,你只需打开 Docker Desktop 设置并切换开启 启用实验性功能,如下所示的屏幕截图:

图 5.17 – Docker desktop 启用实验性功能屏幕
我们将使用 Moby Counter 应用程序,因为它的docker-compose.yml文件已经满足 Docker App 打包的前提要求,且我们正在使用 3.6 及以上版本——在我们这个例子中是 3.7。
定义应用程序
让我们按照以下步骤开始:
-
我们需要创建 Docker App 配置。为此,请切换到
mobycounter文件夹并运行以下命令:mobycounterapp.dockerapp. In that folder, there are three files, as can be seen in the following screenshot:应用程序的版本
版本:0.1.0
应用程序的名称
名称:mobycounterapp
应用程序的简短描述
描述:
包含每个维护者的姓名和邮箱的应用程序维护者列表
维护者:
- 名称:russ.mckendrick
邮箱:
-
让我们从更新
metadata.yml文件开始。我将我的更新为如下内容:# Version of the application version: 0.1.0 # Name of the application name: mobycounterapp # A short description of the application description: Places whales on screen wherever you click !!! # List of application maintainers with name and email for each maintainers: - name: Russ McKendrick email: russ@mckendrick.io上述信息将用于我们分发应用程序。最初,我们将通过 Docker Hub 进行分发,因此请确保你同意这些信息对其他人可见。
-
现在我们有了元数据,接下来在
parameters.yml文件中添加一些参数,如下所示:{ "port": "8080" } -
最后,更新
docker-compose.yml文件,使用我们刚刚定义的参数,如下所示:version: "3.7" services: redis: image: redis:alpine volumes: - redis_data:/data restart: always mobycounter: depends_on: - redis image: russmckendrick/moby-counter ports: - "${port}:80" restart: always volumes: redis_data:
如你所见,我已将${port}添加到docker-compose.yml文件中。当我们启动应用程序时,值将从parameters.yml文件中填充。
验证和检查应用程序
我们可以通过运行以下命令来双重检查我们所做的更改是否正确:
$ docker app validate mobycounterapp.dockerapp
$ docker app inspect mobycounterapp.dockerapp
如果一切正常,终端输出应类似如下内容:

图 5.19 – docker app inspect mobycounterapp.dockerapp 输出
一旦我们验证了应用程序,就可以继续启动它了。
启动应用程序
为了能够原生启动应用程序,我们需要运行一个 Docker Swarm 集群(我们将在未来的章节中详细讲解 Docker Swarm)。实现这一目标的步骤如下:
-
开始时运行以下命令:
$ docker swarm init暂时忽略输出——我们现在不需要创建集群,只需一个节点即可。
-
既然我们已经启用了 Docker Swarm,现在可以使用以下命令安装应用程序:
$ docker app install mobycounterapp.dockerapp --name mobycounterapp -
安装完成后,你可以运行以下命令来检查应用程序的状态:
$ docker app list
你应该看到类似下面的终端输出:

图 5.20 – docker app install 和 docker app list 的输出
该应用程序也应该可以通过http://localhost:8080访问。
推送到 Docker Hub
在应用程序运行时,我们可以将它发布到 Docker Hub。为此,请按以下步骤操作:
-
如果你还没有登录,请通过运行以下命令进行登录:
$ docker login -
登录后,运行以下命令,确保更新 Docker Hub ID(在我的情况下是 russmckendrick),用你自己的 ID 替换:
$ docker app push mobycounterapp --platform="linux/amd64" --tag russmckendrick/mobycounterapp:0.1.0
一分钟后,你应该会看到一条消息,提示包已成功推送,如下图所示:

图 5.21 – hub.docker.com 页面
在浏览器中打开 https://hub.docker.com/应该会显示该应用程序,正如图 5.21所示。
从 Docker Hub 安装
首先,让我们创建一个临时文件夹并卸载应用程序,如下所示:
$ mkdir /tmp/testing
$ cd /tmp/testing
$ docker app uninstall mobycounterapp
$ docker app ls
如你所见,如果你正在跟随步骤进行操作,我们没有运行任何应用程序,而且我们已经改变了文件夹位置,远离了原来的docker-compose.yml文件和mobycounterapp.dockerapp文件夹,所以我们知道它们都不会被使用。
接下来,我们可以通过运行以下命令直接从 Docker Hub 检查应用程序(再次,确保将 Docker Hub ID 替换为你自己的):
$ docker app inspect russmckendrick/mobycounterapp:0.1.0
我们应该看到与我们上次本地运行命令时相似的应用程序信息。首先,让我们使用 Docker Hub 托管版本的应用程序创建一个新的docker-compose.yml文件。为此,请运行以下命令:
$ docker app render --output docker-compose.yml russmckendrick/mobycounterapp:0.1.0
这将会在当前工作目录下创建一个docker-compose.yml文件;然后,我们可以运行以下命令:
$ docker-compose up -d
$ docker-compose ps
你会收到一个警告,提示节点正在运行 Swarm模式;目前可以忽略此警告。你应该能看到两个容器正在运行,并且你的应用程序再次可以通过http://localhost:8080访问。
接下来,让我们重新本地启动应用程序,但这次是和我们通过 Docker Compose 启动的版本一起运行。为此,请运行以下命令:
$ docker app install russmckendrick/mobycounterapp:0.1.0 --set port=8181 --name mobycounterapp8181
如你所见,我们正在启动一个名为mobycounterapp8181的应用程序。我们还使用了--set命令来覆盖我们在parameters.yml文件中最初设置的默认端口8080,将其更改为8181。如果一切顺利,你应该能够通过http://localhost:8181访问该应用程序。
Docker App 有更多功能。然而,我们还不准备深入探讨这些功能。我们将在第八章**, Docker Swarm 和 第十一章**, Docker 和 Kubernetes中进一步介绍。
正如本节顶部所提到的,这个功能仍在积极开发中,至今我们讨论的命令和功能可能会在未来有所变化。但即使在这个开发阶段,我希望你能看到 Docker App 的优势,以及它是如何建立在 Docker Compose 打下的坚实基础上的。
总结
希望你喜欢这一章关于 Docker Compose 的内容,我也希望像我一样,你能看到它是如何从一个极其有用的第三方工具,发展成为 Docker 核心体验中一个极其重要的部分。
Docker Compose 介绍了一些关键概念,帮助你理解如何运行和管理容器。我们将在第八章**,Docker Swarm和第十一章**,Docker 与 Kubernetes中进一步探讨这些概念,在这些章节中,我们将开始讨论如何管理多个 Docker 主机以及如何将容器分布到这些主机上。
在下一章中,我们将不再讨论基于 Linux 的容器,而是快速浏览一下 Windows 容器。
问题
-
Docker Compose 文件使用哪种开源格式?
-
在我们最初的 Moby 反向 Docker Compose 文件中,哪一个标志的工作方式与 Docker 的命令行接口(CLI)完全相同?
-
对与错:你只能使用来自 Docker Hub 的镜像来搭建 Docker Compose 文件吗?
-
默认情况下,Docker Compose 如何决定使用哪个命名空间?
-
要使容器在后台启动,你需要在 docker-compose up 命令中添加哪个标志?
-
运行 Docker Compose 文件时,检查语法的最佳方法是什么?
-
解释 Docker App 如何工作的基本原理。
进一步阅读
有关 Orchard Laboratories 的详细信息,请参见以下内容:
-
Orchard Laboratories 官网:
web.archive.org/web/20171020135129/https://www.orchardup.com/ -
Orchard Laboratories 加入 Docker:
www.docker.com/blog/welcoming-the-orchard-and-fig-team/
要查看完整的 Docker Compose 兼容性矩阵,请参见以下内容:
- Compose 文件版本和升级:
docs.docker.com/compose/compose-file/compose-versioning/
有关 Docker App 项目的更多信息,请参见以下内容:
- GitHub 仓库:
github.com/docker/app/
最后,以下是我们所涵盖的其他主题的一些进一步链接:
-
YAML 项目主页:
www.yaml.org/ -
Docker 示例仓库:
github.com/dockersamples/
第六章:Docker Machine、Vagrant 和 Multipass
本章中,我们将深入探讨 Docker Machine。它可以用于轻松启动并引导目标平台的 Docker 主机,包括本地或云环境。我们还将讨论 Vagrant 和 Multipass,它们是可以用来启动本地 Docker 主机的替代工具。
让我们来看一下本章将涵盖的内容。我们将探讨以下主题:
-
Docker Machine 简介
-
使用 Docker Machine 部署本地 Docker 主机
-
使用 Docker Machine 在云中启动 Docker 主机
-
使用 Vagrant 和 Multipass 启动本地 Docker 主机
-
介绍并使用 Multipass
技术要求
与前几章一样,我们将继续使用本地的 Docker 安装。再次强调,本章中的截图将来自我偏好的操作系统 macOS。我们将查看如何使用 Docker Machine 在本地通过 VirtualBox 以及在公共云中启动基于 Docker 的虚拟机(VMs),因此如果你想跟着本章的示例操作,你需要一个 DigitalOcean 账户。
如前所述,我们将运行的 Docker 命令将在我们已经安装了 Docker 的所有三大操作系统上有效。然而,一些偶尔使用的支持命令可能仅适用于基于 macOS 和 Linux 的操作系统。
查看以下视频,了解代码如何运行:bit.ly/2R6QQmd
Docker Machine 简介
在我们挽起袖子,深入使用 Docker Machine 之前,应该先花点时间讨论一下它在整个 Docker 生态系统中所占的位置,以及它究竟是什么。
Docker Machine 最大的优势在于,它为多个公共云提供商(如Amazon Web Services(AWS)、DigitalOcean、Microsoft Azure和Google Cloud)以及自托管的虚拟机/云平台(包括OpenStack和VMware vSphere)提供了一致的接口,用于快速启动和配置单个 Docker 主机。能够通过单个命令并且最小化用户交互来针对所有这些技术进行操作,节省了大量时间。如果某天你需要快速访问 AWS 中的 Docker 主机,第二天又需要在 DigitalOcean 中访问,你知道你会获得一致的体验。
此外,它还允许你启动本地 Docker 主机,这对于某些操作系统(例如非专业版的 Windows 10)非常有用,因为这些操作系统由于缺乏虚拟化支持,无法原生运行 Docker。
与 Docker Compose 一样,过去它曾与 Docker for Windows 和 Docker for Mac 一起捆绑发布——然而,这一功能已经从最近的版本中去除。这是因为终端用户不再需要启动和管理单独的 Docker 主机。现在,人们更倾向于使用集群技术,如 Docker Swarm 或 Kubernetes,或者使用云服务商的原生 Docker 托管工具,所有这些内容我们将在接下来的章节中详细讨论。
重要提示
本章中我们将讨论的部分工具现在已经被视为遗留工具,并且对它们的支持已经开始逐步取消。之所以提到它们,是因为对于使用旧硬件的用户来说,这可能是他们唯一能体验 Docker 的方式。
我们将从快速讨论其中一个遗留工具开始。
使用 Docker Toolbox 安装 Docker Machine
如果您使用的 macOS 或 Windows 版本不支持我们在前几章中使用的 Docker for Windows 和 Docker for Mac 版本,那么您可以下载并安装 Docker Toolbox,它将安装 Docker 客户端、Docker Compose 和 Docker Machine。您可以从 github.com/docker/toolbox/ 下载 Docker Toolbox;但是,如果您已经在运行 Docker,请不要安装它,因为它可能会与您现有的安装产生冲突,进而造成问题。
Docker Toolbox 现在被视为遗留工具,安装的 Docker 及其支持工具的版本已经较旧;因此,我们在这里不会介绍它们的安装。
使用命令行安装 Docker Machine
如果您已经在运行 Docker,那么在我们开始使用 Docker Machine 之前,我们需要先安装它。以下是您需要在 macOS 和 Linux 上运行的安装 Docker Machine 的命令,从 macOS 开始:
$ base=https://github.com/docker/machine/releases/download/v0.16.2 &&
curl -L $base/docker-machine-$(uname -s)-$(uname -m) >/usr/local/bin/docker-machine &&
chmod +x /usr/local/bin/docker-machine
对于 Linux,您可以使用类似的命令,如下所示:
$ curl -L https://github.com/docker/machine/releases/download/v0.16.2/docker-machine-`uname -s`-`uname -m` >/tmp/docker-machine &&
chmod +x /tmp/docker-machine &&
sudo cp /tmp/docker-machine /usr/local/bin/docker-machine
对于 Windows,以下命令假设您已安装 Git bash:
$ if [[ ! -d '$HOME/bin' ]]; then mkdir -p '$HOME/bin'; fi && \
curl -L https://github.com/docker/machine/releases/download/v0.16.2/docker-machine-Windows-x86_64.exe > '$HOME/bin/docker-machine.exe' && \
chmod +x '$HOME/bin/docker-machine.exe'
如您所见,所有三个命令都在从项目的发布页面下载一个可执行文件。为了确保您使用的是最新版本,您可以查看 github.com/docker/machine/releases/。
现在我们已经在您选择的操作系统上安装了 Docker Machine,接下来可以开始部署运行 Docker 的虚拟机。
使用 Docker Machine 部署本地 Docker 主机
在我们体验云端之前,我们将通过启动 Docker Machine、使用 Oracle VirtualBox 提供虚拟机,来了解 Docker Machine 的基本操作。
重要提示
VirtualBox 是 Oracle 提供的一款免费的虚拟化产品。它允许您在多个平台和中央处理单元(CPU)类型上安装虚拟机。您可以从www.virtualbox.org/wiki/Downloads/下载并安装 VirtualBox。
要启动机器,您只需要运行以下命令:
$ docker-machine create --driver virtualbox docker-local
这将开始部署过程,在此过程中你将看到 Docker Machine 正在运行的任务列表。为了启动 Docker 主机,Docker Machine 启动的每个主机都会经过相同的步骤。
首先,Docker Machine 会运行一些基本检查,比如确认 VirtualBox 是否安装,并创建证书和目录结构,以便存储所有文件和虚拟机,具体如下:
Creating CA: /Users/russ.mckendrick/.docker/machine/certs/ca.pem
Creating client certificate: /Users/russ.mckendrick/.docker/machine/certs/cert.pem
Running pre-create checks...
(docker-local) Image cache directory does not exist, creating it at /Users/russ.mckendrick/.docker/machine/cache...
然后,它会检查是否存在将用于虚拟机的镜像。如果镜像不存在,系统将下载该镜像,如以下代码片段所示:
(docker-local) No default Boot2Docker ISO found locally, downloading the latest release...
(docker-local) Latest release for github.com/boot2docker/boot2docker is v19.03.5
(docker-local) Downloading /Users/russ.mckendrick/.docker/machine/cache/boot2docker.iso from https://github.com/boot2docker/boot2docker/releases/download/v19.03.5/boot2docker.iso...
(docker-local) 0%....10%....20%....30%....40%....50%....60%....70%....80%....90%....100%
一旦检查通过,它会使用所选的驱动程序创建虚拟机,如以下代码片段所示:
Creating machine...
(docker-local) Copying /Users/russ.mckendrick/.docker/machine/cache/boot2docker.iso to /Users/russ.mckendrick/.docker/machine/machines/docker-local/boot2docker.iso...
(docker-local) Creating VirtualBox VM...
(docker-local) Creating SSH key...
(docker-local) Starting the VM...
(docker-local) Check network to re-create if needed...
(docker-local) Found a new host-only adapter: 'vboxnet0'
(docker-local) Waiting for an IP...
Waiting for machine to be running, this may take a few minutes...
如你所见,Docker Machine 会为虚拟机创建一个独特的安全外壳 (SSH) 密钥。这意味着你将能够通过 SSH 访问虚拟机,稍后会详细说明。一旦虚拟机启动,Docker Machine 将建立与虚拟机的连接,如以下代码片段所示:
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with boot2docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
如你所见,Docker Machine 会检测正在使用的操作系统,并选择适当的引导脚本来部署 Docker。一旦 Docker 安装完成,Docker Machine 会在本地主机和 Docker 主机之间生成并共享证书。然后,它会配置远程 Docker 安装进行证书认证,这意味着你的本地客户端可以连接并与远程 Docker 服务器互动。
一旦 Docker 安装完成,Docker Machine 会在你的本地主机和 Docker 主机之间生成并共享证书。然后,它会配置远程 Docker 安装以进行证书认证,这意味着你的本地客户端可以连接并与远程 Docker 服务器互动。以下代码片段进行了说明:
Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this VM, run: docker-machine env docker-local
最后,它会检查你的本地 Docker 客户端是否能够进行远程连接,并通过给出如何配置本地客户端与新启动的 Docker 主机的指示来完成任务。
如果你打开 VirtualBox,你应该能够看到你的新虚拟机,正如以下截图所示:

图 6.1 – 在 VirtualBox 中运行的 Docker 虚拟机
接下来,我们需要配置本地 Docker 客户端,以连接到新启动的 Docker 主机;正如在启动主机时输出的内容中提到的,运行以下命令将展示如何建立连接:
$ docker-machine env docker-local
此命令返回以下内容:
export DOCKER_TLS_VERIFY='1'
export DOCKER_HOST='tcp://192.168.99.100:2376'
export DOCKER_CERT_PATH='/Users/russ.mckendrick/.docker/machine/machines/docker-local'
export DOCKER_MACHINE_NAME='docker-local'
# Run this command to configure your shell:
# eval $(docker-machine env docker-local)
这将覆盖本地 Docker 安装,通过提供新启动的 Docker 主机的 IP 地址和端口号,以及用于认证的证书路径。在输出的最后,它会给你一个命令,供你运行并配置你的终端会话以完成连接。
在我们运行命令之前,先运行 docker version 来获取当前设置的信息,如下所示:

图 6.2 – 检查本地安装的 Docker 版本
这基本上是我正在运行的 Docker for Mac 安装。运行以下命令,然后再次运行 docker version,应该会显示服务器的一些变化:
$ eval $(docker-machine env docker-local)
该命令的输出如以下截图所示:

图 6.3 – 检查新虚拟机上的 Docker 版本
如你所见,Docker Machine 启动的服务器与我们本地安装的几乎完全一致;事实上,唯一的区别是它的版本稍微落后了一些。如你所见,我的 Docker for Mac 安装中的 Docker Engine 二进制文件正在运行版本 19.03.8,而 Docker Machine 启动的主机则运行版本 19.03.5。
从这里开始,我们可以像与本地 Docker 安装一样与 Docker 主机进行交互。在我们继续启动云中的 Docker 主机之前,还有一些其他基本的 Docker Machine 命令需要了解。
第一个命令列出当前配置的 Docker 主机,显示如下:
$ docker-machine ls
命令的输出如下所示:

图 6.4 – 列出虚拟机
如你所见,它列出了机器名称、使用的驱动程序、Docker 端点统一资源定位符(URL),以及主机正在运行的 Docker 版本。
你还会注意到 ACTIVE 列中有一个 *,这表示你的本地客户端当前配置与哪个 Docker 主机进行交互。你也可以通过运行 docker-machine active 来查看当前活动的机器。
下一个命令使用 SSH 连接到 Docker 主机,如下所示:
$ docker-machine ssh docker-local
命令的输出如下所示:

图 6.5 – 使用 SSH 连接到虚拟机
如果你需要安装额外的软件或配置 Docker Machine 之外的内容,这一点非常有用。如果你需要查看日志等信息,它也很有用。你可以通过运行 exit 退出远程 shell。在返回本地机器后,你可以通过运行以下命令找到 Docker 主机的 IP 地址:
$ docker-machine ip docker-local
在本章中,我们会经常使用此命令作为其他命令的一部分来获取虚拟机的 IP 地址。还有一些命令可以获取有关 Docker 主机的更多详细信息,以下代码片段中展示了这些命令:
$ docker-machine inspect docker-local
$ docker-machine config docker-local
$ docker-machine status docker-local
$ docker-machine url docker-local
最后,还有一些命令可以停止、启动、重启和删除 Docker 主机。使用以下代码片段中的最后一个命令来删除你本地启动的主机:
$ docker-machine stop docker-local
$ docker-machine start docker-local
$ docker-machine restart docker-local
$ docker-machine rm docker-local
运行 docker-machine rm 命令时,会提示你确认是否真的要删除该实例,如下所示:
About to remove docker-local
WARNING: This action will delete both local reference and remote instance.
Are you sure? (y/n): y
Successfully removed docker-local
现在我们已经快速了解了基础知识,让我们尝试一些更具冒险性的操作。
使用 Docker Machine 启动云中的 Docker 主机
在这一节中,我们将仅查看 Docker Machine 支持的一个公共云驱动程序。如前所述,Docker Machine 提供了许多驱动程序,但其吸引力之一是它提供一致的体验,因此驱动程序之间的差异不大。
我们将使用 Docker Machine 在DigitalOcean上启动一个 Docker 主机。为此,我们需要一个具有必要权限的应用程序编程接口(API)访问令牌,以便访问并启动 DigitalOcean 账户中的资源。这里不解释如何生成一个,你可以按照www.digitalocean.com/help/api/中的说明进行操作。
重要提示
使用 API 令牌启动 Docker 主机会产生费用;确保跟踪你启动的 Docker 主机。有关 DigitalOcean 定价的详细信息,请参阅www.digitalocean.com/pricing/。此外,保持你的 API 令牌的机密性,因为它可能会被用来未经授权访问你的账户。本章中使用的所有令牌已被撤销。
我们要做的第一件事是将我们的令牌设置为环境变量,这样就不必每次都使用它了。为此,运行以下命令:
$ DOTOKEN=191e004d9a58b964198ab1e8253fc2de367a70fceb9847b7fd44ebf
确保将 API 令牌替换为你自己的。
重要提示
由于我们需要传递给docker-machine命令的附加标志,我将使用/将命令分成多行,以提高可读性。
启动 Docker 主机的命令如下:
$ docker-machine create \
--driver digitalocean \
--digitalocean-access-token $DOTOKEN \
docker-digitalocean
由于 Docker 主机是远程机器,启动、配置并使其可访问需要一些时间。从以下输出可以看到,Docker Machine 启动 Docker 主机的方式也发生了一些变化:
Running pre-create checks...
Creating machine...
(docker-digitalocean) Creating SSH key...
(docker-digitalocean) Creating Digital Ocean droplet...
(docker-digitalocean) Waiting for IP address to be assigned to the Droplet...
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with ubuntu(systemd)...
Installing Docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this VM, run: docker-machine env docker-digitalocean
启动后,你应该能够在 DigitalOcean 控制面板中看到 Docker 主机,如下图所示:

图 6.6 – 在 DigitalOcean 门户中查看 Droplet
通过运行以下命令,重新配置本地客户端以连接到远程主机:
$ eval $(docker-machine env docker-digitalocean)
你还可以运行docker version和docker-machine inspect docker-digitalocean以获取关于 Docker 主机的更多信息。
最后,运行docker-machine ssh docker-digitalocean将通过 SSH 连接到主机。从以下输出以及你首次启动 Docker 主机时的输出可以看到,所使用的操作系统有所不同:

图 6.7 – SSH 连接到我们的 DigitalOcean 机器
你可以通过运行exit退出远程 shell。正如你所看到的,我们不需要告诉 Docker Machine 使用哪个操作系统、Docker 主机的大小,甚至也不需要指定在哪里启动它。这是因为每个驱动程序都有一些相当合理的默认设置。
将这些默认值添加到我们的命令中,命令看起来如下:
docker-machine create \
--driver digitalocean \
--digitalocean-access-token $DOTOKEN \
--digitalocean-image ubuntu-16-04-x64 \
--digitalocean-region nyc3 \
--digitalocean-size 512mb \
--digitalocean-ipv6 \
--digitalocean-backups \
--digitalocean-ssh-user root \
--digitalocean-ssh-port 22 \
docker-digitalocean
正如你所看到的,你可以自定义 Docker 主机的大小、区域和操作系统,甚至是网络。
重要说明
如果你已经启动了一个 droplet,接下来的代码片段中的命令将会报错。
假设我们想要更改操作系统和 droplet 的大小。在这种情况下,我们可以运行以下命令:
$ docker-machine create \
--driver digitalocean \
--digitalocean-access-token $DOTOKEN \
--digitalocean-image ubuntu-18-04-x64 \
--digitalocean-size 1gb \
docker-digitalocean
正如你在 DigitalOcean 控制面板中看到的,这将启动一个类似以下的机器:

图 6.8 – 查看具有不同规格的 droplet
你可以通过运行以下命令来移除 DigitalOcean Docker 主机:
$ docker-machine rm docker-digitalocean
移除主机需要一两分钟时间。
重要说明
请在 DigitalOcean 控制面板中仔细检查,确保你的主机已正确终止;否则,可能会产生意外费用。
这就是我们对 Docker Machine 的介绍。在我们继续之前,让我们讨论一下它的安全性。
Docker Machine 概述
如前所述,Docker Machine 现在被视为一个遗留工具。除了前面提到的原因——人们不再想要启动单个 Docker 主机之外,还有一些其他原因导致了方向的变化。
最重要的是安全性。正如你可能注意到的,当 Docker Machine 启动我们的 Docker 主机时,它不仅生成了一个 SSH 密钥,还生成了证书,并用这些证书配置了 Docker 服务器组件,这意味着我们的本地 Docker 客户端可以与主机交互。现在,对于本地主机来说这并不成问题;然而,在 DigitalOcean 托管的实例上,我们将 Docker 服务器暴露在互联网上。
尽管它已经被安全配置,但如果安装的 Docker 版本出现零日漏洞,我们可能会遇到一些问题,因为不受信任的第三方可能会接管我们的 Docker 安装。因此,我建议只使用 Docker Machine 快速启动一个实例,测试某些内容,然后再关闭实例。
我们将在第十章《在公共云中运行 Docker》和第十三章《在公共云中运行 Kubernetes》中,探讨一些更好的在云上运行容器化应用程序的选项。
现在,让我们看一下使用 Docker Machine 启动本地 Docker 主机的几个替代方案。
使用 Vagrant 和 Multipass 启动本地 Docker 主机
在我们完成本章之前,我们将介绍两种不同的工具,这些工具可以用来本地启动一台机器,然后为你在后续章节中实验配置一个本地的 Docker 主机。
介绍和使用 Vagrant
Vagrant 是HashiCorp出品的宿主机管理工具的鼻祖。它最早发布于 2010 年初,最初作为Mitchell Hashimoto的一个副项目,如今已经成为现代软件开发中一个非常重要的工具,允许开发者快速轻松地启动和移除本地虚拟机,作为开发环境使用。2014 年 5 月,Vagrant 1.6 版本发布,新增了对 Docker 的支持。
安装过程根据操作系统的不同有所差异。如果你使用的是 macOS,可以通过 Homebrew 和 Cask 来安装。使用这些工具的命令如下所示:
$ brew cask install vagrant
Windows 和 Linux 用户可以下载安装程序,下载链接为www.vagrantup.com/downloads.html。在这里,你可以找到适合你操作系统的 32 位和 64 位MSI、deb和RPM安装包,也可以选择 64 位的静态二进制文件,适用于 macOS 和 Linux,如果你不想使用包管理器的话。
Vagrant 使用Vagrantfile,它定义了虚拟机的配置;机器本身被打包成一种叫做 Vagrant Box 的格式——一个包含机器镜像的盒子;此外,还包括启动宿主机所需的元数据,这些宿主机可以是 VirtualBox、VMWare 或任何其他支持的虚拟化工具或云服务。Vagrant Box 通常从 Vagrant Cloud 下载,并由另一个 HashiCorp 产品Packer创建。
要创建一个Vagrantfile,你只需在你想存储 Vagrant Box 相关文件的文件夹中运行以下命令:
$ vagrant init ubuntu/bionic64
默认的Vagrantfile注释非常多;不过,大部分选项都被注释掉了。去掉这些注释后,Vagrantfile 应该看起来像下面这样:
Vagrant.configure('2') do |config|
config.vm.box = 'ubuntu/bionic64'
end
如你所见,这个过程其实并不复杂,也与 Docker 没有太多关系。那么我们来修正一下。更新Vagrantfile,使其如下所示:
Vagrant.configure('2') do |config|
config.vm.box = 'ubuntu/bionic64'
config.vm.provision :docker
end
更新完成后,运行以下命令并等待:
$ vagrant up
如果你还没有下载ubuntu/bionic64盒子,Vagrant 会自动为你下载。下载完成后,它会在 VirtualBox 中启动一个虚拟机,这是 Vagrant 的默认提供者。你应该会看到如下输出:
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/bionic64'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'ubuntu/bionic64' version '20200402.0.0' is up to date...
==> default: Setting the name of the VM: vagrant_default_1586094728360_48806
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
default: Adapter 1: nat
==> default: Forwarding ports...
default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
一旦虚拟机启动,Vagrant 将通过 SSH 连接到机器,并用一个更安全的自生成密钥文件替换默认的 Vagrant 密钥文件,具体过程如下所示:
==> default: Waiting for machine to boot. This may take a few minutes...
default: SSH address: 127.0.0.1:2222
default: SSH username: vagrant
default: SSH auth method: private key
default:
default: Vagrant insecure key detected. Vagrant will automatically replace
default: this with a newly generated keypair for better security.
default:
default: Inserting generated public key within guest...
default: Removing insecure key from the guest if it's present...
default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
现在 SSH 密钥已经安全,Vagrant 会检查虚拟机是否安装了 VirtualBox 来宾附加工具。这些工具允许虚拟机共享你本地机器的文件系统。这个过程如下所示:
==> default: Checking for guest additions in VM...
default: The guest additions on this VM do not match the installed version of
default: VirtualBox! In most cases this is fine, but in rare cases it can
default: prevent things such as shared folders from working properly. If you see
default: shared folder errors, please make sure the guest additions within the
default: VM match the version of VirtualBox you have installed on
default: your host and reload your VM.
default:
default: Guest Additions Version: 5.2.34
default: VirtualBox Version: 6.1
==> default: Mounting shared folders...
default: /vagrant => /Users/russ.mckendrick/vagrant
最后一步是运行我们在Vagrantfile中添加的配置工具,具体如下:
==> default: Running provisioner: docker...
default: Installing Docker onto machine...
现在,我们已经有了一个安装了 Docker 的 Ubuntu 18.04(Bionic Beaver)虚拟机。要访问虚拟机,只需运行以下命令:
$ vagrant ssh
这将打开一个 SSH 会话到虚拟机。从这里,你可以像在本地运行 Docker 时一样使用它。以下是终端输出示例:

图 6.9 – 登录到我们的 Vagrant Box 并与 Docker 交互
完成虚拟机操作后,输入exit返回到主机机器的命令提示符,然后运行以下命令终止虚拟机:
$ vagrant destroy
这将删除与虚拟机关联的所有资源。你将看到以下消息:
default: Are you sure you want to destroy the 'default' VM? [y/N] y
==> default: Forcing shutdown of VM...
==> default: Destroying VM and associated drives...
如果你只是希望停止虚拟机,可以使用以下命令停止和启动它:
$ vagrant stop
$ vagrant up
现在我们已经学习了如何使用 Vagrant 创建虚拟机并配置 Docker,让我们看看一个名为Multipass的替代方案。
介绍并使用 Multipass
Multipass是Canonical(Ubuntu 的开发者)提供的一款工具,用于快速启动和管理本地多个 Ubuntu 虚拟机。它与 Vagrant 的工作方式略有不同,默认情况下,它会使用操作系统的默认虚拟化程序,而不是 VirtualBox。当操作系统没有可用的虚拟化程序时,例如在非专业版 Windows 10 中,它可以回退使用 VirtualBox——稍后我们会详细说明。
要在 macOS 上安装 Multipass,可以运行以下命令,这同样使用 Homebrew 和 Cask:
$ brew cask install multipass
要在现有的 Ubuntu 桌面上安装,可以使用 Snap 包管理器,运行以下命令:
$ snap install multipass --classic
最后,若要在 Windows 上安装,你可以从 GitHub 项目的发布页面下载安装程序,在那里你还可以找到 macOS 安装程序,地址为github.com/canonical/multipass/releases。
一旦安装完成,如果你需要在本地机器的默认虚拟化程序上使用 VirtualBox,可以运行以下命令:
$ multipass set local.driver=virtualbox
现在我们已经安装了 Multipass,并且——如果需要——配置为使用 VirtualBox,我们可以启动虚拟机并安装 Docker。要启动虚拟机,只需运行以下命令:
$ multipass launch --name docker-host
一旦启动,我们可以运行以下命令,将 Docker 引导到新创建的虚拟机上:
$ multipass exec docker-host -- /bin/bash -c 'curl -s https://get.docker.com | sh - && sudo usermod -aG docker ubuntu'
这将下载并安装 Docker 的最新社区版本。安装完成后,它将把ubuntu用户添加到docker组,这意味着连接到虚拟机后,我们将能立即使用 Docker。你将看到执行安装时的命令输出,安装完成后,你将返回到本地机器的 shell。
要连接到虚拟机,请运行以下命令:
$ multipass shell docker-host
一旦连接,你会注意到默认情况下,和我们的 Vagrant 虚拟机一样,Multipass 已下载并启动了Ubuntu 18.04(Bionic Beaver)虚拟机。登录后,你将能够像预期的那样运行 Docker 命令。以下截图展示了这个过程:

图 6.10 – 通过 SSH 连接到我们的 Multipass 虚拟机并与 Docker 交互
与 Vagrant 一样,完成后输入 exit,你将返回到本地计算机的 shell。要删除 docker-host 虚拟机及其所有资源,可以运行以下命令:
$ multipass delete docker-host --purge
如果你想停止和启动虚拟机,可以使用以下命令:
$ multipass stop docker-host
$ multipass start docker-host
如你所见,使用 Multipass 是一个简单便捷的方式,可以在任何主机上本地启动和与 Ubuntu 虚拟机进行交互。
小结
在本章中,我们学习了如何使用 Docker Machine 在 VirtualBox 上本地创建 Docker 主机,并回顾了可以用来与 Docker Machine 启动的 Docker 主机交互和管理的命令。然后,我们了解了如何使用 Docker Machine 将 Docker 主机部署到云环境——具体来说是 DigitalOcean。
我们还讨论了为什么根据主机机器的年龄,使用 Docker Machine 可能不是一个好主意。因此,我们还讨论了如何使用 Vagrant 和 Multipass 启动和配置本地 Docker 主机。
在后续章节中,我们将不再仅与单一 Docker 主机交互,而是启动和运行多个 Docker 主机。然而,在此之前,我们将从 Linux 基础的容器转向 Windows 容器,并在下一章进行简要介绍。如果你没有 Windows 主机也不用担心,因为我们将使用 Vagrant 启动一个。
问题
-
在运行
docker-machine create时,哪个标志可以让你定义 Docker Machine 用来启动 Docker 主机的服务或提供者? -
对错题:运行
docker-machine env my-host会重新配置本地 Docker 客户端,以便与my-host进行交互。 -
对错题:Multipass 开箱即用地支持 Docker。
-
请列出可以用于创建 Vagrant Boxes 的 HashiCorp 工具。
-
解释为什么使用 Docker Machine 在云中创建 Docker 主机不再被视为最佳实践。
进一步阅读
有关我们在本章中使用和提到的各种工具的信息,请参见以下链接:
-
Homebrew:
brew.sh -
Git BASH for Windows:
gitforwindows.org -
VirtualBox:
www.virtualbox.org -
Docker Machine:
docs.docker.com/machine/ -
Docker Toolbox:
docs.docker.com/toolbox/overview/ -
DigitalOcean:
www.digitalocean.com/ -
Vagrant:
www.vagrantup.com -
Vagrant Cloud:
app.vagrantup.com/boxes/search -
Packer:
packer.io -
HashiCorp:
www.hashicorp.com -
Multipass:
multipass.run -
Snap:
snapcraft.io -
Canonical:
canonical.com
第二节:集群与云
在这一节中,我们将应用第一部分所学的内容,聚焦于两种主要的 Docker 集群技术——Docker Swarm 和 Kubernetes,并探讨在公共云上运行容器。
本节包括以下章节:
第七章,从 Linux 迁移到 Windows 容器
第八章,使用 Docker Swarm 进行集群管理
第九章,Portainer – Docker 的图形界面
第十章,在公共云中运行 Docker
第十一章,使用 Docker 和 Kubernetes 进行集群管理
第十二章,探索其他 Kubernetes 选项
第十三章,在公共云中运行 Kubernetes
第七章:从 Linux 转向 Windows 容器
在本章中,我们将讨论并深入了解 Windows 容器。微软已经将容器作为在新硬件上部署旧应用程序的一种方式。与 Linux 容器不同,Windows 容器仅在基于 Windows 的 Docker 主机上可用。
我们将涵盖以下主题:
-
Windows 容器简介
-
为 Windows 容器设置 Docker 主机
-
运行 Windows 容器
-
一个 Windows 容器 Dockerfile
-
Windows 容器与 Docker Compose
技术要求
在本章中,我们将启动的容器只适用于 Windows Docker 主机。我们将使用 macOS 和 Linux 基础的机器上的 VirtualBox 和 Vagrant 来帮助启动和运行 Windows Docker 主机。
请查看以下视频,了解代码如何运行:bit.ly/2DEwopT
Windows 容器简介
作为一个过去 20 年几乎每天都在使用 macOS 和 Linux 电脑及笔记本电脑,并且常常与 Linux 服务器打交道的人,再加上我唯一的 Windows 操作系统经验仅限于我拥有的 Windows XP 和 Windows 10 游戏电脑(以及工作中不得不面对的偶尔 Windows 服务器),Windows 容器的出现是一个有趣的发展。
现在,我从来不会把自己归类为 Linux/Unix 的死忠粉;然而,微软近年来的举动甚至让我感到惊讶。早在 2014 年,在其中一次 Azure 活动上,微软宣布了“Microsoft Linux”,从那以后就再也没有回头过。
自微软宣布对 Linux 表示喜爱以来,出现了一些显著的新闻:
-
Linux 是 Microsoft Azure 中的第一等公民。
-
.NET Core 是跨平台的,这意味着你可以在 Linux、macOS 和 Windows 上运行 .NET 应用程序。
-
SQL Server 已经在 Linux 上提供服务几年了。
-
你可以在 Windows 10 专业版机器上运行 Linux shell,如 Ubuntu。
-
PowerShell 已经被移植到 Linux 和 macOS。
-
它开发了跨平台工具,比如 Visual Studio Code,并将其开源。
-
它以 75 亿美元收购了 GitHub!!
很明显,曾经的微软(前 首席执行官 (CEO) Steve Ballmer 曾因公开抨击开源和 Linux 社区而著名)已经不复存在。
因此,2014 年 10 月,微软宣布与 Docker 合作推动容器在 Windows 操作系统(如 Windows 10 专业版和 Windows Server 2016)上的采用,这一消息并不令人惊讶,特别是在微软公开表示对 Linux 表示喜爱几个月之后。
那么,Windows 容器到底是什么呢?
从表面上看,它们与 Linux 容器没有什么不同。微软在 Windows 内核上的工作引入了与 Linux 相同的进程隔离。而且,像 Linux 容器一样,这种隔离也扩展到沙箱文件系统,甚至是 Windows 注册表。
由于每个容器实际上是一个全新的 Windows Core 或 Windows Nano 安装,它们本身是精简版的 Windows Server 镜像(可以理解为 Windows 版的 Alpine Linux),安装管理员可以在同一主机上运行多个 docker 化的应用程序,而不必担心自定义注册表更改或要求冲突导致的问题。
加上 Docker 命令行客户端所提供的相同易用性,管理员就有了一种将遗留应用程序迁移到更现代硬件并托管操作系统的方式,而且不需要担心和管理多个运行较旧、不再受支持的 Windows 版本的虚拟机(VM)的额外负担。
Windows 容器还提供了另一层隔离。Hyper-V 隔离在容器启动时,将容器进程运行在一个最小化的虚拟机监控程序(hypervisor)中,这进一步将容器进程与主机机器隔离。然而,每个使用 Hyper-V 隔离运行的容器都需要额外的资源,并且这些容器的启动时间也会增加,因为在容器启动之前需要先启动虚拟机监控程序。
虽然 Hyper-V 隔离确实使用了微软的虚拟机监控程序(它可以在 Windows Server、桌面版和 Xbox One 系统软件中找到),但你无法使用标准的 Hyper-V 管理工具来管理 Hyper-V 隔离的容器。你必须使用 Docker。
毕竟,微软在启用 Windows 内核中的容器功能方面付出了大量的工作和努力,为什么他们会选择 Docker,而不是仅仅创建自己的容器管理工具呢?
Docker 已经凭借一组经过验证的应用程序编程接口(API)和庞大的社区,确立了其作为管理容器的首选工具的地位。同时,Docker 还是一个开源应用程序,这意味着微软不仅可以将其适配到 Windows 上,还可以为其开发做出贡献。
下图概述了 Docker 在 Windows 上的工作原理:

图 7.1 – Docker 在 Windows 上的概述
注意我说的是 Docker 在 Windows 上,而不是 Docker 为 Windows;它们是完全不同的产品。Docker 在 Windows 上是与 Windows 内核交互以提供 Windows 容器的 Docker 引擎和客户端的本地版本。而 Docker 为 Windows 提供的是一种尽可能原生的体验,允许开发者在桌面上同时运行 Linux 和 Windows 容器。
现在,让我们来看看如何准备主机,以便我们可以运行 Windows 容器。
为 Windows 容器设置 Docker 主机
如您所料,您将需要访问运行 Docker 的 Windows 主机。如果您没有运行 Windows 10 专业版机器,别担心;您也可以在 macOS 和 Linux 上实现这一目标。在我们讨论这些方法之前,先看看如何在 Windows 10 专业版上使用 Docker for Windows 启动 Windows 容器。
启用 Windows 容器支持(Windows 10 专业版)
Windows 10 专业版开箱即用支持 Windows 容器。然而,默认情况下,它配置为运行 Linux 容器。要从运行 Linux 容器切换到 Windows 容器,请右键点击系统托盘中的Docker图标,并从菜单中选择切换到 Windows 容器…,如下图所示:

图 7.2 – 切换到 Windows 容器
这将弹出以下提示:

图 7.3 – 关于 Linux 容器的一个重要说明
点击切换按钮,几秒钟后,您将开始管理 Windows 容器。您可以通过在主机上打开命令提示符并运行以下命令来查看:
$ docker version
从以下输出可以看到这一点:

图 7.4 – 检查运行 docker version 输出
Docker 引擎的版本为 OS/Arch of windows/amd64,而不是我们之前习惯看到的 linux/amd64 版本。所以,这涵盖了 Windows 10 专业版。那么像我这样喜欢 macOS 或 Linux 的人怎么办呢?
在 MacOS 和 Linux 上开始运行
要在 macOS 和 Linux 机器上访问 Windows 容器,我们将使用 Stefan Scherer 汇总的优秀资源。在本书附带的仓库的 chapter07 文件夹中,有一个 Stefan 的 docker-windows-box 仓库的分支版本作为 Git 子模块,其中包含所有您需要的文件,可以帮助您在 macOS 上启动并运行 Windows 容器。
要查看这个分支版本,您需要在仓库文件夹内运行以下命令:
$ git submodule update --init --recursive
最后,在我们开始启动虚拟机之前,您将需要以下工具:HashiCorp 的 Vagrant 和 Oracle 的 VirtualBox,我们在上一章中已经讲解过。
但是,我们确实需要安装一个vagrant插件。为此,请运行以下命令:
$ vagrant plugin install vagrant-reload
插件安装完成后,我们可以通过打开终端,进入chapter07/docker-machine仓库文件夹,并运行以下命令来开始在 Windows 上使用 Docker:
$ vagrant up
这将下载一个 VirtualBox Windows Server 2019 Core Eval 镜像,其中包含启动并运行 Windows 容器所需的一切。下载文件大约有 6 GB,所以请确保你有足够的带宽和磁盘空间来运行该镜像。
Vagrant 将启动镜像并配置虚拟机上的 Docker,同时还会设置一些其他合理的默认值,例如 Atom 集成开发环境(IDE)、Docker Compose、Docker Machine 和 Git。一旦虚拟机启动,打开你首选的 Microsoft 远程桌面协议(RDP)客户端,然后运行以下命令:
$ vagrant rdp
如果提示输入密码,请输入 vagrant,你将登录到新启动的 Windows 2019 Server 环境,并且已安装并准备好运行 Docker for Windows 所需的所有工具。
此外,如果你不想在本地运行某些东西,可以在 Azure 中运行一个 Windows 10 专业版实例,它已启用所有必要的组件来支持 Docker for Windows,这正如上一节所讨论的那样,它允许你运行 Windows 容器,接下来我们将讨论这个问题。
运行 Windows 容器
正如本章第一部分已经暗示的那样,使用 Docker 命令行客户端启动和与 Windows 容器交互,与我们之前所做的操作没有区别。让我们通过运行 hello-world 容器来测试一下,如下所示:
$ docker container run hello-world
就像之前一样,这将下载 hello-world 容器并返回一条消息,如下所示的屏幕截图所示:

图 7.5 – 运行基于 Windows 的 hello-world 容器
这次唯一的区别是,Docker 拉取的不是 Linux 镜像,而是基于 nanoserver-sac2016 镜像的 windows-amd64 版本。
现在,让我们看一下如何在前台运行一个容器,这次运行 PowerShell,如下所示:
$ docker pull mcr.microsoft.com/windows/servercore
$ docker container run -it mcr.microsoft.com/windows/servercore:ltsc2019 powershell
一旦你的 shell 启动,运行以下命令将显示计算机名称,即容器 ID:
$ Get-CimInstance -ClassName Win32_Desktop -ComputerName .
你可以在以下屏幕截图中看到前面命令的完整终端输出:

图 7.6 – 在容器中运行 PowerShell
一旦你通过运行 exit 退出 PowerShell,你可以通过运行以下命令来查看容器 ID:
$ docker container ls -a
你可以在以下屏幕截图中看到预期的输出:

图 7.7 – 检查容器
现在,让我们来看一下构建一个比运行 PowerShell 更具冒险性的镜像——我们来安装一个 web 服务器。
一个 Windows 容器 Dockerfile
Windows 容器镜像使用与 Linux 容器相同格式的 Dockerfile 命令。以下 Dockerfile 将在容器中下载、安装并启用 Internet Information Services (IIS) Web 服务器:
# escape=`
FROM mcr.microsoft.com/windows/servercore:ltsc2019
RUN powershell -Command `
Add-WindowsFeature Web-Server; `
Invoke-WebRequest -UseBasicParsing -Uri “https://dotnetbinaries.blob.core.windows.net/servicemonitor/2.0.1.10/ServiceMonitor.exe” -OutFile “C:\ServiceMonitor.exe”
EXPOSE 80
ENTRYPOINT [“C:\\ServiceMonitor.exe”, “w3svc”]
你可以使用以下命令来构建镜像:
$ docker image build --tag local:dockerfile-iis .
构建完成后,运行 docker image ls 应该会显示以下内容:

](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/ms-dkr-4e/img/image_00_0081.jpg)
图 7.8 – 构建并列出我们的镜像
关于 Windows 容器镜像,你首先会注意到的是它们非常大。使用以下命令运行容器将启动 IIS 镜像:
$ docker container run -d --name dockerfile-iis local:dockerfile-iis
你可以通过打开浏览器来查看新启动的容器。不过,直接访问 http://localhost:8080/ 是无法工作的,因为我们没有提供任何端口。回想一下,我们是在 Windows 上运行 Docker,因此容器是直接在宿主机上运行的。因此,不需要使用 localhost 或映射端口,因为我们可以直接在容器宿主机上访问容器的 网络地址转换 (NAT) 互联网协议 (IP)。
要查找 NAT IP 地址,你可以使用以下命令:
$ docker container inspect --format=”{{.NetworkSettings.Networks.nat.IPAddress}}” dockerfile-iis
这将输出类似以下内容:

](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/ms-dkr-4e/img/image_00_0091.jpg)
图 7.9 – 运行我们的 Windows 容器镜像
这将给你一个 IP 地址。要访问 IIS,我们只需将 IP 地址放入运行在 Windows 主机上的浏览器中。在这种情况下,使用 http://172.26.30.80/。你应该看到以下默认的保持页面:

](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/ms-dkr-4e/img/image_00_0101.jpg)
图 7.10 – IIS Web 服务器在容器中运行
要停止并移除我们迄今为止启动的容器,请运行以下命令:
$ docker container stop dockerfile-iis
$ docker container prune
到目前为止,我相信你会同意,使用 Docker 与 Linux 容器的体验没有什么不同。
Windows 容器与 Docker Compose
本章的最后部分,我们将探讨如何在 Windows Docker 主机上使用 Docker Compose。正如你可能已经猜到的,使用的命令与我们在 第五章 中运行的命令没有太大区别,Docker Compose。
在仓库的 chapter07 文件夹中,你将找到一个 docker-compose.yml 文件,内容如下:
version: ‘2.1’
services:
db:
image: microsoft/mssql-server-windows-express
environment:
sa_password: “${SA_PASSWORD}”
ACCEPT_EULA: “${SQL_SERVER_ACCEPT_EULA}”
healthcheck:
test: [ “CMD”, “sqlcmd”, “-U”, “sa”, “-P”, “${SA_PASSWORD}”, “-Q”, “select 1” ]
interval: 10s
retries: 10
octopus:
image: octopusdeploy/octopusdeploy:${OCTOPUS_VERSION}
environment:
ADMIN_USERNAME: “${OCTOPUS_ADMIN_USERNAME}”
ADMIN_PASSWORD: “${OCTOPUS_ADMIN_PASSWORD}”
SQLDBCONNECTIONSTRING: “${DB_CONNECTION_STRING}”
ACCEPT_EULA: “${OCTOPUS_ACCEPT_EULA}”
ADMIN_EMAIL: “${ADMIN_EMAIL}”
ports:
- “1322:8080”
depends_on:
db:
condition: service_healthy
stdin_open: true
volumes:
- “./Repository:C:/Repository”
- “./TaskLogs:C:/TaskLogs”
networks:
default:
external:
name: nat
还有一个支持的 .env 文件——这个文件被 Docker Compose 用来填充 Docker Compose 文件中的变量,应该与 docker-compose.yml 文件放在同一文件夹中,如下所示:
SA_PASSWORD=N0tS3cr3t!
OCTOPUS_VERSION=2019.13.4
DB_CONNECTION_STRING=Server=db,1433;Initial Catalog=Octopus;Persist Security Info=False;User ID=sa;Password=N0tS3cr3t!;MultipleActiveResultSets=False;Connection Timeout=30;
OCTOPUS_ADMIN_USERNAME=admin
OCTOPUS_ADMIN_PASSWORD=Passw0rd123
ADMIN_EMAIL=
OCTOPUS_ACCEPT_EULA=Y
SQL_SERVER_ACCEPT_EULA=Y
如你所见,它使用了与我们之前看过的 Docker Compose 文件相同的结构、标志和命令,唯一的区别是我们使用的是为 Windows 容器设计的 Docker Hub 镜像。该 Docker Compose 文件将下载 Microsoft database 和 Octopus Deploy (octopus)。要拉取所需的镜像,只需运行以下命令:
$ docker-compose pull
然后,一旦拉取完成,我们需要创建启动 Octopus Deploy 所需的文件夹,使用以下命令,在与 docker-compose.yml 文件相同的文件夹中执行:
$ mkdir Repository
$ mkdir TaskLogs
最后,我们可以通过运行以下命令启动 Octopus Deploy:
$ docker-compose --project-name Octopus up -d
Octopus 启动大约需要 10 分钟。我建议运行以下命令来查看容器日志,以确保 Octopus Deploy 已经启动并运行:
$ docker-compose --project-name Octopus logs -f
你应该在日志输出中看到消息 Completed System Integrity Check. Overall Result: Passed,其输出应类似以下内容:

图 7.11 – 观察我们容器的输出
和之前一样,你可以使用以下命令来查找 Windows 上的 IP 地址:
$ docker inspect -f “{{ .NetworkSettings.Networks.nat.IPAddress }}” octopus_octopus_1
一旦你获取到 IP 地址,对于我来说是 172.26.30.12,打开浏览器并访问端口 81 的管理员界面。对于我来说,地址是 http://172.26.30.12:81/。这应该会显示一个登录提示——在这里,输入用户名 admin,密码是 Passw0rd123。登录后,你应该会看到类似以下内容:

图 7.12 – Octopus Deploy 启动并运行
当你准备好时,可以通过运行以下命令来删除容器:
$ docker-compose --project-name Octopus down --rmi all --volumes
在我们结束之前,需要注意几点——首先是 .env 文件的使用。如前所述,这可以避免我们在 Docker Compose 文件中使用硬编码的变量;所以,如果你使用过它们,千万不要像我一样将其提交到 Git 仓库。其次,你可能已经注意到,在我们运行 docker-compose 命令时,传递了 --project-name Octopus 参数。这意味着,当我们启动项目时,应用程序不会继承 Docker Compose 文件所在文件夹的名称,而是会以 octopus 为前缀。
总结
在本章中,我们简要介绍了 Windows 容器。如你所见,得益于微软将 Docker 作为 Windows 容器的管理工具,使用过 Docker 管理 Linux 容器的人会对这种体验感到非常熟悉。
在下一章中,我们将介绍 Docker Swarm。这将是我们第一次从单个 Docker 主机转向一组主机集群。
问题
-
Docker 在 Windows 上引入了哪个额外的隔离层?
-
你会使用哪个命令来查找 Windows 容器的 NAT IP 地址?
-
判断对错:Docker 在 Windows 上引入了一组额外的命令,你需要使用这些命令来管理你的 Windows 容器。
进一步阅读
你可以在本章提到的主题中找到更多信息,如下所示:
-
Docker 和微软合作公告:
blog.docker.com/2014/10/docker-microsoft-partner-distributed-applications/ -
Windows Server 和 Docker——将 Docker 和容器引入 Windows 背后的内部机制:
www.youtube.com/watch?v=85nCF5S8Qok -
Stefan Scherer 在 GitHub 上:
github.com/stefanScherer/ -
Octopus Deploy:
octopus.com
第八章:使用 Docker Swarm 进行集群管理
在本章中,我们将介绍 Docker Swarm。通过 Docker Swarm,您可以创建和管理 Docker 集群。Swarm 可用于在多个主机上分发容器,并具有扩展容器的能力。
我们将讨论以下主题:
-
介绍 Docker Swarm
-
创建和管理一个 swarm
-
Docker Swarm 服务和堆栈
-
负载均衡、覆盖网络与调度
技术要求
和前几章一样,我们将继续使用本地的 Docker 安装。本章中的截图将来自我偏好的操作系统 macOS。与之前一样,我们将运行的 Docker 命令适用于目前已安装 Docker 的所有三种操作系统。不过,少数支持命令可能仅适用于基于 macOS 和 Linux 的操作系统。
查看以下视频,看看代码如何运行:bit.ly/334RE0A
介绍 Docker Swarm
在深入之前,我需要提到,Docker Swarm 有两个非常不同的版本。曾经有一个独立版 Docker Swarm——它在 Docker 1.12 之前被支持,但现在已经不再积极开发;然而,您可能会在一些旧文档中看到它的提及。由于 Docker 在 2017 年第一季度结束了对 1.11.x 版本的支持,因此不推荐安装独立版 Docker Swarm。
Docker 版本 1.12 引入了 Docker Swarm 模式。这将独立 Docker Swarm 版本中的所有功能集成到核心 Docker 引擎中,并增加了大量新功能。由于本书讨论的是 Docker 19.03 及更高版本,我们将使用 Docker Swarm 模式,接下来我们将称之为 Docker Swarm。
由于您已安装支持 Docker Swarm 的 Docker 版本,因此无需额外操作来安装 Docker Swarm。您可以通过运行以下命令来验证 Docker Swarm 是否已在您的安装中启用:
$ docker swarm --help
运行该命令时,您应该看到如下的终端输出:

图 8.1 – 查看帮助
如果遇到错误,请确保您正在运行 Docker 19.03 或更高版本,我们在第一章中已经介绍了该版本的安装,Docker 概述。现在我们知道 Docker 客户端支持 Docker Swarm,那么我们所说的 Swarm 是什么意思?
Swarm 是一组运行 Docker 的主机,它们被配置为相互交互并组成集群。一旦配置完成,你就可以像在单个主机上操作一样使用我们之前执行过的所有命令,同时让 Docker Swarm 根据部署策略决定容器的最适合运行主机。Docker Swarm 由两种类型的主机组成。我们现在来看一下这两种主机。
Docker Swarm 集群中的角色
Docker Swarm 中涉及哪些角色?让我们来看一下在 Docker Swarm 集群中运行时,主机可以担任的两种角色。
Swarm 管理器
Swarm 管理器 是一个主机,它是所有 Swarm 主机的中央管理点。Swarm 管理器是你发出所有命令以控制这些节点的地方。你可以在节点之间切换,加入节点,移除节点,并操作这些主机。
每个集群可以运行多个 Swarm 管理器。对于生产环境,建议至少运行五个 Swarm 管理器;这样我们的集群在出现最多两个 Swarm 管理器节点故障之前不会出现错误。Swarm 管理器使用 Raft 共识算法(详见 进一步阅读 部分)来保持所有管理节点之间的一致状态。
Swarm 工作节点
Swarm 工作节点,之前我们提到的 Docker 主机,就是运行 Docker 容器的那些节点。Swarm 工作节点由 Swarm 管理器进行管理,并在下图中展示:

图 8.2 – Swarm 工作节点概览
这是一张所有 Docker Swarm 组件的示意图。我们看到 Docker Swarm 管理器与每个拥有 Docker Swarm 工作节点角色的主机进行通信。工作节点之间确实有一定的连接,我们稍后会进一步了解。
创建和管理 Swarm
现在让我们来看一下如何使用 Swarm,并执行以下任务:
-
创建集群
-
加入工作节点
-
列出节点
-
管理集群
创建集群主机
让我们从创建一个包含三台机器的集群开始。由于我们将在本地机器上创建一个多节点集群,我们将使用之前在 第六章 中讲到的 Multipass,Docker Machine,Vagrant 和 Multipass,通过运行以下命令来启动主机:
$ multipass launch -n node1
$ multipass launch -n node2
$ multipass launch -n node3
这应该会为我们创建三个节点;你可以通过运行以下命令来检查:
$ multipass list
你应该能看到类似以下的输出:

图 8.3 – 使用 Multipass 启动节点
你可能还记得上次使用 Multipass 时,启动一个主机并不意味着 Docker 已经安装;所以,现在,让我们安装 Docker,并将ubuntu用户添加到docker组中,这样当我们使用multipass exec时,就不需要切换用户了。为此,请运行以下三条命令:
$ multipass exec node1 -- \
/bin/bash -c 'curl -s https://get.docker.com | sh - && sudo usermod -aG docker ubuntu'
$ multipass exec node2 -- \
/bin/bash -c 'curl -s https://get.docker.com | sh - && sudo usermod -aG docker ubuntu'
$ multipass exec node3 -- \
/bin/bash -c 'curl -s https://get.docker.com | sh - && sudo usermod -aG docker ubuntu'
现在我们的三个集群节点已经准备就绪,我们可以继续进行下一步,即将 Swarm 管理节点添加到集群中。
将 Swarm 管理节点添加到集群
让我们启动我们的 Swarm 管理节点。为此,我们将把一些 Docker Machine 命令的结果传递给我们的主机。在创建 Swarm 管理节点之前,我们需要获取node1的 IP 地址,因为它将成为我们的 Swarm 管理节点。如果你使用的是 macOS 或 Linux,可以通过运行以下命令设置一个环境变量:
$ IP=$(multipass info node1 | grep IPv4 | awk '{print $2}')
如果你使用的是 Windows 10,请运行multipass list并记下node1的 IP 地址。
用于创建我们管理节点的命令在以下代码片段中显示(如果你正在使用 Windows,请将$IP替换为你记下的 IP 地址):
$ multipass exec node1 -- \
/bin/bash -c 'docker swarm init --advertise-addr $IP:2377 --listen-addr $IP:2377'
你应该收到类似以下的消息:
Swarm initialized: current node (92kts1c9x17gbqv3in9t1w4qm) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-4s8vpkileg2l2sicpyay5fojhis9jygb8mv04tsy9jmeqmzhk8-4cp4hp0i1qjqpuln6q3ytprtc 192.168.64.9:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
从输出中可以看到,一旦你的管理节点初始化完成,你将获得一个唯一的令牌。
在前面的示例中,完整的令牌是:
SWMTKN-1-4s8vpkileg2l2sicpyay5fojhis9jygb8mv04tsy9jmeqmzhk8-4cp4hp0i1qjqpuln6q3ytprtc.
这个令牌将用于工作节点进行身份验证,并加入我们的集群。
将 Swarm 工作节点加入集群
现在是时候将我们的两个工作节点(node2和node3)添加到集群中了。首先,让我们设置一个环境变量来保存我们的令牌,确保你将令牌替换为在初始化管理节点时收到的令牌,如下所示:
$ SWARM_TOKEN=$(multipass exec node1 -- /bin/bash -c 'docker swarm join-token --quiet worker')
同样,Windows 用户需要记下令牌,并且在接下来的命令中,需要将$SWARM_TOKEN和$IP分别替换为他们的实际值。
现在,我们可以运行以下命令将node2添加到集群:
$ multipass exec node2 -- \
/bin/bash -c 'docker swarm join --token $SWARM_TOKEN $IP:2377'
对于node3,你需要运行以下命令:
$ multipass exec node3 -- \
/bin/bash -c 'docker swarm join --token $SWARM_TOKEN $IP:2377'
每次操作,你都应该收到确认信息,表示你的节点已经加入集群,如以下截图所示:

图 8.4 – 将工作节点添加到集群
列出节点
你可以通过运行以下命令来检查 Swarm:
$ multipass exec node1 -- /bin/bash -c 'docker node ls'
这将连接到node1,我们已经将其配置为 Swarm 主节点,并查询构成我们集群的所有节点。
你应该看到所有三个节点都被列出,如以下截图所示:

图 8.5 – 列出集群节点
现在,我们将从本地机器上的 Docker 客户端切换到node1上的 Docker 客户端。要连接到node1的 Shell,我们只需运行以下命令:
$ multipass shell node1
这将把我们留在node1的命令行提示符下,准备开始使用我们的集群。
管理集群
让我们看看如何管理我们正在创建的所有集群节点。
你管理集群中容器的方式只有两种——这两种方式是使用docker service和docker stack命令,我们将在本章的下一部分中介绍这两种方式。
在我们查看如何在集群中启动容器之前,让我们先看一下如何管理集群本身,从了解如何获取集群的更多信息开始。
查找集群信息
正如我们已经看到的,我们可以使用安装在node1上的 Docker 客户端列出集群中的节点。要获取更多信息,我们只需在node1的命令行中输入以下内容:
$ docker info
这将提供关于主机的大量信息,如下所示(我已经截断了输出):
Server:
Containers: 0
Images: 0
Server Version: 19.03.8
Swarm: active
NodeID: 92kts1c9x17gbqv3in9t1w4qm
Is Manager: true
ClusterID: k65y4ke5rmup1n74z9lb9gerx
Managers: 1
Nodes: 3
Default Address Pool: 10.0.0.0/8
SubnetSize: 24
如你所见,在Swarm部分有集群的信息;然而,我们只能对当前配置为与之通信的主机运行docker info命令。幸运的是,docker node命令是集群感知的,所以我们可以使用它来获取集群中每个节点的信息。
提示
使用docker node inspect命令的--pretty标志可以将输出以易读的格式呈现。如果省略--pretty,Docker 将返回包含查询结果的原始JSON对象,这些查询是inspect命令针对集群运行的。
以下是我们需要运行的命令,以获取关于node1的信息:
$ docker node inspect node1 –pretty
这应该会提供有关我们 Swarm 管理节点的以下信息:
ID: 92kts1c9x17gbqv3in9t1w4qm
Hostname: node1
Joined at: 2020-04-12 14:00:02.218330889 +0000 utc
Status:
State: Ready
Availability: Active
Address: 192.168.64.9
Manager Status:
Address: 192.168.64.9:2377
Raft Status: Reachable
Leader: Yes
Platform:
Operating System: linux
Architecture: x86_64
Resources:
CPUs: 1
Memory: 985.7MiB
Plugins:
Log: awslogs, fluentd, gcplogs, gelf, journald, json-file, local, logentries, splunk, syslog
Network: bridge, host, ipvlan, macvlan, null, overlay
Volume: local
Engine Version: 19.03.8
运行相同的命令,但这次针对其中一个工作节点,命令如下:
$ docker node inspect node2 --pretty
这将给我们类似的信息,如下所示:
ID: x5qbl7j7qp07amffbps56p562
Hostname: node2
Joined at: 2020-04-12 14:10:15.08034922 +0000 utc
Status:
State: Ready
Availability: Active
Address: 192.168.64.10
Platform:
Operating System: linux
Architecture: x86_64
Resources:
CPUs: 1
Memory: 985.7MiB
Plugins:
Log: awslogs, fluentd, gcplogs, gelf, journald, json-file, local, logentries, splunk, syslog
Network: bridge, host, ipvlan, macvlan, null, overlay
Volume: local
Engine Version: 19.03.8
但正如你所看到的,关于管理节点功能状态的信息缺失。这是因为工作节点不需要知道管理节点的状态;它们只需要知道它们被允许接收来自管理节点的指令。
这样,我们可以看到关于此主机的详细信息,例如容器数量、主机上的镜像数量、中央处理单元(CPU)和内存信息,以及其他有趣的信息。
现在我们知道如何获取构成我们集群的节点信息后,让我们来看一下如何提升节点在集群中的角色。
提升一个工作节点
假设你想对单个管理节点进行一些维护,但你希望保持集群的可用性。没问题——你可以将一个工作节点提升为管理节点。
在我们的本地三节点集群已经启动并运行的情况下,让我们将node2提升为新的管理节点。为此,运行以下命令:
$ docker node promote node2
执行命令后,你应该会收到一条确认消息,表示你的节点已经被提升,消息如下:
Node node2 promoted to a manager in the swarm.
通过运行以下命令列出节点:
$ docker node ls
这应该会显示出你现在有两个节点在MANAGER STATUS列中显示内容,如下图所示:

图 8.6 – 检查集群中节点的状态
我们的node1节点仍然是主要的管理节点,不过。让我们来处理一下这个问题,将其角色从管理节点切换为工作节点。
降级管理节点
你可能已经猜到如何操作了,要将管理节点降级为工作节点,你只需运行以下命令:
$ docker node demote node1
再次,你将收到即时反馈,显示以下内容:
Manager node1 demoted in the swarm.
现在我们已经降级了节点,你可以通过运行以下命令来检查集群中节点的状态:
$ docker node ls
由于我们连接的是node1,即刚被降级的节点,你将收到以下消息:
Error response from daemon: This node is not a swarm manager. Worker nodes can't be used to view or modify cluster state. Please run this command on a manager node or promote the current node to a manager.
要连接到我们的新 Swarm 管理节点,我们需要通过 SSH 连接到node2。为此,我们只需断开与node1的连接,并通过运行以下命令连接到node2:
$ exit
$ multipass shell node2
现在我们再次连接到一个管理节点,请按以下方式重新运行:
$ docker node ls
它应该如预期那样列出节点,以下截图展示了这一点:

图 8.7 – 检查集群中节点的状态
一切都很顺利,但我们怎么才能将一个节点移出集群,以便对其进行维护呢?现在让我们看看如何排空一个节点。
节点排空
为了临时将一个节点从集群中移除以便进行维护,我们需要将节点的状态设置为Drain。让我们看看如何排空我们原先的管理节点。为此,我们需要运行以下命令:
$ docker node update --availability drain node1
这将停止任何新任务的执行,例如启动或执行新容器,针对我们正在排空的节点。一旦新任务被阻止,所有正在运行的任务将从我们正在排空的节点迁移到状态为Active的节点。
从下面的终端输出可以看到,现在列出的节点显示node1在AVAILABILITY列中的状态是Drain:

图 8.8 – 检查我们集群的状态
现在我们的节点不再接受新任务,所有正在运行的任务都已迁移到剩余的两个节点,我们可以安全地进行维护,例如重启主机。要重启node1,在你的主机第二窗口中运行以下两个命令:
$ multipass shell node1
$ sudo reboot
一旦主机重新启动,在node2上运行此命令:
$ docker node ls
它应该显示该节点的AVAILABILITY状态为Drain。要将节点重新加入集群,只需通过在node2上运行以下命令将AVAILABILITY状态更改为Active:
$ docker node update --availability active node1
从下面的终端输出可以看到,我们的节点现在处于活动状态,意味着可以在其上执行新任务:

图 8.9 – 检查我们集群的状态
现在我们已经了解如何创建和管理 Docker Swarm 集群,我们应该看看如何执行一些任务,比如创建和扩展服务或启动堆栈。
Docker Swarm 服务和堆栈
到目前为止,我们已经看过以下命令:
$ docker swarm <command>
$ docker node <command>
这两个命令允许我们从现有的 Docker 主机集合中启动并管理 Docker Swarm 集群。接下来我们要看的是以下两个命令:
$ docker service <command>
$ docker stack <command>
service和stack命令允许我们执行任务,进而启动、扩展和管理我们 Swarm 集群中的容器。
服务
service命令是一种启动容器的方式,它利用了 Swarm 集群的优势。我们来看一下如何在 Swarm 集群上启动一个非常基础的单容器服务。
提示
别忘了,docker命令需要从你当前的 Swarm 管理节点执行。如果你跟随的话,应该是node2。
为此,请运行以下命令:
$ docker service create \
--name cluster \
--constraint 'node.role == worker' \
-p:80:80/tcp \
russmckendrick/cluster
这将创建一个名为cluster的服务,该服务由一个容器组成,容器的端口80从容器映射到主机机器,并且仅在具有worker角色的节点上运行。
在我们深入了解如何使用该服务之前,我们可以先检查它是否在浏览器中工作。为此,我们需要知道我们两个工作节点的 IP 地址。首先,我们需要通过运行以下命令来确认哪些是工作节点:
$ docker node ls
一旦我们知道每个节点的角色,你可以通过在主机机器上的第二个终端窗口中运行以下命令来找到节点的 IP 地址:
$ multipass list
查看以下终端输出:

图 8.10 – 创建服务并检查节点的 IP 地址
我的工作节点是node1和node3,它们的 IP 地址分别是192.168.64.9和192.168.64.11。
在浏览器中访问你的工作节点的任一 IP 地址,例如 http://192.168.64.9/ 或 http://192.168.64.11/,将显示russmckendrick/cluster应用程序的输出,这包括 Docker Swarm 图形和提供页面的容器的主机名。以下截图中做了说明:

图 8.11 – 我们的集群应用
现在我们的服务已经在集群中运行,我们可以开始获取更多关于它的信息。首先,我们可以通过运行以下命令再次列出服务:
$ docker service ls
在我们的案例中,这应该返回我们启动的单个服务,名为cluster,如下截图所示:

图 8.12 – 列出服务
如你所见,这是一个inspect命令,如下所示:
$ docker service inspect cluster --pretty
这将返回有关服务的详细信息,如下截图所示:

图 8.13 – 获取服务信息
您可能已经注意到,到目前为止,我们并不需要担心我们的两个工作节点中哪个节点正在运行服务。这是 Docker Swarm 的一个非常重要的功能,它完全消除了您需要担心单个容器位置的需求。
在我们查看如何扩展服务之前,我们可以快速查看我们的单个容器运行在哪个主机上,方法是执行以下命令:
$ docker node ps
$ docker node ps node1
$ docker node ps node3
这将列出在每个主机上运行的容器。默认情况下,它将列出命令所针对的主机,在我的情况下是node1,如下所示的屏幕截图:

图 8.14 – 查找我们的服务运行在哪个节点上
让我们看一下如何将服务扩展为六个实例的应用容器。运行以下命令来扩展并检查我们的服务:
$ docker service scale cluster=6
$ docker service ls
$ docker node ps node1
$ docker node ps node3
我们只检查了两个节点,因为我们最初告诉我们的服务在工作节点上启动。正如您从以下终端输出中看到的,我们现在在每个工作节点上都有三个容器在运行:

图 8.15 – 检查容器在我们的节点上的分布
在我们继续查看堆栈之前,让我们先删除我们的服务。为此,请运行以下命令:
$ docker service rm cluster
这将删除所有容器,同时将下载的图像保留在主机上。
堆栈
使用 Swarm 服务,完全有可能创建相当复杂、高可用的多容器应用程序。在非 Swarm 集群中,手动启动应用程序的每一部分容器集可能会变得有些繁琐,并且也使共享服务变得困难。为此,Docker 创建了一个功能,允许您在 Docker Compose 文件中定义您的服务。
以下的 Docker Compose 文件,名为docker-compose.yml,将创建与我们在services部分启动的服务相同的服务:
version: '3'
services:
cluster:
image: russmckendrick/cluster
ports:
- '80:80'
deploy:
replicas: 6
restart_policy:
condition: on-failure
placement:
constraints:
- node.role == worker
正如您所看到的,堆栈可以由多个服务组成,每个服务都在 Docker Compose 文件的services部分中定义。
除了普通的 Docker Compose 命令,您还可以添加一个deploy部分;在这里,您可以定义与堆栈的 Swarm 元素相关的一切。
在之前的示例中,我们说过我们希望有六个副本,并且这些副本应分布在我们的两个工作节点上。同时,我们更新了默认的重启策略,您在检查前一部分中的服务时看到过,它显示为paused,这样如果容器变得无响应,它会始终被重新启动。
要启动我们的堆栈,请将之前的内容复制到docker-compose.yml文件中,然后运行以下命令:
$ docker stack deploy --compose-file=docker-compose.yml cluster
Docker 将—就像启动 Docker Compose 容器时一样—创建一个新的网络,然后在其上启动你的服务。你可以通过运行以下命令来检查堆栈的状态:
$ docker stack ls
这将显示一个服务已被创建。你可以通过运行以下命令获取由 stack 创建的服务的详细信息:
$ docker stack services cluster
最后,运行以下命令将显示堆栈中容器的运行位置:
$ docker stack ps cluster
请查看以下终端输出:

图 8.16 – 部署我们的堆栈
同样,你将能够使用节点的 IP 地址访问堆栈,系统将会将你路由到其中一个正在运行的容器。要删除堆栈,只需运行以下命令:
$ docker stack rm cluster
这将删除堆栈启动时创建的所有服务和网络。
删除一个 Swarm 集群
在继续之前,由于接下来的部分不再需要它,你可以通过运行以下命令删除你的 Swarm 集群:
$ multipass delete --purge node1
$ multipass delete --purge node2
$ multipass delete --purge node3
如果你因某种原因需要重新启动 Swarm 集群,只需按照本章开头的说明重新创建集群。
负载均衡、覆盖网络和调度
在接下来的几个部分中,我们查看了启动服务和堆栈的过程。要访问我们启动的应用程序,我们可以使用集群中任何主机的 IP 地址;这是怎么实现的呢?
入口负载均衡
Docker Swarm 内置了一个入口负载均衡器,使我们能够轻松地将流量分配到我们面向公共的容器。
这意味着你可以将 Swarm 集群中的应用程序暴露给外部服务——例如,像 Amazon Elastic Load Balancer(ELB)这样的外部负载均衡器——并且你知道,无论当前哪个主机托管容器,您的请求都会被路由到正确的容器,如下图所示:

图 8.17 – Swarm 集群中负载均衡的概述
这意味着我们的应用程序可以扩展、缩减、故障或更新,所有这些都无需重新配置外部负载均衡器与各个容器通信,因为 Docker Swarm 会为我们处理这些。
网络覆盖
在我们的示例中,我们启动了一个运行单个应用程序的简单服务。假设我们想在应用程序中添加一个数据库层,通常这是网络中的一个固定点;我们该怎么做呢?
Docker Swarm 的网络覆盖层扩展了你在多个主机上启动容器的网络,这意味着每个服务或堆栈都可以在其自己的隔离网络中启动。这意味着我们的数据库容器(运行 MongoDB)将可以通过端口 27017 访问所有在同一覆盖网络上运行的其他容器,无论这些容器运行在哪个主机上。
你可能会在心里想:等一下,这是不是意味着我需要将 IP 地址硬编码到我的应用配置中? 嗯,这与 Docker Swarm 想要解决的问题不太匹配,所以答案是否定的,你不需要这样做。
每个覆盖网络都有自己的内建 mongodb:27017,它将连接到我们的 MongoDB 容器。
这将使我们的图表如下所示:

图 8.18 – Docker Swarm 集群中覆盖网络的概述
在采用这种模式时,你需要考虑一些其他因素,但我们将在第十五章中讨论这些问题,Docker 工作流。
调度
在写作时,Docker Swarm 中只有一种调度策略,叫做 spread。该策略的作用是将任务调度到负载最轻的节点,该节点满足你在启动服务或堆栈时定义的约束条件。通常情况下,你不需要为你的服务添加太多约束。
Docker Swarm 当前不支持的一项功能是亲和性和反亲和性规则。虽然可以通过使用这种约束来规避,但我建议你不要过于复杂化事情,因为在定义服务时,如果施加太多约束,很容易导致主机过载或创建单点故障。
总结
在本章中,我们探讨了 Docker Swarm。我们了解了如何安装 Docker Swarm 以及构成 Docker Swarm 的各个组件。我们还探讨了如何使用 Docker Swarm,加入、列出和管理 Swarm 管理节点和工作节点。我们回顾了 service 和 stack 命令及其使用方法,并讲解了 Swarm 内置的入口负载均衡器、覆盖网络和调度器。
目前,Docker Swarm 在写作时处于一个有趣的状态,因为 Docker Swarm 是 Mirantis 在收购 Docker 企业版时获得的技术之一,尽管 Mirantis 表示他们将在 2 年内为现有的 Docker Swarm 集群提供支持(这是在 2019 年 11 月),但他们并没有提供关于 Docker Swarm 未来发展的更多信息。
这并不令人惊讶,因为 Mirantis 在另一种容器集群 Kubernetes 上做了很多工作,而我们将在第十一章中讨论它,Docker 和 Kubernetes。在此之前,在下一章中,我们将看一下 Docker 的图形用户界面(GUI)——Portainer。
问题
-
判断题:你应该使用独立的 Docker Swarm 运行 Docker Swarm,而不是使用内置的 Docker Swarm 模式。
-
在启动 Docker Swarm 管理节点后,您需要哪些两项内容来将工作节点添加到 Docker Swarm 集群?
-
你会使用哪个命令来查找 Docker Swarm 集群中每个节点的状态?
-
你会在 Swarm 管理节点上为
docker node inspect命令添加哪个标志,以使其更易读? -
如何将一个节点提升为管理节点?
-
你可以使用哪个命令来缩放你的服务?
进一步阅读
关于 Raft 共识算法的详细解释,我推荐阅读一份优秀的演讲《数据的秘密生活》,可以在thesecretlivesofdata.com/raft/找到。它通过一个易于跟随的动画解释了在管理节点上发生的所有后台过程。
第九章:Portainer – Docker 的图形用户界面
在本章中,我们将介绍 Portainer。Portainer 是一款通过 web 界面管理 Docker 资源的工具。
由于 Portainer 本身是以容器形式分发的,因此它易于安装,你可以在任何可以启动容器的地方运行它,这使得它成为那些不希望像我们在前几章中那样通过命令行管理容器的用户的完美界面。
在本章中,我们将覆盖以下主题:
-
通向 Portainer 的道路
-
启动 Portainer 并使其运行
-
使用 Portainer
-
Portainer 和 Docker Swarm
技术要求
如同之前的章节一样,我们将继续使用本地安装的 Docker。此外,本章中的截图将来自我偏好的操作系统 macOS。接近本章结束时,我们将使用 Multipass 启动一个本地 Docker Swarm 集群。
与之前一样,我们将执行的 Docker 命令将在我们安装 Docker 的所有三种操作系统上有效,然而,一些支持命令,虽然很少见,可能仅适用于 macOS 和 Linux 系统。
查看以下视频,了解代码的实际应用:bit.ly/3jR2HBa
通向 Portainer 的道路
在我们开始动手安装和使用 Portainer 之前,应该先讨论一下项目的背景。本书的第一版介绍了Docker UI。Docker UI 由 Michael Crosby 编写,经过大约一年的开发后,将项目交给了 Kevan Ahlquist。正是在这个阶段,由于商标问题,项目更名为 UI for Docker。
UI for Docker 的开发一直持续到 Docker 开始加速引入 Swarm 模式等功能到 Docker 核心引擎中。大约在这个时候,UI for Docker 项目被分支出来,成为了后来 Portainer 项目的雏形,并于 2016 年 6 月发布了第一次重大版本。从首次公开发布以来,Portainer 团队估计大部分代码已经更新或重写,到 2017 年中期,新增了角色权限控制和 Docker Compose 支持等新特性。
2016 年 12 月,一条通知被提交到 UI for Docker 的 GitHub 仓库,声明该项目已不再维护,并建议使用 Portainer。自发布以来,Portainer 已被下载超过 13 亿次。
现在我们已经了解了 Portainer 的一些背景信息,接下来让我们看看启动和配置它所需的步骤。
启动 Portainer 并使其运行
我们将首先查看如何使用 Portainer 来管理一个本地运行的单个 Docker 实例。我正在运行 Docker for Mac,因此将使用它,但这些指令同样适用于其他 Docker 安装。
首先,为了从 Docker Hub 获取容器镜像,我们只需运行以下命令:
$ docker image pull portainer/portainer
$ docker image ls
如果你正在跟着做,运行 docker image ls 命令时,你可以看到 Portainer 镜像只有 78.6 MB。要启动 Portainer,只需在 macOS 或 Linux 上运行以下命令:
$ docker volume create portainer_data
$ docker container run -d \
-p 9000:9000 \
-v /var/run/docker.sock:/var/run/docker.sock \
portainer/portainer
Windows 用户需要运行以下命令:
$ docker container run -d -p 9000:9000 -v \\.\pipe\docker_engine:\\.\pipe\docker_engine portainer/portainer
正如我们刚才运行的命令所示,我们正在将 Docker 主机上的 Docker 引擎套接字文件挂载到 Portainer。这样做将允许 Portainer 完全、无任何限制地访问 Docker 引擎。Portainer 需要这样做,以便管理主机上的 Docker;但是,这也意味着你的 Portainer 容器对主机具有完全访问权限,因此在给它访问权限时要小心,尤其是在将 Portainer 公共暴露到远程主机时。
以下截图展示了在 macOS 上执行的情况:

图 9.1 – 下载并启动 Portainer 容器
对于最基本的安装类型,以上就是我们需要运行的命令。完成安装还需要几个步骤,这些步骤都在浏览器中完成。要完成这些步骤,请访问 localhost:9000/。你将首先看到一个屏幕,要求你为管理员用户设置密码。
设置完密码后,你将进入登录页面:输入用户名 admin 和你刚刚配置的密码。登录后,系统会询问你希望管理的 Docker 实例。
有四个选项:
-
管理 Portainer 运行所在的 Docker 实例
-
管理远程 Docker 实例
-
连接到 Portainer 代理
-
连接到 Microsoft Azure 容器实例(ACI)
目前,我们想要管理 Portainer 运行的实例,即 Local 选项,而不是默认的 Remote 选项:

图 9.2 – 选择你希望用 Portainer 管理的环境
由于我们在启动 Portainer 容器时已经考虑了挂载 Docker 套接字文件,因此我们可以点击 Connect 来完成安装。这样我们将直接进入 Portainer,看到仪表盘。Portainer 启动并配置完毕后,我们现在可以看看一些功能。
使用 Portainer
现在,Portainer 已经启动并配置为与我们的 Docker 安装进行通信,我们可以开始浏览左侧菜单中列出的功能,从顶部的仪表盘开始,仪表盘也是 Portainer 安装的默认登录页面,如下图所示:

图 9.3 – 查看默认页面
首先,你会进入端点列表。由于我们只有本地安装,点击它,然后就可以开始探索了。
仪表盘
从以下截图可以看到,仪表板给出了 Portainer 配置与之通信的 Docker 实例的当前状态概览:

图 9.4 – 获取概览
就我而言,这显示了我运行的容器数量,目前只有已经运行的 Portainer 容器,以及我下载的镜像数量。我们还可以看到 Docker 实例中可用的卷和网络数量。它还会显示正在运行的堆栈数量:
它还显示了 Docker 实例本身的基本信息;正如你所看到的,Docker 实例正在运行 Moby Linux,拥有 6 个 CPU 和 2.1GB 的 RAM。这是 Docker for Mac 的默认配置。
仪表板将根据你运行 Portainer 的环境进行自适应调整,因此我们将在将 Portainer 附加到 Docker Swarm 集群时重新审视它。
应用模板
接下来,在左侧菜单中,我们有应用模板。这一部分可能是唯一一个不直接包含在核心 Docker 引擎中的功能;它实际上是通过使用从 Docker Hub 下载的容器启动常见应用程序的方式:

图 9.5 – 探索模板
默认情况下,Portainer 附带大约 25 个模板。模板是以 JSON 格式定义的。例如,NGINX 模板看起来如下所示:
{
'type': 'container',
'title': 'Nginx',
'description': 'High performance web server',
'categories': ['webserver'],
'platform': 'linux',
'logo': 'https://portainer.io/images/logos/nginx.png',
'image': 'nginx:latest',
'ports': [
'80/tcp',
'443/tcp'
],
'volumes': ['/etc/nginx', '/usr/share/nginx/html']
}
你可以添加更多的选项,例如 MariaDB 模板:
{
'type': 'container',
'title': 'MariaDB',
'description': 'Performance beyond MySQL',
'categories': ['database'],
'platform': 'linux',
'logo': 'https://portainer.io/images/logos/mariadb.png',
'image': 'mariadb:latest',
'env': [
{
'name': 'MYSQL_ROOT_PASSWORD',
'label': 'Root password'
} ],
'ports': ['3306/tcp' ],
'volumes': ['/var/lib/mysql']
}
正如你所看到的,模板看起来类似于 Docker Compose 文件;然而,这种格式仅由 Portainer 使用。大多数选项都非常直观,但我们应该讲一下名称和标签选项。
对于通常需要通过环境变量传递自定义值来定义选项的容器,名称和标签选项允许你为用户提供自定义表单字段,在容器启动之前需要填写这些字段,如以下截图所示:

图 9.6 – 使用模板启动 MariaDB
正如你所看到的,我们有一个字段,可以在其中输入希望用于 MariaDB 容器的 root 密码。填写此字段后,系统会将该值作为环境变量传递,构建以下命令以启动容器:
$ docker container run --name [Name of Container] -p 3306 -e MYSQL_ROOT_PASSWORD=[Root password] -d mariadb:latest
如果想了解更多关于应用模板的信息,我建议查看文档——链接可以在本章的进一步阅读部分找到。
容器
我们接下来要查看的左侧菜单中的内容是容器。这是启动和与 Docker 实例上运行的容器交互的地方。点击容器菜单项,将会显示 Docker 实例上所有容器的列表,包括正在运行和已停止的容器:

图 9.7 – 列出容器
如你所见,我目前只运行了一个容器,而这个容器恰好是 Portainer 容器。我们不打算与它互动,而是点击+ 添加容器按钮,启动一个运行我们在前几章中使用的集群应用的容器。
创建容器页面有多个选项,这些选项应按照以下方式填写:
-
cluster -
russmckendrick/cluster -
始终拉取镜像:开
-
将所有暴露的网络端口发布到随机的主机端口:开
最后,通过点击+ 发布新的网络端口,将主机的 8080 端口映射到容器的 80 端口。你填写的表单应该类似于以下截图:

图 9.8 – 启动一个容器
完成后,点击部署容器,几秒钟后,正在运行的容器列表将返回,你应该能看到你新启动的容器:

图 9.9 – 列出容器
在列表中每个容器左侧的勾选框可以启用顶部的按钮,你可以通过这些按钮控制容器的状态。请确保不要Kill或Remove Portainer 容器。点击我们所使用的集群容器名称,将显示更多关于容器本身的信息:

图 9.10 – 深入查看我们的容器
如你所见,容器的信息与你运行以下命令所得到的信息相同:
$ docker container inspect cluster
你可以通过点击Inspect来查看该命令的完整输出。你还会注意到,有Stats、Logs、Console和Attach按钮,我们接下来将讨论这些按钮。
Stats
Stats 页面显示了 CPU、内存和网络利用率,以及正在检查的容器的进程列表:

图 9.11 – 查看容器的统计信息
如果你保持页面打开,图表会自动刷新,而刷新页面会将图表清零并重新开始。这是因为 Portainer 使用以下命令从 Docker API 获取这些信息:
$ docker container stats cluster
每次刷新页面时,命令都会从头开始执行,因为 Portainer 当前不会在后台轮询 Docker 来记录每个正在运行的容器的统计信息。
日志
接下来是Logs页面。这里显示了运行以下命令的结果:
$ docker container logs cluster
它显示了 STDOUT 和 STDERR 日志:

图 9.12 – 查看容器日志
您还可以选择向输出中添加时间戳;这相当于运行以下命令:
$ docker container logs --timestamps cluster
如我们之前所讨论的,请记住,根据主机的时区设置,时间戳可能会有所不同。
控制台和附加
接下来,我们有/bin/bash、/bin/sh或/bin/ash,以及选择以哪个用户进行连接——root 是默认值。虽然集群镜像中已安装了这两种 shell,我选择使用/bin/bash:

图 9.13 – 打开与容器的会话
这相当于运行以下命令来访问您的容器:
$ docker container exec -it cluster /bin/sh
如您从截图中可以看到,bash 进程的 PID 是 15。该进程是由docker container exec命令创建的,当您断开与 shell 会话的连接时,它将是唯一一个被终止的进程。
如果我们在启动容器时使用了TTY标志,我们也可以使用容器的TTY,而不是像使用控制台时那样生成一个 shell 进行连接,类似于在命令行上附加时,当断开连接时您的进程会停止。
镜像
左侧菜单中的下一个选项是镜像。在这里,您可以管理、下载和上传镜像:

图 9.14 – 管理您的镜像
在页面顶部,您可以选择拉取一个镜像。例如,只需在框中输入amazonlinux,然后点击拉取镜像,就会从 Docker Hub 下载一份 Amazon Linux 容器镜像。Portainer 执行的命令将是:
$ docker image pull amazonlinux:latest
您可以通过点击镜像 ID 来获取关于每个镜像的更多信息;这将带您进入一个页面,页面上会清晰地显示运行此命令的输出:
$ docker image inspect russmckendrick/cluster
请看以下截图:

图 9.15 – 获取有关您的镜像的更多信息
您不仅可以获取关于镜像的所有信息,还可以选择将镜像的副本推送到您选择的注册表,或者默认情况下推送到 Docker Hub。您还可以查看镜像中包含的每个层的详细信息,显示在构建过程中执行的命令和每个层的大小。
菜单中的下两个项目允许您管理网络和卷;由于它们并没有太多内容,我这里不打算详细介绍。
网络
在这里,您可以使用默认的 bridge 驱动程序快速添加一个网络。点击高级设置将带您到一个页面,其中有更多选项。这些选项包括使用其他驱动程序、定义子网、添加标签和限制对网络的外部访问。与其他部分一样,您还可以删除网络并检查现有的网络。
卷
这里没有太多选项,除了添加或删除卷。当添加卷时,你可以选择驱动程序,并可以填写选项传递给驱动程序,这样就可以使用第三方驱动程序插件。除此之外,几乎没有什么可看的,甚至没有检查选项。
事件
事件页面显示了过去 24 小时内的所有事件;你还可以选择过滤结果,这意味着你可以快速找到你需要的信息:

图 9.16 – 查看 Docker 事件
这相当于运行以下命令:
$ docker events --since '2020-04-17T16:30:00' --until '2020-04-
17T16:30:00'
这为我们提供了一个额外的选择需要覆盖。
主机
最后的条目仅显示以下命令的输出:
$ docker info
以下显示了命令的输出:

图 9.17 – 查看 Docker 主机的信息
如果你正在针对多个 Docker 实例端点,并且需要了解端点运行的环境,这将非常有用。
到这时,我们将开始查看在 Docker Swarm 上运行的 Portainer,所以现在是时候删除正在运行的容器以及我们最初启动 Portainer 时创建的卷了。你可以使用以下命令删除该卷:
$ docker volume prune
现在我们已经清理完毕,接下来让我们来看一下如何启动一个 Docker Swarm 集群。
Portainer 与 Docker Swarm
在上一节中,我们介绍了如何在独立的 Docker 实例上使用 Portainer。Portainer 也支持 Docker Swarm 集群,界面中的选项会根据集群环境进行调整。我们应该先启动一个 Swarm,然后将 Portainer 作为服务启动,看看有什么变化。
所以让我们从启动一个新的 Docker Swarm 集群开始。
创建 Swarm
如同在 Docker Swarm 章节中,我们将使用 Multipass 在本地创建 Swarm;为此,运行以下命令启动三个节点:
$ multipass launch -n node1
$ multipass launch -n node2
$ multipass launch -n node3
现在安装 Docker:
$ multipass exec node1 -- \
/bin/bash -c 'curl -s https://get.docker.com | sh - && sudo
usermod -aG docker ubuntu'
$ multipass exec node2 -- \
/bin/bash -c 'curl -s https://get.docker.com | sh - && sudo
usermod -aG docker ubuntu'
$ multipass exec node3 -- \
/bin/bash -c 'curl -s https://get.docker.com | sh - && sudo
usermod -aG docker ubuntu'
安装完 Docker 后,初始化并创建集群:P=$(multipass info node1 | grep IPv4 | a
$ multipass exec node1 -- \
/bin/bash -c 'docker swarm init --advertise-addr $IP:2377
--listen-addr $IP:2377'
$ SWARM_TOKEN=$(multipass exec node1 -- /bin/bash -c 'docker
swarm join-token --quiet worker')
$ multipass exec node2 -- \
/bin/bash -c 'docker swarm join --token $SWARM_TOKEN
$IP:2377'
$ multipass exec node3 -- \
/bin/bash -c 'docker swarm join --token $SWARM_TOKEN
$IP:2377'
通过运行以下命令,记下node1的 IP 地址:
$ multipass list
最后,登录到 Swarm 管理节点:
$ multipass shell node1
你现在应该已登录到主 Swarm 节点,并准备继续。
Portainer 服务
现在我们有了 Docker Swarm 集群,我们可以通过简单地运行以下命令启动 Portainer 堆栈:
$ curl -L https://downloads.portainer.io/portainer-agent-stack.yml -o portainer-agent-stack.yml
$ docker stack deploy --compose-file=portainer-agent-stack.yml portainer
这应该会给你如下所示的输出:

图 9.18 – 在我们的 Docker Swarm 集群上启动 Portainer Stack
一旦堆栈创建完成,你应该能够在浏览器中访问node1的 IP 地址,并在末尾加上:9000;例如,我打开了192.168.64.9:9000。
Swarm 的区别
如前所述,当 Portainer 连接到 Docker Swarm 集群时,界面会有一些变化。在本节中,我们将介绍这些变化。如果界面的一部分没有提到,那么在单主机模式和 Docker Swarm 模式下运行 Portainer 是没有区别的。我们要看的第一个变化是,当您首次登录新启动的 Portainer 时会发生什么变化。
端点
登录后,您首先需要选择一个端点。正如您在下面的屏幕中看到的,只有一个名为primary的端点:

图 9.19 – 查看端点
点击端点将带您进入仪表板。我们将在本节末尾再次查看端点。
仪表板和 Swarm
您会注意到的第一个变化是,仪表板现在显示了一些关于 Swarm 集群的信息。正如您在下面的屏幕中看到的,顶部有一个集群信息部分:

图 9.20 – 获取集群概览
点击前往集群可视化工具将带您进入 Swarm 页面。这为您提供了集群的可视化概览,目前唯一正在运行的容器是提供和支持 Portainer 服务所需的容器:

图 9.21 – 可视化集群
堆栈
我们还没有覆盖的左侧菜单项是堆栈。从这里,您可以启动堆栈,就像我们在查看 Docker Swarm 时所做的那样。事实上,让我们使用之前的 Docker Compose 文件,它看起来像这样:
version: '3'
services:
redis:
image: redis:alpine
volumes:
- redis_data:/data
restart: always
mobycounter:
depends_on:
- redis
image: russmckendrick/moby-counter
ports:
- '8080:80'
restart: always
volumes:
redis_data:
点击MobyCounter。不要在名称中添加任何空格或特殊字符,因为这是 Docker 使用的。然后点击部署堆栈。
部署完成后,您可以点击MobyCounter并管理堆栈:

图 9.22 – 启动堆栈
堆栈是服务的集合。接下来,我们将一起查看它们。
服务
此页面是您可以创建和管理服务的地方;它应该已经显示了多个服务,包括 Portainer。为了避免与正在运行的 Portainer 容器发生冲突,我们将创建一个新服务。为此,请点击+ 添加服务按钮。在加载的页面上,输入以下内容:
-
cluster -
russmckendrick/cluster -
Replicated -
1
这一次,我们需要为主机上的端口7000添加端口映射,以映射到容器的端口80,这是因为由于我们之前已经启动的服务和堆栈,一些常用的端口已经在主机上被占用:

图 9.23 – 启动服务
输入完信息后,点击创建服务按钮。你将被带回服务列表页面,列表中应该会显示我们刚刚添加的集群服务。你可能已经注意到,在调度模式部分,有一个可以进行扩展的选项。点击它,并将我们的集群服务的副本数增加到6。
点击名称部分中的集群,我们将看到服务的概述。正如你所见,关于服务有大量信息:

图 9.24 – 查看服务详情
你可以在运行时对服务进行很多更改,包括放置约束、重启策略、添加服务标签等等。在页面底部,有一个与服务相关的任务列表:

图 9.25 – 查看任务
如你所见,我们有六个正在运行的任务,每个节点上有两个任务。点击左侧菜单中的容器可能会显示与预期不同的内容:

图 9.26 – 列出所有容器
你可以看到集群中所有正在运行的容器,而不仅仅是我们部署 Portainer 的节点上的容器。你可能记得,当我们查看集群可视化工具时,集群内每个节点上都有 Portainer Agent 容器,这些容器是作为堆栈的一部分启动的。它们会将信息反馈回来,为我们提供集群的整体视图。
返回集群可视化工具后,显示我们现在运行了更多的容器:

图 9.27– 查看可视化工具
让我们看看迁移到运行 Docker Swarm 后,还有什么变化。
应用模板
现在进入应用模板页面会显示堆栈,而不是容器:

图 9.28 – 查看堆栈模板
如你所见,有许多默认选项,点击其中一个,比如Wordpress,将带你进入一个页面,你只需输入一些信息,然后点击部署堆栈按钮。部署后,你可以进入堆栈页面,查看已分配给服务的端口:

图 9.29 – 启动 Wordpress
一旦知道端口,输入任何节点的 IP 地址和端口,你将进入应用程序,按照安装说明操作后,页面应显示如下:

图 9.30 – WordPress 启动并运行
这些模板托管在 GitHub 上,你可以在进一步阅读部分找到链接。
移除集群
一旦你完成了对 Docker Swarm 上 Portainer 的探索,你可以通过在本地机器上运行以下命令来移除集群:
$ multipass delete --purge node1
$ multipass delete --purge node2
$ multipass delete --purge node3
在本地机器上移除正在运行的节点非常重要,因为如果不移除,它们将继续运行并消耗资源。
总结
这就是我们对 Portainer 的深入探讨。如你所见,Portainer 非常强大,且易于使用,随着新特性发布,它将继续发展并整合更多 Docker 生态系统的功能。使用 Portainer,你不仅可以操作主机,还能管理单机或集群主机上运行的容器和服务。
在下一章,我们将介绍 Docker 支持的另一种容器集群解决方案——Kubernetes。
问题
-
在 macOS 或 Linux 机器上,Docker 套接字文件的挂载路径是什么?
-
Portainer 默认运行在哪个端口?
-
对还是错:你可以将 Docker Compose 文件用作应用程序模板。
-
对还是错:Portainer 显示的统计数据仅为实时数据,无法查看历史数据。
进一步阅读
你可以在这里找到更多关于 Portainer 的信息:
-
官方网站:
www.portainer.io -
GitHub 上的 Portainer:
github.com/portainer/
第十章:在公共云中运行 Docker
到目前为止,我们一直使用 Digital Ocean 在基于云的基础设施上启动容器。在本章中,我们将探讨Amazon Web Services(AWS)、Microsoft Azure 和 Google Cloud 提供的容器解决方案。
在讨论这些容器服务之前,我们还将介绍每个云服务提供商的历史背景,并介绍启动容器服务所需的任何命令行工具的安装。
本章将覆盖以下主题:
-
AWS
-
Microsoft Azure
-
Google Cloud Run
技术要求
在本章中,我们将使用 AWS、Microsoft Azure 和 Google Cloud,因此如果你跟随操作,你需要拥有一个或多个云服务提供商的活跃账户。
请记住,如果你跟随操作,可能会对你在公共云提供商处启动的服务产生费用——请确保在使用完资源后终止它们,以避免任何意外费用。
Amazon Web Services
本章我们将首先介绍的公共云提供商是 AWS。它最早于 2002 年 7 月作为内部服务推出,用于在亚马逊内部提供一些分散的服务,支持亚马逊零售网站。大约一年后,亚马逊的一次内部演示为 AWS 的发展奠定了基础:一个标准化且完全自动化的计算基础设施,用来支持亚马逊构建基于网络的零售平台的愿景。
在演示的最后,提到亚马逊可能会出售一些 AWS 提供的服务访问权限,以帮助资助为启动平台所需的基础设施投资。2004 年底,这些公共服务中的第一个服务——Amazon Simple Queue Service(Amazon SQS)——作为分布式消息队列服务发布。大约在这个时候,亚马逊开始开发既能为零售网站提供服务,又能向公众销售的服务。
2006 年,AWS 重新启动,Amazon SQS后加入了Amazon Simple Storage Service(Amazon S3)和Amazon Elastic Compute Cloud(Amazon EC2),从那时起,AWS 的受欢迎程度不断增长。撰写本文时,AWS 的年收入超过 250 亿美元,平台提供的服务数量也从最初的 3 个增长到了 210 多个。
这些服务包括我们在第三章中讨论过的Amazon Elastic Container Registry(Amazon ECR),存储和分发镜像,Amazon Elastic Container Service(Amazon ECS)和AWS Fargate。虽然后两个服务实际上是为了协同工作而设计的,但让我们快速回顾一下在 AWS 引入 AWS Fargate 之前,使用 Amazon ECS 时有哪些可用的选项。
Amazon ECS – 基于 EC2
亚马逊对 Amazon ECS 的描述如下:
"一个高度可扩展、快速的容器管理服务,方便在集群上运行、停止和管理 Docker 容器。"
本质上,它允许你在启动第一个 ECS 集群之前,管理 Docker 容器在计算资源集群上的位置和可用性。让我们快速了解一下相关术语,并大致了解它是如何组合的。当我们启动托管在 Amazon ECS 上的应用时,我们需要定义以下内容:
-
容器
-
任务
-
服务
-
集群
以下图示将给你一个如何在集群中使用前述元素的概念;你可以清晰地看到容器、任务和服务,在这个图示中,服务是跨越三个实例的框:

图 10.1 – Amazon ECS 容器实例
前述图示与如果我们使用 AWS Fargate 之间的一个重要区别在于我们正在运行容器实例。要启动 EC2 实例集群,请按照以下步骤操作:
-
打开 AWS 控制台:
console.aws.amazon.com/。 -
登录。
-
在页面左上角的 服务 菜单中选择 ECS。
-
打开 Amazon ECS 页面后,在右上角的区域切换器中选择你喜欢的区域——由于我在英国,所以选择了伦敦。
-
点击左侧菜单中的 集群,这是第一个选项:
![图 10.2 – 创建 Amazon ECS 集群]()
图 10.2 – 创建 Amazon ECS 集群
-
点击 创建集群 按钮后,你将看到三个选项:
仅限网络
EC2 Linux + 网络
EC2 Windows + 网络
-
由于我们接下来将启动一个 AWS Fargate 集群,并且我们的测试应用程序是基于 Linux 的,请选择 EC2 Linux + 网络,然后点击底部的 下一步 按钮,你应该会看到 配置集群 页面。
-
我们需要为集群命名。集群名称是唯一的,我将我的命名为
RussTestECSCluster。 -
保持
1至3;使用竞价实例时,确保至少有一个以上的实例,以保持基本的可用性。EC2 Ami Id:此选项无法更改。
EBS 存储(GiB):我将其保留为默认值,22GB。
密钥对:我将其保留为无 - 无法 SSH。
-
对于 网络、容器实例 IAM 角色、Spot Fleet IAM 角色 和 标签,我将所有选项保持为默认值,直到最后一个选项 CloudWatch 容器洞察,我勾选了 启用容器洞察。所有内容填写完毕后,点击 创建 按钮。点击后,集群将被启动,你可以跟踪资源配置和启动的进度:
![图 10.3 – 启动 ECS 集群]()
图 10.3 – 启动 ECS 集群
-
启动后,你可以点击查看集群按钮,这将带你到以下界面:
![图 10.4 – 查看 ECS 集群]()
图 10.4 – 查看 ECS 集群
-
现在我们已经有了集群,我们需要创建一个任务定义。为此,点击左侧菜单中的任务定义,然后点击创建新任务定义按钮。首先给出的选项是选择我们要使用的启动类型。我们刚刚创建了一个基于 EC2 的集群,因此选择EC2:
![图 10.5 – 选择我们需要的任务类型]()
图 10.5 – 选择我们需要的任务类型
-
选择后,点击
cluster-task。1gb。1 vcpu。 -
这将带我们进入容器定义部分。由于任务定义是容器的集合,你可以在这里输入多个容器。在这个示例中,我们将添加一个单独的容器。首先,点击
cluster-container。russmckendrick/cluster:latest。128。80用于tcp。 -
保持表单的其余部分为默认设置,然后点击添加。
-
我们需要定义的任务定义的最后一部分是
cluster-task,保持卷类型为绑定月,并将源路径留空,然后点击添加按钮。现在,点击创建,然后通过左侧的集群菜单返回到集群。 -
现在我们已经有了集群和任务定义,我们可以创建一个服务。为此,在
cluster-service中进行创建。服务类型:保持为REPLICA。
任务数量:输入 2。
最小健康百分比:保持为100。
最大百分比:保持为200。
-
保持部署和任务放置选项为默认值,并点击页面底部的下一步按钮,这将带你到配置网络页面:
负载均衡器类型:选择无。
启用服务发现集成:取消勾选此选项。
-
点击下一步,将服务自动扩展设置为默认的不调整服务的期望数量选项,然后点击下一步。在审核选项后,点击创建服务按钮。
-
在打开正在运行的容器之前,我们需要打开防火墙规则。为此,选择
30000-60000。0.0.0.0/0。容器端口。 -
然后,点击保存规则按钮。这样你应该看到如下所示的内容:
![图 10.6 – 更新安全组规则]()
图 10.6 – 更新安全组规则
-
设置完规则后,返回到你的 ECS 集群,点击任务标签,然后选择两个正在运行的任务之一。任务概览页面加载后,向下滚动到容器并展开表格中列出的容器,然后点击外部链接。这将直接带你到正在运行的容器:

图 10.7 – 我们的工作应用程序
现在您已经看到正在运行的容器,接下来让我们删除集群。为此,请前往集群概览页面,并点击右上角的删除集群按钮 – 然后按照屏幕上的指示操作。
这是以前在 AWS 中启动容器的传统方式;我说是“传统方式”,因为我们仍然需要启动 EC2 实例来为我们的集群提供支持,即便我们使用的是抢占实例,我们依然会有许多未被使用的资源,这些本应收费 —— 那么如果我们只需要启动容器,而不必担心管理一堆 EC2 资源怎么办呢?
亚马逊 ECS – AWS Fargate 支持
2017 年 11 月,亚马逊宣布他们一直在开发 AWS Fargate —— 这是一项与 Amazon ECS 兼容的服务,消除了启动和管理 EC2 实例的需求。相反,您的容器将在亚马逊的后台启动,且按秒计费,这意味着您只需为容器化应用在生命周期中所请求的 vCPU 和内存付费。
我们将稍微“作弊”一下,先通过亚马逊 ECS 的首次运行流程来操作。您可以通过访问以下网址进入:https://console.aws.amazon.com/ecs/home?#/firstRun。
这将引导我们完成启动容器到 Fargate 集群中的四个步骤。启动我们的 AWS Fargate 托管容器的第一步是配置容器和任务定义:
-
对于我们的示例,有三个预定义选项和一个自定义选项。点击
cluster-container。russmckendrick/cluster:latest。80,并保持选择tcp。 -
然后,点击
cluster-task。awsvpc;您不能更改此选项。ecsTaskExecutionRole。FARGATE,并且您不应该能够编辑它。任务内存和任务 CPU:将两者保持在默认选项。
-
一旦一切都已更新,点击保存按钮。现在,您可以点击页面底部的下一步按钮。这将带我们进入第二步,也就是定义服务的地方。
-
如我们在上一节讨论的,服务运行任务,这些任务又与容器相关联。默认的服务设置是没问题的,因此点击下一步按钮继续进行启动过程的第三步。第一步是创建集群。再次强调,默认值是没问题的,因此点击下一步按钮进入审核页面。
-
这是您在启动任何服务之前最后一次检查任务、服务和集群定义的机会。如果您对一切都满意,请点击创建按钮。从这里,您将进入一个页面,在该页面上您可以查看构建我们 AWS Fargate 集群的各种 AWS 服务的状态:
![图 10.8 – 启动我们的 Fargate 集群]()
图 10.8 – 启动我们的 Fargate 集群
-
一旦所有内容从 待处理 状态变为 完成,你将能够点击 查看服务 按钮,进入 服务 概览页面:
![图 10.9 – 查看我们的 Fargate 集群]()
图 10.9 – 查看我们的 Fargate 集群
-
现在,我们需要知道容器的公网 IP 地址。要找到这个地址,点击 任务 标签,然后选择正在运行任务的唯一 ID。在页面的 网络 部分,你应该能够找到任务的私有和公网 IP 地址。将公网 IP 输入浏览器后应该能够打开我们熟悉的集群应用:

图 10.10 – 我们的工作应用程序
你会注意到显示的容器名称是容器的主机名,并包含了内部 IP 地址。你还可以通过点击 日志 标签查看容器的日志:

图 10.11 – 查看容器日志
那么,这个费用是多少呢?要运行一个容器整整一个月的费用大约是 14 美元,按每小时约 0.019 美元计算。
这个费用意味着,如果你计划 24/7 全天候运行多个任务,那么 Fargate 可能不是最具成本效益的容器运行方式。相反,你可能希望选择 Amazon ECS EC2 选项,在那里你可以将更多容器打包到你的资源中,或者选择 Amazon EKS 服务,我们将在本章稍后讨论。然而,对于快速启动一个容器然后终止它,Fargate 是非常出色的——启动容器的门槛低,支持的资源数量也很少。
一旦你完成了 Fargate 容器的操作,应该删除集群。这将移除与集群关联的所有服务。删除集群后,进入 任务定义 页面,必要时注销它们。
总结 AWS
在本章的这一部分,我们仅介绍了 Amazon ECS 的基本工作原理;我们没有涵盖的是 Amazon ECS 管理的容器与其他 AWS 服务(如 弹性负载均衡、Amazon Cloud Map 和 AWS App Mesh)的紧密集成。此外,通过使用 Amazon ECS 命令行工具,你可以将 Docker Compose 文件部署到 Amazon ECS 管理的集群中。
现在我们已经掌握了 AWS 的基础知识,接下来让我们来看一下它的主要竞争对手之一,Microsoft Azure。
Microsoft Azure
Microsoft Azure,或最初称为 Windows Azure,是微软进入公共云市场的产品。它提供了软件即服务(SaaS)、平台即服务(PaaS)和基础设施即服务(IaaS)的组合服务。它最初是一个内部微软项目,代号为Project Red Dog,大约在 2005 年启动。Project Red Dog 是Red Dog 操作系统的延续,后者是一个基于 Windows 操作系统的分支,专注于使用核心 Windows 组件提供数据中心服务。
该服务在 2008 年的微软开发者大会上公开宣布,由五个核心服务组成:
-
Windows Azure:允许用户启动并管理计算实例、存储和各种网络服务。
-
Microsoft SQL 数据服务:微软 SQL 数据库的云版本。
-
Microsoft .NET 服务:允许您将 .NET 实例部署到云端,而无需担心实例问题的服务。
-
Microsoft SharePoint 和 Microsoft Dynamics 服务:这些将是微软的企业内部网和客户关系管理(CRM)软件的 SaaS 提供。
它于 2010 年初推出,评价不一,因为有些人认为它相对于 AWS 来说功能有限,而 AWS 此时已经发布 4 年,且更加成熟。然而,微软坚持不懈,在过去的 10 年里增加了许多服务,远远超越了其 Windows 根基。这促使了 Windows Azure 在 2014 年更名为 Microsoft Azure。
从那时起,微软的云在功能上迅速赶上了 AWS,根据你阅读的新闻来源,企业选择微软的云来运行其云工作负载,得益于与其他微软服务(如Microsoft Office 和 Microsoft 365)的紧密集成。
Azure 容器 Web 应用
Microsoft Azure 应用服务是一个完全托管的平台,允许您部署应用程序,并让 Azure 负责管理它们运行的平台。在启动应用服务时,有几种选项可供选择。您可以运行用.NET、.NET Core、Ruby、Node.js、PHP 和 Python 编写的应用程序,或者直接从容器镜像注册表中启动一个镜像。
在这次简短的演练中,我们将从 Docker Hub 启动集群镜像:
-
登录 Azure 门户:
portal.azure.com/。 -
从左侧菜单中选择应用服务,可以通过屏幕左上角的汉堡菜单图标访问:
![图 10.12 – 准备启动应用服务]()
图 10.12 – 准备启动应用服务
-
在加载的页面上,点击
Docker 容器。操作系统:保留为Linux。
区域:选择你偏好的区域。
应用服务计划:默认情况下,选择了一个更昂贵的生产就绪计划,所以在 Sku and size 部分点击 Change size 将为您提供更改定价层次的选项。对于我们的需求,Dev/Test 计划将是合适的选择。
-
一旦您选择并填写了上述选项,请点击
Single container。从下拉列表中选择
Docker Hub。这将在表单下方打开 Docker Hub 的选项。public.russmckendrick/cluster:latest.启动命令:留空。
-
完成所有步骤后,浏览 Monitoring 和 Tag 标签,然后在查看信息后点击 Create 按钮。一两分钟后,您将看到如下屏幕:
![Figure 10.13 – 部署应用服务]()
图 10.13 – 部署应用服务
-
点击 Go to resource 按钮将带您到新启动的应用程序:

图 10.14 – 查看我们运行的应用服务
我们的应用程序已经启动,您应该能够通过 Azure 提供的 URL 访问服务,例如,我的是 masteringdocker4thedition.azurewebsites.net。打开这个链接,您的浏览器将显示集群应用程序:

图 10.15 – 显示集群应用程序
正如您所见,这一次我们有容器 ID,而不是在启动 AWS Fargate 上时得到的完整主机名。这种规格的容器每月大约花费我们 $10。
将容器作为应用程序启动,而不仅仅是一个普通的容器还有一些其他非常有用的优势,例如,您可能已经注意到我们的容器 URL 启用了SSL 证书。虽然当前使用的是覆盖 azurewebsites.net 的证书,您可以添加自定义域并提供自己的 SSL 证书。
另一个方便的功能是,您可以通过来自 Webhook 的触发器自动更新单个容器。例如,当您的新容器镜像成功构建后,您可以在应用程序的Container settings页面找到此选项:

图 10.16 – 启动多个容器
此外,正如我们首次配置应用程序时提到的,您可以使用 Docker Compose 文件在 Microsoft Azure Web 应用程序中启动多个容器。
完成 Web 应用程序后,删除它和资源组,假设它不包含其他您需要的资源。
Azure 容器实例
现在我们已经学习了如何使用 Azure Web 应用启动 Docker 容器,接下来让我们看看 Azure 容器实例服务。可以将其视为类似于 AWS Fargate 服务的概念,它允许你直接在微软的共享底层架构上启动容器。
让我们配置一个容器实例:
-
在屏幕顶部的搜索栏中输入
Container instances,然后点击容器实例服务的链接。 -
页面加载完成后,点击
Public。russmckendrick/cluster:latest。Linux。1GB。 -
填写完信息后,点击
是。配置了
80端口和TCP协议。DNS 名称标签:为你的容器输入 DNS 名称。
-
跳过高级和标签部分,直接进入审核 + 创建。验证通过后,点击创建按钮:
![图 10.17 – 启动我们的 Azure 容器实例]()
图 10.17 – 启动我们的 Azure 容器实例
-
如之前所示,点击转到资源按钮,你将被带到新创建的 Azure 容器实例,正如下面的截图所示:

图 10.18 – 我们的 Azure 容器实例概览
选项比我们在 Web 应用中看到的要少,这是因为 Azure 容器实例的设计目的是执行一项任务:运行容器。
如果你点击了/bin/sh选项:

图 10.19 – 打开会话连接到我们的 Azure 容器实例
将概览页面中提供的 URL 输入浏览器,在我的例子中是 masteringdocker4thedition-aci.uksouth.azurecontainer.io/,你将看到你的容器应用:

图 10.20 – 我们正在运行的应用程序
如你所见,这次我们有了容器 ID,在我的案例中是 wk-caas-ca0c275b8f3e4ce2848c5802ee406a13-4e02f5281687aa1e58d98f。你可能会注意到这次没有 HTTPS,只有普通的 HTTP,使用 80 端口\。
一旦你完成 Azure 容器实例的操作,点击删除,如果需要的话,还要删除资源组。
总结 Microsoft Azure
表面上看,我们在本节中讨论的这两项服务似乎很相似,实际上它们非常不同。微软 Web 应用是微软提供的托管服务,基于容器技术。通常,启动的容器是为最终用户启动的代码。然而,在运行容器时,它们最终会在 Docker 中运行 Docker。Azure 容器实例仅仅是运行中的容器——没有额外的包装或帮助程序,只有原生容器。
我们现在已经在 Microsoft Azure 上打下了基础。随着 Amazon 和 Microsoft 已经在云计算领域站稳脚跟,Google 推出了自己的竞争产品也就不足为奇了。接下来我们来看看它。
Google Cloud
在三大公共云平台中,Google Cloud 是最年轻的。它最初于 2008 年作为 Google App Engine 启动。App Engine 是 Google 的 PaaS 产品,支持 Java、PHP、Node.js、Python、C#、.Net、Ruby 和 Go 应用程序。与 AWS 和 Microsoft Azure 不同,Google 作为 PaaS 服务存在了超过 4 年,直到推出 Google Compute Engine。
在下一章中,我们将学习更多关于 Google 进入云计算的历程,尤其是当我们开始讨论 Kubernetes 时,所以我在这里不会详细展开。接下来,我们直接进入正题。
Google Cloud Run
Google Cloud Run 与我们在本章中查看的其他容器服务略有不同。我们需要做的第一件事是将镜像托管在 Google Container Registry 中,以便使用该服务:
-
让我们从 Docker Hub 获取一个集群镜像的副本:
$ docker image pull russmckendrick/cluster -
现在,我们需要使用 Google Cloud 命令行工具登录到我们的 Google Cloud 账户。为此,运行以下命令:
$ gcloud init -
登录后,我们可以通过运行以下命令来配置 Docker 使用 Google Container Registry:
$ gcloud auth configure-docker -
现在,Docker 已配置为与 Google Container Registry 交互,我们可以运行以下命令来创建标签并推送我们的镜像:
masterdocker4; you will need to replace that with your own Google Cloud project name. You can verify that the image has been pushed by logging in to the Google Cloud console and navigating to Google Container Registry by entering Cloud Registry into the search bar at the top of the page. You should see something like the following:Figure 10.21 – Viewing our cluster image in Google Container Registry -
现在,在页面顶部的搜索栏中,输入
Cloud Run并点击链接。 -
一旦 Cloud Run 页面加载完成,点击页面顶部的创建服务按钮。
-
首先,你需要做的是选择要使用的镜像。点击选择,然后突出显示集群镜像,并选择你想要使用的镜像版本:
![图 10.22 – 选择 Google Container Registry 镜像]()
图 10.22 – 选择 Google Container Registry 镜像
-
选择完成后,点击继续。
-
对于部署平台,我们将使用Cloud Run(完全托管)。从下拉框中选择离你最近的区域。
-
接下来,我们需要为服务器命名——我把它命名为
masteringdocker4cluster——然后勾选允许未认证调用复选框。 -
由于我们的容器监听的是端口
80,我们需要更新修订设置,因为默认端口是8080。点击8080改为80。 -
一切填写完毕后,滚动到页面底部并点击创建按钮。稍等片刻,你应该能看到类似以下屏幕的内容:

图 10.23 – 查看我们的 Cloud Run 应用
如你所见,我们有关于正在运行的容器的信息,包括一个 URL,在本例中是cluster-5iidnzldtq-ez.a.run.app——在浏览器中打开该 URL 会显示如下内容:

图 10.24 – 我们正在运行的应用程序
这是我们预期会看到的内容;然而,Google Cloud Run 与我们在本章 AWS 和 Azure 部分所涵盖的其他服务之间存在差异。在其他服务中,我们的容器一直在运行,但在 Google Cloud Run 中,它只在需要时才会运行。Google Cloud Run 构建在 Knative 之上,Knative 是一个开源的无服务器平台,旨在运行在 Kubernetes 之上,我们一直在 Google 自家的 Kubernetes 集群上运行我们的容器。
总结 Google Cloud
正如你可能猜到的,Google 更倾向于运行基于 Kubernetes 的服务,正如我们在 Google Cloud Run 中已经看到的那样。所以,在我们深入了解 Kubernetes 后,我们将在后面的章节重新审视 Google Cloud。
摘要
在本章中,我们探讨了如何将 Docker 容器部署到三大公共云提供商的容器服务中。我们查看的这些服务在容器的部署和管理方式上有很大的不同,从 Azure 应用服务中完全托管的基于 Docker 的 Web 应用,到 AWS 自有的集群服务 Amazon ECS。
这些服务之间的差异非常重要,因为这意味着如果你想使用它们,你就只能依赖于一个云提供商。虽然在大多数情况下,这不会造成太大问题,但从长远来看,它可能会限制你。
在下一章(以及后续章节)中,我们将探索自 Docker 以来最激动人心的服务之一:Kubernetes。
问题
-
在 Azure 中,我们需要启动什么类型的应用程序?
-
如果你直接使用 Amazon Fargate,Amazon 服务中哪些不需要你管理?
-
对还是错:Azure 容器实例开箱即用支持 HTTPS。
-
命名 Google Cloud Run 所依赖的开源服务。
-
我们所查看的服务中,哪些支持 Docker Compose?
进一步阅读
有关我们在本章中使用的每项服务的详细信息,请参考以下内容:
-
AWS:
aws.amazon.com/ -
Amazon ECS:
aws.amazon.com/ecs/ -
AWS Fargate:
aws.amazon.com/fargate/ -
Microsoft Azure:
azure.microsoft.com/ -
Azure App Service:
azure.microsoft.com/en-gb/services/app-service/ -
Azure Container Instances:
azure.microsoft.com/en-gb/services/container-instances/ -
Google Cloud:
cloud.google.com -
Google Cloud Run:
cloud.google.com/run -
Knative:
knative.dev
第十一章:Docker 与 Kubernetes
在本章中,我们将深入了解 Kubernetes。与 Docker Swarm 类似,你可以使用 Kubernetes 创建和管理运行容器化应用程序的集群。
本章将讨论以下内容:
-
Kubernetes 简介
-
在 Docker Desktop 中启用 Kubernetes
-
使用 Kubernetes 和 Docker Desktop
-
Kubernetes 与其他 Docker 工具
技术要求
Docker 中的 Kubernetes 仅支持 Docker for Mac 和 Docker for Windows 桌面客户端。如果你使用的是 Linux 系统,那么在下一章第十二章**,发现更多 Kubernetes 选项中,我们将探讨一些对你有帮助的选项。
与之前的章节一样,我将使用我偏好的操作系统——macOS。和以前一样,一些支持命令虽然较少且不常用,但可能仅适用于 macOS。
查看以下视频,了解代码的实际操作:bit.ly/3m1WRiw
Kubernetes 简介
如果你曾经考虑过研究容器,你可能在某个时刻遇到过 Kubernetes,因此在我们在 Docker 桌面安装中启用它之前,让我们先回顾一下 Kubernetes 的起源。
Kubernetes,发音为koo-ber-net-eez,源自希腊语,意为船舶的舵手或船长。
信息
Kubernetes 团队采用的缩写方式叫做数字缩写(numeronym),这种方式在 80 年代被提出,至今仍在使用。更多信息请参见en.wikipedia.org/wiki/Numeronym。
Kubernetes,也叫K8s——K8s 中的 8 代表字母 K 和 S 之间的字母数量,即‘ubernete’部分——是一个开源项目,源自 Google,允许你自动化容器化应用程序的部署、管理和扩展。
Google 容器简史
Google 已经在 Linux 容器解决方案方面工作了相当长一段时间。它于 2006 年开始了这项工作,首先研究了名为Control Groups(cgroups)的 Linux 内核功能。该功能在 2008 年随 2.6.24 版本合并进 Linux 内核。
该功能允许你隔离资源,例如 CPU、RAM、网络和磁盘 I/O,或者一个或多个进程。Control Groups 仍然是 Linux 容器的核心要求,不仅 Docker 使用它,其他容器工具也使用它。
接下来,Google 尝试了一个名为lmctfy的容器堆栈,lmctfy是Let Me Contain That For You的缩写,它是LXC工具和库集合的替代方案。它是 Google 内部工具集的开源版本,Google 用它来管理应用程序中的容器。
下次 Google 因容器使用而登上新闻的时刻是在 2014 年 5 月 Joe Beda 在 Gluecon 上的演讲中。在演讲中,Beda 揭示了 Google 几乎所有的工作都基于容器,并且每周会启动大约 20 亿个容器。并且指出,这个数字并不包括任何长期运行的容器,也就是说这些容器只会在短时间内激活。然而,通过快速计算,Google 平均每秒启动大约 3,000 个容器!
在演讲的后半部分,Beda 提到 Google 正在使用一个调度器,这样他们就不需要手动管理每周 20 亿个容器,甚至不必担心容器的启动位置,以及在一定程度上,每个容器的可用性。
Google 还发布了一篇名为 在 Google 使用 Borg 进行大规模集群管理 的论文。这篇论文不仅让 Google 外部的人知道了他们使用的调度器的名字——Borg,还详细介绍了他们在设计调度器时所做的设计决策。
论文提到,除了他们的内部工具外,Google 还将面向客户的应用程序,如 Google Docs、Google Mail 和 Google Search,运行在由 Borg 管理的容器集群中。
Borg 这个名字来源于《星际迷航:下一代》中的外星种族 Borg。在该剧中,Borg 是一种赛博生命体,他们的文明建立在名为集体意识的蜂巢思维基础上。这使得他们不仅能够共享相同的思想,而且通过亚空间网络,确保每个集体成员都能从集体意识中获得指导和监督。我相信你会同意,Borg 种族的特征与我们希望容器集群运行的方式非常契合。
Borg 在 Google 内部运行了几年,最终被一个更现代的调度器 Omega 所取代。就在这时,Google 宣布将会把 Borg 的一些核心功能提取出来,并以新的开源项目形式进行再现。这个项目在内部被称为Seven,由几位 Borg 的核心贡献者共同开发。它的目标是创建一个更加友好的 Borg 版本,避免与 Google 内部的程序和工作方式过于紧密地绑定。
Seven 这个名字来源于 星际迷航:航海家号 中的角色 Seven of Nine,她是一个从集体中脱离出来的 Borg,最终在首次公开提交时,它被命名为 Kubernetes。
既然我们了解了 Kubernetes 的由来,现在可以更深入地探讨 Kubernetes 是什么。
Kubernetes 概述
项目的大部分内容,写作时占比 90.7%,是用 Go 编写的,这并不奇怪,因为 Go 是 Google 内部开发的编程语言,在 2011 年开源。项目的其余部分由 Python、Shell 辅助脚本和 HTML 文档组成。
一个典型的 Kubernetes 集群由承担主控或节点角色的服务器组成。你也可以运行一个独立的安装,既承担主控角色也承担节点角色。
主控角色是魔法发生的地方,它是集群的大脑。它负责决定在哪些位置启动 pods,并监控集群本身以及集群内运行的 pods 的健康状态。我们将在讨论完这两个角色后再讨论 pods。
通常,部署到被赋予主控角色的主机上的核心组件如下:
-
kube-apiserver:该组件暴露了主要的 Kubernetes API。它被设计为水平扩展,这意味着你可以不断添加更多实例来使你的集群高度可用。 -
etcd:这是一个高可用的一致性键值存储。它用于存储集群的状态。 -
kube-scheduler:该组件负责决定 pods 启动的位置。 -
kube-controller-manager:该组件运行控制器。这些控制器在 Kubernetes 中有多个功能,如监控节点、关注复制、管理端点以及生成服务账户和令牌。 -
cloud-controller-manager:该组件负责管理与第三方云交互的各类控制器,以启动和配置支持服务。
现在我们已经涵盖了管理组件,我们需要讨论它们所管理的内容。一个节点由以下元素组成:
-
kubelet:该代理运行在集群中的每个节点上,它是管理者与节点交互的手段。它还负责管理 pods。 -
kube-proxy:该组件管理节点和 pods 的请求和流量的路由。 -
container runtime:这可以是 Docker、CRI-O 或任何其他符合 OCI 标准的运行时。
你可能已经注意到,我到目前为止并没有提到容器。这是因为 Kubernetes 实际上并不直接与容器交互;它与 pod 进行通信。可以把 pod 看作是一个完整的应用程序,有点像我们之前使用 Docker Compose 启动由多个容器组成的应用程序。
Docker 是如何与 Kubernetes 配合的?
Docker 与 Kubernetes 的关系是多种多样的。首先,Docker 这个容器引擎以某种方式驱动了许多 Kubernetes 的安装,例如作为 Docker 或 ContainerD。
然而,Kubernetes 最初被视为 Docker Swarm 的竞争技术,后者是 Docker 自己的集群技术。然而,在过去几年中,Kubernetes 已经基本成为容器集群/编排的事实标准。
所有主要的云服务提供商都提供 Kubernetes 服务。我们有以下几个选择:
-
Google Cloud: Google Kubernetes 引擎 (GKE)
-
Microsoft Azure: Azure Kubernetes 服务 (AKS)
-
Amazon Web Services: Amazon 弹性容器服务 for Kubernetes (EKS)
-
IBM: IBM 云 Kubernetes 服务
-
Oracle Cloud: Oracle 容器引擎 for Kubernetes
-
DigitalOcean: DigitalOcean 上的 Kubernetes
乍一看,所有主要支持 Kubernetes 的主要参与者似乎并不像看上去那么重要。但是,请考虑到我们现在知道了在多个平台上部署我们容器化应用程序的一致方式。传统上,这些平台一直是封闭的生态系统,并且与它们交互的方式非常不同。
当 Docker 在 DockerCon Europe 的 2017 年 10 月发布桌面版本支持 Kubernetes 的消息初听起来让人吃惊,但当风波平息后,这一消息就变得合情合理了。为开发者提供一个环境,他们可以在本地使用 Docker for Mac 和 Docker for Windows 开发应用程序,然后使用 Docker Enterprise Edition 部署和管理他们自己的 Kubernetes 集群,或者使用之前提到的云服务,这与我们在 第一章 中讨论的解决 '在我的机器上可运行' 问题非常契合,Docker 概述。
现在让我们看看如何在 Docker 软件中启用支持并开始使用它。
在 Docker Desktop 中启用 Kubernetes
Docker 已经使安装过程变得极其简单。要启用 Kubernetes 支持,您只需打开 Preferences 并点击 Kubernetes 选项卡:

图 11.1 – Docker for Mac 中的 Kubernetes 首选项
如您所见,有三个选项。勾选 Enable Kubernetes 框,并选择 Deploy Docker Stacks to Kubernetes by default。暂时不要勾选 Show systems containers (advanced);我们稍后会更详细地讨论这一点。
单击 Apply & Restart 将会做如其字,重启 Docker 并启用 Kubernetes:

图 11.2 – 在 Docker for Mac 上启用 Kubernetes
Docker 将花一点时间下载、配置和启动集群。完成后,您应该在设置窗口的左下角看到 Docker 和 Kubernetes 列表。两者旁边应该有一个绿点,表示服务正在运行:

图 11.3 – Kubernetes 在 Docker for Mac 上成功启用
打开终端并运行以下命令:
$ docker container ls -a
这应该表明没有运行任何异常的内容。执行以下命令:
$ docker image ls
同样,这没有显示任何有用的信息;然而,正如你可能猜到的那样,在 设置 窗口中勾选 显示系统容器(高级) 选项将会改变这一点。现在勾选它并重新执行以下命令:
$ docker container ls -a
由于执行前面的命令时输出内容较多,以下截图仅显示容器的名称。为此,我执行了以下命令:
$ docker container ls --format {{.Names}}
执行命令后,我得到如下结果:

图 11.4 – 列出构成我们 Kubernetes 安装的容器
有 20 个运行中的容器,这就是你可以选择隐藏它们的原因。如你所见,几乎所有我们在上一节中讨论的组件都已覆盖,此外还有一些额外的组件,它们提供了与 Docker 的集成。
执行以下命令:
$ docker image ls
它仍然不列出任何镜像,尽管我们通过运行以下命令可以获取正在使用的镜像列表:
$ docker container ls --format {{.Image}}
从以下输出中可以看到,镜像来自 Docker 和 Google 容器注册中心(k8s.gcr.io)提供的官方 Kubernetes 镜像,也有一些是本地构建的镜像:

图 11.5 – 查看用于支持 Kubernetes 安装的镜像
暂时建议取消勾选 显示系统容器(高级) 选项,因为我们不需要每次查看运行中的容器时都看到 20 个容器的列表。
另一个需要注意的事项是,Docker 应用中的 Kubernetes 菜单项现在已有内容。此菜单可用于在 Kubernetes 集群之间切换。由于当前只有一个集群处于活动状态,因此仅列出一个集群:

图 11.6 – 检查 Kubernetes 菜单项
现在我们已经启动并运行了本地 Kubernetes 集群,可以开始使用它。
使用 Kubernetes 和 Docker Desktop
现在我们已经在 Docker 桌面安装上启动了 Kubernetes 集群,可以开始与其交互。首先,我们将查看与 Docker 桌面组件一起安装的命令行工具 kubectl。
如前所述,kubectl 是与 Docker 一起安装的。以下命令将显示有关客户端以及它连接的集群的一些信息:
$ kubectl version
和运行 docker version 时一样,这应该为你提供关于客户端和服务器的信息:

图 11.7 – 检查客户端和服务器的版本
接下来,我们可以运行以下命令查看 kubectl 是否能看到我们的节点:
$ kubectl get nodes
由于我们只有一个节点,所以我们应该只看到一个被列出:

图 11.8 – 列出我们的节点
现在我们的客户端与节点进行交互,我们可以通过运行以下命令查看 Kubernetes 默认配置的命名空间:
$ kubectl get namespaces
然后我们可以使用以下命令查看某个命名空间内的 Pod:
$ kubectl get pods --namespace kube-system
接下来是我运行前面命令时收到的终端输出:

图 11.9 – 检查命名空间
Kubernetes 中的命名空间是隔离集群内资源的好方法。如从终端输出中看到的,我们的集群内有四个命名空间。default 命名空间通常是空的。还有两个命名空间用于主要的 Kubernetes 服务:docker 和 kube-system。这些包含了组成我们集群的 Pod,最后一个命名空间 kube-public,和默认命名空间一样,也是空的。
在我们启动自己的 Pod 之前,先快速看看我们如何与正在运行的 Pod 进行交互,从如何获取关于 Pod 更多信息开始:
$ kubectl describe pods kube-scheduler-docker-desktop
--namespace kube-system
前面的命令将打印出 kube-scheduler-docker-desktop Pod 的详细信息。你可能会注意到,我们必须通过 --namespace 标志传递命名空间。如果不这样做,kubectl 会默认为默认命名空间,而那里并没有名为 kube-scheduler-docker-desktop 的 Pod 在运行。
命令的完整输出显示在这里,从一些关于 Pod 的基本信息开始:
Name: kube-scheduler-docker-desktop
Namespace: kube-system
Priority: 2000000000
Priority Class Name: system-cluster-critical
Node: docker-desktop/192.168.65.3
Start Time: Sun, 03 May 2020 12:11:02 +0100
和 Docker 一样,你可以为 Pod 应用标签。以下截图展示了这一点,并附带了一些关于 Pod 的详细信息:
Labels: component=kube-scheduler
tier=control-plane
Annotations: kubernetes.io/config.hash: 131c3f63daec7c
0750818f64a2f75d20
kubernetes.io/config.mirror: 131c3f63daec
7c0750818f64a2f75d20
kubernetes.io/config.seen:
2020-05-03T11:10:56.315367593Z
kubernetes.io/config.source: file
Status: Running
IP: 192.168.65.3
接下来是有关运行在 Pod 中的容器的信息。这里的信息从基本信息开始,如容器 ID、镜像和端口:
Containers:
kube-scheduler:
Container ID: docker://1b7ca730cd85941a5550d816239edc14953
f07b98763751ecb1caf7dfcced087
Image: k8s.gcr.io/kube-scheduler:v1.15.5
Image ID: docker-pullable://k8s.gcr.io/kube-scheduler@
sha256:ec985e27f41e3ceec552440502dbfa723924d5e6d72fc9193d140972
e24b8b77
Port: <none>
Host Port: <none>
接着,我们进入容器内运行的命令:
Command:
kube-scheduler
--bind-address=127.0.0.1
--kubeconfig=/etc/kubernetes/scheduler.conf
--leader-elect=true
现在我们看它的当前状态:
State: Running
Started: Sun, 03 May 2020 12:11:03 +0100
Ready: True
Restart Count: 0
然后我们可以看到一些关于其使用情况的信息:
Requests:
cpu: 100m
Liveness: http-get http://127.0.0.1:10251/healthz
delay=15s timeout=15s period=10s #success=1 #failure=8
Environment: <none>
Mounts:
/etc/kubernetes/scheduler.conf from kubeconfig (ro)
接下来,我们回到关于 Pod 的信息。在这里,我们可以看到当前状态:
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
然后,我们可以看到关于 Pod 挂载的卷以及一些其他选项,如服务质量(QoS)的详细信息:
Volumes:
kubeconfig:
Type: HostPath (bare host directory volume)
Path: /etc/kubernetes/scheduler.conf
HostPathType: FileOrCreate
QoS Class: Burstable
Node-Selectors: <none>
Tolerations: :NoExecute
最后,你可以看到列出的事件:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulled 39m kubelet, docker-desktop Container
image 'k8s.gcr.io/kube-scheduler:v1.15.5' already present on
machine
Normal Created 39m kubelet, docker-desktop Created
container kube-scheduler
Normal Started 39m kubelet, docker-desktop Started
container kube-scheduler
如你所见,这里有大量关于 Pod 的信息,包括容器列表;我们只有一个名为 kube-scheduler 的容器。我们可以看到容器 ID、使用的镜像、容器启动时的标志,以及 Kubernetes 调度器用于启动和维护 Pod 的数据。
现在我们知道了容器名称,可以开始与之交互。例如,运行以下命令将打印出我们唯一容器的日志:
$ kubectl logs kube-scheduler-docker-desktop -c kube-scheduler
--namespace kube-system
我得到了以下输出:

图 11.10 – 检查 pod 中容器的日志
运行以下命令将获取 pod 中每个容器的日志:
$ kubectl logs --namespace kube-system kube-scheduler-docker-
desktop
与 Docker 类似,你也可以在你的 pod 和容器上执行命令。
提示
请确保在以下两个命令中的--后加上空格。如果不加空格,将会导致错误。
例如,以下命令将运行uname -a命令:
$ kubectl exec --namespace kube-system kube-scheduler-docker-
desktop -c kube-scheduler -- uname -a
$ kubectl exec --namespace kube-system kube-scheduler-docker-
desktop -- uname -a
同样,我们可以选择在指定容器上运行命令,或者在 pod 内的所有容器上运行命令:

图 11.11 – 在 pod 中运行一个命令跨所有容器
让我们通过安装并登录基于 Web 的仪表板,进一步了解我们的 Kubernetes 集群。
尽管这在 Docker 中默认没有安装,但通过 Kubernetes 项目提供的定义文件安装非常简单。我们只需要运行以下命令:
$ kubectl apply -f https://raw.githubusercontent.com/
kubernetes/dashboard/v2.0.0/aio/deploy/recommended.yaml
一旦你运行命令,你应该看到类似以下的输出:

图 11.12 – 部署基于 Web 的仪表板
一旦服务和部署创建完成,启动过程将需要几分钟。你可以通过运行以下命令来检查状态:
$ kubectl get namespaces
$ kubectl get deployments --namespace kubernetes-dashboard
$ kubectl get services --namespace kubernetes-dashboard
一旦你的输出类似于以下内容,仪表板应该已安装并准备就绪:

图 11.13 – 检查部署状态
你可能已经注意到,仪表板有自己的命名空间,叫做kubernetes-dashboard。现在我们已经让仪表板运行起来,我们将找到访问它的方法。我们可以使用kubectl中的内建代理服务来实现。只需运行以下命令来启动它:
$ kubectl proxy
这将打开一个长时间运行的前台进程:

图 11.14 – 启动代理服务
现在代理服务已在运行,打开浏览器并访问 127.0.0.1:8001/version/ 将显示集群的一些信息:

图 11.15 – 集群信息
但是,我们想要看到的是仪表板。可以通过以下 URL 访问它:
localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/
你应该看到类似于以下的屏幕:

图 11.16 – 仪表板登录界面
如你所见,它要求我们登录;然而,我们还没有创建任何凭证,所以现在让我们创建凭证。
信息
服务账户是一个系统账户,通常使用令牌来验证 Kubernetes API 并执行操作。服务账户可以用于在 Kubernetes 集群内运行的服务,也可以用于我们这种场景,即用户希望使用 API 令牌访问仪表板。
打开一个新的终端窗口并输入以下命令来创建服务账户:
$ kubectl create serviceaccount dashboard-admin-sa
服务账户将在默认命名空间中创建;然而,这不会是一个问题,因为我们现在将通过运行以下命令为服务账户分配 cluster-admin 角色:
$ kubectl create clusterrolebinding dashboard-admin-sa --clusterrole=cluster-admin --serviceaccount=default:dashboard-admin-sa
这应该已经创建了一个密钥,我们可以通过运行以下命令找到密钥的名称:
$ kubectl get secrets
以下终端输出展示了至今为止所采取的步骤:

图 11.17 – 创建服务账户、分配权限并查看密钥
现在,我们的服务账户已经创建,权限已经正确设置,并且我们知道了密钥的名称(你的密钥名称会有所不同,因为密钥名称会附加一个五个字符的随机字符串),我们可以获取需要的令牌副本以便登录。
我们只需运行以下命令,确保将密钥名称更新为你自己的:
$ kubectl describe secret dashboard-admin-sa-token-z9x4g
这应该会给你类似于以下的终端输出:

图 11.18 – 查看密钥
记下令牌并将其输入仪表板登录页面中提供的令牌框中,然后点击登录按钮。登录后,你将看到类似以下页面的内容:

图 11.19 – 仪表板首次登录
如你所见,仪表板使用的是default命名空间。默认情况下,点击命名空间名称将打开一个下拉列表,显示所有可用的命名空间。现在,从列表顶部选择所有命名空间,你会注意到视图发生了变化,概览页面上显示了更多的信息。
现在我们的集群已经启动并运行,我们可以开始启动一些示例应用程序了。
Kubernetes 和其他 Docker 工具
当我们启用 Kubernetes 时,我们选择了 docker stack 命令以在 Docker Swarm 中启动我们的 Docker Compose 文件,正如你可能已经猜到的那样,运行这些相同的命令现在将会在我们的 Kubernetes 集群中启动我们的堆栈。
我们使用的 Docker Compose 文件如下所示:
version: '3'
services:
cluster:
image: russmckendrick/cluster
ports:
- '80:80' deploy:
replicas: 6
restart_policy:
condition: on-failure
placement:
constraints:
- node.role == worker
在我们启动 Kubernetes 上的应用程序之前,我们需要做一个小调整,移除 placement,这使得我们的文件看起来像以下内容:
version: '3'
services:
cluster:
image: russmckendrick/cluster
ports:
- '80:80' deploy:
replicas: 6
restart_policy:
condition: on-failure
一旦文件编辑完成,运行以下命令将启动堆栈:
$ docker stack deploy --compose-file=docker-compose.yml cluster
如你所见,Docker 会等待堆栈可用后才返回到命令提示符:

图 11.20 – 启动堆栈
我们还可以运行与启动 Docker Swarm 集群时查看堆栈信息相同的命令:
$ docker stack ls
$ docker stack services cluster
$ docker stack ps cluster
终端输出给出了与我们使用 Docker Swarm 集群启动堆栈时类似的输出:

图 11.21 – 运行 Docker 堆栈命令
然而,请注意,在写作时,似乎docker stack services返回了错误,这个问题是由于 Docker 随附的 Kubernetes 版本更新后引入的。
我们还可以使用kubectl查看详细信息:
$ kubectl get deployments
$ kubectl get services
您可能已经注意到,这次我们不需要提供命名空间。这是因为我们的堆栈是在默认命名空间中启动的:

图 11.22 – 查看部署和服务的详细信息
同时,当列出服务时,显示了localhost和端口为80。
在浏览器中打开http://localhost/,显示应用程序:

图 11.23 – 查看在 Kubernetes 中运行的集群应用程序
如果您仍然打开着仪表盘,可以探索您的堆栈,甚至打开终端连接到某个容器:

图 11.24 – 打开终端连接到容器
这是通过选择集群部署的六个 Pod 中的一个,然后点击下图中突出显示的进入 Pod按钮完成的:

图 11.25 – 进入 Pod
您可以通过运行以下命令来删除堆栈:
$ docker stack rm cluster
还有一件事…您可能会想,“太好了,我可以在任何 Kubernetes 集群中运行我的 Docker Compose 文件。”但这并不完全准确。
如前所述,当我们第一次启用 Kubernetes 时,会启动一些仅限 Docker 的组件。这些组件确保 Docker 尽可能紧密地集成。然而,由于这些组件在非 Docker 管理的集群中不存在,因此您将无法使用docker stack命令。
但幸运的是,并不是一切都失去了。有一个名为Kompose的工具,它是 Kubernetes 项目的一部分,可以将 Docker Compose 文件实时转换为 Kubernetes 定义文件。
要在 macOS 上使用 Homebrew 安装 Kompose,请运行以下命令:
$ brew install kompose
Windows 10 用户可以使用 Chocolatey。
信息
在 Linux 机器上使用yum或apt-get,在 macOS 上使用brew。
要使用 Chocolatey 安装 Kompose,您可以运行以下命令:
$ choco install kubernetes-kompose
安装完成后,您可以通过运行以下命令启动您的 Docker Compose 文件:
$ kompose up
您将得到类似以下输出的结果:

图 11.26 – 运行 kompose up
如输出所示,运行以下命令将显示我们刚刚启动的服务和 Pod 的详细信息:
$ kubectl get deployment,svc,pods,pvc
如你所见,我们的 Docker Compose 应用程序已经启动并运行:

图 11.27 – 检查应用程序的状态
你可以通过运行以下命令来删除服务和 Pod:
$ kompose down
这样你应该能得到类似以下内容:

图 11.28 – 运行 kompose down
虽然你可以使用 kompose up 和 kompose down,但我建议你生成 Kubernetes 定义文件并根据需要进行调整。为此,只需运行以下命令:
$ kompose convert
你会注意到,执行此命令会生成两个文件:

图 11.29 – 运行 kompose convert
你将能够看到 Docker Compose 文件和生成的两个文件之间的明显差异。cluster-pod.yaml 文件看起来如下:
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
io.kompose.service: cluster
name: cluster
spec:
containers:
- image: russmckendrick/cluster
name: cluster
ports:
- containerPort: 80
resources: {}
restartPolicy: OnFailure
status: {}
And the cluster-service.yaml file contains the following:
apiVersion: v1
kind: Service
metadata:
annotations:
kompose.cmd: kompose convert
kompose.version: 1.21.0 ()
creationTimestamp: null
labels:
io.kompose.service: cluster
name: cluster
spec:
ports:
- name: '80'
port: 80
targetPort: 80
selector:
io.kompose.service: cluster
status:
loadBalancer: {}
你可以通过运行以下命令来启动这些文件:
$ kubectl create -f cluster-pod.yaml
$ kubectl create -f cluster-service.yaml
$ kubectl get deployment,svc,pods,pvc
如果你没有跟着一起操作,以下截图显示了终端的输出:

图 11.30 – 使用 cluster-pod.yaml 和 cluster-service.yaml 启动应用程序
要删除集群 Pod 和服务,我们只需运行以下命令:
$ kubectl delete service/cluster pod/cluster
虽然我们将在接下来的两章中继续使用这个方法,但你可能想要在 Docker 桌面版中禁用 Kubernetes 集成,因为它在主机空闲时会对机器带来轻微的负担。要做到这一点,只需取消勾选启用 Kubernetes。当你点击应用时,Docker 将停止所有需要运行 Kubernetes 的容器;但它不会删除镜像,这样当你重新启用时,启动的时间不会太长。
总结
在本章中,我们从 Docker 桌面版软件的角度看待 Kubernetes。Kubernetes 远不止我们在本章中所涉及的内容,请不要认为这就是全部。在讨论了 Kubernetes 的起源后,我们介绍了如何使用 Docker for Mac 或 Docker for Windows 在本地计算机上启用 Kubernetes。
然后我们讨论了 kubectl 的一些基本用法,接着介绍了如何使用 docker stack 命令来启动我们的应用程序,方法与 Docker Swarm 中相同。
本章最后,我们讨论了 Kompose,这是 Kubernetes 项目中的一个工具。它帮助你将 Docker Compose 文件转换为 Kubernetes 兼容格式,让你能够提前开始将应用程序迁移到纯 Kubernetes 环境中。
虽然我们在本章中一直提到 Kubernetes 集群,但实际上我们一直在运行一个单节点集群,这实际上根本算不上集群。
在下一章,我们将探讨如何在本地启动 Kubernetes 的更多选项。这里,我们将欢迎 Linux 用户的回归,并且还将查看启动多个节点的选项。
问题
-
判断题:当执行
docker image ls命令时,结果是什么? -
哪个命名空间托管着用于运行 Kubernetes 并启用 Docker 支持的容器?
-
你会运行哪个命令来查看运行在 Pod 中的容器的详细信息?
-
你会使用哪个命令来启动 Kubernetes 定义的 YAML 文件?通常,
kubectl proxy命令会在你的本地机器上打开哪个端口? -
Google 容器编排平台的原始名称是什么?
进一步阅读
本章开始时提到的一些 Google 工具、演示文稿和白皮书可以在这里找到:
-
lmctfy:
github.com/google/lmctfy/ -
Joe Beda 在 GluCon 上的《大规模容器管理》幻灯片:
slides.eightypercent.net/GlueCon%202014%20-%20Containers%20At%20Scale.pdf -
Google 的大规模集群管理与 Borg:
ai.google/research/pubs/pub43438
你可以在这里找到本章提到的云服务的详细信息:
-
Google Kubernetes Engine(GKE):
cloud.google.com/kubernetes-engine -
Azure Kubernetes Service(AKS):
azure.microsoft.com/en-gb/services/kubernetes-service/ -
亚马逊弹性容器服务(Kubernetes)(Amazon EKS):
aws.amazon.com/eks/ -
IBM Cloud Kubernetes 服务:
www.ibm.com/cloud/container-service -
Oracle 容器引擎(Kubernetes):
cloud.oracle.com/containers/kubernetes-engine -
DigitalOcean 上的 Kubernetes:
www.digitalocean.com/products/kubernetes/
你可以在这里找到 Docker 关于 Kubernetes 支持的公告:
- Docker 平台和 Moby 项目添加了 Kubernetes:
www.docker.com/blog/top-5-blogs-2017-docker-platform-moby-project-add-kubernetes/
最后,Kompose 的主页可以在这里找到:
- Kompose:
kompose.io/
第十二章:探索其他 Kubernetes 选项
在这一章中,我们将讨论使用 Docker 内建支持来运行您自己的本地 Kubernetes 单节点和多节点集群的替代方案。
我们将讨论并查看以下工具:
-
Minikube
-
Kind(Kubernetes in Docker)
-
MicroK8s
-
K3s
技术要求
我们将使用我们在第六章中讨论的工具之一,Docker Machine、Vagrant 和 Multipass,以及一些可以用于引导您本地 Kubernetes 安装的独立工具。
再次提醒,本章中的截图将来自我偏好的操作系统——macOS
查看以下视频,看看代码实际操作:bit.ly/3byY104。
使用 Minikube 部署 Kubernetes
在我们讨论的所有 Kubernetes 集群安装器中,Minikube 排在第一位。它最初于 2016 年 5 月发布,是我们在本章中讨论的工具中最古老的一个。
在我们讨论安装 Minikube 之前,或许我们应该先讨论一下为什么最初需要它。
在最初发布时,Kubernetes 1.2已经发布了几个月,而距离 Kubernetes 1.0 的发布几乎已经过去了一年。
尽管自最初发布以来,安装 Kubernetes 已变得更加简单,但它通常仍然归结为一堆安装脚本和逐步指令,这些指令大多数是为了引导使用云服务提供商 API 或命令行工具的云托管集群而设计的。
如果您想在本地进行开发目的的安装,那么您必须从现有脚本中拼凑出安装程序,或者下载一个 Vagrant box,在这种情况下,您需要信任该 box 的作者,认为他们按照云原生计算基金会(CNCF)所提倡的最佳实践来构建它,这样可以确保集群之间的一致性。
尽管 Minikube 最初是作为在 Linux 和 macOS 主机上创建本地 Kubernetes 节点的方式开始的,但很快就加入了 Windows 支持,且该项目已经发展成为 Kubernetes 项目中的一个重要组成部分,因为它是许多人第一次接触 Kubernetes 的途径,并且它是官方 Kubernetes 文档的一部分。
现在我们对 Minikube 的背景有了一些了解,让我们看看如何安装它并启动一个单节点集群。
安装 Minikube
Minikube 设计用于运行一个单一的二进制文件,像 Kubernetes 本身一样,它是用 Go 编写的,这意味着它可以轻松编译并在我们在本书中讨论的三个平台上运行。首先,让我们看看如何在 macOS 上安装 Minikube。
在 macOS 上安装 Minikube
在我们前面的章节中讨论 macOS 上的软件和工具安装时,我们提到了并使用了 Homebrew。
安装 Minikube 的方式是这样的,这意味着你可以通过一个简单的命令来安装,正如你可能已经猜到的那样,命令如下:
$ brew install minikube
安装完成后,你可以通过运行以下命令验证一切是否按预期工作:
$ minikube version
这将输出已安装的 Minikube 版本以及该版本编译时的提交 ID。
在 Windows 10 上安装 Minikube
与 macOS 上的 Homebrew 类似,在 Windows 上谈到包管理器时,首选的是 Chocolatey。如果你已经安装了它,可以使用以下命令安装 Minikube:
$ choco install minikube
如果你没有使用 Chocolatey,那么可以从 storage.googleapis.com/minikube/releases/latest/minikube-installer.exe 下载 Windows 安装程序。
一旦 Minikube 安装完成,你可以运行以下命令:
$ minikube version
这将确认已安装的版本和提交信息。
在 Linux 上安装 Minikube
根据你所运行的 Linux 版本,安装 Minikube 的方式有所不同。
提示
在执行接下来的命令之前,请查看 GitHub 上的项目发布页面,确认要安装哪个版本的 Minikube。该页面可以在 github.com/kubernetes/minikube/releases/ 找到。写本文时,最新发布的版本是 1.9.2-0,因此本说明将使用该版本。
如果你正在运行基于 Debian 的系统,比如 Ubuntu 或 Debian 本身,那么你可以使用以下命令下载并安装 .deb 包:
$ MK_VER=1.9.2-0
$ curl -LO https://storage.googleapis.com/minikube/releases/
latest/minikube_$MK_VER_amd64.deb
$ sudo dpkg -i minikube_$MK_VER _amd64.deb
如果你正在运行基于 RPM 的系统,如 CentOS 或 Fedora,你可以使用以下命令:
$ MK_VER=1.9.2-0
$ curl -LO https://storage.googleapis.com/minikube/releases/
latest/minikube-$MK_VER.x86_64.rpm
sudo rpm -ivh minikube-$MK_VER.x86_64.rpm
最后,如果你不想使用包管理器,你可以使用以下命令下载最新的静态二进制文件:
$ curl -LO https://storage.googleapis.com/minikube/releases/
latest/minikube-linux-amd64
$ sudo install minikube-Linux-amd64 /usr/local/bin/minikube
无论你选择了哪种方式来安装 Minikube,你可以运行以下命令确认一切是否已经安装并按预期工作:
$ minikube version
同样,这将确认已安装的版本和它的编译提交。
Minikube 驱动程序
如前所述,Minikube 是一个独立的静态二进制文件,帮助你在本地机器上启动一个 Kubernetes 节点;它通过与多个虚拟化管理程序交互来实现这一功能。在我们开始使用 Minikube 之前,先快速了解一下可选项:
-
Docker (macOS、Windows 和 Linux):此驱动程序使用 Docker Machine 启动容器来托管 Kubernetes 节点。在 Linux 上,它只会使用容器,而在 macOS 和 Windows 上,它也会启动一个小型虚拟机,并在其中部署容器。
-
VirtualBox (macOS、Windows 和 Linux):这个驱动程序适用于三种操作系统,它会启动一个虚拟机,然后配置你的节点。
-
HyperKit (macOS):它使用 macOS 内置的虚拟化管理程序来托管一个虚拟机,在这个虚拟机中配置你的节点。
-
Hyper-V (Windows 10 Pro):此驱动程序使用 Windows 10 专业版内置的本地虚拟化程序来托管一个虚拟机,在其中启动并配置你的节点。
-
KVM2 (Linux):此驱动程序使用 (基于内核的虚拟机 KVM) 启动虚拟机。
-
Podman (Linux):此实验性驱动程序使用一个名为 Podman 的 Docker 替代品来启动构成集群的容器。
-
Parallels (macOS):使用仅限 macOS 的 Parallels 来托管一个虚拟机,在其中启动并配置你的节点。
-
VMware (macOS):使用仅限 macOS 的 VMware Fusion 来托管一个虚拟机,在其中启动并配置你的节点。
-
None (Linux):这个最终选项不使用在本地虚拟化程序中启动的容器和虚拟机;它直接在你运行的宿主机上安装集群节点。
如果你已经读到这里,那么你应该已经在宿主机上安装了上述提到的至少一个驱动程序的先决条件。Minikube 足够智能,可以判断出宿主机上最适合使用的驱动程序,因此我们可以继续进行,而不用担心会出现问题。
然而,如果因为找不到所选宿主操作系统的支持驱动程序而导致错误,请参阅 进一步阅读 部分,获取每个先前项目的链接。
提示
如果你发现没有支持的驱动程序安装,我建议安装 VirtualBox,因为它比所有驱动程序的要求和复杂性都要低。
现在我们已经安装了 Minikube,并且了解了它需要的驱动程序来启动并配置你的集群节点,我们可以开始工作并启动我们的集群节点。
使用 Minikube 启动集群节点
无论你的宿主操作系统是什么,使用 Minikube 启动 Kubernetes 集群节点只需要一个简单的命令:
$ minikube start
当你第一次运行命令时,在检测到本地配置后,可能需要几分钟的时间,因为它必须下载并配置各种支持工具、虚拟机镜像和容器(取决于它使用的驱动程序)。
你可能还会发现,它在安装过程中会要求输入密码,用于需要提升权限的部分。这个步骤是一次性的,一旦你启动了初始 Kubernetes 集群节点,之后的集群节点启动应该会更快。
以下是我的初始 Minikube 集群节点启动的终端输出:

图 12.1 – 在 macOS 上启动 Kubernetes 集群节点
一旦集群节点启动并运行,你可能会注意到输出底部的消息,提示 kubectl 的版本已过时,并且可能与在 Minikube 启动的集群节点上安装的 Kubernetes 版本不兼容。
幸运的是,您可以通过在 kubectl 命令前加上 minikube,并在后面加上 – 来解决此问题。这将下载并安装与 kubectl 兼容的版本,但它将在我们的 Minikube 工作空间中被隔离:
$ minikube kubectl -- get pods
您应该看到类似以下的输出:

图 12.2 – 使用 Minikube 初始化兼容版本的 kubectl
那么,为什么我们一开始会收到这个消息呢?好吧,Mac 版 Docker 的安装包中捆绑了一个支持 Docker 本地支持的 Kubernetes 版本的 kubectl,而该版本的 Kubernetes 在写作时是版本 1.15.5,而 Minikube 拉取的是 Kubernetes 的最新稳定版本 1.18.0。
使用 Minikube 而不是本地 Docker 支持的 Kubernetes 的一个优势是,您可以运行不同版本的 Kubernetes,并通过在 kubectl 命令前加上 minikube,并在后面加上 --,轻松隔离与不同版本交互所需的支持工具版本。
当您需要测试应用程序在更新生产 Kubernetes 集群时如何反应时,这非常有用。
与您的 Kubernetes 集群节点交互
现在我们已经启动了集群节点,我们可以运行一些常见命令并启动一个测试应用程序。
首先,让我们获取一些关于集群节点的信息。在第十一章**, Docker 和 Kubernetes中,我们运行了以下命令来获取集群节点和命名空间的信息:
$ kubectl get nodes
$ kubectl get namespaces
让我们再次运行这些命令,记得在每个命令前加上 minikube,并在后面加上 --,使其看起来像这样:
$ minikube kubectl -- get nodes
$ minikube kubectl -- get namespaces
$ minikube kubectl -- get --namespace kube-system pods
请查看以下终端输出:

图 12.3 – 获取集群节点信息
当我们运行这些命令时,输出结果与在第十一章中执行等效命令时得到的结果类似,Docker 和 Kubernetes,尽管列出的 kube-system 命名空间中的 pods 不同。
接下来,我们可以通过运行以下命令来启动一个测试应用程序:
$ minikube kubectl -- create deployment hello-node --image=k8s.
gcr.io/echoserver:1.4
部署创建完成后,您可以运行以下命令查看其状态和相关的 pod:
$ minikube kubectl -- get deployments
$ minikube kubectl -- get pods
您应该看到类似以下的终端输出:

图 12.4 – 启动应用程序部署并检查状态
现在我们的应用程序部署已启动,我们需要一种与其交互的方式。为此,我们可以运行以下命令,这将启动一个位于端口 8080 的负载均衡器服务:
$ minikube kubectl -- expose deployment hello-node
--type=LoadBalancer --port=8080
一旦服务被暴露,我们可以运行以下命令获取更多正在运行的服务信息:
$ minikube kubectl -- get services
如果我们的集群节点托管在公共云上,命令将告诉我们服务的外部 IP 地址。然而,当我们运行命令时,得到如下输出:

图 12.5 – 在 8080 端口公开服务
如您所见,EXTERNAL-IP 显示为 <pending>,那么我们如何访问我们部署的应用程序呢?
为此,我们需要使用以下命令:
$ minikube service hello-node
该命令将打开 hello-node 服务在您机器的默认浏览器中,并在终端中打印出您可以访问该服务的 URL:

图 12.6 – 打开 hello-node 服务
hello-node 应用程序只是回显浏览器发送的请求头:

图 12.7 – 在浏览器中查看 hello-node 应用程序
在我们完成对 Minikube 的查看之前,我们应该看看如何启动我们在前几章中运行的集群应用程序。为此,运行以下命令来创建部署,公开服务,并获取正在运行的 Pod 和服务的一些信息:
$ minikube kubectl -- create deployment cluster
--image=russmckendrick/cluster:latest
$ minikube kubectl -- expose deployment cluster
--type=LoadBalancer --port=80
$ minikube kubectl -- get svc,pods
启动后,我们可以通过运行以下命令列出集群节点上所有公开服务的 URL:
$ minikube service list
在浏览器中打开集群服务的 URL 应该会显示应用程序,如预期所示:

图 12.8 – 在浏览器中查看集群应用程序
在我们继续下一个工具之前,让我们快速查看一下其他一些 Minikube 命令。
管理 Minikube
我们可以使用一些其他命令来管理我们的集群节点。其中第一个命令允许您快速访问 Kubernetes 仪表盘。
Minikube 仪表盘
使用 Minikube 访问 Kubernetes 仪表盘稍微简单一点;事实上,它只需要一个命令:
$ minikube dashboard
这将启用仪表盘,启动代理,并在您的默认浏览器中打开仪表盘:

图 12.9 – 查看 Kubernetes 仪表盘
这次不需要额外配置认证工作。此外,您可以使用以下命令直接获取访问仪表盘所需的 URL:
$ minikube dashboard
这将返回类似如下的输出:

图 12.10 – 获取仪表盘 URL
现在让我们来看一下如何启动不同版本的 Kubernetes。
Minikube 启动时使用的 Kubernetes 版本
当我们第一次启动集群节点时,我提到过可以启动一个运行不同版本 Kubernetes 的集群节点。使用以下命令,我们可以启动一个运行 Kubernetes v1.15.5 的第二个集群节点,这也是 Docker for Mac 当前支持的版本:
$ minikube start --kubernetes-version=1.15.5 -p node2
这个命令应该会显示类似于我们最初启动集群节点时所看到的输出:

图 12.11 – 在第二个集群节点上安装 Kubernetes v1.15.5
如你所见,这个过程相当简单,这次我们没有收到关于本地安装的 kubectl 可能存在兼容性问题的警告。
我们可以通过运行以下命令查看不同的集群节点:
$ minikube profile list
这将列出可用的集群节点。要检查当前使用的是哪个节点,请运行以下命令:
$ minikube profile
要切换到其他集群节点,请运行此命令:
$ minikube profile node2
确保使用你想要切换到的集群节点名称:

图 12.12 – 切换集群节点
现在我们可以切换集群节点,那如何访问这些节点呢?
Minikube SSH
虽然你不需要直接访问集群节点,但你可以运行这个命令以获取当前选定集群节点的 Shell 访问权限:
$ minikube ssh
如果你想了解背后的原理,这个命令非常有用。
Minikube 停止并删除
我们接下来要看的最后一组命令是停止集群节点或完全移除它们的命令;你可能已经猜到,停止节点的命令是:
$ minikube stop
这将停止当前选定的集群节点,可以通过以下命令轻松重新启动:
$ minikube start
你可以添加 -p <profile name> 来停止或启动另一个集群节点。要移除集群节点,你可以运行这个命令:
$ minikube delete
这将移除当前选定的集群节点。同样,你可以添加 -p <profile name> 来与其他集群节点交互。运行以下命令将删除所有集群节点:
$ minikube delete --all
运行 minikube delete 命令时没有警告或“确定吗?”的提示,因此请小心操作。
Minikube 概览
正如我相信你会同意的,Minikube 选项丰富,使用起来非常简便。由于它是 Kubernetes 项目的一部分,你会发现它总是提供比在 Docker for Mac 或 Docker for Windows 上启用 Kubernetes 更为最新的 Kubernetes 体验,并且它还支持 Linux。
最后,你还可以获得一个更接近 CNCF 兼容的 Kubernetes 集群环境,这个环境运行在公共云上,我们将在 第十三章 中详细讲解,在公共云中运行 Kubernetes。
Minikube 采用类似于 Docker 的方法,通过部署一个小型托管虚拟机来运行你的环境。我们接下来要介绍的工具,采用了一种更现代的、在撰写时仍为实验性的方式来运行 Kubernetes。
使用 kind 部署 Kubernetes
接下来我们要介绍的工具是 Kind,Kubernetes in Docker的缩写。正如其名字所示,它就是将 Kubernetes 集群节点压缩成一个容器。Kind 是一个非常新的项目——事实上,它在写作时仍在进行大量活跃的开发。因此,我们不会花太多时间在它上面。
安装 Kind
和 Minikube 一样,Kind 作为一个单独的静态二进制文件进行分发——这意味着它的安装过程非常相似。
要在 macOS 上安装它,我们需要运行以下命令:
$ brew install kind
在 Windows 上,运行以下命令:
$ choco install kind
最后,在 Linux 上,你可以运行以下命令:
$ KIND_VER=v0.8.1
$ curl -LO https://kind.sigs.k8s.io/dl/$KIND_VER/kind-$(uname)-
amd64
$sudo install kind-$(uname)-amd64 /usr/local/bin/kind
你可以在github.com/kubernetes-sigs/kind/releases/找到确认版本号的发布页面。
由于我们已经安装了 Docker,我们不需要担心驱动程序、虚拟机管理程序或任何其他支持虚拟机的内容,因为 Kind 将直接使用我们本地的 Docker 安装。
启动 Kind 集群
一旦安装了 Kind 二进制文件,启动集群节点是一个非常简单的过程;只需运行以下命令:
$ kind create cluster
如下所示的终端输出将下载必要的镜像,并配置集群以及在本地 Docker 主机上创建一个上下文,以便你可以使用kubectl与集群节点交互:

图 12.13 – 使用 Kind 启动集群节点
现在我们的集群节点已经启动并运行,我们来做点什么吧。
与你的 Kubernetes 集群节点交互
现在我们的集群节点已经启动并运行,我们可以像本章前面部分那样重新运行命令并启动一个测试应用程序。
这次,我们将使用 Docker 主机上的kubectl命令,而不是使用包装器;然而,我们会添加一个上下文,以确保使用我们的 Kind Kubernetes 节点集群。这意味着我们需要运行的命令如下所示:
$ kubectl --context kind-kind get nodes
$ kubectl --context kind-kind get namespaces
$ kubectl --context kind-kind get --namespace kube-system pods
如下所示的终端输出中,我们再次看到与上次查询节点、命名空间和系统 Pod 时类似的输出:

图 12.14 – 查看节点、命名空间和系统 Pod
接下来,让我们再次部署hello-node应用程序,使用以下命令:
$ kubectl --context kind-kind create deployment hello-node
--image=k8s.gcr.io/echoserver:1.4
$ kubectl --context kind-kind get deployments
$ kubectl --context kind-kind get pods
$ kubectl --context kind-kind expose deployment hello-node
--type=LoadBalancer --port=8080
$ kubectl --context kind-kind get services
到目前为止一切顺利,你可能在想,但不幸的是,使用当前配置的安装过程只能进行到这一步——虽然我们可以部署 Pod 和服务,但 Kind 目前默认并不附带 Ingress 控制器。
要启用 Ingress 控制器,我们首先需要删除我们的集群。为此,运行以下命令:
$ kind delete cluster
一旦集群被删除,我们可以重新启动它,并启用 Ingress 配置。配置内容太长,无法在此列出,但你可以在随书附带的仓库中的 chapter12/kind 文件夹中找到一份副本。要使用配置启动集群,请在终端中切换到 chapter12/kind 文件夹,并运行以下命令:
$ kind create cluster --config cluster-config.yml
启动后,下一步是启用 NGINX Ingress 控制器。你需要执行以下命令来完成这一步:
$ kubectl --context kind-kind apply -f https://raw.
githubusercontent.com/kubernetes/ingress-nginx/master/deploy/
static/provider/kind/deploy.yaml
这将配置集群使用 NGINX Ingress 控制器。控制器本身需要一两分钟才能启动——你可以通过运行以下命令检查其状态:
$ kubectl --context kind-kind get --namespace ingress-nginx
pods
当 Ingress-nginx-controller pod 准备好并正在运行时,你可以使用 chapter12/kind 文件夹中的 hello-node.yml 文件重新启动 hello-node 应用程序,命令如下:
$ kubectl --context kind-kind apply -f hello-node.yml
启动后,你应该可以访问 hello-node 应用程序,网址为 localhost/hello-node/,如下图所示:

](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/ms-dkr-4e/img/image_00_0152.jpg)
图 12.15 – hello-node 应用程序的输出
完成后,你可以使用 kind delete cluster 命令删除集群。
Kind 总结
所以,你可能会想,Kind 的意义何在——为什么你会想要在一个容器中运行一个 Kubernetes 集群呢?其实,它的主要用途是测试 Kubernetes 本身;不过,它也可以用于测试部署,作为持续交付或持续部署流水线的一部分,在这里你需要测试 Kubernetes 定义文件是否按预期工作,并且你的应用程序能够无问题地启动。
目前,Kind 可能在开发中过于缓慢且过于笨重,无法用于开发。
让我们进入下一个工具,它将我们带回到运行虚拟机来部署本地 Kubernetes 集群的场景。
使用 MicroK8s 部署 Kubernetes
接下来,我们来看看由 Canonical 提供的 MicroK8s,正如你可能还记得的 第六章,Docker Machine, Vagrant 和 Multipass,他们是 Multipass 和 Ubuntu Linux 发行版的创始人。
MicroK8s 项目的口号是提供一个轻量级的 Kubernetes 节点,默认启用的仅是最少的基本服务,同时可以根据需要通过插件提供额外服务。
安装 MicroK8s
与 Minikube 和 Kind 不同,MicroK8s 的独立二进制文件仅适用于基于 Linux 的机器。由于这个原因,我们将使用 Multipass 启动虚拟机,并将其作为我们的安装目标。
启动虚拟机,我们需要运行以下命令:
$ multipass launch --name microk8s
一旦虚拟机启动并运行,我们可以通过以下命令启用并安装 MicroK8s:
$ multipass exec microk8s -- \
/bin/bash -c 'sudo snap install microk8s --classic'
安装完成后,MicroK8s 启动并准备好使用需要一些时间。运行以下命令来轮询状态并检查 MicroK8s 是否已启动:
$ multipass exec microk8s -- \
/bin/bash -c 'sudo microk8s status --wait-ready'
接下来,默认情况下 MicroK8s 是一个最小的 Kubernetes 集群节点,我们需要启用 dns 和 Ingress 插件,这将允许我们在启动应用程序时访问它们:
$ multipass exec microk8s -- \
/bin/bash -c 'sudo microk8s enable dns ingress'
一旦启用,我们需要在主机上获取配置的最后一步。为此,请运行以下命令:
$ multipass exec microk8s -- \
/bin/bash -c 'sudo microk8s.config' > microk8s.yml
这将在我们的主机当前目录留下一个名为 microk8s.yml 的文件。现在,我们可以在运行 kubectl 时使用此配置来访问我们新启动和配置的 MicroK8s Kubernetes 集群节点。
与您的 Kubernetes 集群节点进行交互
现在我们的集群节点已经启动并运行,我们可以重新运行命令并启动一个测试应用程序,就像我们在本章的前一节中所做的那样。
如前所述,这次我们将在主机上使用 kubectl 命令,并传递标志以确保它使用 microk8s.yml 配置文件。这意味着我们需要运行的命令如下所示:
$ kubectl --kubeconfig=microk8s.yml get nodes
$ kubectl --kubeconfig=microk8s.yml get namespaces
$ kubectl --kubeconfig=microk8s.yml get --namespace kube-system
pods
正如您从以下终端输出中可以看到的那样,我们再次看到类似于上次查询节点和命名空间直到系统 pod 的输出:

图 12.16 – 查看节点、命名空间和系统 pod
之所以看不到 coredns 之外的任何 Pod 是因为我们下载的配置所附带的用户没有必要的权限。尽管如此,这对我们来说并不是问题,因为我们不需要触及那些 pod。
接下来,我们可以启动 hello-node 应用程序。这次,我们将直接从 GitHub 存储库中使用 YAML 文件,运行以下命令:
$ kubectl --kubeconfig=microk8s.yml apply -f https://raw.
githubusercontent.com/PacktPublishing/Mastering-Docker-Fourth-
Edition/master/chapter12/kind/hello-node.yml
我们使用这个文件的原因是它具有 Ingress 控制器的定义,这意味着我们只需获取 MicroK8s 集群节点的 IP 地址,然后在浏览器中输入该 URL。要获取 IP 地址,请运行以下命令:
$ multipass info microk8s
一旦您知道 IP 地址,应该列为 IPv4,打开浏览器并转到 https://<IP Address>/hello-node/。在我的情况下,URL 是 https://192.168.64.16/hello-node/。您会注意到,这次我们使用的是 HTTPS 而不是 HTTP。这是因为我们启用的 Ingress 控制器安装了自签名证书,并将所有流量重定向到 HTTPS。根据浏览器设置,可能会要求您在访问页面时接受或安装证书。
完成后,您可以使用 multipass delete --purge microk8s 命令删除集群节点。还要记得删除 microk8s.yml 文件。一旦删除集群,如果要重新启动它,配置文件将不起作用。
MicroK8s 总结
MicroK8s 兑现了它的小巧、轻量,但仍然功能强大且可扩展的 Kubernetes 集群控制器的承诺。结合 Multipass,您可以轻松启动一个虚拟机,并快速在本地工作站引导 Kubernetes 集群节点。
此外,Canonical 确保了 MicroK8s 不仅仅是为了本地使用;集群节点本身可以被视为生产就绪,这意味着它非常适合在边缘计算和物联网设备上运行 Kubernetes——这两种设备通常规格较低,通常只会运行单个节点。
我们要查看的最后一个工具将允许我们在本地运行多个 Kubernetes 节点,让我们更接近生产环境的样子。
使用 K3s 部署 Kubernetes
我们要查看的最终工具是 Rancher 的 K3s。像 MicroK8s 一样,K3s 是一个轻量级的 Kubernetes 发行版,专为边缘计算和物联网设备设计。这使得它同样非常适合本地开发,因为 K3s 也是一个认证的 Kubernetes 发行版——就像 Docker、Kind 和 MicroK8s 一样。
重要提示
你可能会想,为什么它叫 K3s。背后有一定的逻辑。Rancher 设计 K3s 的主要目标是创造一个内存占用只有传统 Kubernetes 发行版一半的工具,因此他们决定既然 Kubernetes 是一个 10 个字母的词,并且以 K8s 样式呈现,那么他们的发行版将会缩小一半——5 个字母——因此,K3s 也采用了这样的命名方式。然而,K3s 并没有长形式,也没有官方发音。
最后,K3s 支持多节点集群,因此我们将学习如何构建一个三节点集群。本章这一部分的命令将覆盖 macOS 和 Linux 系统,因为我们将创建环境变量并使用非 Windows 工具,以尽可能简化安装过程。
安装 K3s
像 MicroK8s 一样,我们将使用 Multipass 来启动我们的主机。为此,请运行以下命令:
$ multipass launch --name k3smaster
$ multipass launch --name k3snode1
$ multipass launch --name k3snode2
一旦我们启动了三台虚拟机,就可以通过运行以下命令来配置主节点:
$ multipass exec k3smaster -- \
/bin/bash -c 'curl -sfL https://get.k3s.io | K3S_KUBECONFIG_
MODE='644' sh -'
一旦我们的主节点启动并运行,我们需要一些信息来引导剩余的两个节点。
我们需要的第一条信息是主节点的 URL。要创建环境变量,我们可以运行以下命令:
$ K3SMASTER='https://$(multipass info k3smaster | grep 'IPv4' |
awk -F' ' '{print $2}'):6443'
现在我们有了主节点的 URL,接下来我们需要获取访问令牌。为此,请运行以下命令:
$ K3STOKEN='$(multipass exec k3smaster -- /bin/bash -c 'sudo
cat /var/lib/rancher/k3s/server/node-token')'
现在我们有了引导节点所需的两条信息,可以运行以下两个命令:
$ multipass exec k3snode1 -- \
/bin/bash -c 'curl -sfL https://get.k3s.io | K3S_
URL=${K3SMASTER} K3S_TOKEN=${K3STOKEN} sh -'
$ multipass exec k3snode2 -- \
/bin/bash -c 'curl -sfL https://get.k3s.io | K3S_
URL=${K3SMASTER} K3S_TOKEN=${K3STOKEN} sh -'
现在我们应该已经配置好三台节点,剩下的就是配置我们的本地kubectl,以便它能够与集群节点进行交互。我们需要做的第一件事是将配置复制到本地机器。为此,请运行以下命令:
$ multipass exec k3smaster -- \
/bin/bash -c 'sudo cat /etc/rancher/k3s/k3s.yaml' >
${HOME}/.kube/k3s.yml
如果我们按原样使用配置文件,那么它会失败,因为默认情况下,K3s 配置为仅在本地主机上通信,因此要更新这一点,运行以下命令:
$ sed -ie s,https://127.0.0.1:6443,${K3SMASTER},g ${HOME}/.
kube/k3s.yml
如你所见,这将用我们本地 k3s.yml 配置文件中的 ${K3SMASTER} 值替换 127.0.0.1:6443。替换后,我们可以配置 kubectl 使用我们的 k3s.yml 配置文件,以便在接下来的终端会话中运行以下命令:
$ export KUBECONFIG=${HOME}/.kube/k3s.yml
现在,通过我们的本地 kubectl 二进制文件访问集群后,我们可以启动我们的应用程序。
与你的 Kubernetes 集群节点进行交互
现在我们的集群节点已经启动并运行,我们可以重新运行命令,并像在本章前面的部分那样启动测试应用程序。这次,鉴于我们已经配置 kubectl 使用我们的 k3s.yml 配置文件,我们只需运行以下命令:
$ kubectl get nodes
$ kubectl get namespaces
$ kubectl get --namespace kube-system pods
这将给你类似以下的终端输出:

图 12.17 – 查看节点、命名空间和系统 pod
接下来,启动 hello-node 应用程序,这次使用以下命令:
$ kubectl create deployment hello-node --image=k8s.gcr.io/
echoserver:1.4
$ kubectl expose deployment hello-node --type=LoadBalancer
--port=8080
$ kubectl get services
这将给你以下输出,如你所见,我们有任何EXTERNAL-IP和PORT(S):

图 12.18 – 启动并暴露 hello-node 应用程序
添加第二端口的 IP 地址应该会给你访问应用程序的 URL,例如,我访问了192.168.64.19:31846/,并展示了hello-node应用程序。
接下来,让我们用以下命令启动我们的 cluster 应用程序:
$ kubectl create deployment cluster --image=russmckendrick/cluster:latest
$ kubectl expose deployment cluster --type=LoadBalancer --port=80
$ kubectl get services
你可能会注意到这次在 EXTERNAL-IP 中,它显示 <pending>:

图 12.19 – 启动并暴露集群应用程序
你不必担心这个;只需使用第二端口和另一个服务器暴露的外部 IP。这给了我一个访问cluster应用程序的 URL,192.168.64.19:32042/。
你还可以使用以下命令扩展集群应用程序:
$ kubectl get deployment/cluster
$ kubectl scale --replicas=3 deployment/cluster
$ kubectl get deployment/cluster
如果你没有跟上,终端输出看起来像下面这样:

图 12.20 – 扩展集群应用程序
在我们结束之前,鉴于我们还有更多节点要探索,先安装 Kubernetes 仪表板。为此,从你的主机运行以下命令。首先需要获取仪表板的当前版本。为此,运行以下命令:
$ GITHUB_URL=https://github.com/kubernetes/dashboard/releases
$ VERSION_KUBE_DASHBOARD=$(curl -w '%{url_effective}' -I -L -s
-S ${GITHUB_URL}/latest -o /dev/null | sed -e 's|.*/||')
现在我们已经将当前版本设置为环境变量,可以运行以下命令启动仪表板,添加用户并配置用户的访问权限:
$ kubectl create -f 'https://raw.githubusercontent.com/
kubernetes/dashboard/${VERSION_KUBE_DASHBOARD}/aio/deploy/
recommended.yaml'
$ kubectl create -f 'https://raw.githubusercontent.com/
PacktPublishing/Mastering-Docker-Fourth-Edition/master/
chapter12/k3s/dashboard.admin-user.yml'
$ kubectl create -f 'https://raw.githubusercontent.com/
PacktPublishing/Mastering-Docker-Fourth-Edition/master/
chapter12/k3s/dashboard.admin-user-role.yml'
安装了仪表板并配置了用户后,我们可以通过运行以下命令来获取登录仪表板所需的访问令牌:
$ kubectl -n kubernetes-dashboard describe secret admin-user-
token | grep ^token
记下令牌,因为我们稍后需要它。在访问仪表板之前,我们需要做的最后一件事是通过运行以下命令启动 Kubernetes 代理服务:
$ kubectl proxy
在代理服务器运行时,打开 localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ 在你喜欢的浏览器中,输入你记下的令牌,然后登录:

图 12.21 – 在我们的 K3s 节点集群上打开 Kubernetes 仪表板
一旦你完成了 K3s 集群的使用,你可以通过 the multipass delete --purge k3smaster k3snode1 k3snode2 命令删除它。
还有一件事 – K3d
最后,Rancher 也提供了 K3d。像 Kind 一样,这是一个包含整个 Kubernetes 发行版的单一容器,这意味着你不仅可以将 K3s 用作本地开发环境,而且也很容易将其引入到 CI/CD 流水线中。
在总结 K3s 之前,让我们非常快速地了解一下如何在 macOS 和 Linux 主机上启动 K3d,先从 Linux 开始。要安装 k3d 命令,运行以下命令:
$ curl -s https://raw.githubusercontent.com/rancher/k3d/master/
install.sh | bash
或者,如果你使用的是 macOS,你可以通过 Homebrew 安装它,使用以下命令:
$ brew install k3d
一旦安装了 K3d(我安装的是 1.7 版本),我们将要查看四个命令:
-
k3d cluster create k3s-default:此命令将创建一个名为 k3s-default 的 K3d 驱动的 Kubernetes 集群。 -
k3d kubeconfig merge k3s-default --switch-context:这将配置你的本地kubectl使其与 k3s-default 集群通信。
现在你已经启动了一个集群,你可以像与其他 Kubernetes 集群一样与它互动,例如,通过运行以下命令:
$ kubectl get nodes
$ kubectl get namespaces
$ kubectl cluster-info
这将输出以下内容:

图 12.22 – 在我们的 K3d 集群上运行命令
k3dcluster delete k3s-default是我们将要查看的四个命令中的最后一个,正如你可能猜到的,这个命令会删除集群。
K3d 正在积极开发中。事实上,K3d 包装器的完整重写刚刚完成,所以如果前面的命令(针对新版本 3)不工作,尝试以下命令,这些命令是针对旧版本的:
$ k3d create
$ export KUBECONFIG='$(k3d get-kubeconfig --name='k3s-
default')': This will configure your local kubectl to talk to
the k3s-default cluster.
$ k3d delete
想要获取关于 K3d 开发的最新消息,可以查看项目的 GitHub 页面,链接在本章的进一步阅读部分中。
K3s 概述
像 MicroK8s 一样,K3s 实现了它作为轻量级 Kubernetes 发行版的承诺。
就个人而言,我认为 K3s 更好,因为它比 MicroK8s 更像一个完整的 Kubernetes 发行版。
K3s 另一个优点是,部署本地多节点集群是一种相对轻松的体验。这将使你的本地开发环境更接近生产环境,尽管 Minikube 也允许你启动多节点集群,但其功能仍处于初期阶段,尚未准备好供公众使用。
总结
在本章中,我们介绍了四种不同的工具,用于启动单节点和多节点的 Kubernetes 集群。我们发现,虽然启动每个集群的方法略有不同,但一旦集群启动并运行,你会发现使用标准的 Kubernetes 工具(如 kubectl)与它们交互时,体验大致一致。
此时,我应该坦白一下:我们在本章中介绍的四个工具中的两个,实际上并没有像传统意义上的 Docker 那样使用 Docker——MicroK8s 和 K3s 实际上使用的是 containerd。
正如你可能从 第一章《Docker 概述》中回忆的那样,containerd 是一个易于嵌入的容器运行时。它最初由 Docker Inc. 开发,但该项目已捐赠给 云原生计算基金会 (CNCF)——它是 Moby 项目的容器运行时,而 Docker 使用 Moby 作为其上游项目。
它不仅小巧轻便,还提供了完整的 OCI 镜像和 OCI 运行时规范支持,这意味着它与 Docker 镜像及 Docker 运行容器的方式完全兼容。
在下一章中,我们将不再在本地运行 Kubernetes,而是将集群带入云端。
问题
-
对错题:Kind 推荐用于生产环境使用吗?
-
请列举出本章中至少两个能够在 Docker 容器中运行的工具。
-
如果你有一个 ARM 架构的 IoT 设备,你可以使用本章中介绍的哪两个 Kubernetes 发行版?
深入阅读
本章开头提到的部分 Google 工具、演示文稿和白皮书可以在这里找到:
-
Minikube:
minikube.sigs.k8s.io/ -
Kind:
kind.sigs.k8s.io/ -
MicroK8s:
microk8s.io/ -
K3s:
k3s.io/ -
containerd:
containerd.io/ -
OCI 镜像和运行时规范:
www.opencontainers.org/ -
认证的 Kubernetes 发行版:
www.cncf.io/certification/software-conformance/
第十三章:在公共云中运行 Kubernetes
在前两章中,我们已经了解了在本地机器上创建、配置和与 Kubernetes 集群交互的各种方式。
现在是时候将我们的 Kubernetes 之旅带入云端,了解如何在以下公共云提供商中使用 Web 门户和命令行工具启动、配置和使用 Kubernetes:
-
Microsoft Azure Kubernetes 服务(AKS)
-
Google Kubernetes 引擎(GKE)
-
Amazon 弹性 Kubernetes 服务(EKS)
-
DigitalOcean Kubernetes
技术要求
本章假设你可以访问我们将涵盖的一个或多个云服务提供商。因此,请注意,根据你的帐户,启动这些服务将会产生费用。你还应该记得在完成使用后删除任何资源,或者了解如果你选择让它们运行一段时间的费用。
与前几章一样,我将使用我偏好的操作系统——macOS。因此,某些支持命令可能仅适用于 macOS,并且出现频率较少。
查看以下视频,了解代码在实践中的应用:bit.ly/336J7dE
Microsoft Azure Kubernetes 服务(AKS)
微软一直是容器工作负载在 Microsoft Azure 上运行的支持者。最初,微软通过提供 Azure 容器服务来支持三种不同的容器编排工具:Kubernetes、Mesosphere DC/OS 和 Docker Swarm。
然而,在 2017 年 10 月,微软宣布将用新开发的 Azure Kubernetes 服务替代 Azure 容器服务——如你所料,这也意味着取消了对 Mesosphere DC/OS 和 Docker Swarm 的支持。
自那时以来,这项服务作为一个 CNCF 认证的 Kubernetes 托管平台,已经取得了巨大的进展,最近的一个发展是 Windows 容器支持的普及。
与其深入探讨这个话题,不如直接启动一个 AKS 集群。我们将介绍两种方法:使用 Azure Web 门户和使用 Azure 命令行工具从本地机器进行设置。我们先从 Azure Web 门户开始。
使用 Web 门户启动集群
首先,你需要登录到 Azure 网络门户,地址是 portal.azure.com/。登录后,在顶部搜索框中输入 AKS,然后点击搜索结果中 服务 部分的 Kubernetes 服务 项。
页面加载后,你将看到一个如下所示的界面:

图 13.1:准备创建集群
大多数人可能已经猜到我们启动 AKS 集群的下一步是什么了。没错:点击+ 添加按钮,然后选择添加 Kubernetes 集群。我们将通过七个部分来启动集群,具体如下:
-
基础
-
节点池
-
身份验证
-
网络
-
集成
-
标签
-
审查并创建
我们从基础开始。在这里,您将被要求提供几项信息,从项目详情开始。在这里,我们需要定义应该使用的订阅和资源组。
选择所需的订阅,然后点击创建新链接,在资源组下选择。输入您希望将集群放置的资源组名称——我将其命名为mastering-docker-aks-rg。
一旦输入了这两项信息,我们就可以继续进行1.15.10版本(截至写作时)。
最后一项基础信息是主节点池。在这里,我们可以定义节点大小和节点数量。默认情况下,它们设置为标准 DS2 v2和3——为了测试,我们将保持这些默认设置。要进入下一步,点击下一步:节点池 >按钮。
在节点池界面上,我们可以添加更多的池——例如,如果您希望在我们启动的 Linux 主机池旁边运行一个 Windows 主机池,您可以点击+ 添加节点池按钮。
在这里,您将被要求输入以下信息:
-
节点池名称:为新的节点池命名。
-
操作系统类型:您希望新节点池使用的底层操作系统。您可以选择 Linux 或 Windows。
-
节点大小:新节点池中节点的虚拟机大小。
-
节点数量:您希望在池中拥有的节点数量。
由于我们正在启动一个测试集群,因此不需要任何额外的节点池,因此我们可以继续选择下一个选项。
虚拟节点允许您使用Azure 容器实例,我们在第十章中介绍了它,在公共云中运行 Docker。保持此设置为禁用。
将VM 扩展集设置为启用将允许我们按需扩展集群,因此我们点击下一步:身份验证 >按钮。
身份验证设置允许您配置 Kubernetes 集群如何与其他 Azure 服务进行交互,甚至启动这些服务。对于我们的集群,保持默认设置,然后点击下一步:网络 >按钮。
网络页面有两组不同的选项,第一组是网络配置。在这里,我们可以选择一个现有的虚拟网络,或者创建一个新的——为了方便起见,我们还是使用默认设置,这些设置已经为我们预填了,然后继续选择网络设置。
在这里,我们可以继续使用默认设置,这意味着我们唯一需要提供的信息是DNS 名称前缀—我使用的是mastering-docker-aks。
点击下一步:集成 >将带你进入选项页面,在那里你可以将 AKS 集群连接到Azure 容器注册表,我们在第三章《存储与分发镜像》中讨论过。但由于我们没有使用系统分配的托管身份,因此此选项不可用。我们可以配置的另一个集成是Azure Monitor。保持这些设置为默认值,因为我们希望为我们设置一个日志分析工作区。
由于我们的集群只是临时的,你可以点击查看 + 创建按钮,而不是添加标签。
首先发生的事情是你的配置会被验证。如果有任何问题,你将无法继续,直到问题解决。如果一切按计划进行,你应该会看到类似下面的屏幕:

图 13.2:已验证的配置
一旦验证通过,点击创建按钮。现在是时候去拿个饮料了,因为集群的部署可能需要 10 到 20 分钟。一旦部署完成,你将看到以下屏幕:

图 13.3:部署已完成
点击kubectl实例。
由于 Microsoft 在 Azure 门户中提供了一个内置的 shell,预安装了 Azure 命令行工具和 kubectl;因为我们是通过门户启动集群的,所以我们将使用它。
点击 Cloud Shell 图标,它是屏幕右上角搜索栏后的第一个图标(上面有>_)。如果你之前使用过 Cloud Shell,那么你会直接登录。如果没有,请按照屏幕上的提示操作。
Cloud Shell 有两种不同的版本:Bash 和 PowerShell。对于我们的目的,你需要确保使用的是Bash。
一旦进入命令提示符,输入以下命令,确保更新资源组和集群名称,如果你使用了与我不同的值:
$ az aks get-credentials --resource-group mastering-docker-
aks-rg --name mastering-docker-aks
一旦你运行了命令,你可以通过运行以下命令来测试与集群的连接:
$ kubectl get nodes
$ kubectl get namespaces
这应该返回三个节点,每个节点都运行着我们在首次配置集群时选择的 Kubernetes 版本:

图 13.4:使用 Cloud Shell 连接 kubectl 到我们的 AKS 集群
现在我们的集群已经启动并运行,你可以跳过本节的下一部分,我们将使用 Azure 命令行工具启动集群,并直接进入启动应用程序。
使用命令行工具启动集群
通过 Azure Web 门户启动集群的替代方法是使用 Azure 命令行工具。如果您还没有安装它,可以按照以下说明进行安装,从 macOS 开始:
$ brew install azure-cli
Windows 用户可以以管理员身份打开 PowerShell 提示符并运行以下命令:
$ Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows
-OutFile .\AzureCLI.msi; Start-Process msiexec.exe -Wait
-ArgumentList '/I AzureCLI.msi /quiet'; rm .\AzureCLI.msi
使用 Debian 系统的 Linux 用户可以运行以下命令:
$ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
运行 Red-Hat 系统的 Linux 用户可以执行以下命令:
$ sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
$ sudo sh -c 'echo -e '[azure-cli]
name=Azure CLI
baseurl=https://packages.microsoft.com/yumrepos/azure-cli
enabled=1
gpgcheck=1
gpgkey=https://packages.microsoft.com/keys/microsoft.asc' > /
etc/yum.repos.d/azure-cli.repo'
$ sudo yum install azure-cli
安装完 Azure CLI 包后,您应该运行的第一个命令如下:
$ az login
这将提示您登录到您的 Azure 账户。
重要提示
请确保您登录的账户已将您想要启动资源的订阅设置为默认订阅——如果没有,请访问docs.microsoft.com/en-us/cli/azure/manage-azure-subscriptions-azure-cli了解如何更改活动订阅。
现在我们已经安装并配置了 Azure 命令行工具,可以启动集群了。对于 macOS 和 Linux 用户,我们可以设置一些环境变量,这些值将在整个集群创建过程中使用。
重要提示
Windows 用户应该将启动集群时使用的命令中的变量替换为以下四个变量中的相应值。
这些值与我们在上一部分使用的相同,当时我们是通过 Azure Web 门户启动集群的:
$ AKSLOCATION=eastus
$ AKSRG=mastering-docker-aks-rg
$ AKSCLUSTER=mastering-docker-aks
$ AKSNUMNODES=3
启动集群的第一步是创建资源组。为此,请运行以下命令:
$ az group create --name $AKSRG --location $AKSLOCATION
这应该返回一个 JSON 文件,其中的预配状态应为 Succeeded。资源组创建完成后,我们可以通过运行以下命令来启动集群:
$ az aks create \
--resource-group $AKSRG \
--name $AKSCLUSTER \
--node-count $AKSNUMNODES \
--enable-addons monitoring \
--generate-ssh-keys
在集群启动时,您将看到如下界面:

图 13.5:使用 Azure CLI 启动集群
就像通过 Azure Web 门户启动集群时一样,完成过程需要大约 10 到 20 分钟,所以如果您看到前面的输出一段时间,请不用担心——这只是需要一些时间。
完成后,最后一步与我们通过 Azure Web 门户启动集群时相同。我们需要配置 kubectl 以指向新启动的集群。为此,请运行以下命令:
$ az aks get-credentials --resource-group $AKSRG --name
$AKSCLUSTER
配置合并完成后,您可以运行以下命令检查与集群的连接性:
$ kubectl get nodes
$ kubectl get namespaces
这应该返回类似以下的输出:

图 13.6:使用 kubectl 检查集群连接性
我们现在应该拥有一个与通过 Azure Web 门户启动的集群处于相同状态的 AKS 集群,这意味着我们现在可以启动应用程序了。
启动应用程序
由于我们不再使用本地计算机来运行 Kubernetes,因此我们可以启动一个资源需求更高的应用程序。为此,我们将使用由 Weave 创建和维护的微服务示例。
要启动应用程序,你只需运行以下命令:
$ kubectl create namespace sock-shop
$ kubectl -n sock-shop apply -f 'https://github.com/
microservices-demo/microservices-demo/blob/master/deploy/
kubernetes/complete-demo.yaml?raw=true'
这将创建一个名为sock-shop的命名空间,并创建组成应用程序的资源,这是一个销售袜子的电子商务网站。
一两分钟后,你可以运行以下命令查看 pod 的状态:
$ kubectl -n sock-shop get pods
这应该会显示类似以下的输出:

图 13.7:检查 pod 的状态
一旦所有 pod 都在运行,你可以通过运行以下命令来检查服务的状态:
$ kubectl -n sock-shop get services
再次,你应该看到类似以下的内容:

图 13.8:检查服务的状态
现在应用程序已经部署完毕,我们需要将其暴露出来以便可以访问。为此,请运行以下命令:
$ kubectl -n sock-shop expose deployment front-end
--type=LoadBalancer --name=front-end-lb
一旦暴露,你可以运行以下命令获取端点信息:
$ kubectl -n sock-shop describe services front-end-lb
在输出中,你需要查找LoadBalancer Ingress和Port。请参考以下屏幕截图查看示例输出:

图 13.9:获取已暴露服务的信息
在浏览器中输入LoadBalancer Ingress的 IP 和Port—对于我来说是52.191.99.154:8079/,应该能打开商店:

图 13.10:Sock Shop 应用程序
在应用程序中点击—将一些袜子放入购物车,刷新页面,等等。接下来,我们将查看 Azure Web 门户中的集群。
集群信息
如果你登录到 Azure Web 门户(portal.azure.com/),然后在页面顶部的搜索框中搜索你的集群名称,你应该会看到一个类似以下的页面:

图 13.11:在 Azure Web 门户中查看集群
如果你点击Monitor container按钮,你将进入Cluster概览页面。进入后,点击Containers。添加sock-shop命名空间的过滤器将仅选择在我们的应用程序的 pod 中运行的容器。选择一个容器后,点击View live data (preview)按钮,将把该容器的日志实时流式传输到页面上:

图 13.12:在 Azure Web 门户中查看前端容器的日志
我建议点击其他选项卡和设置,了解你可以使用 AKS 做些什么。
一旦你完成了 AKS 集群的设置,我建议你通过 Azure Web 门户或 Azure CLI 删除创建的所有资源,方法是删除资源组。为此,在顶部搜索框中输入Resource Groups,然后选择与你的集群相关的资源组。例如,作为我启动集群的一部分,我有三个资源组,下面的截图中已将它们突出显示:

图 13.13 – 查找要删除的资源组
要删除它们,点击资源组名称,仔细检查其中的资源,然后如果你确定其中仅包含测试集群的资源,点击 删除资源组 按钮并按照屏幕上的指示操作。
Microsoft Azure Kubernetes 服务概述
我相信你也会同意,使用 Azure Web 门户和 Azure CLI 启动集群是一个相当简单的过程。一旦集群启动并且 kubectl 配置完成,可以与集群进行交互,启动应用程序所使用的命令与我们在第十二章中使用的命令几乎相同,发现其他 Kubernetes 选项。
我们启动的集群的运行成本约为每月 215 美元,唯一的费用是虚拟机资源的费用。Microsoft 不收取集群管理所需资源的费用;如果我们想为集群添加一个正常运行时间的 SLA,我们需要再支付每月 73 美元。
能够运行多个节点池,混合使用 Linux 和 Windows 容器,这是该服务的一大亮点,此外它还与 Azure Monitor 和 Azure 容器注册表服务进行了集成。再加上如 Azure DevOps 和 Azure Sentinel 安全信息与事件管理(SIEM)等服务,你将拥有一个相当强大的平台。
接下来,我们将从 Microsoft Azure 转移到 Google Cloud。
Google Kubernetes 引擎(GKE)
如你可能已经猜到的,GKE 与 Google 的云平台紧密集成。与其详细介绍 Kubernetes 在 Google 的起步历程,不如直接深入并启动一个集群。
接下来,我假设你已经拥有一个 Google Cloud 账户,并且启用了计费功能的项目。
使用 Web 门户启动集群
一旦你登录到 console.cloud.google.com/,在页面顶部的搜索框中输入 Kubernetes,并选择 Kubernetes Engine。如果你的项目尚未启用该服务,它将自动启用,几秒钟后,你将看到如下所示的页面:

图 13.14 – Google Cloud Web 门户中的 Kubernetes 页面
如你可能已经猜到的,启动集群的第一步是点击创建集群按钮。接下来,你会看到许多选项;不过,我们将使用默认选项,除了名称,我会将其更改为mastering-docker-gke。更改名称后,点击创建按钮:

图 13.15 – 输入集群详情
启动集群大约需要 10 分钟。创建完成后,你应该会看到如下内容:

图 13.16 – 集群已就绪
与 Microsoft Azure 类似,Google Cloud 在其 Web 界面中内置了一个 Shell,且该 Shell 已安装并配置了 kubectl 和 gcloud 命令行工具。通过点击kubectl 进入 Shell 后,你就可以访问你的集群:
$ gcloud container clusters get-credentials mastering-docker-gke --zone us-central1-c
如果你使用了与我们设置的不同名称和区域,请更新名称和区域。配置完成后,你应该能够运行以下两个命令:
$ kubectl get nodes
$ kubectl get namespaces
为了测试连接,如果一切按计划进行,你应该会看到如下信息:

图 13.17 – 将 kubectl 连接到我们的集群
使用 Google Cloud 网络门户启动基本集群是一个非常简单的过程;然而,使用 gcloud CLI 同样简单。
使用命令行工具启动集群
gcloud 命令由 Google Cloud SDK 包提供。要在 macOS 上使用 Homebrew 和 Cask 安装,运行以下命令:
$ brew cask install google-cloud-sdk
如果你使用的是 Windows 系统,可以从dl.google.com/dl/cloudsdk/channels/rapid/GoogleCloudSDKInstaller.exe下载安装程序。
使用基于 Debian 的操作系统的 Linux 用户需要运行以下命令:
$ echo 'deb [signed-by=/usr/share/keyrings/cloud.google.gpg]
http://packages.cloud.google.com/apt cloud-sdk main' | sudo tee
-a /etc/apt/sources.list.d/google-cloud-sdk.list
$ curl https://packages.cloud.google.com/apt/doc/apt-key.gpg |
sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add
-
$ sudo apt-get update && sudo apt-get install google-cloud-sdk
最后,运行基于 Red-Hat 系统的 Linux 用户需要运行以下命令:
$ sudo tee -a /etc/yum.repos.d/google-cloud-sdk.repo << EOM
[google-cloud-sdk]
name=Google Cloud SDK
baseurl=https://packages.cloud.google.com/yum/repos/cloud-sdk-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg
https://packages.cloud.google.com/yum/doc/rpm-package-
key.gpg
EOM
一旦存储库文件就位,你可以使用以下命令安装包:
$ sudo yum install google-cloud-sdk
如果你在执行上述命令时遇到任何问题,包含常见故障排除提示的快速入门链接可以在本章的进一步阅读部分找到。
安装完成后,你需要登录并告诉命令行工具使用哪个项目。你可以通过运行以下命令来完成:
$ gcloud auth login
$ gcloud config set project masteringdocker4
如你可能已经猜到的,我使用的项目名为masteringdocker4;你应该更新为你自己的项目名称。
现在我们已经配置好了 gcloud,可以启动集群了。为此,请运行以下命令:
$ gcloud container clusters create mastering-docker-gke --num-nodes=3 --zone=us-central1-c
这将会在美国中部 1c 区域创建一个三节点集群。启动后,你的 kubectl 会自动配置为与集群通信,这意味着你只需通过运行以下命令来测试连接:
$ kubectl get nodes
$ kubectl get namespaces
这应该会显示类似以下输出的内容:

图 13.18 – 测试与 Google Kubernetes Engine 集群的连接
现在我们已经成功创建并启动了 Google Kubernetes Engine 集群,接下来可以启动我们的应用程序。
启动应用程序
由于我们在本章的Microsoft Azure Kubernetes Service部分已经详细介绍了应用程序的启动过程,因此这里不再详细讲解,只需说一下,您需要运行以下命令来启动应用程序:
$ kubectl create namespace sock-shop
$ kubectl -n sock-shop apply -f 'https://github.com/
microservices-demo/microservices-demo/blob/master/deploy/
kubernetes/complete-demo.yaml?raw=true'
您可以通过运行以下命令检查 Pod 和服务的状态:
$ kubectl -n sock-shop get pods,services
一旦一切看起来都已经启动并运行,您可以使用以下命令:
$ kubectl -n sock-shop expose deployment front-end
--type=LoadBalancer --name=front-end-lb
这将暴露应用程序。输入此后,您可以输入以下命令:
$ kubectl -n sock-shop describe services front-end-lb
正如我们之前提到的,要获取暴露的服务信息,您需要 LoadBalancer Ingress 和 Port。将这两个值粘贴到浏览器中(例如,我使用了 http://104.154.45.136:8079/),您应该能看到 Sock Shop 已经成功启动。
集群信息
返回 Google Cloud 网络门户并点击您集群中的工作负载,将显示非系统工作负载的列表。点击前端部署将显示如下内容:

图 13.19 – 查看部署详情
点击位于前一屏幕底部的容器日志,将带您进入一个页面,您可以在其中查看工作负载生成的日志:

图 13.20 – 查看前端部署的日志
再次花些时间浏览其他标签和选项,了解您可以用 GKE 做些什么。一旦您花时间了解 GKE 后,应该删除所有已启动的资源。为此,只需在门户的Kubernetes 引擎部分的集群页面选择您的集群,然后点击删除。在删除集群前,您会收到一个警告:

图 13.21 – 您确定要删除集群吗?
如果您决定继续,请点击删除,并大约等待 10 分钟。
Google Kubernetes Engine 总结
我相信您也同意,Google Kubernetes 服务是另一个简单的配置服务,而且正如您现在可能已经注意到的,一旦集群启动并运行,与之交互的体验是始终如一的。再说一次,Google 提供了与其他 Google Cloud 服务的深度集成,如监控、数据库和负载均衡服务。
以我们设定的规格运行集群大约每月花费 $150;然而,其中 $73 的费用是 GKE 集群管理费,若要以与 Microsoft Azure Kubernetes 服务相同的规格运行集群,每月将花费约 $220,GKE 集群管理费保持不变。
Google 和 Microsoft 的 Kubernetes 服务已经推出有一段时间了,接下来我们来看看三大云服务商中的最后一个 Kubernetes 服务,并继续了解亚马逊 Web 服务。
亚马逊弹性 Kubernetes 服务(EKS)
接下来我们将介绍的 Kubernetes 服务是亚马逊弹性容器服务 Kubernetes(简称 Amazon EKS)。这是我们迄今为止介绍的三项服务中最新推出的服务。事实上,您可以说亚马逊在 Kubernetes 的推广上来得很晚。
不幸的是,亚马逊的命令行工具并不像我们用于 Microsoft Azure 和 Google Cloud 的工具那样用户友好。因此,我们将使用一个名为 eksctl 的工具,它由 Weave 开发,Weave 同样也是我们一直使用的示范商店的创建者。该工具已被亚马逊作为 EKS 的官方命令行客户端,而不是他们自己客户端中内建的命令。
因此,我们将绕过基于 Web 的门户,专注于 eksctl,它本身利用了 AWS 的命令行工具。
使用命令行工具启动集群
在安装 eksctl 之前,我们需要先安装 AWS 命令行工具。若要在 macOS 上通过 Homebrew 安装,请运行以下命令:
$ brew install awscli
如果您是 Windows 用户,您可以从 awscli.amazonaws.com/AWSCLIV2.msi 下载安装程序。
最后,Linux 用户可以运行以下命令来下载并安装命令行工具:
$ curl 'https://awscli.amazonaws.com/awscli-exe-linux-x86_64.
zip' -o 'awscliv2.zip'
$ unzip awscliv2.zip
$ sudo ./aws/install
安装完成后,您需要配置凭据。与 az 和 gcloud 命令不同,您需要登录到 AWS 网站门户进行配置。有关所需步骤的详细信息,请参阅 docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html。
安装并配置好 AWS 命令行工具后,您可以继续安装 eksctl,再次从 macOS 和 Homebrew 开始:
$ brew tap weaveworks/tap
$ brew install weaveworks/tap/eksctl
建议 Windows 用户使用 Chocolatey:
$ chocolatey install eksctl
最后,Linux 用户可以使用以下命令直接从 GitHub 下载预编译的二进制文件:
$ curl --silent --location 'https://github.com/weaveworks/
eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.
gz' | tar xz -C /tmp
$ sudo mv /tmp/eksctl /usr/local/bin
安装完成后,您将能够运行以下命令来创建并配置您的集群:
$ eksctl create cluster
启动后,您应该会看到类似以下的内容,正如您所看到的,它非常具有描述性:

图 13.22 – 使用 eksctl 启动 EKS 集群
在启动过程中,eksctl 已经配置了你的本地 kubectl 环境,这意味着你可以运行以下命令:
$ kubectl get nodes
$ kubectl get services
现在集群已经启动并运行,我们可以像之前一样启动演示商店。
启动应用程序
现在你应该已经相当熟练地部署 Sock Shop 应用程序了,下面是你需要的所有命令回顾:
$ kubectl create namespace sock-shop
$ kubectl -n sock-shop apply -f 'https://github.com/
microservices-demo/microservices-demo/blob/master/deploy/
kubernetes/complete-demo.yaml?raw=true'
$ kubectl -n sock-shop get pods,services
$ kubectl -n sock-shop expose deployment front-end
--type=LoadBalancer --name=front-end-lb
$ kubectl -n sock-shop describe services front-end-lb
你可能会注意到,LoadBalancer Ingres 实际上是一个完全合格的域名,而不是 IP 地址:

](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/ms-dkr-4e/img/image_023.jpg)
图 13.23 – 查看暴露服务的详情
你可能需要等几分钟,等待负载均衡器的 DNS 更新,但你应该能够使用该 URL 和端口。对于我的集群,我访问了 http://a5fecbaee10a04cfaa19846e116081f8-624222238.eu-west-1.elb.amazonaws.com:8079 来访问 Sock Shop。
集群信息
如果我说实话,这一部分其实有些多余,因为在 AWS 网页门户中,默认情况下,集群暴露的内容信息并不多,除了组成节点集群的虚拟机的基本信息。当然,你可以在 CloudWatch 中启用一些功能来开始监控集群,但目前并不是默认启用的。
以下命令将立即删除集群:
$ eksctl get cluster
$ eksctl delete cluster --name=extravagant-outfit-1590399495
但是,我建议你再次检查 AWS 网页门户中是否有剩余的资源,以确保不会收到意外账单。
亚马逊弹性 Kubernetes 服务总结
如前所述,亚马逊弹性 Kubernetes 服务是三大公共云提供商中的最后一个 Kubernetes 即服务产品上线的,依我个人的看法,它是三者中最弱的一个。
尽管它是 CNCF 认证的托管平台,但它的集成感和直观性不如微软和谷歌的服务——一切似乎都只是附加到通常不错的 AWS 服务上;亚马逊容器服务也有类似的问题。
在成本方面,实例按标准 EC2 费率收费,每个 Kubernetes 集群每小时收费 0.10 美元。
在我们结束本章之前,让我们再看一个 Kubernetes 服务,这次是来自 DigitalOcean 的。
DigitalOcean Kubernetes
随着本章接近尾声,我们将很快浏览一下 DigitalOcean 的 Kubernetes 服务,因为它物有所值且配置简单。首先,登录到你的 DigitalOcean 网页门户,在右侧菜单的 Manage 部分,点击 Kubernetes:

](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/ms-dkr-4e/img/image_024.jpg)
图 13.24 – DigitalOcean Kubernetes 服务详情
点击创建 Kubernetes 集群按钮,将带你进入创建集群页面,在那里选择一个数据中心区域并滚动到页面底部;DigitalOcean 提供了一些很好的默认设置,所以我们可以跳过它们。到底部后,点击创建集群按钮并等待。大约五分钟后,你的集群就可以使用了:

](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/ms-dkr-4e/img/image_025.jpg)
图 13.25 – 查看新创建的 DigitalOcean Kubernetes 集群
现在集群已经启动并运行,你可以点击你的集群的kubectl配置,或者,如果你已经安装了(参见后续阅读部分的链接),你也可以使用doctl命令下载并配置本地的kubectl,使其与新创建的集群进行通信。
执行此操作的命令如下;请确保你在最后更新集群的名称以匹配自己的集群:
$ doctl kubernetes cluster kubeconfig save k8s-1-17-5-do-0-
nyc3-1590403077241
配置完成后,你知道接下来该做什么:使用本章中一直使用的命令启动 Sock Shop 应用程序——嗯,算是吧;如果你想继续并尝试启动应用程序,你会收到几个错误。试试看吧。
这些错误,像下面的截图一样,出现是因为 Kubernetes 在后续版本中对 API 进行了更改,而 DigitalOcean 默认启动的版本就是基于这个更改:

](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/ms-dkr-4e/img/image_026.jpg)
图 13.26 – Sock Shop 错误
不过不用担心——这个问题很容易解决。首先,让我们删除其中部分部署的应用程序所在的命名空间:
$ kubectl delete namespace sock-shop
删除这些后,我们需要克隆 Sock Shop 仓库并重新创建命名空间。为此,请运行以下命令:
$ git clone https://github.com/microservices-demo/
microservices-demo.git
$ kubectl create namespace sock-shop
接下来,我们需要切换到 Kubernetes 部署文件夹,更新定义文件,然后启动应用程序。为此,我们需要运行以下命令:
$ cd microservices-demo/deploy/kubernetes
$ kubectl convert -f . | kubectl create -f -
在这里,你可以检查 pods 和服务,暴露前端,并使用以下命令获取暴露的服务信息:
$ kubectl -n sock-shop get pods,services
$ kubectl -n sock-shop expose deployment front-end
--type=LoadBalancer --name=front-end-lb
$ kubectl -n sock-shop describe services front-end-lb
可能需要一两分钟才能将服务暴露,因为 DigitalOcean 会启动一个负载均衡器。
在我们返回 DigitalOcean 网络门户之前,让我们启用高级集群指标。为此,我们需要部署另一个应用程序。你可以通过运行以下两个命令来完成:
$ git clone https://github.com/kubernetes/kube-state-metrics.
git
$ kubectl create -f kube-state-metrics/examples/standard/
安装完成后,可能需要一两分钟时间,指标才会在门户中显示。你可以通过选择集群然后点击Insights标签来找到它们:

](https://github.com/OpenDocCN/freelearn-devops-pt5-zh/raw/master/docs/ms-dkr-4e/img/image_027.jpg)
图 13.27 – 在 DigitalOcean 网络门户查看集群详情
我建议在删除集群之前,先在 DigitalOcean 网络门户上浏览一下,因为我相信你会同意这个服务整合得非常好,且价格非常实惠。我们启动的三节点集群每月只需 30 美元!
你可以通过 Web 门户终止集群。同时,确保删除负载均衡器,因为在集群终止后它仍然存在,以避免月底产生意外费用。
总结
在本章中,我们研究了如何在不同云中启动 Kubernetes 集群,并在其中运行相同的示范应用程序。我敢肯定,到本章结束时,你可能已经厌倦了一次又一次地启动相同的应用程序;然而,这正是本章的重点。
我们已经研究了四个非常不同且传统上不兼容的云服务提供商,并在它们上使用相同的工具和命令部署了相同的应用程序。诚然,由于使用了不同版本的 Kubernetes,我们做了一些调整,但大部分情况下,一旦我们开始使用kubectl,就不需要做任何特定于提供商的调整。
这正是 Kubernetes 成为如此流行的关键原因之一:它确实允许你以平台无关的方式定义和分发应用程序——即使是几年前,能够在本地部署一个应用程序,然后在四个公共云提供商之间使用相同的命令和配置部署,似乎是不可能的,没有大量复杂性的。而我相信,你也会同意,在过去几章中并没有复杂性。
在下一章,我们将回到 Docker,并学习如何最好地保护你的容器。
问题
-
一旦我们的集群启动,我们需要运行什么命令来为 Sock Shop 商店创建命名空间?
-
如何查找负载均衡器的完整详细信息?
-
请列出官方的亚马逊弹性 Kubernetes 服务 CLI。
深入阅读
每个 Kubernetes 服务的产品页面可以在以下链接找到:
-
Azure Kubernetes 服务:
azure.microsoft.com/en-gb/services/kubernetes-service/ -
谷歌 Kubernetes 引擎:
cloud.google.com/kubernetes-engine/ -
亚马逊弹性容器服务 Kubernetes 版:
aws.amazon.com/eks/ -
DigitalOcean Kubernetes:
www.digitalocean.com/products/kubernetes/
本章中使用的各种命令行工具的快速入门可以在以下链接找到:
-
Azure CLI:
docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest -
谷歌云 SDK:
cloud.google.com/sdk/ -
AWS 命令行接口:
aws.amazon.com/cli/ -
eksctl,Amazon EKS 官方 CLI:
eksctl.io/
最后,关于示范商店的更多详情,请访问以下链接:
- Sock Shop:
microservices-demo.github.io/
第三部分:最佳实践
在本节中,我们将结合前面章节所学的技能,探讨它们如何在现实世界中应用。
本节包含以下章节:
第十四章,保护你的 Docker 环境
第十五章,Docker 工作流
第十六章,Docker 的下一步
第十四章:Docker 安全性
在本章中,我们将重点讨论 Docker 安全性,这是当今每个人都关注的话题。我们将把本章分成以下五个部分:
-
容器考虑因素
-
Docker 命令
-
最佳实践
-
Docker Bench 安全性 "应用程序"
-
第三方安全服务
技术要求
在本章中,我们将使用 Docker Desktop,并且将使用 Multipass 启动一个 Docker 主机,随后在其上启动一些配置不当的容器。和之前的章节一样,我将使用我偏好的操作系统,即 macOS。
和之前一样,我们将运行的 Docker 命令适用于我们至今已安装 Docker 的所有三种操作系统。然而,某些辅助命令(虽然很少见)可能只适用于 macOS 和基于 Linux 的操作系统。
查看以下视频,看看代码的实际操作:bit.ly/3m8ubVd
容器考虑因素
当 Docker 首次发布时,有很多关于 Docker 与虚拟机对比的讨论。我记得曾在杂志上阅读相关文章,在 Reddit 上评论线程,并阅读无数的博客文章。在 Docker 的 alpha 和 beta 版本的早期,人们曾像对待虚拟机一样使用 Docker 容器,因为当时并没有其他参考点,我们把它们看作是微型虚拟机。
过去,我会启用 SSH,在容器中运行多个进程,甚至通过启动容器并运行命令来安装我的软件栈,从而创建我的容器镜像。我们在 第二章《构建容器镜像》中讨论过,你不应该在容器上安装、配置和导出 SSH,因为这被视为一种不良做法,而 Docker 提供了不需要使用 SSH 就能访问容器的方法。
所以,与其讨论容器与虚拟机的对比,不如看看在运行容器时需要做的一些考虑事项,而不是虚拟机。
优势
当你启动一个 Docker 容器时,Docker 引擎在后台做了很多工作。Docker 引擎启动容器时执行的其中一项任务是设置命名空间和控制组。这意味着什么呢?通过设置命名空间,Docker 将每个容器中的进程进行隔离,不仅与其他容器隔离,还与宿主系统隔离。控制组确保每个容器获得自己的资源份额,例如 CPU、内存和磁盘 I/O。更重要的是,它们确保一个容器不会耗尽 Docker 主机上的所有资源。
正如我们在第四章**《容器管理》中看到的那样,能够在 Docker 控制的网络中启动容器意味着你可以在应用层隔离你的容器;应用程序 A 的所有容器将无法在网络层访问应用程序 B 的容器。
此外,通过使用默认的网络驱动程序,这种网络隔离可以在单个 Docker 主机上运行,或者通过使用 Docker Swarm 内置的多主机网络驱动程序,或者使用 Weave 提供的 Weave Net 驱动程序,可以跨多个 Docker 主机进行。
最后,我认为 Docker 相较于典型虚拟机的最大优势之一是你不需要登录到容器中。Docker 正在尽最大努力避免你需要登录容器来管理它正在运行的进程。通过诸如 docker container exec、docker container top、docker container logs 和 docker container stats 等命令,你可以在不暴露任何多余服务的情况下完成所需的所有操作。
你的 Docker 主机
当你处理虚拟机时,你可以控制谁有权访问哪台虚拟机。假设你只希望用户 1,一个开发人员,能够访问开发环境的虚拟机。
然而,用户 2 是负责开发和生产环境的操作员,因此他们需要访问所有虚拟机。大多数虚拟机管理工具允许你为虚拟机授予基于角色的访问权限。
使用 Docker 时,你处于一个轻微的劣势,因为任何能够访问你主机上 Docker Engine 的人,无论是通过被授予 sudo 权限,还是通过将其用户添加到 Docker Linux 组,都将能够访问你在该主机上运行的所有 Docker 容器。
它们可以运行新容器,可以停止现有容器,还可以删除镜像。小心你向谁授予访问主机上 Docker Engine 的权限。因为他们基本上掌握了所有容器的控制权。知道这一点后,建议仅将 Docker 主机用于 Docker,避免将其他服务与 Docker 主机混合使用。
镜像信任
如果你运行虚拟机,通常你会自己从零开始设置它们。由于下载的体积较大(并且启动它也需要一些努力),你很可能不会下载一个由互联网上某个随机人创建的预构建机器镜像。通常,如果你这样做,那会是一个来自受信软件供应商的预构建虚拟设备。
因此,你将知道虚拟机内部的内容以及没有的内容,因为你负责构建和维护它。
Docker 的吸引力之一是它的易用性;然而,这种易用性可能会让人忽视一个非常关键的安全问题——你知道容器内正在运行什么吗?
我们在前面的章节中已经提到了镜像的信任问题。例如,我们提到过不发布或下载未使用 Dockerfile 定义的镜像,也不将自定义代码或秘密(等)直接嵌入到您推送到 Docker Hub 的镜像中。
虽然容器具备命名空间、控制组和网络隔离的保护机制,但我们讨论了如何不慎下载镜像可能会引入安全隐患和风险。例如,运行未打补丁的合法容器软件可能会对您的应用程序和数据的可用性带来风险。
现在我们已经介绍了一些基本原则,让我们看看可以用于增强安全性并查看您可能使用的镜像信息的 Docker 命令。
Docker 命令
我们将要查看两条命令。第一条是 docker container run 命令,您可以看到在该命令下可以利用的一些项。第二条是 docker container diff 命令,您可以使用它查看您计划使用的镜像已经发生了什么变化。
让我们看看如何使用这两个命令来帮助加固我们的容器安全。
Docker 运行命令
在 docker run 命令方面,我们将主要关注一个选项,它允许您将容器内的所有内容设置为只读,而不是指定某个目录或卷。这有助于限制恶意“应用程序”造成的损害,因为它们也可能通过更新二进制文件劫持一个脆弱的应用程序。
让我们看看如何启动一个只读容器,然后逐步解析它的作用,如下所示:
$ docker container run -d --name mysql --read-only -v /
var/lib/mysql -v /tmp -v /var/run/mysqld -e MYSQL_ROOT_
PASSWORD=password mysql
在这里,我们运行一个 MySQL 容器并将整个容器设置为只读,除了以下文件夹:
-
/var/lib/mysql -
/var/run/mysqld -
/tmp
这些将被创建为三个独立的卷,并以读写方式挂载。如果您没有添加这些卷,MySQL 将无法启动,因为它需要读写访问权限来创建 /var/run/mysqld 中的套接字文件、/tmp 中的一些临时文件,以及最终在 /var/lib/mysql 中的数据库文件。
容器内的其他位置将无法进行写入。如果您尝试运行以下命令,它将失败:
$ docker container exec mysql touch /trying_to_write_a_file
上述命令会给出以下信息:
touch: cannot touch '/trying_to_write_a_file': Read-only file
system
如果您希望控制容器的写入位置(或不允许写入的位置),这将非常有用。务必明智地使用此功能,并进行彻底测试,因为当“应用程序”无法写入某些位置时可能会产生后果。
与之前的命令类似,使用 docker container run 时,我们将所有内容设置为只读(除非指定了某个卷),我们也可以做到相反的操作,只将一个单独的卷(如果使用多个 -v 参数,更多卷也可以)设置为只读。
关于卷需要记住的一点是,当你在容器中使用并挂载一个卷时,它会作为一个空卷覆盖容器内的目录,除非你使用--volumes-from开关或在容器启动后以其他方式向容器添加数据;例如,你可能会使用如下命令(此命令将无法运行):
$ docker container run -d -v /local/path/to/html/:/var/www/
html/:ro nginx
这将把 Docker 主机上的/local/path/to/html/挂载到/var/www/html/,并将其设置为只读模式。如果你不希望正在运行的容器写入卷以保持数据或配置文件不被更改,这将非常有用。
Docker 的diff命令
让我们再次查看docker diff命令;因为它涉及容器的安全性方面,你可能想使用托管在 Docker Hub 或其他相关仓库上的镜像。
请记住,任何能够访问你 Docker 主机和 Docker 守护进程的人,都能够访问你所有正在运行的 Docker 容器。换句话说,如果你没有监控措施,某人可能会针对你的容器执行命令并做出恶意行为。
让我们来看一下我们在前一节中启动的 MySQL 容器:
$ docker container diff mysql
你会注意到没有返回任何文件。这是为什么呢?
docker diff命令告诉你自容器启动以来镜像的变化。在前面的部分,我们以只读模式启动了 MySQL 容器,并挂载了卷,以便 MySQL 能够进行读写操作——这意味着我们下载的镜像和正在运行的容器之间没有文件差异。
停止并删除 MySQL 容器,然后运行以下命令修剪卷:
$ docker container stop mysql
$ docker container rm mysql
$ docker volume prune
然后,重新启动相同的容器,但去掉只读标志和卷挂载;这样会得到不同的情况,具体如下:
$ docker container run -d --name mysql -e MYSQL_ROOT_
PASSWORD=password mysql
$ docker container exec mysql touch /trying_to_write_a_file
$ docker container diff mysql
正如你所见,已经创建了两个文件夹,并且添加了几个文件:
C /run
C /run/mysqld
A /run/mysqld/mysqld.pid
A /run/mysqld/mysqld.sock
A /run/mysqld/mysqld.sock.lock
A /run/mysqld/mysqlx.sock
A /run/mysqld/mysqlx.sock.lock
A /trying_to_write_a_file
这是一个很好地发现容器内任何异常或意外情况的方法。现在我们已经了解了如何更安全地启动容器,接下来让我们讨论一些可以应用的其他最佳实践。
最佳实践
在本节中,我们将探讨与 Docker 相关的最佳实践。我们之前的一些章节已经提到过其中的部分内容。接下来,我们将讨论互联网安全中心指南,它详细记录了如何正确地保护你 Docker 环境的各个方面。
Docker 最佳实践
在深入探讨《互联网安全中心指南》之前,让我们先回顾一些使用 Docker 的最佳实践,如下所示:
-
每个容器只启动一个应用程序:Docker 就是为此设计的,它让一切变得更加简单。最终,这种隔离性正是我们之前讨论的关键所在。
-
只安装必要的软件:正如我们在前几章中提到的,如果你必须安装更多的服务来支持容器应运行的唯一进程,我建议你重新审视这些原因。这不仅有助于保持镜像的小巧和可移植性,还能减少潜在的攻击面。
-
审查谁能访问你的 Docker 主机:记住,任何拥有 root 或 sudo 权限的人都可以访问你的 Docker 主机,操作主机上所有的镜像和正在运行的容器,并且能够启动新的容器。
-
始终使用最新版本的 Docker:这将确保所有的安全漏洞都已经修补,并且你也可以使用到最新的功能。虽然更新安全问题,使用社区版可能会带来由于功能变化或新增功能所引发的问题。如果这对你来说是个问题,那么你可能需要查看 Docker 提供的 LTS 企业版。
-
如果需要帮助,利用可用资源:Docker 社区庞大且极为友好。在规划 Docker 环境和评估平台时,利用他们的网站、文档和 Slack 聊天室。有关如何访问 Slack 和社区其他部分的信息,请参见 第十六章,与 Docker 的下一步。
互联网安全中心基准
互联网安全中心(CIS)是一个独立的非营利组织,旨在提供安全的在线体验。他们发布的基准和控制措施被视为 IT 各个方面的最佳实践。
Docker 的 CIS 基准可以免费下载。你应该注意到,它目前是一本 257 页的 PDF,发布在创意共享许可下,涵盖了 Docker CE 18.09 及更高版本。
当你实际运行扫描并得到需要修复(或建议修复)的结果时,你将参考本指南(在本章的下一部分)。该指南分为以下几个部分:
-
主机配置:本指南的这一部分涉及 Docker 主机的配置。这是 Docker 环境中运行所有容器的地方,因此,保持其安全性至关重要。这是防御攻击者的第一道防线。
-
Docker 守护进程配置:本指南的这一部分提供了确保 Docker 守护进程安全运行的建议。你对 Docker 守护进程配置的每一项操作都会影响到每个容器。这里是你可以附加到 Docker 守护进程的开关,之前我们已经提到过,并且接下来在我们运行工具时,你将会看到相关项。
-
Docker 守护进程配置文件:本部分指南处理 Docker 守护进程使用的文件和目录。这些包括权限设置和所有权。有时,这些区域可能包含一些你不希望他人知道的信息,而这些信息可能是以纯文本格式存储的。
-
容器镜像/运行时和构建文件:本部分指南包含了有关保护容器镜像和构建文件的信息。第一部分涉及镜像,包括基础镜像,以及使用的构建文件。正如我们之前所述,你需要对自己使用的镜像非常确定,不仅仅是基础镜像,还包括 Docker 环境中的任何方面。本节将涵盖你在创建自己基础镜像时应遵循的事项。
-
容器运行时:这一部分原本是后面章节的一部分,但已被移到 CIS 指南中的独立章节。容器运行时涉及很多与安全相关的内容。请小心使用的运行时变量。在某些情况下,攻击者可能会利用这些变量达到自己的目的,而你可能认为它们有利于自己。如果在容器中暴露过多内容,比如将应用程序的密钥和数据库连接作为环境变量暴露出来,可能不仅会危及容器的安全,还会危及 Docker 主机以及在该主机上运行的其他容器的安全。
-
Docker 安全操作:本部分指南涵盖了涉及部署的安全领域,这些项目与 Docker 最佳实践紧密相关。因此,遵循这些建议是最好的做法。
Docker Bench Security 应用程序
在本节中,我们将介绍可以安装和运行的 Docker Bench Security 应用程序。该工具将检查以下内容:
-
主机配置
-
Docker 守护进程配置文件
-
容器镜像和构建文件
-
容器运行时
-
Docker 安全操作 Docker Swarm 配置
看起来很熟悉吗?应该是的,因为这些项目正是我们在上一节中审查过的内容,只不过它们已经被整合到一个应用程序中,帮你完成了许多繁重的工作。它会展示你配置中出现的警告,并提供其他配置项的信息,甚至包括通过测试的项。
现在,我们将介绍如何运行该工具、一个实际示例,以及该过程的输出意味着什么。
在 macOS 上运行 Docker 工具和在 Windows 上运行 Docker 工具
运行该工具非常简单。它已经打包在一个 Docker 容器中了。虽然你可以获取源代码并自定义输出或以某种方式处理它(比如通过邮件发送输出),但默认配置可能已经足够满足你的需求。
该工具的 GitHub 项目可以在 github.com/docker/docker-bench-security/ 找到,要在 macOS 或 Windows 机器上运行此工具,只需将以下内容复制并粘贴到你的终端中。以下命令缺少检查 systemd 所需的行,因为 Moby Linux(Docker for macOS 和 Docker for Windows 的底层操作系统)并不运行 systemd。当我们在 Ubuntu Docker 主机上运行容器时,我们将查看一个基于 systemd 的系统:
docker container run -it --net host --pid host \
--cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /var/lib:/var/lib \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /etc:/etc \
--label docker_bench_security \
docker/docker-bench-security
一旦镜像下载完成,它会启动并立即开始审计你的 Docker 主机,打印出审计结果,具体如下面的截图所示:

图 14.1 – 运行 Docker Bench 安全检查
如你所见,有一些警告 [WARN],还有提示 [NOTE] 和信息 [INFO];然而,由于该主机由 Docker 管理,正如你所预期的那样,问题并不严重,不需要过多担心。
运行在 Ubuntu Linux 上
在详细查看审计输出之前,我将使用 multipass 启动一个纯净的 Ubuntu 服务器,并通过运行以下命令使用官方安装程序进行 Docker 的清洁安装:
$ multipass launch --name docker-host
$ multipass exec docker-host -- /bin/bash -c 'curl -s https://
get.docker.com | sh - && sudo usermod -aG docker ubuntu'
$ multipass shell docker-host
安装完成后,我将启动几个容器,它们的设置都不太合理。我将从 Docker Hub 启动以下两个容器:
$ docker container run -d --name root-nginx -v /:/mnt nginx
$ docker container run -d --name priv-nginx --privileged=true
nginx
然后,我将构建一个基于 Ubuntu 16.04 的自定义镜像,并通过以下 Dockerfile 运行 SSH:
FROM ubuntu:16.04
RUN apt-get update && apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN echo 'root:screencast' | chpasswd
RUN sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin
yes/' /etc/ssh/sshd_config
RUN sed 's@session\s*required\s*pam_loginuid.so@session
optional pam_loginuid.so@g' -i /etc/pam.d/sshd
ENV NOTVISIBLE 'in users profile'
RUN echo 'export VISIBLE=now' >> /etc/profile
EXPOSE 22
CMD ['/usr/sbin/sshd', '-D']
我将使用以下命令构建并启动前面的 Dockerfile:
$ docker image build --tag sshd .
$ docker container run -d -P --name sshd sshd
如你所见,在这张图片中,我们将主机的根文件系统以完全读/写权限挂载在 root-nginx 容器中。我们还在 priv-nginx 中以扩展权限运行,最后,在 sshd 中运行 SSH。
重要提示
请务必在此测试之外运行前面的 Dockerfile 或容器;我们故意启动不遵循最佳实践的容器,以便从扫描中获取结果。
为了开始在我们的 Ubuntu Docker 主机上进行审计,我运行了以下命令:
$ docker container run -it --net host --pid host \
--cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /var/lib:/var/lib \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/lib/systemd:/usr/lib/systemd \
-v /etc:/etc --label docker_bench_security \
docker/docker-bench-security
由于我们运行的操作系统支持 systemd,我们将 /usr/lib/systemd 挂载以便进行审计。
有很多输出内容需要消化,但这些都意味着什么呢?让我们一起来看看并逐一解析每个部分。
理解输出内容
我们将看到四种类型的输出,具体如下:
-
[PASS]:这些项目是稳健且无问题的,可以继续使用。它们无需任何关注,但阅读它们会让你心里暖暖的。越多越好! -
[WARN]:这些是需要修复的项目。我们不希望看到这些。 -
[INFO]:这些是你应该检查并修复的项目,如果你认为它们与你的设置和安全需求相关的话。 -
[NOTE]:这些是最佳实践建议。
如前所述,审计涵盖了七个主要部分,如下所示:
-
主机配置
-
Docker 守护进程配置
-
Docker 守护进程配置文件
-
容器镜像和构建文件
-
容器运行时
-
Docker 安全操作
-
Docker Swarm 配置
让我们看看在每个扫描部分中看到的内容。这些扫描结果来自一个默认的 Ubuntu Docker 主机,当前系统没有做任何调整。我们将重点关注每个部分中的[WARN]项。
运行时可能会出现其他警告,但这些是大多数人(如果不是所有人)初次使用时会遇到的警告。
主机配置
我的主机配置中有五个[WARN]状态项,如下所示:
[WARN] 1.1 - Ensure a separate partition for containers has
been created
默认情况下,Docker 在主机机器上使用/var/lib/docker来存储所有文件,包括所有镜像、容器和由默认驱动程序创建的卷。这意味着该文件夹可能会迅速增长。由于我的主机机器只运行一个分区(并且取决于容器的操作),这可能会填满整个硬盘,从而使我的主机无法使用:
[WARN] 1.5 - Ensure auditing is configured for the Docker
daemon
[WARN] 1.6 - Ensure auditing is configured for Docker files
and directories - /var/lib/docker
[WARN] 1.7 - Ensure auditing is configured for Docker files
and directories - /etc/docker
[WARN] 1.10 - Ensure auditing is configured for Docker files
and directories - /etc/default/docker
这些警告被触发是因为auditd未安装,且没有为 Docker 守护进程及相关文件配置审计规则;有关auditd的更多信息,请参阅博客文章:www.linux.com/topic/desktop/customized-file-monitoring-auditd/。
Docker 守护进程配置
我的 Docker 守护进程配置标记了七个[WARN]状态项,如下所示:
[WARN] 2.1 - Ensure network traffic is restricted between
containers on the default bridge
默认情况下,Docker 允许在同一主机上的容器之间不受限制地传递流量。你可以更改此行为;有关 Docker 网络的更多信息,请参阅:docs.docker.com/network/:
[WARN] 2.8 - Enable user namespace support
默认情况下,用户命名空间没有被重新映射。尽管可以进行映射,但这目前可能会导致与多个 Docker 功能相关的问题;有关已知限制的详细信息,请参阅:docs.docker.com/engine/reference/commandline/dockerd/:
[WARN] 2.11 - Ensure that authorization for Docker client commands is enabled
Docker 的默认安装允许不受限制地访问 Docker 守护进程;你可以通过启用授权插件来限制访问仅限于经过认证的用户。详细信息请参阅:docs.docker.com/engine/extend/plugins_authorization/:
[WARN] 2.12 - Ensure centralized and remote logging is configured
由于我只运行一个主机,我没有使用像rsyslog这样的服务将我的 Docker 主机日志传送到中央服务器,也没有在 Docker 守护进程上配置日志驱动程序;有关更多详细信息,请参阅:docs.docker.com/config/containers/logging/configure/:
[WARN] 2.14 - Ensure live restore is Enabled
--live-restore标志启用 Docker 守护进程无状态容器的完全支持;这意味着,当守护进程关闭时,容器不会停止运行,而是继续运行,并且在重启后能够正确重新连接到容器。
默认情况下未启用此功能,这是由于向后兼容性问题。有关更多详细信息,请参阅 docs.docker.com/config/containers/live-restore/:
[WARN] 2.15 - Ensure Userland Proxy is Disabled
你的容器可以通过两种方式连接到外部世界:使用发夹 NAT 或用户空间代理。对于大多数安装,发夹 NAT 模式是首选模式,因为它利用了 iptables,并且性能更好。在此不可用时,Docker 会使用用户空间代理。大多数现代操作系统上的 Docker 安装都支持发夹 NAT。有关如何禁用用户空间代理的详细信息,请参阅 docs.docker.com/config/containers/container-networking/:
[WARN] 2.18 - Ensure containers are restricted from acquiring
new privileges
通过设置 suid 或 sgid 位,阻止容器内的进程可能获得额外的权限;这可以限制任何尝试访问特权二进制文件的危险操作的影响。
Docker 守护进程配置文件
在本节中没有 [WARN] 状态,这是预期的,因为 Docker 是通过 Docker 安装程序部署的。
容器镜像和构建文件
我在容器镜像和构建文件中发现了三个 [WARN] 状态;你可能注意到,多行警告在状态后会以 * 为前缀:
[WARN] 4.1 - Ensure a user for the container has been created
[WARN] * Running as root: sshd
[WARN] * Running as root: priv-nginx
[WARN] * Running as root: root-nginx
我正在运行的容器中的进程都以 root 用户身份运行;这是大多数容器的默认行为。有关更多信息,请参阅 docs.docker.com/engine/security/security/:
[WARN] 4.5 - Ensure Content trust for Docker is Enabled
启用 Docker 的内容信任可以确保你拉取的容器镜像的来源,因为当你推送镜像时,它们会被数字签名;这意味着你总是运行你打算运行的镜像。有关内容信任的更多信息,请参阅 docs.docker.com/engine/security/trust/content_trust/:
[WARN] 4.6 - Ensure HEALTHCHECK instructions have been added
to the container image
[WARN] * No Healthcheck found: [sshd:latest]
[WARN] * No Healthcheck found: [nginx:latest]
[WARN] * No Healthcheck found: [ubuntu:16.04]
在构建镜像时,可以设置 HEALTHCHECK;这确保当从你的镜像启动容器时,Docker 会定期检查容器的状态,如果需要,它会重启或重新启动容器。更多详情请参阅 docs.docker.com/engine/reference/builder/#healthcheck。
容器运行时
由于在启动我们审核的 Docker 主机上的容器时有些不太谨慎,我们知道这里会有很多漏洞,总共有 11 个:
[WARN] 5.2 - Ensure SELinux security options are set, if
applicable
[WARN] * No SecurityOptions Found: sshd
[WARN] * No SecurityOptions Found: root-nginx
前述的漏洞是一个假阳性——我们并没有运行 SELinux,因为这是一个 Ubuntu 系统,而 SELinux 仅适用于基于 Red Hat 的系统。相反,5.1 向我们展示了结果,即一个 [PASS],这是我们希望看到的:
[PASS] 5.1 - Ensure AppArmor Profile is Enabled
下一个 [WARN] 状态是我们自己造成的,如下所示:
[WARN] 5.4 - Ensure privileged containers are not used
[WARN] * Container running in Privileged mode: priv-nginx
以下内容也是我们自己造成的:
[WARN] 5.6 - Ensure ssh is not run within containers
[WARN] * Container running sshd: sshd
这些可以安全忽略;应该非常少见需要启动运行在 Privileged mode 模式下的容器。只有当你的容器需要与 Docker 主机上的 Docker 引擎进行交互时才需要这种模式;例如,当你运行 GUI(如 Portainer)时,我们在 第九章 中讨论过,Portainer - Docker 的图形界面。
我们还讨论过你不应该在容器中运行 SSH。有一些用例,比如在特定网络中运行跳板主机;但这些应该是例外。
接下来的两个 [WARN] 状态被标记,因为在 Docker 中,默认情况下,所有运行中的容器会平等地共享 Docker 主机上的资源;为容器设置内存和 CPU 优先级限制将确保你希望具有更高优先级的容器不会被较低优先级的容器饿死资源:
[WARN] 5.10 - Ensure memory usage for container is limited
[WARN] * Container running without memory restrictions:
sshd
[WARN] * Container running without memory restrictions:
priv-nginx
[WARN] * Container running without memory restrictions:
root-nginx
[WARN] 5.11 - Ensure CPU priority is set appropriately on the
container
[WARN] * Container running without CPU restrictions: sshd
[WARN] * Container running without CPU restrictions: priv-
nginx
[WARN] * Container running without CPU restrictions: root-
nginx
正如我们在本章前面讨论过的那样,如果可能的话,你应该以只读方式启动容器,并挂载你知道需要写入数据的卷:
[WARN] 5.12 - Ensure the container's root filesystem is
mounted as read only
[WARN] * Container running with root FS mounted R/W: sshd
[WARN] * Container running with root FS mounted R/W: priv-
nginx
[WARN] * Container running with root FS mounted R/W: root-
nginx
提出以下标志的原因是我们没有告诉 Docker 将我们暴露的端口绑定到 Docker 主机上的特定 IP 地址:
[WARN] 5.13 - Ensure incoming container traffic is binded to a
specific host interface
[WARN] * Port being bound to wildcard IP: 0.0.0.0 in sshd
由于我的测试 Docker 主机只有一个网络接口卡,这不是一个大问题。然而,如果我的 Docker 主机有多个接口,那么该容器将暴露给所有网络,如果我有外部和内部网络,这可能会成为问题。更多细节请参考 docs.docker.com/network/:
[WARN] 5.14 - Ensure 'on-failure' container restart policy is
set to '5'
[WARN] * MaximumRetryCount is not set to 5: sshd
[WARN] * MaximumRetryCount is not set to 5: priv-nginx
[WARN] * MaximumRetryCount is not set to 5: root-nginx
尽管我没有使用 --restart 标志启动我的容器,但 MaximumRetryCount 没有默认值。这意味着,如果一个容器反复失败,它会安稳地继续尝试重启。这可能会对 Docker 主机产生负面影响;设置 MaximumRetryCount 为 5 将意味着容器在放弃之前会尝试重启五次:
[WARN] 5.25 - Ensure the container is restricted from
acquiring additional privileges
[WARN] * Privileges not restricted: sshd
[WARN] * Privileges not restricted: priv-nginx
[WARN] * Privileges not restricted: root-nginx
默认情况下,Docker 不会限制进程或其子进程通过 suid 或 sgid 位获得新权限。要了解如何停止这种行为的细节,请参考 www.projectatomic.io/blog/2016/03/no-new-privs-docker/:
[WARN] 5.26 - Ensure container health is checked at runtime
[WARN] * Health check not set: sshd
[WARN] * Health check not set: priv-nginx
[WARN] * Health check not set: root-nginx
再次说明,我们没有使用任何健康检查,这意味着 Docker 不会定期检查容器的状态。要查看引入此功能的拉取请求的 GitHub 问题,请参考 github.com/moby/moby/pull/22719:
[WARN] 5.28 - Ensure PIDs cgroup limit is used
[WARN] * PIDs limit not set: sshd
[WARN] * PIDs limit not set: priv-nginx
[WARN] * PIDs limit not set: root-nginx
潜在地,攻击者可以通过在你的容器内输入一个命令触发 fork bomb,这可能会崩溃你的 Docker 主机,而恢复的唯一方法就是重启主机。你可以通过使用 --pids-limit 参数来防止这种情况。有关更多信息,请参见 github.com/moby/moby/pull/18697。
Docker 安全操作
本节包括有关最佳实践的 [INFO] 信息,如下所示:
[INFO] 6.1 - Avoid image sprawl
[INFO] * There are currently: 4 images
[INFO] 6.2 - Avoid container sprawl
[INFO] * There are currently a total of 4 containers, with
4 of them currently running
Docker Swarm 配置
本节包括 [PASS] 信息,因为我们没有在主机上启用 Docker Swarm。
移除 Multipass 机器
一旦你完成了对 Ubuntu 服务器的操作,你可以运行以下命令来移除它:
$ multipass delete docker-host –purge
记住,运行上述命令时不会出现任何警告;它会立即删除正在运行的机器。
总结 Docker Bench
正如你所见,通过对 Docker 主机运行 Docker Bench,能更好地了解你的 Docker 主机在 CIS Docker 基准测试中的表现;这显然比手动检查 257 页文档中的每个测试更为高效。
现在我们已经介绍了如何评估和保护 Docker 主机,接下来快速讨论一下如何保护镜像。
第三方安全服务
在我们结束本章之前,我们将看看一些第三方服务,它们可以帮助你评估镜像的漏洞。
Quay
Quay 是由 Red Hat 提供的镜像注册中心,类似于 Docker Hub/Registry;不同之处在于,Quay 在镜像推送/构建后,会对每个镜像执行安全扫描。
你可以通过查看所选镜像的Repository Tags来查看扫描结果。在这里,你会看到一个Security Scan的列。如以下截图所示,在我们创建的示例镜像中,没有发现任何问题:

图 14.2 – Quay 上通过的安全扫描
点击 Passed 会将你带到更详细的漏洞报告页面,展示图像中检测到的任何漏洞。由于目前没有漏洞(这是好事),此页面不会显示太多信息。然而,点击左侧菜单中的 Packages 图标,我们将看到扫描所发现的软件包列表。
对于我们的测试镜像,它发现了 34 个没有漏洞的软件包,所有这些软件包都显示在这里,并确认了软件包的版本及其如何被引入镜像:

图 14.3 – 所有已安装包的列表
正如你也可以看到,Quay 正在扫描我们公开的镜像,该镜像托管在 Quay 提供的免费开源计划中。安全扫描是 Quay 所有计划的标准配置。
Clair
Clair 是 Red Hat 的一个开源项目。本质上,它是一个为托管版 Quay 和商业支持的企业版提供静态分析功能的服务。
它的工作原理是创建以下漏洞数据库的本地镜像:
-
Debian 安全漏洞追踪器:
security-tracker.debian.org/tracker/ -
Ubuntu CVE 追踪器:
launchpad.net/ubuntu-cve-tracker/ -
Red Hat 安全数据:
www.redhat.com/security/data/metrics/ -
Oracle Linux 安全数据:
linux.oracle.com/security/ -
Alpine SecDB:
git.alpinelinux.org/cgit/alpine-secdb/ -
NIST NVD:
nvd.nist.gov/
一旦它镜像了数据源,它会挂载镜像的文件系统,然后执行安装包的扫描,将其与前面数据源中的签名进行比较。
Clair 不是一个简单的服务;它仅有一个 API 驱动的接口,并没有默认随 Clair 提供的华丽的基于 Web 的或命令行工具。API 的文档可以在 app.swaggerhub.com/apis/coreos/clair/3.0 找到。
安装说明可以在项目的 GitHub 页面找到,网址是 github.com/quay/clair/。
此外,你还可以在 Clair 的集成页面找到支持 Clair 的工具列表,网址是 github.com/quay/clair/blob/master/Documentation/integrations.md。
在结束之前,我们还有一个工具需要介绍,它是我们可以在本地运行的工具。
Anchore
我们将要介绍的最终工具是 Anchore。它有多个版本;有基于云的版本和一个“本地”企业版本,二者都带有完整的基于 Web 的图形界面。还有一个版本可以与 Jenkins 集成,此外还有一个开源的命令行扫描工具,这就是我们现在要介绍的内容。
这个版本作为 Docker Compose 文件分发,因此我们将首先创建所需的文件夹,并下载 Docker Compose 文件:
$ mkdir anchore
$ cd anchore
$ curl https://docs.anchore.com/current/docs/engine/quickstart/
docker-compose.yaml -o docker-compose.yaml
现在我们已经完成了基本配置,你可以拉取镜像并启动容器,具体步骤如下:
$ docker-compose pull
$ docker-compose up -d
在与我们的 Anchore 部署交互之前,我们需要命令行客户端。幸运的是,我们下载的 Docker Compose 文件自带一个配置好的客户端容器:
$ docker-compose exec api anchore-cli system status
这将显示你安装的整体状态;从你第一次启动开始,可能需要一两分钟,才能显示所有内容正常运行:

图 14.4 – 检查 Anchore 引擎的状态
下一个命令显示了 Anchore 在数据库同步中的位置:
$ docker-compose exec api anchore-cli system feeds list
正如你在以下截图中看到的,我的安装目前正在同步数据库。这个过程可能需要几个小时;但是,对于我们的示例,我们将扫描一个基于 Alpine Linux 的数据库,这些是第一个下载的数据库:

图 14.5 – 检查源下载状态
接下来,我们需要获取一个镜像进行扫描;让我们获取一个旧的镜像,如下所示:
$ docker-compose exec api anchore-cli image add russmckendrick/
moby-counter:old
执行初始扫描可能需要一两分钟;你可以通过运行以下命令来检查状态:
$ docker-compose exec api anchore-cli image list
稍等一会,状态应从分析中更改为已分析:
$ docker-compose exec api anchore-cli image add russmckendrick/
moby-counter:old
这将显示镜像的概览,如下所示:

图 14.6 – 查看已分析镜像的信息
你可以通过运行以下命令来查看问题列表(如果有的话):
$ docker-compose exec api anchore-cli image vuln
russmckendrick/moby-counter:old os
正如你在以下截图中看到的,每个列出的包都有当前版本、CVE 问题的链接,并确认了修复该问题的版本号:

图 14.7 – 审查 Anchore 查找到的漏洞
你可以使用以下命令来移除 Anchore 容器和数据库卷:
$ docker-compose stop
$ docker-compose rm
$ docker volume rm anchore_anchore-db-volume
另外,别忘了删除我们在本节开始时创建的anchore文件夹。
总结
本章我们介绍了 Docker 安全性的几个方面。首先,我们查看了运行容器时(与典型虚拟机相比)在安全性方面必须考虑的一些问题。我们查看了 Docker 主机的优势,接着讨论了镜像信任。然后,我们了解了可以用于安全目的的 Docker 命令。
我们启动了一个只读容器,以便最小化潜在的入侵者在我们运行的容器中可能造成的损害。由于并非所有“应用程序”都适合在只读容器中运行,我们随后查看了如何追踪自镜像启动以来所做的更改。在排查任何问题时,能够轻松发现运行时在文件系统上所做的更改总是非常有用的。
接下来,我们讨论了互联网安全中心(Center for Internet Security)针对 Docker 的指导方针。该指南将帮助你设置 Docker 环境的多个方面。最后,我们查看了 Docker Bench Security。我们了解了如何启动并运行它,并演示了输出结果的示例。随后,我们分析了输出内容并解释其含义。请记住该应用程序涵盖的七个项目:主机配置、Docker 守护进程配置、Docker 守护进程配置文件、容器镜像和构建文件、容器运行时、Docker 安全操作和 Docker Swarm 配置。
在下一章中,我们将探讨 Docker 如何融入你现有的工作流程,以及一些与容器协作的新方式。
问题
-
启动容器时,如何使整个容器或部分容器为只读模式?
-
每个容器应该运行多少个进程?
-
检查你的 Docker 安装是否符合 CIS Docker 基准测试的最佳方法是什么?
-
运行 Docker Bench Security 应用程序时,应该挂载什么?
-
判断正误:Quay 仅支持对私有镜像进行扫描
进一步阅读
欲了解更多信息,请访问网站 www.cisecurity.org/。Docker 基准测试可以在 www.cisecurity.org/benchmark/docker/ 找到。
第十五章:Docker 工作流
到目前为止,您应该已经在思考如何将 Docker 引入到您的日常工作流程中。在本章中,我们将把所有部分整合在一起,这样您就可以在本地开发环境中开始使用 Docker。我们还将讨论在规划生产环境时需要考虑的一些事项。
本章将涉及以下主题,所有这些都将在我们之前章节所学内容的基础上展开:
-
用于开发的 Docker
-
监控 Docker 和 Kubernetes
-
生产环境是什么样的?
技术要求
在本章中,我们将在桌面上使用 Docker。与之前的章节一样,我将使用我偏好的操作系统,即 macOS。我们将运行的 Docker 命令适用于我们已经安装 Docker 的所有三种操作系统;然而,某些支持命令——虽然不多——可能仅适用于基于 macOS 和 Linux 的操作系统。
用于开发的 Docker
我们将通过讨论 Docker 如何帮助开发人员来开始了解工作流。回到第一章的开头,Docker 概述部分,我们讨论的第一个话题之一是开发人员和“只在我机器上能运行”的问题。到目前为止,我们还没有完全解决这个问题,现在让我们来解决它。
在本节中,我们将讨论如何在本地计算机上使用 Docker for macOS 或 Docker for Windows 以及 Docker Compose 开发 WordPress 项目。
目标是启动一个 WordPress 安装,您可以通过以下步骤来实现:
-
下载并安装 WordPress。
-
允许桌面编辑器(如 Atom、Visual Studio Code 或 Sublime Text)从本地计算机访问 WordPress 文件。
-
使用WordPress 命令行工具(WPCLI)配置和管理 WordPress。这样,您可以停止、启动甚至删除容器而不会丢失工作。
在启动 WordPress 安装之前,让我们看一下 Docker Compose 文件,您可以在随附的仓库中的chapter14/docker-wordpress文件夹中找到它:
version: '3'
services:
我们将启动四个不同的服务,从web开始:
web:
image: nginx:alpine
ports:
- '8080:80'
volumes:
- './wordpress/web:/var/www/html'
- './wordpress/nginx.conf:/etc/nginx/conf.d/default.conf'
depends_on:
- wordpress
紧接着是wordpress服务:
wordpress:
image: wordpress:php7.2-fpm-alpine
volumes:
- './wordpress/web:/var/www/html'
depends_on:
- mysql
接下来,我们来看一下mysql数据库服务:
mysql:
image: mysql:5
environment:
MYSQL_ROOT_PASSWORD: 'wordpress'
MYSQL_USER: 'wordpress'
MYSQL_PASSWORD: 'wordpress'
MYSQL_DATABASE: 'wordpress'
volumes:
- './wordpress/mysql:/var/lib/mysql'
最后,我们有一个名为wp的辅助服务:
wp:
image: wordpress:cli-2-php7.2
volumes:
- './wordpress/web:/var/www/html'
- './wordpress/export:/export'
我们可以使用PMSIpilot的docker-compose-viz工具来可视化 Docker Compose 文件。
要实现此操作,请在与docker-compose.yml文件相同的文件夹中运行以下命令:
$ docker container run --rm -it --name dcv -v $(pwd):/input
pmsipilot/docker-compose-viz render -m image docker-compose.yml
这将输出一个名为docker-compose.png的文件,您应该会看到类似这样的内容:

图 15.1:运行 docker-compose-viz 时我们 WordPress Docker Compose 文件的输出
第一个服务叫做 web。这个服务是我们四个服务中唯一暴露给主机网络的,它作为我们 WordPress 安装的前端。它运行的是来自store.docker.com/images/nginx/的官方 NGINX 镜像,并执行两个角色。看看 NGINX 配置,看看你是否能猜出它们是什么:
server {
server_name _;
listen 80 default_server;
root /var/www/html;
index index.php index.html;
access_log /dev/stdout;
error_log /dev/stdout info;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ .php$ {
include fastcgi_params;
fastcgi_pass wordpress:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}
}
你可以看到,我们除了 PHP 内容外,所有内容都通过 NGINX 从/var/www/html/提供,这些内容是通过 NGINX 从主机挂载的,所有针对 PHP 文件的请求都被代理到我们的第二个服务,名为wordpress,端口为9000。NGINX 配置本身是从主机挂载到/etc/nginx/conf.d/default.conf。
这意味着我们的 NGINX 容器充当静态内容的 Web 服务器,这是第一个角色;同时,它也充当了通往 WordPress 容器的代理,处理动态内容,这是容器承担的第二个角色——你猜对了吗?
第二个服务是wordpress。这是来自hub.docker.com/images/wordpress的官方 WordPress 镜像,我使用的是php7.2-fpm-alpine标签。这让我们能够在 PHP 7.2 上运行一个 WordPress 安装,使用基于 Alpine Linux 的PHP-FPM。
重要提示
PHP FastCGI 处理器管理器(PHP-FPM)是一个具有许多优秀功能的 PHP FastCGI 实现。对我们而言,它允许 PHP 作为一个服务运行,我们可以将其绑定到一个端口并传递请求;这与 Docker 每个容器运行一个单独服务的方法相契合。
我们正在挂载与我们为 web 服务使用的相同的 web 根目录,在主机上是wordpress/web,在服务上是/var/www/html/。首先,主机上的文件夹将是空的;然而,一旦 WordPress 服务启动,它会检测到没有核心的 WordPress 安装,并将其复制到该位置,从而有效地引导我们的 WordPress 安装并将其复制到主机上,准备好供我们开始工作。
第三个服务是 MySQL,使用官方的 MySQL 镜像,可以在hub.docker.com/images/mysql找到,它是我们使用的四个镜像中唯一没有使用 Alpine Linux 的(来吧 MySQL,赶紧发布一个基于 Alpine Linux 的镜像吧!)。它使用的是debian:buster-slim。
我们传递了一些环境变量,以便在容器第一次运行时创建数据库、用户名和密码;如果你以后将这个作为项目的基础,密码是你应该更改的内容。
与 web 和 wordpress 容器一样,我们也从主机上挂载了一个文件夹。在这种情况下,是wordpress/mysql,我们将其挂载到/var/lib/mysql/,这是 MySQL 存储其数据库和相关文件的默认文件夹。
第四个也是最后一个服务被简单地称为wp。它与其他三个服务不同:当执行此服务时,它会立即退出,因为容器内没有长时间运行的进程。与长时间运行的进程不同,我们只有一个单一进程,用于与 WordPress 安装进行交互和管理。
在容器中运行此工具的优点是,我们运行命令行工具的环境与主wordpress容器完全匹配。
你会注意到,我们像在 web 和 WordPress 中一样挂载了 web 根目录,这意味着容器可以完全访问我们的 WordPress 安装,并且还有一个名为/export的第二个挂载;一旦我们配置好 WordPress,我们将更详细地查看这一点。
要启动 WordPress,我们只需运行以下命令来拉取镜像:
$ docker-compose pull
这将拉取镜像并启动 web、wordpress和mysql服务,并准备wp服务。在服务启动之前,我们的wordpress文件夹看起来像这样:

图 15.2:启动 WordPress 之前
如你所见,那里只有nginx.conf,它是 Git 仓库的一部分。然后,我们可以使用以下命令来启动容器并检查它们的状态:
$ docker-compose up -d
$ docker-compose ps
你的终端输出应该类似于以下屏幕:

图 15.3:启动并检查 WordPress 安装的状态
你应该看到在wordpress文件夹中创建了三个文件夹:export、mysql和web。另外,请记住,我们期望dockerwordpress_wp_1的退出状态为Exit 1,所以这没问题:

图 15.4:检查启动 WordPress 后创建的文件夹
打开浏览器并访问http://localhost:8080/,应该会显示标准的 WordPress 预安装欢迎页面,你可以选择安装时使用的语言:

图 15.5:WordPress 设置页面
不要点击继续,因为它会带你进入图形界面安装的下一屏。相反,返回到你的终端。
我们不打算使用图形界面(GUI)来完成安装,而是使用wp-cli。这有两个步骤。第一步是创建wp-config.php文件。为此,运行以下命令:
$ docker-compose run wp core config \
--dbname=wordpress \
--dbuser=wordpress \
--dbpass=wordpress \
--dbhost=mysql \
--dbprefix=wp_
正如你将在以下终端输出中看到的,在我运行命令之前,我只有 wp-config-sample.php 文件,这是 WordPress 核心自带的文件。然后,在运行命令之后,我得到了我自己的 wp-config.php 文件:

图 15.6:使用 wp-cli 创建 wp-config.php 文件
你会注意到,在命令中,我们传递了在 Docker Compose 文件中定义的数据库详情,并告知 WordPress 它可以通过 mysql 地址连接到数据库服务。
现在我们已经配置了数据库连接信息,我们需要配置我们的 WordPress 站点,并创建一个管理员用户并设置密码。为此,请运行以下命令:
$ docker-compose run wp core install \
--title='Blog Title' \
--url='http://localhost:8080' \
--admin_user='admin' \
--admin_password='password' \
--admin_email='email@domain.com'
运行此命令会在电子邮件服务中产生错误;不要担心这个信息,因为这只是一个本地开发环境。我们不太担心 WordPress 安装中的电子邮件发送:

图 15.7:使用 cp-cli 配置 WordPress
我们已经使用 wp-cli 在 WordPress 中配置了以下内容:
-
我们的 URL 是 http://localhost:8080。
-
我们的站点标题应该是博客标题。
-
我们的管理员用户名是 admin,密码是 password,用户的电子邮件地址是 email@domain.com。
返回浏览器并输入 http://localhost:8080/ 应该会展示一个原生的 WordPress 站点:

图 15.8:默认的 WordPress 站点
在我们进一步操作之前,先稍微自定义一下我们的安装,首先安装并启用 JetPack 插件:
$ docker-compose run wp plugin install jetpack –activate
命令的输出如下:

图 15.9:安装 JetPack 插件
然后,install 并启用 Sydney 主题:
$ docker-compose run wp theme install sydney --activate
命令的输出如下:

图 15.10:安装 Sydney 主题
刷新我们的 WordPress 页面 http://localhost:8080/ 应该会显示类似如下内容:

图 15.11:查看更新主题后的站点
在我们打开 IDE 之前,使用以下命令销毁运行我们 WordPress 安装的容器:
$ docker-compose down
命令的输出如下:

图 15.12:停止并移除正在运行 WordPress 的容器
由于我们的整个 WordPress 安装,包括所有文件和数据库,都存储在本地机器上,我们应该能够运行以下命令来恢复到我们停留的位置:
$ docker-compose up -d
一旦确认它已按预期启动并运行,可以访问http:// localhost:8080/,然后在桌面编辑器中打开docker-wordpress文件夹。我使用的是 Visual Studio Code。
在编辑器中,打开wordpress/web/wp-blog-header.php文件,在开头的 PHP 语句中添加以下行,并保存:
echo 'Testing editing in the IDE';
文件应该像下面这样:

图 15. 13:在 Visual Studio Code 中编辑 wp-blog-header.php
保存后,刷新浏览器。你应该能在页面最底部看到消息测试 IDE 中的编辑功能(以下屏幕已缩放;如果你在屏幕上跟随操作,可能较难找到该消息,因为文字很小):

图 15.14:查看我们在页面上的编辑
我们最后要查看的是为什么我们将wordpress/export文件夹挂载到wp容器上。
正如本章前面提到的,你其实不应该随便修改wordpress/mysql文件夹的内容;这也包括共享它。虽然如果你将项目文件夹压缩并传给同事可能能工作,但这并不被认为是最佳实践。因此,我们将导出文件夹挂载到容器中,以便使用 WPCLI 进行数据库转储和导入。
为此,执行以下命令:
$ docker-compose run wp db export --add-drop-table /export/
wordpress.sql
根据你运行的 Docker 版本,你可能在执行前述命令时遇到权限拒绝错误;如果遇到,请改用以下命令:
$ docker-compose run wp db export --add-drop-table /var/www/
html/wordpress.sql
这样会将数据库转储复制到wordpress/wordpress目录,而不是wordpress/export。之所以这样做,是因为不同的主机操作系统处理本地文件的创建方式不同,这可能会导致容器内的权限问题。
以下终端输出展示了导出操作的过程,以及执行命令前后wordpress/export的内容,最后是 MySQL 转储文件的顶部几行:

图 15.15:导出 WordPress 数据库
如果我在开发过程中犯了个错误,意外地破坏了部分数据库,我可以通过运行以下命令恢复到我之前备份的数据库:
$ docker-compose run wp db import /export/wordpress.sql
命令的输出结果如下:

图 15.16:导入 WordPress 数据库
如您所见,我们已经安装了 WordPress,并使用 WordPress 命令行工具 wp-cli 和网页浏览器进行交互,编辑了代码,备份并恢复了数据库,所有这些都不需要在本地机器上安装或配置 NGINX、PHP、MySQL 或 wp-cli。我们也没有需要登录到容器中。通过将宿主机的卷挂载进去,当我们拆卸 WordPress 容器时,内容是安全的,我们没有丢失任何工作。
此外,如果需要,我们可以轻松地将项目文件夹的副本传递给一位安装了 Docker 的同事,通过一条命令,他们就能在完全相同的环境中运行我们的代码,就像我们自己安装的环境一样。
提示
如果您愿意,您可以通过运行 docker-compose down 停止并删除您的 WordPress 容器。如果您正在跟随本书,您可能想保留 WordPress 以便在下一个章节中监控运行中的容器。
最后,由于我们使用的是来自 Docker Hub 的官方镜像,我们知道可以安全地将其部署到生产环境,因为它们是按照 Docker 的最佳实践构建的。
您可能会发现一个非常实用的功能,那就是 Docker 能与您选择的 IDE 集成得如此顺畅。在前面的几页中,当我们编辑 wp-blog-header.php 文件时,您可能注意到左侧出现了 Docker 图标。在我们结束本章节之前,让我们快速讨论一下微软是如何将 Docker 支持集成到 Visual Studio Code 中的,接下来我们将简称其为 VS Code。
首先,您需要安装 VS Code,您可以在code.visualstudio.com/找到它,并安装 Microsoft 的 Docker 扩展,您可以在 Visual Studio 市场上找到该扩展:marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker。
您会注意到这个扩展并不会直接出现在您的面前。这是因为它与 VS Code 深度集成——例如,如果您右键点击资源管理器中的docker-compose.yml文件,您会发现菜单中有一些选项,允许您与 Docker Compose 进行交互:

图 15.17:从 VS Code 运行 Docker Compose
点击左侧的Docker图标将弹出一个列表,显示正在运行的容器、可用的镜像、您连接的注册表、网络和卷:

图 15.18:查看您正在运行的容器
右键点击容器,您可以选择使用 VS Code 内置的终端连接到正在运行的容器:

图 15.19:使用内置终端附加到容器
从 Git 仓库中打开与本书配套的文件夹,在 VS Code 中按下CMD +Shift + P将打开 VS Code 中的命令提示符。从这里,输入 Dockerfile 文件,并询问你想要构建哪个:

图 15.20:选择一个 Dockerfile 进行构建
构建完成后,你的镜像将在 Docker 部分列出,你可以右键点击标签并将其推送到你连接的任何注册表:

图 15.21:推送我们新构建的镜像
你也可以运行容器。一旦容器运行,你可以右键点击它并选择在浏览器中打开,直接进入应用程序:

图 15.22:在浏览器中打开运行中的容器
VS Code Docker 扩展的最终技巧是一个非常有用的功能。假设你有一个没有 Dockerfile 的仓库——例如,可以在 github.com/go-training/helloworld/ 找到的 Go Training helloworld 仓库,它没有 Dockerfile 或 docker-compose.yml 文件。下载该仓库并在 VS Code 中打开它。
打开后,按下CMD +Shift + P,输入 Add Docker,然后选择 3000。点击 Dockerfile 后,将打开如下内容:

图 15.23:VS Code 生成的多阶段 Dockerfile
你还会注意到一个 docker-compose.yml 文件以及一个 .dockerignore 文件和其他几个文件已被生成。从这里,你可以构建镜像,然后运行它。我建议使用交互式运行选项,因为应用程序所做的只是打印Hello World!然后退出,如下图所示:

图 15.24:运行应用程序
正如你肯定已经看到的,Docker 与 VS Code 的集成非常强大,使你可以在 VS Code 中运行我们在前几章中介绍的几乎所有 Docker 命令。其他 IDE 也有类似的扩展。相关链接在进一步阅读部分。
Docker 和 Azure DevOps
在第三章**,存储和分发镜像的审查第三方注册表部分,我们介绍了如何使用 GitHub 来托管和构建我们的容器镜像。我们还讨论了 Azure 容器注册表。
为了结束本章这一部分,我们将快速了解如何配置一个 Azure DevOps 管道,该管道构建我们在第二章《构建容器镜像》中介绍的多阶段 Dockerfile。
在我们配置管道之前,先来讨论一下 Azure DevOps 是什么。它是 Microsoft 提供的一项服务,具有以下功能:
-
版本控制
-
报告
-
需求管理
-
项目管理
-
自动构建
-
测试
-
发布管理
这看起来可能是很多不同的服务,的确如此,但 Azure DevOps 就是那种能够将各种 Microsoft 服务(包括 Microsoft Azure 生态系统中的服务)以及编程语言(如 .NET)和工具(如 Visual Studio)紧密结合在一起的粘合剂。涵盖所有内容将占用整本书的篇幅;事实上,关于这个主题已有几本书,因此我们将只介绍构建容器并将其推送到 Docker Hub 所需的基本功能。
开始使用 Azure DevOps 的唯一要求是拥有一个账户——要免费注册,请访问dev.azure.com/,并按照屏幕上的提示操作。创建账户后,点击 + 新建项目 按钮。
当你进入 创建新项目 页面时,会看到可以填写 项目名称 和 描述 的选项,并选择 可见性;默认情况下,项目是 私有的,但你也可以将其设置为 公开。
填写完详细信息后,点击 创建。我建议将项目设置为 私有。
创建项目后,点击位于左侧菜单最底部的 项目设置 选项。当 项目设置 页面加载完毕后,点击 服务连接,它位于 管道 下方。
从这里,点击 创建服务连接 按钮,从显示的服务列表中选择 Docker Registry。
在此,选择 Docker Hub 旁边的单选图标,输入你的 Docker ID,然后输入你的 Docker 密码。如果你的 Docker Hub 账户启用了多重身份验证(我强烈建议你配置此项),那么你将需要一个用户访问令牌——我们在第三章《存储和分发镜像》一节中讲解过这个问题:

图 15.25:设置与 Docker Hub 的服务连接
输入详细信息后,点击 验证 按钮,如果你输入的详情正确,将显示一个绿色勾号。在点击 验证并保存 按钮之前,你需要输入一个 服务连接名称;我输入的是 Docker,但你可以随意选择,只需记住它,因为稍后我们将需要用到。
接下来,您需要一个包含 Dockerfile 和名为 azure-pipelines.yml 的文件的 Git 仓库—您可以 fork github.com/russmckendrick/DevOpsContainerBuild 中的示例仓库。
一旦获得您的仓库,返回到 Azure DevOps 项目,然后点击左侧菜单中的Pipelines,此时您将看到如下界面:

图 15.26:第一次查看 Pipelines 页面
如您所猜测,您需要点击创建管道,这会要求您输入几个信息项:
-
您的代码在哪里? 请选择 GitHub。您会注意到旁边有 YAML。我们将在管道配置完成后讨论 YAML 文件。
-
按照屏幕上的指示将 Azure DevOps 与您的 GitHub 账户链接。一旦链接完成,系统会要求您选择一个仓库。请选择您之前 fork 的仓库。
-
如果
azure-pipelines.yml文件没有被自动选中并且您停留在配置界面上,请点击现有 Azure Pipelines YAML 文件选项,从下拉列表中选择文件,然后点击继续。 -
审核 页面为您提供了审核您的管道 YAML 文件的选项,还可以选择运行它;不过,在此之前,请点击变量。
-
我们需要添加两个变量。第一个变量将让管道知道我们在本节中配置的 Docker Hub 服务连接的名称,第二个变量将让管道知道我们希望 Azure DevOps 管道将构建好的镜像推送到的 Docker Hub 仓库名称。
-
点击
Docker中的targetRegistry,然后点击targetRepo,接着为russmckendrick/AzureDevOpsBuild设置值。点击确定,然后点击保存。保存后,点击运行按钮触发构建。
azure-pipeline.yml 文件如下所示。首先,我们有 触发器 配置;该配置设置为 master,意味着每当主分支更新时,都会触发构建:
trigger:
- master
接下来是pool。这告诉 Azure DevOps 在管道执行时启动哪个虚拟镜像;如您所见,我们使用的是 Ubuntu:
pool:
vmImage: 'ubuntu-latest'
azure-pipeline.yml 文件的其余部分是构建 Docker@2 任务,用于登录到 Docker Hub:
steps:
- task: 'Docker@2'
displayName: 'Login to Docker Hub'
inputs:
command: 'login'
containerRegistry: '$(targetRegistry)'
我们正在使用在设置管道时定义的变量 $(targetRegistry)。这会告诉任务使用哪个服务连接。下一个任务是构建并推送我们的容器镜像:
- task: Docker@2
displayName: 'Build & Push container'
inputs:
command: 'buildAndPush'
containerRegistry: '$(targetRegistry)'
repository: '$(targetRepo)'
tags: |
latest
如您所见,语法非常易于理解。我们还使用了第二个变量 $(targetRepo) 来定义我们希望推送到的目标镜像仓库。最后一个任务是退出 Docker Hub:
- task: 'Docker@2'
displayName: 'Logout of Docker Hub'
inputs:
command: 'logout'
containerRegistry: '$(targetRegistry)'
最后一项任务可能不太需要,因为在 Azure DevOps 启动构建镜像时,虚拟机会在构建完成后被终止,如果构建过程中发生任何错误,虚拟机也会被终止,因此我们不必担心虚拟机被重复使用或我们的登录被第三方访问。
完成的管道运行大致如下:

图 15.27:已完成的管道运行
完成后,你应该能够在你的 Docker Hub 账户中看到新构建的容器。如前所述,在我们开始配置 Azure DevOps 管道之前,我们几乎只触及了 Azure DevOps 能做的事情;有关 Azure DevOps 的一些有趣链接,请参阅本章的进一步阅读部分。
接下来,我们将看看如何监控我们的容器和 Docker 主机。
监控 Docker 和 Kubernetes
在第四章,管理容器一章中,我们讨论了docker container top和docker container stats命令。你可能记得,这两个命令仅显示实时信息——没有保存历史数据。
如果你正在调试一个正在运行的问题,或者想快速了解容器内部发生了什么,这非常有用;然而,如果你需要回顾一个问题,这就不太有帮助了。例如,你可能已配置容器在变得无响应时重启。虽然这有助于应用程序的可用性,但如果你需要查看容器为什么变得无响应,它帮助不大。
在 GitHub 仓库的 /chapter14 文件夹中,有一个名为 prometheus 的文件夹,里面有一个 Docker Compose 文件,该文件在两个网络上启动了三个不同的容器。我们不直接查看 Docker Compose 文件本身,而是来看一下可视化效果:

图 15.28:Prometheus Docker Compose 文件的可视化
你可以通过运行以下命令自行生成这个:
$ docker container run --rm -it --name dcv -v $(pwd):/input
pmsipilot/docker-compose-viz render -m image docker-compose.yml
如你所见,事情正在发生变化。我们正在运行的三个服务如下:
-
Cadvisor
-
Prometheus
-
Grafana
在我们启动并配置 Docker Compose 服务之前,我们应该先讨论每个服务的必要性,从 cadvisor 开始:
cadvisor 服务是一个由 Google 发布的项目。如图中所示的 Docker Hub 用户名,我们正在使用的服务部分在 Docker Compose 文件中如下所示:
cadvisor:
image: google/cadvisor:latest
container_name: cadvisor
volumes:
- /:/rootfs:ro
- /var/run:/var/run:rw
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
restart: unless-stopped
expose:
- 8080
networks:
- back
我们正在挂载宿主机的各种文件系统部分,以便 cadvisor 访问我们的 Docker 安装,方式与我们在 第九章 中的 Portainer – Docker 图形用户界面 相似。这样做的原因是,在我们的案例中,我们将使用 cadvisor 来收集容器的统计信息。虽然它可以作为独立的容器监控服务使用,但我们不希望公开暴露 cadvisor 容器。相反,我们仅将其提供给我们 Docker Compose 堆栈中的其他容器,在后台网络中使用。
cadvisor 服务是一个自包含的 Docker 容器状态命令的 Web 前端,能够显示图表,并允许你通过简洁的界面从 Docker 主机深入到容器中;但是,它不会保留超过五分钟的数据指标。
由于我们试图记录的指标可能会在几小时甚至几天后才能获取,只有五分钟的指标数据意味着我们需要使用额外的工具来记录它所处理的指标。cadvisor 服务以结构化数据的形式暴露我们希望记录在容器中的信息,访问地址为 http://cadvisor:8080/metrics/。
稍后我们会看为什么这很重要。cadvisor 端点会被我们的下一个服务 prometheus 自动抓取。这是大多数重负载处理的地方。prometheus 是一个由 SoundCloud 编写并开源的监控工具:
prometheus:
image: prom/prometheus
container_name: prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.
yml
- prometheus_data:/prometheus
restart: unless-stopped
expose:
- 9090
depends_on:
- cadvisor
networks:
- back
如前面服务定义所示,我们挂载了一个名为 ./prometheus/prometheus.yml 的配置文件和一个名为 prometheus_data 的数据卷。该配置文件包含了我们希望抓取的数据源信息,正如以下配置所示:
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
monitor: 'monitoring'
rule_files:
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
我们指示 Prometheus 每 15 秒抓取一次我们的端点数据。端点在 scrape_configs 部分定义,如你所见,我们在那里定义了 cadvisor 和 Prometheus 本身。我们创建并挂载 prometheus_data 数据卷的原因是 Prometheus 将存储我们所有的指标数据,因此我们需要确保其安全。
本质上,Prometheus 是一个时间序列数据库。它获取抓取的数据,处理数据以找到指标名称和值,然后将其与时间戳一起存储。
Prometheus 还带有一个强大的查询引擎和 API,使其成为这类数据的完美数据库。虽然它确实具备基本的图表绘制功能,但建议使用 Grafana,这也是我们最终的服务,并且是唯一公开暴露的服务。
Grafana 是一个开源工具,用于显示监控图表和指标分析,它允许你使用时间序列数据库(如 Graphite、InfluxDB 以及 Prometheus)来创建仪表盘。还有其他可作为插件提供的后端数据库选项。
Grafana 的 Docker Compose 定义遵循与我们其他服务类似的模式:
grafana:
image: grafana/grafana
container_name: grafana
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning/:/etc/grafana/provisioning/
env_file:
- ./grafana/grafana.config
restart: unless-stopped
ports:
- 3000:3000
depends_on:
- prometheus
networks:
- front
- back
我们使用grafana_data卷来存储 Grafana 自身的内部配置数据库,而不是将环境变量存储在 Docker Compose 文件中,而是从一个名为./grafana/grafana.config的外部文件加载它们。
变量如下所示:
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=password
GF_USERS_ALLOW_SIGN_UP=false
如你所见,我们在这里设置了用户名和密码,因此将它们放在外部文件中意味着你可以在不编辑核心 Docker Compose 文件的情况下更改这些值。
现在我们知道了这三个服务各自的角色,接下来让我们启动它们。
为此,只需从prometheus文件夹中运行以下命令:
$ docker-compose pull
$ docker-compose up -d
这将创建一个网络和卷,并从 Docker Hub 拉取镜像。然后,它将启动三个服务:

图 15.29:运行 docker-compose up -d 启动我们的 Prometheus 应用
你可能会立刻想进入 Grafana 仪表盘。如果你这么做,你什么也看不见,因为 Grafana 需要几分钟来初始化。你可以通过查看日志来跟踪它的进度:
$ docker-compose logs -f grafana
命令的输出如下所示:

图 15.30:检查日志以查看 Grafana 是否准备就绪
一旦你看到HTTP Server Listen消息,Grafana 将可用。从 Grafana 版本5开始,你可以导入数据源和仪表盘,这就是我们将./grafana/provisioning/文件夹从主机挂载到/etc/grafana/provisioning/的原因。
这个文件夹包含自动配置 Grafana 与我们 Prometheus 服务对接并导入仪表盘的配置,该仪表盘将显示 Prometheus 从cadvisor抓取的数据。
打开浏览器并输入localhost:3000/,你应该会看到登录界面:

图 15.31:Grafana 登录页面
使用admin和password登录。一旦登录,如果你已经配置了数据源,你应该看到以下页面:

图 15.32:登录到 Grafana
如你所见,添加你的第一个数据源和创建你的第一个仪表盘的初步步骤已经完成。点击左上角的主页按钮会弹出一个菜单,列出可用的仪表盘:

图 15.33:查看可用的仪表盘
如你所见,我们有一个名为Docker Monitoring的仪表盘。点击它会带你到以下页面:

图 15.34:Docker Monitoring 仪表盘
如你在屏幕右上角的时间信息中所看到的,默认情况下,它显示的是过去五分钟的数据。点击它可以更改时间范围的显示。例如,以下屏幕显示的是过去 15 分钟的数据,这显然比 cadvisor 记录的五分钟更多:

图 15.35:查看 15 分钟的数据
我已经提到过,这是一个复杂的解决方案;最终,Docker 会扩展最近内建的 Prometheus 端点,目前它仅暴露 Docker 引擎的信息,而不包括容器本身。有关内建端点的更多信息,请查看官方 Docker 文档,链接是 docs.docker.com/config/daemon/prometheus/。
目前有其他的监控解决方案;它们大多数以第三方软件即服务(SaaS)的形式存在。正如你从进一步阅读部分的服务列表中看到的那样,市面上有一些已经成熟的监控解决方案。事实上,你可能已经在使用它们,因此在扩展配置时,考虑到这些解决方案来监控你的容器会非常容易。
那 Kubernetes 呢?你可能会问。我已经提到过,Prometheus 最初是由SoundCloud开发的,但它也是第一个被捐赠给云原生计算基金会(CNCF)的 Kubernetes 以外的项目之一。
这意味着 Kubernetes 和外部服务(如 Azure AKS)中支持 Prometheus——例如,Azure Monitor 与 Prometheus 有无缝集成。
为了演示这一点,请查看 Azure Friday 的演讲 如何使用 Prometheus 监控 Azure Monitor 中的容器,由Keiko Harada 与 Scott Hanselman 主讲,视频可以在 Microsoft Azure YouTube 频道找到,链接是 www.youtube.com/watch?v=5ARJ6DzqTYE。
生产环境是什么样的?
本章的最后部分,我们将讨论生产环境应该是什么样的。这个部分不会像你想象的那样长,因为可用的选项实在太多,无法一一覆盖。你也应该已经根据前面的部分和章节,形成了一个关于什么最适合你的良好概念。
相反,我们将讨论在规划你的环境时,你应该问自己的一些问题。
你的 Docker 主机
Docker 主机是你环境中的关键组件。如果没有这些,你就没有地方运行你的容器。正如我们在前面章节中看到的,运行 Docker 主机时有一些需要考虑的事项。
你需要首先考虑的一点是,如果你的主机正在运行 Docker,它们不应该运行任何其他服务。
进程混合
你应该抵制在现有主机上快速安装 Docker 并启动容器的诱惑。这不仅可能对安全性产生影响,因为在单个主机上混合了隔离和非隔离的进程,还可能导致性能问题,因为你无法为非容器化应用程序添加资源限制,这意味着它们可能会对正在运行的容器产生负面影响。
多个隔离的 Docker 主机
如果你有超过几个 Docker 主机,你打算如何管理它们?运行像 Portainer 这样的工具很好,但当尝试管理超过几个主机时,它可能会变得麻烦。此外,如果你运行多个隔离的 Docker 主机,你就没有将容器在主机之间移动的选项。
当然,你可以使用像 Weave Net 这样的工具,将容器网络跨多个独立的 Docker 主机进行扩展。根据你的托管环境,你也许有机会在外部存储上创建卷,并在需要时将其呈现给 Docker 主机,但你实际上是在创建一个手动过程,用来管理容器在主机之间的迁移。
路由到你的容器
如果你有多个主机,你需要考虑如何在容器之间路由请求。
例如,如果你有一个外部负载均衡器,比如 AWS 中的 ELB,或者一个位于本地集群前面的专用设备,你是否能够动态地为流量添加路由,将流量从负载均衡器上的端口 x 路由到 Docker 主机上的端口 y,然后将流量路由到你的容器?
如果你有多个容器都需要在相同的外部端口上进行访问,你打算如何处理?
你是否需要安装代理,比如 Traefik、HAProxy 或 NGINX,用于根据虚拟主机(基于域名或子域名)来接收并路由请求,而不是仅仅使用基于端口的路由?
集群
我们在前一节中讨论的许多问题可以通过引入集群工具来解决,比如 Docker Swarm 和 Kubernetes。我们快速讨论一下,在评估集群技术时,你应该问自己的一些问题。
兼容性
即使一个应用程序在开发人员的本地 Docker 安装上运行良好,你也需要能够保证,当你将应用程序部署到比如 Kubernetes 集群时,它能够以相同的方式运行。
十有八九,你不会遇到问题,但你确实需要考虑应用程序如何与同一应用程序集内的其他容器进行内部通信。
参考架构
是否有适用于你所选择的集群技术的参考架构?在部署集群时最好先检查一下。总有一些最佳实践指南,与你拟议的环境非常接近或完全匹配。毕竟,没有人希望创建一个大的单点故障。
另外,推荐的资源有哪些?部署一个有五个管理节点和一个 Docker 主机的集群是没有意义的,就像部署五个 Docker 主机和一个管理服务器一样没有意义,因为这样你有一个很大的单点故障。
你的集群技术支持哪些辅助技术(例如远程存储、负载均衡器和防火墙)?
集群通信
集群与管理主机或 Docker 主机通信时有什么要求?你是否需要一个内部或独立的网络来隔离集群流量?
你是否可以轻松地将一个集群成员锁定为仅限于你的集群?集群通信是否加密?关于你的集群有哪些信息可能被暴露?这会使其成为黑客的目标吗?
集群需要访问哪些外部 API,比如你的公共云提供商?任何 API/访问凭证是如何安全存储的?
镜像注册表
你的应用程序是如何打包的?你是否已经将代码嵌入到镜像中?如果是,你是否需要托管一个私有的本地镜像注册表,还是可以接受使用外部服务,例如 Docker Hub、Docker Trusted Registry(DTR)或 Quay?
如果你需要托管自己的私有注册表,应该将它放在你的环境中的哪里?谁有权限访问?它能否与目录提供商(例如 Active Directory 安装)连接?
总结
在本章中,我们查看了几种不同的 Docker 工作流,并学习了如何为容器和 Docker 主机设置一些监控。
当涉及到自己的环境时,你能做的最好的事情就是建立一个概念验证,并尽力覆盖你能想到的所有灾难场景。你可以通过使用云服务提供商提供的容器服务或寻找一个好的参考架构来提前开始,这两者都能减少你的试错率。
在下一章中,我们将探讨在容器世界中,你的下一步可能是什么。
问题
-
哪个容器为我们的 WordPress 网站提供服务?
-
为什么
wp容器没有保持运行? -
cadvisor会保留多少分钟的指标数据? -
哪个 Docker Compose 命令可以用来删除与应用程序相关的所有内容?
深入阅读
你可以在以下网站找到我们在本章中使用的软件的详细信息:
-
WordPress:
wordpress.org/ -
WP-CLI:
wp-cli.org/ -
PHP-FPM:
php-fpm.org/ -
Cadvisor:
github.com/google/cadvisor/ -
Prometheus:
prometheus.io/ -
Grafana:
grafana.com/ -
Prometheus 数据模型:
prometheus.io/docs/concepts/data_model/ -
Traefik:
containo.us/traefik/ -
HAProxy:
www.haproxy.org/ -
NGINX:
nginx.org/
有关 Docker 和 Azure DevOps 的更多信息,请访问以下链接:
-
Azure DevOps Docker 构建任务:
docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/docker?view=azure-devops -
Azure DevOps Docker Compose 构建任务:
docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/docker-compose?view=azure-devops -
Azure DevOps 和 Azure 容器注册表:
docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/containers/acr-template?view=azure-devops -
Packt Publishing 上的 Azure DevOps 书籍:
www.packtpub.com/catalogsearch/result/?q=Azure%20DevOps
其他外部托管的 Docker 监控平台包括以下内容:
-
Sysdig Cloud:
sysdig.com/ -
SignalFx:
signalfx.com/docker-monitoring/ -
New Relic:
newrelic.com/partner/docker -
Sematext:
sematext.com/docker/
还有其他自托管选项,例如以下内容:
-
Elastic Beats:
www.elastic.co/products/beats -
Sysdig:
sysdig.com/opensource/ -
Zabbix:
github.com/monitoringartist/zabbix-docker-monitoring
以下列表显示了一些其他 IDE 的扩展:
-
Atom Docker 包:
atom.io/packages/docker -
Sublime Text Docker 插件:
github.com/domeide/sublime-docker -
Jetbrains Docker 支持:
www.jetbrains.com/help/idea/docker.html
第十六章:Docker 的下一步
你已经到了本书的最后一章,并且坚持到了最后!在本章中,我们将讨论 Moby 项目,以及你如何为 Docker 和社区做出贡献。最后,我们将简要回顾云原生计算基金会。
本章将涵盖以下主题:
-
Moby 项目
-
为 Docker 做贡献
-
云原生计算基金会
Moby 项目
2017 年 DockerCon 大会上发布了 Moby 项目。当这个项目宣布时,我有一些问题,向同事们询问这个项目是什么,因为乍一看,Docker 似乎发布了另一个容器系统。
我的回答如下:
Moby 项目是 Docker 公司创建的开源框架,它允许 Docker 及任何愿意为该项目做出贡献的人,组装容器系统,“无需重新发明轮子”。
可以把这个框架看作是由几十个组件组成的一组构建模块,允许你自己组装定制的容器平台。Moby 项目用于创建 Docker 的开源社区版本和商业支持的 Docker Enterprise 版本。
对于任何询问类似项目的例子的人,我会解释红帽是如何处理 Red Hat Enterprise Linux 的:它结合了一个前沿版本、一个稳定的开源发布和一个企业支持版本。
可以把它看作是红帽在 Red Hat Enterprise Linux 中的做法。你有 Fedora,这是红帽操作系统开发者用于引入新软件包和功能的前沿版本开发平台,同时也会去除旧的、过时的组件。
通常,Fedora 的功能版本领先于 Red Hat Enterprise Linux 一两年,而后者是基于 Fedora 项目工作的商业长期支持发布;除了这个版本,还有社区支持版本——CentOS。
你可能会想,为什么这个项目只在本书的最后提到? 这个项目并不是为最终用户设计的。它是为了软件工程师、集成商和那些希望在基于容器的系统中进行实验、发明和构建的爱好者而设的——这不一定是 Docker。
在撰写本文时,由于 Docker 公司发生了变化,包括 Docker Enterprise 的出售给 Mirantis 以及 Docker 将注意力重新集中到开发者身上,关于将 Moby 代码重新归入 Docker 的讨论也在进行中。因此,在你阅读本文时,Moby 项目可能会发生变化,我不会进一步深入介绍它;相反,我建议你收藏以下页面,以便跟踪项目的发展动态:
-
该项目的官方网站是
mobyproject.org/ -
Moby 项目的 GitHub 页面:
github.com/moby/ -
Moby 项目的 Twitter 账号,是一个不错的新闻来源和教程链接:
twitter.com/moby/ -
关于将 Moby 移回 Docker 的讨论:
github.com/moby/moby/issues/40222和www.theregister.com/2019/11/22/moby_docker_naming/
为 Docker 做贡献
所以,您想为 Docker 做贡献吗?您是否有一个很棒的想法,想在 Docker 或其某个组件中实现?让我们为您提供所需的信息和工具。如果您不是程序员,也有其他方式可以贡献。Docker 拥有庞大的用户群,您可以通过帮助支持其他用户的服务来做出贡献。
让我们来了解一下如何做到这一点。
为代码做贡献
您为 Docker 做出贡献的最大方式之一是通过帮助 Moby 代码的开发,因为这是 Docker 所依赖的上游项目。
由于 Moby 是开源的,您可以将代码下载到本地机器上,开发新功能并将它们以拉取请求的形式提交回 Moby。然后,它们会定期进行审查,如果他们认为您的贡献应当成为服务的一部分,他们将批准您的拉取请求。这对于知道自己写的内容被接受是一种非常谦虚的体验。
首先,您需要了解如何为贡献做准备:对于 Docker(github.com/docker/)和 Moby 项目(github.com/moby/)来说,这几乎涵盖了所有内容。
但是我们该如何开始准备贡献呢?最好的起点是按照官方 Moby 文档中提供的指南,您可以在 github.com/moby/moby/blob/master/CONTRIBUTING.md 找到它。
正如您可能已经猜到的,您并不需要太多来建立开发环境,因为大部分开发是在容器内进行的。例如,除了拥有一个 GitHub 账户之外,Moby 列出了以下三款软件作为最低要求:
-
Git:
git-scm.com/ -
Docker:如果你已经看到这里,应该不需要链接了。
您可以在 github.com/moby/moby/blob/master/docs/contributing/software-required.md 查找关于如何为 Mac 和 Linux 准备 Docker 开发环境的更多细节,关于 Windows 的请访问 github.com/moby/moby/blob/master/docs/contributing/software-req-win.md。
要成为一个成功的开源项目,必须有一些社区指南。我建议阅读这个优秀的快速入门指南,以及更详细的贡献工作流程文档。
提供 Docker 支持
你还可以通过其他方式为 Docker 做出贡献,超越对 Docker 代码或功能的贡献。你可以利用你获得的知识帮助他人在支持渠道中解决问题。社区非常开放,总有人愿意提供帮助。
当我遇到问题时,发现自己在困惑时,我觉得这很有帮助。能得到帮助的同时,也能帮助他人,这是一种良好的互动。这也是一个极好的地方,可以收集供你使用的想法。你可以看到其他人在根据他们的设置提出了哪些问题,这可能激发你想到一些你可以在自己的环境中使用的想法。
你还可以关注 GitHub 上关于服务的 issues。这些可能是功能请求及 Docker 如何实现它们,也可能是服务使用过程中出现的问题。你可以帮助测试其他人遇到的问题,看看是否能够重现问题,或者找到解决他们问题的可能方案。
Docker 拥有一个非常活跃的社区,可以在www.docker.com/docker-community找到;在这里,你不仅能看到最新的社区新闻和活动,还能在他们的 Slack 频道中与 Docker 用户和开发者进行交流。在撰写本书时,已有超过 80 个频道,涵盖了各种主题,比如 Docker for Mac、Docker for Windows、Alpine Linux、Swarm、存储和网络等,任何时刻都有数百名活跃用户。
最后,还有 Docker 论坛,可以在forums.docker.com/找到。如果你想搜索主题、问题或关键字,这里是一个很好的资源。
Docker 社区由行为准则管理,涵盖了其员工和社区整体应如何行动。该准则是开源的,并且采用创作共用署名 3.0 许可证,内容如下:
我们致力于为每个人提供一个无骚扰的体验,我们不容忍任何形式的骚扰行为。我们要求你考虑他人,保持专业和尊重,尊重所有其他参与者的行为。本守则及相关程序也适用于发生在社区活动范围外的不可接受行为,无论是在所有社区场所——线上和线下——还是在所有一对一的交流中,任何可能对社区成员的安全与福祉产生不利影响的行为。Docker, Inc(DockerCon,聚会,用户组)组织的活动或在 Docker, Inc 设施举办的活动的展览者、演讲者、赞助商、工作人员及所有其他参与者都必须遵守这些社区指南和行为守则。
多样性和包容性使 Docker 社区更强大。我们鼓励来自各种背景的参与,并希望明确表明我们的立场。
我们的目标是为每个人维持一个安全、有帮助和友好的 Docker 社区,无论其经验、性别认同与表达、性取向、残障、外貌、体型、种族、民族、年龄、宗教、国籍或其他受适用法律保护的类别如何。
完整的行为守则可以在github.com/docker/code-of-conduct/找到。
其他贡献
还有其他方式可以为 Docker 做出贡献。你可以做一些事情,比如在你的机构中推广该服务并收集兴趣。你可以通过自己组织的通信渠道开始这项沟通,无论是电子邮件分发列表、群体讨论、IT 圆桌会议还是定期安排的会议。
你也可以在组织内安排聚会,以促进成员之间的交流。这些聚会旨在不仅包括你的组织,还包括所在城市或镇的成员,从而实现更广泛的沟通和服务推广。
你可以访问www.docker.com/community/meetup-groups/搜索你所在地区是否已经有聚会。
云原生计算基金会
我们在第十一章中简要讨论了云原生计算基金会,Docker 和 Kubernetes。云原生计算基金会(简称 CNCF)成立的目的是为那些允许你管理容器和微服务架构的项目提供一个中立的、非厂商特定的家园。
其会员包括 Docker、亚马逊 Web 服务、谷歌云、微软 Azure、红帽、甲骨文、VMWare 和 Digital Ocean 等。2020 年 6 月,Linux 基金会报告称 CNCF 有 452 个成员。这些成员不仅贡献项目,还贡献工程时间、代码和资源。
毕业项目
在撰写本书时,已经有十个毕业的项目,其中一些我们在之前的章节中已经讨论过。已讨论的两个项目也是基金会维护的十个项目中最著名的,分别如下:
-
Kubernetes (
kubernetes.io):这是第一个捐赠给基金会的项目。正如我们之前提到的,它最初由 Google 开发,现在基金会成员和开源社区的贡献者已经超过 2300 名。 -
Prometheus (
prometheus.io):该项目由 SoundCloud 捐赠给基金会。正如我们在 第十五章,Docker 工作流 中所看到的,它是一个实时监控和告警系统,依赖强大的时间序列数据库引擎。
还有以下内容:
-
Envoy (
www.envoyproxy.io/):最初在 Lyft 内部创建并被 Apple、Netflix 和 Google 等公司使用,Envoy 是一个高度优化的服务网格,提供负载均衡、追踪以及跨环境的数据库和网络活动的可观察性。 -
CoreDNS (
coredns.io/):这是一个小巧、灵活、可扩展且高度优化的 DNS 服务器,采用 Go 语言编写,并从底层设计上旨在运行在可以支持成千上万个容器的基础设施中。 -
Containerd (
containerd.io/):我们在 第一章,Docker 概述 中简要提到过 Containerd,作为 Docker 正在开发的开源项目之一,我们在 第十二章,发现更多 Kubernetes 选项 中也使用了它。它是一个标准的容器运行时,允许开发者在其平台或应用程序中嵌入一个可以管理 Docker 和 OCI 兼容镜像的运行时。 -
Fluentd (
www.fluentd.org/):该工具允许你从大量来源收集日志数据,并将日志数据路由到多个日志管理、数据库、归档和告警系统中,如 Elastic Search、AWS S3、MySQL、SQL Server、Hadoop、Zabbix 和 DataDog 等。 -
Jaeger (
www.jaegertracing.io/):这是一个完全分布式的追踪系统,最初由 Uber 开发,用于监控其广泛的微服务环境。现在,像 Red Hat 这样的公司也在使用它,拥有现代化的用户界面,并原生支持 OpenTracing 以及多种后端存储引擎。它被设计为与其他 CNCF 项目(如 Kubernetes 和 Prometheus)集成。 -
Vite (
vitess.io/):自 2011 年起,Vite 一直是 YouTube MySQL 数据库基础设施的核心组成部分。它是一个集群系统,通过分片水平扩展 MySQL。 -
Helm (
helm.sh/):为 Kubernetes 构建,Helm 是一个包管理器,允许用户将他们的 Kubernetes 应用程序打包成易于分发的格式,并迅速成为标准。
要毕业,一个项目必须完成以下要求:
-
采用了 CNCF 行为规范,该规范类似于 Docker 发布的规范。完整的行为规范可以在
github.com/cncf/foundation/blob/master/code-of-conduct.md找到。 -
获得了Linux 基金会(LF)核心基础设施倡议(CII)最佳实践徽章,证明该项目正在使用一套已建立的最佳实践进行开发——完整的标准可以在
github.com/coreinfrastructure/best-practices-badge/blob/master/doc/criteria.md找到。 -
至少获得两个组织的贡献者参与该项目。
-
通过
GOVERNANCE.md和OWNERS.md文件在项目的代码库中公开定义了提交者流程和项目治理结构。 -
在
ADOPTERS.md文件或项目网站上的徽标中公开列出项目的采用者。 -
获得技术监督委员会(TOC)的超级多数投票。你可以在
github.com/cncf/toc了解更多关于该委员会的信息。
还有一种项目状态是目前大多数项目所处的状态。
孵化中的项目
处于孵化阶段的项目最终应该获得毕业状态。以下项目都已经完成以下要求:
-
演示该项目至少被三个独立的最终用户使用(不是项目发起人)。
-
获得了足够数量的贡献者,包括内部和外部的。
-
展示了增长和良好的成熟度。
TOC 积极参与与项目合作,确保活动水平足够满足前述标准,因为指标可能因项目而异。
当前的项目列表如下:
-
OpenTracing (
opentracing.io/):这是目前在 CNCF 框架下的两个追踪项目之一,另一个是 Jaeger。它不是一个应用程序,而是作为一组库和 API 下载并使用,让你能够将行为追踪和监控集成到基于微服务的应用程序中。 -
gRPC (
grpc.io): 与 Kubernetes 类似,gRPC 是由 Google 捐赠给 CNCF 的。它是一个开源、可扩展且优化性能的 RPC 框架,已经在 Netflix、Cisco 和 Juniper Networks 等公司投入生产使用。 -
CNI (
github.com/containernetworking): CNI(容器网络接口)并不是一个你可以直接下载使用的工具。它是一个网络接口标准,旨在嵌入到容器运行时中,例如 Kubernetes 和 Mesos。拥有统一的接口和 API 集合,可以通过第三方插件和扩展更一致地支持这些运行时中的高级网络功能。 -
公证人 (
github.com/theupdateframework/notary): 该项目最初由 Docker 编写,是 TUF 的实现,接下来我们将介绍 TUF。它的设计旨在为开发者提供一个加密工具,使其能够签署容器镜像,并提供一个机制来验证容器镜像和内容的来源。 -
TUF (
theupdateframework.github.io): 更新框架(TUF)是一个标准,通过使用加密密钥,允许软件产品在安装和更新过程中自我保护。它由纽约大学工程学院开发。 -
NATS (https://nats.io): 这是一个为运行微服务或支持物联网设备的架构环境设计的消息传递系统。
-
Linkerd (
linkerd.io): 由 Twitter 构建,Linkerd 是一个服务网格,旨在扩展并处理每秒数万个安全请求。 -
Rook (
rook.io): 该项目专注于提供一个编排层,用于在 Kubernetes 上管理 Ceph、Red Hat 的分布式存储系统等。 -
Harbor (
goharbor.io/): 这是一个开源镜像注册中心,专注于安全性和访问控制,内置镜像扫描、RBAC 控制和镜像签名。最初由 VMWare 开发。 -
etcd (
etcd.io/): 这是一个简单的分布式键值存储系统,提供 REST API,旨在低延迟操作,并使用 raft 一致性算法。raft 一致性算法 -
Open Policy Agent (
www.openpolicyagent.org/): 这是一个统一的工具集和框架,用于管理你的云原生堆栈中的策略。 -
cri-o (
cri-o.io/): 这是为 Kubernetes 构建的轻量级容器运行时;它允许你运行任何符合 OCI 标准的容器。 -
Cloudevents (
cloudevents.io/): 这是一个用于以通用方式描述事件数据的规范。它还提供了 Go、JavaScript、Java、C#、Ruby 和 Python 的 SDK,使你能够轻松地将该规范引入到自己的项目中。 -
Falco (
falco.org/): Falco 提供一个云原生的运行时安全引擎,可以在执行过程中解析来自内核的 Linux 系统调用。 -
Dragonfly (
d7y.io/en-us/): 这是一个基于 P2P 的开源镜像和文件分发系统。
我们在本书的各个章节中使用了一些这些项目,随着你开始解决诸如容器路由和在环境中监控应用程序等问题,其他项目也肯定会引起你的兴趣。
CNCF 景观
CNCF 提供了一个交互式地图,展示了他们及其成员管理的所有项目,可以在landscape.cncf.io/找到。一个关键的要点如下:
你正在查看 1,403 张卡片,总共有 2,263,137 颗星,市值为 16.98 万亿美元,资金为 660.5 亿美元。
虽然我相信你会同意这些都是一些非常令人印象深刻的数字,但这有什么意义呢?多亏了 CNCF 的工作,我们有了像 Kubernetes 这样的项目,它们为跨多个云基础设施提供商以及本地和裸金属服务提供了一个标准化的工具集、API 和方法——为你创建和部署自己的高可用、可扩展、性能强大的容器和微服务应用程序提供了构建模块。
摘要
我希望本章能给你提供一些关于容器旅程中下一步行动的思路。我发现,虽然使用这些服务很简单,但通过成为大型、友好且热情的开发者和其他用户社区的一部分,你能从中获得更多,这些社区就像你一样,围绕着各种软件和项目蓬勃发展。
这种社区感和协作精神通过云原生计算基金会的成立得到了进一步加强。这个基金会汇集了许多大型企业,这些企业直到几年前还不会考虑与那些曾被视为竞争对手的大型企业在大规模项目中公开合作。
评估
第一章,Docker 概述
这是本章中提出的问题的一些示例答案:
-
Docker Hub: https://hub.docker.com/
-
$ docker image pull nginx -
Moby 项目
-
Mirantis 公司
-
$ docker container help
第二章,构建容器镜像
这是本章中提出的问题的一些示例答案:
-
错误;它用于向镜像添加元数据。
-
你可以将
CMD追加到ENTRYPOINT,但不能反过来。 -
正确。
-
快照化一个故障容器,以便你能在 Docker 主机之外查看它。
-
EXPOSE指令暴露容器上的端口,但不会映射主机上的端口。
第三章,存储与分发镜像
这里是本章提出的问题的示例答案:
-
正确。
-
这允许你在上游 Docker 镜像更新时自动更新你的 Docker 镜像。
-
是的,它们是(如本章中的示例所示)。
-
正确;如果你使用命令行登录,你已登录到 Docker for Mac 和 Docker for Windows。
-
你将通过名称删除它们,而不是使用镜像 ID。
-
端口
5000。
第四章,管理容器
这里是本章提出的问题的示例答案:
-
-a或--all。 -
错误;应当是反过来的。
-
当你按下 Ctrl + C 时,你会回到终端;然而,保持容器活动的进程依然在运行,因为我们已经从进程中分离,而不是终止它。
-
错误;它会在指定的容器内生成一个新的进程。
-
你可以使用
--network-alias [别名名称]标志。 -
运行
docker volume inspect [卷名称]会给你该卷的信息。
第五章,Docker Compose
这里是本章提出的问题的示例答案:
-
YAML,或称为 YAML 不是标记语言。
-
restart标志与--restart标志相同。 -
错误;你可以使用 Docker Compose 在运行时构建镜像。
-
默认情况下,Docker Compose 使用存储 Docker Compose 文件的文件夹名称。
-
你使用
-d标志来启动容器的分离模式。 -
使用
docker-compose config命令可以暴露出 Docker Compose 文件中的语法错误。 -
Docker 应用将你的 Docker Compose 文件打包成一个小的 Docker 镜像,可以通过 Docker Hub 或其他注册表分享,Docker 应用的命令行工具可以根据镜像中包含的数据渲染出有效的 Docker Compose 文件。
第六章,Docker Machine,Vagrant 和 Multipass
这里是本章提出的问题的示例答案:
-
使用了
--driver标志。 -
错误;它会给你命令。相反,你需要运行
eval $(docker-machine env my-host)。 -
错误;需要先安装 Docker。
-
Packer。
-
Docker 守护进程配置不再被认为是最佳实践。
第七章,从 Linux 迁移到 Windows 容器
这里是本章提出的问题的示例答案:
-
你可以使用 Hyper-V 隔离来在最小化的虚拟化管理程序中运行你的容器。
-
命令是
docker inspect -f “{{ .NetworkSettings.Networks.nat.IPAddress }}” [CONTAINER NAME]。 -
错误;管理 Windows 容器时,所需运行的 Docker 命令没有任何区别。
第八章,使用 Docker Swarm 进行集群管理
以下是本章中提出问题的一些示例答案:
-
错误;独立的 Docker Swarm 已不再支持,也不被认为是最佳实践。
-
你需要 Docker Swarm 管理节点的 IP 地址,以及用于认证工作节点的令牌。
-
你会使用
docker node ls。 -
你会添加
--pretty标志。 -
你会使用
docker node promote [node name]。 -
你会运行
docker service scale cluster=[x] [service name],其中[x]是你希望扩展的容器数量。
第九章,Portainer — Docker 的图形用户界面
以下是本章中提出问题的一些示例答案:
-
路径是
/var/run/docker.sock。 -
端口是
9000。 -
错误;应用程序有自己的定义。当运行 Docker Swarm 时,你可以使用 Docker Compose 启动堆栈。
-
正确;所有的统计数据都是实时显示的。
第十章,在公共云中运行 Docker
以下是本章中提出问题的一些示例答案:
-
一个 Azure Web 应用
-
一个 EC2 实例
-
错误
-
Knative
-
亚马逊 ECS
第十一章,Docker 和 Kubernetes
以下是本章中提出问题的一些示例答案:
-
错误;你始终可以查看 Kubernetes 使用的镜像。
-
docker和kube-system命名空间。 -
你会使用
kubectl describe --namespace [NAMESPACE] [POD NAME]。 -
你会运行
kubectl create -f [FILENAME OR URL]。 -
端口
8001。 -
它曾被称为 Borg。
第十二章,探索其他 Kubernetes 选项
以下是本章中提出问题的一些示例答案:
-
错误
-
MiniKube、Kind 和 K3d
-
MicroK8s 和 K3s
第十三章,在公共云中运行 Kubernetes
以下是本章中提出问题的一些示例答案:
-
kubectl create namespace sock-shop -
kubectl -n sock-shop describe services front-end-lb -
eksctl
第十四章,Docker 安全
以下是本章中提出问题的一些示例答案:
-
你会添加
--read-only标志;或者,如果你想将一个卷设置为只读,可以添加:ro。 -
在理想的世界里,每个容器只运行一个进程。
-
通过运行 Docker Bench Security 应用程序。
-
Docker 的套接字文件,可以在
/var/run/docker.sock找到;另外,如果你的主机系统运行的是 Systemd,文件路径为/usr/lib/systemd。 -
错误;Quay 扫描的是公共和私有镜像。
第十五章,Docker 工作流
这里是本章问题的一些示范答案:
-
Nginx(web)容器提供网站服务;WordPress(WordPress)容器运行传递给 Nginx 容器的代码。
-
wp容器运行一个单一的进程,该进程一旦运行就会存在。 -
cAdvisor 仅保留 5 分钟的指标数据。
-
你可以使用
docker-compose down --volumes --rmi all。




















浙公网安备 33010602011771号