Kubernetes从入门到精通系列——Docker与容器基础知识

23 阅读30分钟

容器已经成为一种非常流行且有影响力的技术,它带来了与传统应用程序显著的变化。从科技公司到大企业再到终端用户,每个人都广泛采用容器来处理日常任务。值得注意的是,传统的安装现成商业应用程序的方法正逐渐转变为完全容器化的方式。考虑到这种技术转变的巨大影响,信息技术领域的从业者有必要掌握容器的相关知识并理解其概念。

本章将概述容器旨在解决的问题。我们将首先强调容器的重要性,然后介绍Docker——这个在容器化兴起中起到关键作用的运行时,并讨论它与Kubernetes的关系。

本章旨在帮助你理解如何在Docker中运行容器。你可能听过一个常见问题:“Docker与Kubernetes的关系是什么?”其实在当今的技术环境中,Docker与Kubernetes并无直接关系——你不需要Docker来运行Kubernetes,也不需要它来创建容器。我们在本章讨论Docker是为了让你掌握在本地运行容器以及在将镜像部署到Kubernetes集群之前进行测试的技能。

到本章末,你将清楚地了解如何安装Docker以及如何有效使用常用的Docker命令行界面(CLI)命令。

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

  • 理解容器化的需求
  • 理解Kubernetes为何弃用Docker
  • 理解Docker
  • 安装Docker
  • 使用Docker CLI

技术要求

本章有以下技术要求:

  • 一台运行Docker的Ubuntu 22.04+服务器,至少具有4 GB的内存,建议8 GB内存。
  • 章节1文件夹中的脚本,您可以通过以下链接访问该资源库:github.com/PacktPublis…

理解容器化的需求

你可能在办公室或学校遇到过这样的对话:

开发人员:“这是新的应用程序。它已经经过了数周的测试,你是第一个拿到新版本的人。”
……过了一会儿……
用户:“它不工作了。当我点击提交按钮时,出现了关于缺少依赖项的错误。”
开发人员:“这很奇怪,在我的电脑上运行得很好。”

在部署应用程序时,开发人员遇到这样的情况会感到非常沮丧。这种问题通常是由于最终包中缺少开发人员自己机器上所拥有的某个库导致的。有人可能认为一个简单的解决方案是将所有库都包含在发布版本中,但如果这个发布版本包含了某个库的较新版本,而另一个应用程序可能仍然依赖于旧版本,这该怎么办?

开发人员必须仔细考虑新版本发布时可能与用户工作站上现有软件产生的冲突。这是一项需要小心平衡的任务,通常需要更大的部署团队在各种系统配置上彻底测试应用程序。这种情况可能会为开发人员带来额外的工作,或者在极端情况下,导致该应用程序与现有应用程序完全不兼容。

多年来,已经有几种尝试简化应用程序交付的方案。其中一个解决方案是VMware的ThinApp,旨在虚拟化一个应用程序(不要与整个操作系统的虚拟化混淆)。它允许你将应用程序及其依赖项打包成一个单一的可执行包。通过这种方式,所有应用程序的依赖项都被包含在包中,消除了与其他应用程序依赖项的冲突。这不仅确保了应用程序的隔离性,还增强了安全性并减少了操作系统迁移的复杂性。

你可能之前没有听说过像应用程序打包或“随身携带的应用程序”这样的术语,但这似乎是解决著名的“在我的机器上能运行”问题的一个很好的解决方案。然而,这些解决方案并没有像预期的那样广泛应用是有原因的。首先,大多数这类解决方案是付费产品,需投入大量资金。其次,它们需要一个“干净的PC”,也就是说,为每个你想要虚拟化的应用程序,你需要从一个全新的系统开始。你创建的包捕捉了基础安装和之后所做更改之间的差异。然后这些差异被打包成一个可分发的文件,可以在任何工作站上执行。

我们提到应用程序虚拟化是为了强调,诸如“在我的机器上能运行”这类问题多年来有过不同的解决方案。ThinApp等产品只是解决该问题的一次尝试。其他尝试还包括使用Citrix在服务器上运行应用程序、远程桌面、Linux容器、chroot环境,甚至虚拟机。

理解Kubernetes为何移除Docker

Kubernetes在1.24版本中移除了对Docker作为容器运行时的支持。虽然Docker作为运行时引擎的选项被移除,但你仍然可以使用Docker创建新的容器,并且它们可以在支持Open Container Initiative(OCI)规范的任何运行时上运行。OCI是一套关于容器及其运行时的标准。这些标准确保容器具有可移植性,无论使用哪种容器平台或运行时来执行它们。

当你使用Docker创建容器时,你创建的容器完全符合OCI规范,因此它仍然可以在任何运行Kubernetes兼容容器运行时的Kubernetes集群上运行。

为了全面解释这一变化的影响以及支持的替代方案,我们首先需要了解什么是容器运行时。容器运行时的高层次定义是:它是运行和管理容器的软件层。像Kubernetes集群中的许多组件一样,运行时并不包含在Kubernetes本身中——它是一个可插拔模块,需要由供应商或你自己提供,以创建一个可运行的集群。

有许多技术原因促使了移除Docker的决定,但从总体上看,主要的顾虑如下:

  • Docker的运行时中包含多个支持其自身远程API和用户体验(UX)的组件。而Kubernetes只需要其中一个组件,即dockerd,这是管理容器的运行时进程。Docker的其他组件对于在Kubernetes集群中使用Docker并无贡献。这些额外的组件使得二进制文件臃肿,并可能引发额外的错误、安全或性能问题。
  • Docker不符合容器运行时接口(CRI)标准,该标准旨在简化容器运行时在Kubernetes中的集成。由于Docker不符合该标准,Kubernetes团队不得不额外做工作来支持Docker。

在本地容器测试和开发中,你仍然可以在工作站或服务器上使用Docker。根据前述内容,如果你在Docker上构建一个容器,并且该容器可以在Docker运行时系统上成功运行,它同样可以在不使用Docker作为运行时的Kubernetes集群上运行。

对于大多数Kubernetes新集群用户而言,移除Docker几乎不会有任何影响。容器仍然可以通过标准方式运行,就像使用Docker作为容器运行时一样。如果你管理的是一个集群,你可能需要在排查Kubernetes节点问题时学习新的命令——你将无法在节点上使用Docker命令查看正在运行的容器、清理卷等操作。

Kubernetes支持许多替代Docker的运行时。两个最常用的运行时如下:

  • containerd
  • CRI-O

虽然这是两个常用的运行时,但还有许多其他兼容的运行时可用。你可以随时在Kubernetes GitHub页面查看最新支持的运行时:github.com/kubernetes/…

关于弃用和移除Docker的影响的更多详细信息,请参阅Kubernetes.io网站上的文章《Don’t Panic: Kubernetes and Docker》:kubernetes.io/blog/2020/1…

介绍Docker

整个行业和终端用户一直在寻求一种既方便又经济的解决方案,这就是Docker容器的诞生。虽然容器技术在不同阶段以不同的方式被利用,但Docker通过为日常用户和开发者提供运行时和工具,带来了变革。

Docker为大众带来了一个抽象层。它使用简单,不需要为每个应用程序在创建包之前使用干净的PC,从而提供了依赖问题的解决方案,但最具吸引力的是,它是免费的。Docker成为了许多GitHub项目的标准,团队经常创建一个Docker容器并分发Docker镜像或Dockerfile给团队成员,提供了一个标准的测试或开发环境。终端用户的广泛采用最终将Docker带入了企业级应用,并使其成为今天的行业标准。

在本书的范围内,我们将重点介绍你在尝试使用本地Kubernetes环境时需要了解的内容。Docker有着悠久且有趣的发展历史,它逐渐演变成了我们今天使用的标准容器镜像格式。我们鼓励你去了解这家公司,以及他们如何引领了我们今天所熟知的容器世界。

虽然我们的重点不是彻底教会你Docker的所有知识,但我们认为对于那些刚接触Docker的读者来说,快速了解一些通用的容器概念将非常有帮助。

如果你有一定的Docker经验,并且了解诸如“短暂的(ephemeral)”和“无状态的(stateless)”等术语,你可以直接跳到“安装Docker”部分。

Docker 与 Moby 的对比

在开发Docker运行时时,它是一个单一的代码库。这个单一的代码库包含了Docker提供的所有功能,无论你是否使用过它们。这导致了效率低下,并开始阻碍Docker和容器技术的进一步发展。

下表展示了Docker和Moby项目之间的区别:

特性DockerMoby
开发Docker是主要贡献者,部分社区提供支持开源软件,社区大量开发和支持
项目范围包含构建和运行容器的所有组件的完整平台模块化平台,用于构建基于容器的组件和解决方案
所有权Docker, Inc.提供的品牌产品开源项目,用于构建各种容器解决方案
配置提供完整的默认配置,便于用户快速使用提供更多的定制选项,允许用户根据具体需求进行调整
商业支持提供全面的支持,包括企业级支持作为开源软件提供,不提供直接的Moby项目支持

表1.1: Docker与Moby的特性对比

总结——Moby是Docker发起的一个项目,但它不是完整的Docker运行时。Docker运行时使用了来自Moby的组件来构建Docker运行时,其中包括Moby的开源组件和Docker自己的开源组件。

现在,让我们更深入地了解Docker,以及如何使用它来创建和管理容器。

理解Docker

本书假设你已经具备了Docker和容器概念的基础知识。然而,我们知道并非所有读者都拥有Docker或容器的使用经验。因此,我们提供了这个速成课程,旨在向你介绍容器概念,并引导你如何使用Docker。

如果你是容器新手,我们建议你阅读Docker官网上的文档以获取更多信息:docs.docker.com/

容器是短暂的

首先要理解的是,容器是短暂的。

“短暂”意味着存在时间很短。容器可以被有意终止,或自动重启,而无需用户参与且不产生任何后果。为了更好地理解这一概念,我们来看一个例子——想象有人在运行中的容器中向一个web服务器交互式地添加了文件。上传的文件是临时的,因为它们最初并不属于基础镜像的一部分。

这意味着一旦容器被构建并运行,任何对容器所做的更改都不会在容器从Docker主机上被移除或销毁后保留。来看一个完整的示例:

  • 你在主机上启动了一个使用NGINX运行的web服务器容器,但没有任何基础的HTML页面。
  • 通过Docker命令,你执行了一个复制命令,将一些web文件复制到容器的文件系统中。
  • 为了测试复制是否成功,你访问了该网站并确认其正确提供了网页内容。
  • 你对结果感到满意,于是停止了容器并将其从主机上移除。当天晚些时候,你想向同事展示该网站,并重新启动了你的NGINX容器。当你再次访问该网站时,却收到404错误(页面未找到)。

你之前上传的文件在停止并移除容器后发生了什么?

原因在于所有容器都是短暂的。每次容器启动时,只包含基础容器镜像中的内容。你在容器内所做的任何更改都是短暂的。

如果你需要向现有镜像中添加永久性文件,你需要重新构建包含这些文件的镜像,或者正如我们将在本章后面的“持久化数据”部分中解释的那样,你可以在容器中挂载一个Docker卷。

此时,主要需要理解的概念是:容器是短暂的

但等一下!你可能会想:“如果容器是短暂的,那我怎么能向服务器添加网页呢?”短暂性意味着更改不会被保存,但并不阻止你对正在运行的容器进行更改。

对正在运行的容器所做的任何更改都会写入一个临时层,称为容器层,它是本地主机文件系统中的一个目录。Docker使用一个存储驱动程序来处理使用容器层的请求。该存储驱动程序负责管理和存储Docker主机上的镜像和容器。它控制存储和管理的机制和过程。

这个位置会存储容器文件系统中的所有更改,因此当你向容器中添加HTML页面时,这些页面会存储在本地主机上。容器层与运行中的镜像的容器ID绑定,并会一直保留在主机系统上,直到通过CLI命令或运行Docker清理任务(如Docker prune)将容器从Docker中移除(见下页的图1.1)。

考虑到容器是临时的并且是只读的,你可能会想:如何在容器内修改数据呢?Docker通过利用镜像分层技术解决了这个问题,它涉及创建相互关联的层,这些层共同作为一个文件系统来运行。通过这种方式,即使底层镜像保持不变,仍然可以对容器中的数据进行修改。

Docker 镜像

Docker镜像由多个镜像层组成,每一层都带有一个JavaScript对象表示法(JSON)文件,用于存储与该层相关的元数据。当容器镜像启动时,这些层会被组合起来,形成用户可以交互的应用程序。

你可以在Docker的GitHub页面上了解更多关于镜像内容的信息:github.com/moby/moby/b…

镜像层

正如我们在上一节中提到的,运行中的容器使用一个位于基础镜像层“之上”的容器层,如下图所示:

image.png

镜像层是只读的,无法进行写操作,但临时的容器层是可写的。你添加到容器中的任何数据都会存储在这个层中,并且只要容器在运行,它就会被保留。

为了高效处理多个层,Docker实现了写时复制(copy-on-write)机制,这意味着如果某个文件已经存在,它将不会被重新创建。然而,如果当前镜像中不存在所需的文件,它将会被写入。在容器世界中,如果某个文件存在于较低的层次,较高的层次就不需要再包含该文件。例如,如果层1中有一个名为/opt/nginx/index.xhtml的文件,那么层2就不需要在其层中包含相同的文件。

这解释了系统如何处理存在或不存在的文件,但如果文件已被修改呢?有时你可能需要替换较低层中的文件,比如在构建镜像时,或者作为对正在运行的容器问题的临时修复。写时复制系统知道如何处理这些问题。因为镜像是从上到下读取的,容器只使用最高层的文件。如果系统在层1中有一个/opt/nginx/index.xhtml文件,而你对该文件进行了修改并保存,运行中的容器会将新文件存储在容器层中。由于容器层是最顶层,新的index.xhtml副本将始终优先于镜像层中的旧版本被读取。

持久化数据

如果仅限于使用短暂性的容器,那么Docker的使用场景将会受到严重限制。你可能会遇到需要持久存储的情况,或者即使容器停止后数据也必须保留的需求。

请记住,当你将数据存储在容器镜像层时,基础镜像不会改变。当容器从主机上移除时,容器层也会被移除。如果使用相同的镜像启动一个新的容器,将创建一个新的容器镜像层。虽然容器本身是短暂的,但你可以通过引入Docker卷来实现数据持久化。通过使用Docker卷,数据可以存储在容器外部,从而在容器生命周期结束后仍然保留数据。

访问运行在容器中的服务

与物理机或虚拟机不同,容器不会直接连接到网络。当容器需要发送或接收流量时,它通过Docker主机系统使用桥接的网络地址转换(NAT)连接。这意味着当你运行一个容器并希望接收传入的流量请求时,需要为希望接收流量的每个容器暴露端口。在基于Linux的系统上,iptables有相应的规则将流量转发到Docker守护进程,该进程将为每个容器分配的端口提供服务。你无需担心如何创建iptables规则,因为Docker会在启动容器时根据提供的端口信息为你处理这些规则。如果你是Linux新手,iptables可能是一个新的概念。

从高层次来看,iptables用于管理网络流量并确保集群内部的网络安全。它控制集群中组件之间的网络连接流向,决定哪些连接被允许,哪些被阻止。

这就结束了容器基础知识和Docker概念的介绍。在下一节中,我们将指导你在主机上安装Docker的过程。

安装 Docker

本书中的实践练习要求你有一个正在运行的 Docker 主机。为了安装 Docker,我们在本书的 GitHub 仓库中提供了一个脚本,位于第1章的目录下,名为 install-docker.sh

如今,你几乎可以在任何硬件平台上安装 Docker。每个平台上的 Docker 都具有相同的功能和外观,这使得开发跨平台应用程序变得更容易。通过在不同平台上保持相同的功能和命令,开发人员无需学习不同的容器运行时即可运行镜像。

下表列出了可用的 Docker 平台。正如你所看到的,有多个操作系统和多种架构的安装版本:

桌面平台x86_64/amd64arm64 (Apple Silicon)
Docker Desktop (Linux)Docker Desktop (macOS)Docker Desktop (Windows)
服务器平台x86_64/amd64arm64/aarch64arm (32-bit)ppc64les390x
CentOSDebianFedoraRaspberry Pi OSRHEL (s390)SLES

表1.2:可用的Docker平台

使用一种架构创建的镜像无法在另一种架构上运行。这意味着你无法基于x86硬件创建一个镜像,然后期望该镜像在运行ARM处理器的Raspberry Pi上运行。同样需要注意的是,虽然你可以在Windows机器上运行Linux容器,但不能在Linux机器上运行Windows容器。

虽然镜像默认情况下不是跨架构兼容的,但现在有一些新工具可以创建所谓的多平台镜像。多平台镜像是可以在单个容器中跨不同架构或处理器使用的镜像,而不需要为每种架构创建多个镜像,比如x86上的NGINX一个镜像、ARM上的一个镜像,以及PowerPC上的另一个镜像。这将帮助你简化容器化应用程序的管理和部署。由于多平台镜像包含了不同架构的多个版本,因此在部署镜像时需要指定架构。幸运的是,容器运行时会帮助自动选择镜像清单中的正确架构。

使用多平台镜像为容器在云平台、边缘部署和混合基础架构中的可移植性、灵活性和可扩展性提供了支持。随着ARM服务器在行业中的使用越来越广泛,以及许多人在学习Kubernetes时大量使用Raspberry Pi,跨平台镜像将帮助加快和简化容器的使用。

例如,2020年,Apple发布了M1芯片,结束了Apple使用Intel处理器的时代,转而采用ARM处理器。我们不会详细讨论它们之间的区别,只需知道它们不同,并且这对容器开发者和用户来说带来了重要的挑战。Docker提供了Docker Desktop,这是一个用于在macOS上运行容器的工具,让你可以使用与在Linux、Windows或x86 macOS上安装Docker时相同的工作流程。当拉取或构建镜像时,Docker会尝试匹配底层主机的架构。在ARM系统上,如果你尝试拉取没有ARM版本的镜像,Docker会因为架构不兼容而抛出错误。如果你尝试构建一个镜像,它将构建一个ARM版本的镜像,而该镜像无法在x86机器上运行。

创建多平台镜像可能比较复杂。如果你想了解更多关于创建多平台镜像的详细信息,请访问Docker网站上的多平台镜像页面

安装Docker的步骤因平台而异。幸运的是,Docker已经在他们的网站上记录了许多平台的安装过程:docs.docker.com/install/

在本章中,我们将在Ubuntu 22.04系统上安装Docker。如果你没有可供安装的Ubuntu机器,你仍然可以阅读安装步骤,因为每个步骤都会详细解释,且不需要你有一个正在运行的系统即可理解这个过程。如果你使用的是其他Linux发行版,你可以参考Docker网站上的安装步骤:docs.docker.com/。其中提供了CentOS、Debian、Fedora和Ubuntu的步骤,也有适用于其他Linux发行版的通用步骤。

准备安装 Docker

现在我们已经介绍了 Docker,接下来的步骤是选择安装方法。Docker 的安装不仅在不同的 Linux 发行版之间有所不同,甚至在同一发行版的不同版本之间也有变化。我们的脚本基于 Ubuntu 22.04 服务器,因此它可能不适用于其他版本的 Ubuntu。你可以通过两种方法之一安装 Docker:

  1. 将 Docker 的仓库添加到主机系统
  2. 使用 Docker 脚本进行安装

第一个选项被认为是最佳选择,因为它便于安装和更新 Docker 引擎。第二个选项是为测试/开发环境设计的,不建议在生产环境中使用。

由于首选方法是将 Docker 仓库添加到我们的主机,因此我们将使用该选项。

在 Ubuntu 上安装 Docker

现在我们已经添加了所需的仓库,下一步是安装 Docker。

我们在 Git 仓库的第1章目录中提供了一个名为 install-docker.sh 的脚本。执行该脚本后,它将自动安装运行 Docker 所需的所有必要二进制文件。

简要总结该脚本,它首先修改了 /etc/needrestart/needrestart.conf 文件中的一个特定值。在 Ubuntu 22.04 中,守护进程的重启方式发生了变化,用户可能需要手动选择要重启的系统守护进程。为了简化本书中描述的练习,我们将 needsrestart.conf 文件中的重启值更改为“自动”,而不是每次提示更改的服务。

接下来,我们安装一些实用工具,如 vimca-certificatescurlGnuPG。前三个工具比较常见,但最后一个 GnuPG 可能对一些读者较新,需要一些解释。GnuPG(GNU Privacy Guard 的缩写)为 Ubuntu 提供了一系列加密功能,如加密、解密、数字签名和密钥管理。

在我们的 Docker 部署中,我们需要添加 Docker 的 GPG 公钥,这是一个加密密钥对,用于确保通信安全并维护数据完整性。GPG 密钥使用不对称加密,涉及使用两把不同但相关的密钥,称为公钥和私钥。这些密钥是成对生成的,但它们提供不同的功能。私钥(保持机密)用于生成下载文件的数字签名。公钥是公开可用的,用于验证由私钥生成的数字签名。

接下来,我们需要将 Docker 的仓库添加到我们的本地仓库列表中。当我们将仓库添加到列表中时,我们需要包含 Docker 证书。该脚本从 Docker 网站下载 docker.gpg 证书,并将其存储在本地服务器的 /etc/apt/keyings/docker.gpg 中。当我们将仓库添加到仓库列表中时,通过 /etc/apt/sources.list.d/docker.list 文件中的 signed-by 选项添加密钥。完整的命令如下所示:

deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu jammy stable

通过将 Docker 仓库包含在我们的本地 apt 仓库列表中,我们可以轻松地安装 Docker 的二进制文件。这个过程涉及使用一个简单的 apt-get install 命令,它将安装 Docker 的五个必要二进制文件:docker-cedocker-ce-clicontainerd.iodocker-buildx-plugindocker-compose-plugin。如前所述,所有这些文件都使用 Docker 的 GPG 密钥进行了签名。由于我们服务器上包含了 Docker 的密钥,我们可以确信这些文件是安全的,并且来自可靠的来源。

一旦成功安装 Docker,下一步是启用并配置 Docker 守护进程,使其在系统启动时自动启动,这可以使用 systemctl 命令实现。此过程遵循 Linux 服务器上大多数系统守护进程的标准流程。

我们没有逐行详细解释每个脚本的代码,而是在脚本中包含了注释,以帮助你理解每个命令和步骤的执行方式。在有助于理解某些主题的地方,我们将在章节中包含一些代码段供参考。

安装 Docker 后,我们将进行一些配置。首先,在现实中你很少以 root 身份执行命令,因此我们需要授予你的用户使用 Docker 的权限。

授予 Docker 权限

在默认安装中,Docker 需要 root 访问权限,因此你需要以 root 身份运行所有 Docker 命令。为了避免在每个 Docker 命令前都使用 sudo,你可以将你的用户账户添加到服务器上的一个新组中,使其可以访问 Docker 而无需为每个命令使用 sudo

如果你以普通用户身份登录并尝试运行 Docker 命令,将会收到如下错误:

Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.40/images/json: dial unix /var/run/docker.sock: connect: permission denied

要允许你的用户(或你可能希望添加的任何其他用户)执行 Docker 命令,需要将这些用户添加到安装 Docker 时创建的名为 docker 的新组中。你可以使用以下命令将当前登录的用户添加到该组:

sudo usermod -aG docker $USER

为了使新的组成员生效,你可以注销并重新登录到 Docker 主机,或者使用 newgrp 命令激活组更改:

newgrp docker

现在,我们可以通过运行标准的 hello-world 镜像来测试 Docker 是否正常工作(注意,运行 Docker 命令时不再需要 sudo):

docker run hello-world

你应该看到如下输出,验证你的用户已经有了 Docker 的访问权限:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:37a0b92b08d4919615c3ee023f7ddb068d12b8387475d64c622ac30f45c29c51
Status: Downloaded newer image for hello-world:latest
Hello from Docker!

这个消息表明你的安装工作正常——恭喜你!

为生成此消息,Docker 进行了以下步骤:

  1. Docker 客户端联系了 Docker 守护进程。
  2. Docker 守护进程从 Docker Hub(amd64)拉取了 hello-world 镜像。
  3. Docker 守护进程从该镜像创建了一个新容器,并运行生成你当前看到的输出的可执行文件。
  4. Docker 守护进程将该输出流传送到 Docker 客户端,客户端将其发送到你的终端。

如果你想尝试更复杂的操作,你可以通过以下命令运行一个 Ubuntu 容器:

docker run -it ubuntu bash

有关更多示例和想法,请访问 docs.docker.com/get-started…

现在我们已经授予了 Docker 权限,我们可以开始学习如何使用 Docker CLI 来解锁最常用的 Docker 命令。

使用 Docker CLI

当你运行 hello-world 容器以测试安装时,你已经使用了 Docker CLI。Docker 命令是你与 Docker 守护进程交互的方式。通过这个单一的可执行文件,你可以执行以下操作以及更多内容:

  • 启动和停止容器
  • 拉取和推送镜像
  • 在活动容器中运行 shell
  • 查看容器日志
  • 创建 Docker 卷
  • 创建 Docker 网络
  • 清理旧的镜像和卷

本章不会包含每个 Docker 命令的详尽解释,而是会介绍一些与 Docker 守护进程和容器交互时常用的命令。

你可以将 Docker 命令分为两类:常规 Docker 命令和 Docker 管理命令。标准的 Docker 命令允许你管理容器,而管理命令则允许你管理 Docker 选项,例如管理卷和网络。

docker help

忘记命令的语法或选项是很常见的情况,Docker 也考虑到了这一点。如果你遇到无法回忆起命令的情况,可以随时依赖 docker help 命令。它将帮助你了解命令的功能以及如何使用它。

docker run

要运行一个容器,可以使用带有镜像名称的 docker run 命令。但是,在执行 docker run 命令之前,你需要了解启动容器时可以提供的选项。

在其最简单的形式下,你可以使用以下示例命令来运行一个 NGINX web 服务器:docker run bitnami/nginx:latest。这将启动一个运行 NGINX 的容器,它将在前台运行,显示容器中运行的应用程序日志。按下 Ctrl + C 会停止正在运行的容器并终止 NGINX 服务器:

nginx 22:52:27.42
nginx 22:52:27.42 Welcome to the Bitnami nginx container
nginx 22:52:27.43 Subscribe to project updates by watching https://github.com/bitnami/bitnami-docker-nginx
nginx 22:52:27.43 Submit issues and feature requests at https://github.com/bitnami/bitnami-docker-nginx/issues
nginx 22:52:27.44
nginx 22:52:27.44 INFO  ==> ** Starting NGINX setup **
nginx 22:52:27.49 INFO  ==> Validating settings in NGINX_* env vars
nginx 22:52:27.50 INFO  ==> Initializing NGINX
nginx 22:52:27.53 INFO  ==> ** NGINX setup finished! **
nginx 22:52:27.57 INFO  ==> ** Starting NGINX **

正如你所看到的,当你使用 Ctrl + C 停止容器时,NGINX 也停止了。在大多数情况下,你希望容器启动并持续运行而不是在前台运行,这样系统可以继续执行其他任务,而容器也能继续运行。要将容器作为后台进程运行,你需要在 Docker 命令中添加 -d--detach 选项,这会让你的容器在分离模式下运行。现在,当你运行一个分离的容器时,你只会看到容器 ID,而不会看到交互式或附加的屏幕:

[root@localhost ~]# docker run -d bitnami/nginx:latest
13bdde13d0027e366a81d9a19a56c736c28feb6d8354b363ee738d2399023f80
[root@localhost ~]#

默认情况下,容器启动时会被分配一个随机名称。在我们之前的分离模式示例中,如果我们列出正在运行的容器,会看到容器被分配了名称 silly_keldysh,如下输出所示:

CONTAINER ID      IMAGE                      NAMES
13bdde13d002      bitnami/nginx:l

如果你不给容器分配名称,当你在单个主机上运行多个容器时,很容易感到混乱。为了简化管理,你应该始终为你的容器指定一个易于管理的名称。Docker 提供了一个 run 命令选项:--name 选项。在我们之前的示例基础上,我们将容器命名为 nginx-test。我们的新 docker run 命令如下:

docker run --name nginx-test -d bitnami/nginx:latest

就像运行任何分离模式的镜像一样,这将返回容器 ID,但不会返回你提供的名称。为了验证容器是否以名称 nginx-test 运行,我们可以使用接下来要介绍的 docker ps 命令列出容器。

docker ps

通常,你需要获取正在运行的容器列表或已停止的容器列表。Docker CLI 有一个 ps 标志,它将列出所有正在运行的容器或通过添加额外标志列出已停止的容器。输出将包括容器 ID、镜像标签、启动命令、创建日期、状态、端口和容器名称。以下是当前正在运行的容器示例:

CONTAINER ID   IMAGE                  COMMAND                 CREATED
13bdde13d002   bitnami/nginx:latest   "/opt/bitnami/script…"  Up 4 hours
3302f2728133   registry:2             "/entrypoint.sh /etc…"  Up 3 hours

如果你正在查找的容器正在运行,这很有帮助,但如果容器已停止,或者容器启动失败并随后停止了怎么办?你可以通过为 docker ps 命令添加 -a 标志,查看所有容器的状态,包括之前运行的容器。当你执行 docker ps -a 时,你会看到与标准 ps 命令相同的输出,但列表中可能会包含其他容器。

如何区分哪些容器正在运行,哪些容器已停止?查看列表中的 STATUS 字段,运行中的容器会显示运行时间,例如 “Up xx hours” 或 “Up xx days”。然而,如果容器因某种原因停止,状态会显示停止时间,例如 “Exited (0) 10 minutes ago”。

IMAGE                  COMMAND                  CREATED         STATUS
bitnami/nginx:latest   "/opt/bitnami/script…"   10 minutes ago  Up 10 minutes
bitnami/nginx:latest   "/opt/bitnami/script…"   12 minutes ago  Exited (0) 10 minutes ago

容器停止并不意味着运行镜像时出现了问题。有些容器可能只执行单个任务,任务完成后容器会正常停止。判断容器是正常退出还是由于启动失败导致退出的一种方法是查看退出状态码。可以通过多个退出码来找出容器退出的原因。

退出码描述
0命令执行成功,没有任何问题。
1命令因意外错误而失败。
2命令未找到指定资源或遇到类似问题。
125命令因 Docker 相关错误而失败。
126命令失败,因为 Docker 二进制文件或脚本无法执行。
127命令失败,因为 Docker 二进制文件或脚本未找到。
128+命令因特定的 Docker 相关错误或异常而失败。

表1.3:Docker 退出码

docker start 和 stop

由于系统资源有限,你可能需要停止一些容器,从而限制同时运行的容器数量。要停止一个正在运行的容器并释放资源,可以使用 docker stop 命令,并提供容器的名称或容器 ID。
如果你需要在未来某个时候再次启动该容器进行进一步的测试或开发,执行 docker start <name> 命令,该命令将以最初启动时的所有选项(包括分配的任何网络或卷)启动该容器。

docker attach

为了排查问题或检查日志文件,可能需要与容器进行交互。一种连接到当前正在运行的容器的方法是使用 docker attach <container ID/name> 命令。执行此操作时,你将与正在运行的容器的活动进程建立连接。如果你附加到正在执行某个进程的容器,可能不会看到任何提示符,事实上,在容器开始输出内容到屏幕之前,可能会看到一段时间的空白屏幕。

连接到容器时需要格外小心,容易不小心停止正在运行的进程,进而停止容器。让我们用一个运行 NGINX 的 web 服务器的示例来说明。首先,我们需要使用 docker ps 验证容器正在运行:

CONTAINER ID   IMAGE                 COMMAND                   STATUS
4a77c14a236a   nginx                 "/docker-entrypoint.…"    Up 33 seconds

使用 attach 命令执行 docker attach 4a77c14a236a

连接到进程后,你只能与正在运行的进程交互,看到的唯一输出是发送到标准输出的数据。在 NGINX 容器的情况下,attach 命令将附加到 NGINX 进程。为了演示这一点,我们将保持连接并通过另一个会话使用 curl 请求 web 服务器。一旦请求容器,我们将看到日志输出到附加的控制台:

[root@astra-master manifests]# docker attach 4a77c14a236a
172.17.0.1 - - [15/Oct/2021:23:28:31 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"
172.17.0.1 - - [15/Oct/2021:23:28:33 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"
172.17.0.1 - - [15/Oct/2021:23:28:34 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"

我们提到过,连接到容器时要小心。对 Docker 新手来说,可能会附加到 NGINX 容器上并认为服务器没有响应或进程似乎卡住了,因此可能决定使用标准的 Ctrl + C 组合键退出容器。这会停止容器并返回到 Bash 提示符,在这里他们可以运行 docker ps 来查看正在运行的容器:

root@localhost:~# docker ps
CONTAINER ID      IMAGE  COMMAND    CREATED    STATUS
root@localhost:~#

NGINX 容器发生了什么?我们没有执行 docker stop 命令,容器在我们连接到它之前是正在运行的。为什么容器在我们连接后停止了?

正如我们提到的,连接到容器时,你实际上是连接到了正在运行的进程。所有键盘命令的行为就像你在物理服务器上运行 NGINX 一样。这意味着,当用户使用 Ctrl + C 返回提示符时,他们实际上停止了正在运行的 NGINX 进程。

如果我们按下 Ctrl + C 退出容器,会显示进程已终止的输出。以下是 NGINX 示例中发生的情况:

2023/06/27 19:38:02 [notice] 1#1: signal 2 (SIGINT) received, exiting
2023/06/27 19:38:02 [notice] 31#31: exiting
2023/06/27 19:38:02 [notice] 30#30: exiting

如果容器的运行进程停止,容器也会停止,这就是 docker ps 命令未显示正在运行的 NGINX 容器的原因。

要退出 attach 会话而不停止进程,不应该使用 Ctrl + C,而应使用 Ctrl + P 后接 Ctrl + Q,这会退出容器而不停止运行的进程。

还有一个替代 attach 命令的方法:docker exec 命令。exec 命令与 attach 命令的不同之处在于,exec 允许你在容器中执行指定的进程。

docker exec

与容器交互时,exec 命令是一个更好的选择。你可以使用 docker exec 命令在容器中执行进程。你需要提供容器的名称和要执行的进程。当然,进程必须包含在运行的镜像中——如果镜像中没有 Bash 可执行文件,尝试在容器中执行 Bash 会报错。

我们再次以 NGINX 容器为例。我们会使用 docker ps 验证 NGINX 是否正在运行,然后使用容器 ID 或名称进入容器。命令语法是 docker exec <options> <container name> <command>

root@localhost:~# docker exec -it nginx-test bash
I have no name!@a7c916e7411:/app$

我们使用的选项是 -it,它告诉 exec 以交互式 TTY 会话运行。在这里,我们要执行的进程是 Bash。

请注意,提示符名称从原始用户和主机名变了。主机名是 localhost,而容器名称是 a7c916e7411。你可能还注意到,当前工作目录从 ~ 变为 /app,并且提示符以 $ 结尾,表示当前用户不是 root。

你可以像使用标准 SSH 连接一样使用这个会话;你是在容器中运行 Bash,并且由于我们没有连接到容器中的正在运行的进程,Ctrl + C 不会停止任何进程。

要退出交互会话,只需输入 exit,然后按 Enter 键即可退出容器。如果你随后运行 docker ps,你会注意到容器仍然处于运行状态。

接下来,让我们看看 Docker 日志文件。

docker logs

docker logs 命令允许你使用容器名称或容器 ID 检索容器的日志。你可以查看 ps 命令中列出的任何容器的日志,无论它是当前正在运行还是已停止。

日志文件通常是排查容器无法启动或容器处于退出状态原因的唯一方法。例如,如果你尝试运行镜像,镜像启动后立即停止,你可以通过查看该容器的日志找到答案。

要查看某个容器的日志,你可以使用 docker logs <container ID or name> 命令。

例如,要查看容器 ID 为 7967c50b260f 的容器的日志,你可以使用以下命令:

docker logs 7967c50b260f

这将把容器的日志输出到你的屏幕,日志可能非常长且冗长。由于许多日志可能包含大量信息,你可以通过为 logs 命令提供附加选项来限制输出。下表列出了查看日志时可用的选项:

日志选项描述
-f跟随日志输出(也可以使用 --follow)。
--tail xx从文件末尾开始显示日志输出,并检索 xx 行。
--until xxx显示 xxx 时间戳之前的日志输出。xxx 可以是时间戳或相对时间。
--since xxx显示 xxx 时间戳之后的日志输出。xxx 可以是时间戳或相对时间。

表1.4:日志选项

检查日志文件是你经常要做的事情,由于日志可能非常长,了解 tailuntilsince 选项将帮助你更快找到日志中的关键信息。

docker rm

一旦你给容器分配了一个名称,除非你使用 docker rm 命令删除它,否则无法将该名称用于其他容器。如果你有一个名为 nginx-test 的容器已停止运行,并且你尝试启动另一个同名的容器,Docker 守护进程会返回错误,提示该名称已被占用:

Conflict.  The container name "/nginx-test" is already in use

虽然原来的 nginx-test 容器没有在运行,但守护进程知道该容器名称已被使用,并且它仍然存在于之前运行的容器列表中。

如果你想重用某个特定名称,必须先删除现有的容器,然后才能使用相同的名称启动新的容器。这种情况在容器镜像测试中很常见。你可能最初启动了一个容器,但遇到了应用程序或镜像的问题。在这种情况下,你会停止容器,解决镜像或应用程序的问题,然后希望使用相同的名称重新部署它。然而,由于具有该名称的前一个容器仍然存在于 Docker 历史记录中,因此在重新使用该名称之前,必须删除该容器。

你还可以在 Docker 命令中添加 --rm 选项,以便在容器停止后自动删除镜像。

要删除 nginx-test 容器,只需执行 docker rm nginx-test

root@localhost ~:# docker rm nginx-test
nginx-test
root@localhost ~:#

假设容器名称正确且容器未在运行中,你将只会看到已删除的镜像名称。

我们尚未讨论 Docker 卷,但当删除一个附加了卷的容器时,最好在删除命令中添加 -v 选项。将 -v 选项添加到 docker rm 命令中将删除附加到容器的所有卷。

docker pull/run

在执行拉取操作时,请确保指定架构。docker pulldocker run 用于拉取镜像或运行镜像。如果你尝试运行一个尚未存在于 Docker 主机上的容器,它会发出拉取请求以获取容器并运行它。

当你尝试拉取或运行一个容器时,Docker 将下载与主机架构兼容的容器。如果你想下载基于不同架构的镜像,可以在构建命令中添加 --platform 标签。例如,如果你在 arm64 架构系统上运行,并且想拉取一个 x86 镜像,可以将 linux/arm64 作为平台添加。在运行拉取命令时,确保指定架构:

root@localhost ~:# docker pull --platform=linux/amd64 ubuntu:22.04
22.04: Pulling from library/ubuntu
6b851dcae6ca: Pull complete
Digest: sha256:6120be6a2b7ce665d0cbddc3ce6eae60fe94637c6a66985312d1f02f63cc0bcd
Status: Downloaded newer image for ubuntu:22.04
WARNING: image with reference ubuntu was found but does not match the specified platform: wanted linux/amd64, actual: linux/arm64/v8
docker.io/library/ubuntu:22.04

添加 --platform=linux/amd64 告诉 Docker 获取正确的架构。你可以使用相同的参数来运行 docker run,以确保使用正确的容器镜像平台。

docker build

pullrun 类似,Docker 将尝试基于主机架构(arm64)构建镜像。假设你在基于 arm64 镜像的系统上构建,你可以使用 buildx 子命令告诉 Docker 创建一个 x86 镜像:

root@localhost ~:# docker buildx build --platform linux/amd64 --tag docker.io/mlbiam/openunison-kubernetes-operator --no-cache -f ./src/main/docker/Dockerfile .

这个命令告诉 Docker 生成 x86 版本,该版本可以在任何基于 x86 的硬件上运行。

总结

在本章中,你学到了如何使用 Docker 解决常见的开发问题,包括令人头痛的“在我的机器上能运行”的问题。我们还介绍了你日常使用的最常见的 Docker CLI 命令。

在下一章中,我们将开始 Kubernetes 的旅程,介绍 KinD 这一工具,它提供了一种简单的方法,可以在单个工作站上运行多节点的 Kubernetes 测试服务器。