您现在对容器是什么有所了解,但我还没有解释它们是如何工作的。在我们开始之前,您需要创建一个应用程序,将其打包到容器映像中并运行它。你需要 Docker,所以让我们先安装它并运行一个 Hello world 容器。

安装 Docker 并运行 Hello World 容器

理想情况下,您将直接在 Linux 计算机上安装 Docker,因此您不必处理在主机操作系统中运行的 VM 内运行容器的额外复杂性。但是,如果您使用的是 macOS 或 Windows,并且不知道如何设置 Linux VM,Docker 桌面应用程序将为您设置它。用于运行容器的 Docker 命令行 (CLI) 工具将安装在主机操作系统中,但 Docker 守护程序将在 VM 中运行,它创建的所有容器也将在VM中运行。

Docker 平台由许多组件组成,但您只需要安装 Docker Engine 即可运行容器。如果您使用 macOS 或 Windows,请安装 Docker Desktop。有关详细信息,请按照 http://docs.docker.com/install 上的说明进行操作。

运行 Hello World 容器

安装完成后,您可以使用 docker CLI 工具运行 docker 命令。首先,让我们尝试从 Docker Hub 中拉取并运行一个现有的镜像,这是一个公共镜像仓库,其中包含许多知名软件包的现成容器镜像。其中一个是 busybox 映像,您将使用它在您的第一个容器中运行一个简单的 echo “Hello world” 命令。

如果您不熟悉busybox,它是一个单一的可执行文件,它结合了许多标准的UNIX 命令行工具,例如echo、ls、gzip 等。除了busybox 镜像,您还可以使用任何其他成熟的操作系统容器镜像,例如Fedora、Ubuntu 或任何其他包含echo 可执行文件的镜像。

您无需下载或安装任何东西即可运行 busybox 映像。您可以使用单个 docker run 命令执行所有操作,方法是指定要下载的镜像和要在其中运行的命令。要运行简单的 Hello world 容器,请执行以下清单中显示的命令。

  1. Listing 2.1 Running a Hello World container with Docker
  2. $ docker run busybox echo "Hello World"
  3. Unable to find image 'busybox:latest' locally
  4. latest: Pulling from library/busybox
  5. 7c9d20b9b6cd: Pull complete
  6. Digest: sha256:fe301db49df08c384001ed752dff6d52b4
  7. Status: Downloaded newer image for busybox:latest
  8. Hello World

这看起来不太令人印象深刻,但请记住,整个“应用程序”是使用单个命令下载和执行的,而无需安装该应用程序或其任何依赖项。

在您的情况下,该应用程序只是一个可执行文件,但它可能是一个非常复杂的应用程序,包含数十个库和附加文件。设置和运行应用程序的整个过程是相同的。不明显的是,该应用程序在一个容器中运行,与计算机上的其他进程隔离。你会在接下来的练习中看到这是正确的。

了解运行容器时会发生什么

image.png
docker CLI 工具向 Docker 守护进程发送了一条运行容器的指令,该守护进程检查 busybox 镜像是否已经存在于其本地镜像缓存中。因为不存在,所以它从 Docker Hub 仓库中提取了它。

将镜像下载到您的计算机后,Docker 守护程序从该镜像创建一个容器并在其中执行 echo 命令。该命令将文本打印到标准输出,然后进程终止并且容器停止。

如果您的本地计算机运行 Linux 操作系统,则 Docker CLI 工具和守护程序都在此操作系统中运行。如果它运行 macOS 或 Windows,则守护程序和容器在 Linux VM 中运行。

运行其他镜像

运行其他现有容器镜像与运行 busybox 镜像非常相似。事实上,它通常更简单,因为您通常不需要指定要执行的命令,就像前面示例中的 echo 命令一样。应该执行的命令通常写在映像本身中,但是您可以在运行时覆盖它。

例如,如果要运行 Redis 数据存储,可以在 http://hub.docker.com 或其他公共仓库上找到镜像名称。在 Redis 的情况下,其中一个镜像称为 redis:alpine,因此您可以像这样运行它:

  1. docker run redis:alpine

要停止并退出容器,请按 Control-C(或 Mac 上的 Command-C)。

如果要从不同的仓库运行镜像,则必须指定仓库以及镜像名称。例如,如果要从 Quay.io 仓库(另一个可公开访问的镜像仓库)运行镜像,请按如下方式运行它:docker run quay.io/some/image。

了解镜像标签

如果您在 Docker Hub 上搜索过 Redis 镜像,您会注意到有很多镜像标签可供您选择。对于 Redis,标签有 latest、buster、alpine,还有 5.0.7-buster、5.0.7-alpine 等。

Docker 允许您在同一名称下拥有同一镜像的多个版本或变体。每个变体都有一个唯一的标签。如果您在没有明确指定标签的情况下引用镜像,Docker 会假定您引用的是特定的 latest。上传新版本的镜像时,镜像作者通常会同时使用实际版本号和 latest对其进行标记。当您想要运行最新版本的镜像时,请使用 latest标签而不是指定版本。

docker run 命令只会在之前没有拉取过镜像的情况下拉取镜像。使用最新标签可确保您在首次运行镜像时获得最新版本。从那时起使用本地缓存的镜像。

即使对于单个版本,通常也有镜像的多个变体。对于 Redis,我提到了 5.0.7-buster 和 5.0.7-alpine。它们都包含相同版本的 Redis,但它们构建的基础映像不同。 5.0.7-buster 基于 Debian 版本“Buster”,而 5.0.7-alpine 基于 Alpine Linux 基础镜像,一个非常精简的镜像,总共只有 5MB——它只包含一小部分您在典型的 Linux 发行版中看到的已安装二进制文件。

要运行图像的特定版本或变体,请在镜像名称中指定 tag。例如,要运行 5.0.7-alpine 标签,您需要执行以下命令:

  1. docker run redis:5.0.7-alpine

创建容器化的 Node.js Web 应用程序

现在你已经有了一个工作的 Docker 设置,你将创建一个在本书中使用的应用程序。您将创建一个简单的 Node.js Web 应用程序并将其打包到容器镜像中。该应用程序将接受 HTTP 请求并以运行它的计算机的主机名进行响应。

这样,您将看到在容器中运行的应用程序会看到的虚拟主机名,而不是真实的主机名,即使它像任何其他进程一样在主机上运行。稍后,当您在 Kubernetes 上部署应用程序并将其扩展(水平扩展;即运行应用程序的多个实例)时,这将很有用。你会看到你的 HTTP 请求访问了应用程序的不同实例。

该应用程序由一个名为 app.js 的文件组成,其内容显示在下一个清单中。

  1. Listing 2.2 A simple Node.js web application: app.js
  2. const http = require('http');
  3. const os = require('os');
  4. const listenPort = 8080;
  5. console.log("Kubia server starting...");
  6. console.log("Local hostname is " + os.hostname());
  7. console.log("Listening on port " + listenPort);
  8. var handler = function(request, response) {
  9. let clientIP = request.connection.remoteAddress;
  10. console.log("Received request for "+request.url+" from "+clientIP);
  11. response.writeHead(200);
  12. response.write("Hey there, this is "+os.hostname()+". ");
  13. response.write("Your IP is "+clientIP+". ");
  14. response.end("\n");
  15. };
  16. var server = http.createServer(handler);
  17. server.listen(listenPort);

清单中的代码应该很容易理解。它在端口 8080 上启动一个 HTTP 服务器。对于每个请求,它会将请求信息记录到标准输出,并发送带有状态码 200 OK 和的响应。

响应中的主机名是服务器的实际主机名,而不是客户端在请求的 Host 标头中发送的主机名。这个细节稍后会很重要。

您现在可以在本地下载并安装 Node.js 并直接测试您的应用程序,但这不是必需的。将其打包到容器镜像中并使用 Docker 运行它更容易。这使您可以在任何其他支持 Docker 的主机上运行应用程序,而无需在那里安装 Node.js。

创建 Dockerfile 来构建容器镜像

要将您的应用程序打包到镜像中,您必须首先创建一个名为 Dockerfile 的文件,其中包含 Docker 在构建映像时应执行的指令列表。在与 app.js 文件相同的目录中创建该文件,并确保它包含以下清单中的三个指令。

  1. FROM node:12
  2. ADD app.js /app.js
  3. ENTRYPOINT ["node", "app.js"]

FROM 行定义了您将用作起点的容器镜像(您正在构建的基础镜像)。在您的情况下,您使用节点容器映像,标记为 12。在第二行中,将本地目录中的 app.js 文件添加到映像的根目录中,并使用相同的名称 (app.js)。最后,在第三行中,指定 Docker 在执行镜像时应该运行的命令。在您的情况下,命令是 node app.js。

您可能想知道为什么使用此特定镜像作为您的基础。由于您的应用程序是 Node.js 应用程序,因此您的镜像需要包含节点二进制文件才能运行该应用程序。您可以使用任何包含此二进制文件的镜像,或者您甚至可以使用 Linux 发行版基础镜像,例如 fedora 或 ubuntu,并在构建镜像时将 Node.js 安装到容器中。但由于 node镜像已经包含运行 Node.js 应用程序所需的所有内容,因此从头开始构建镜像没有意义。然而,在某些组织中,使用特定的基本镜像并在构建时向其添加软件可能是强制性的。

构建容器镜像

Dockerfile 和 app.js 文件是构建映像所需的一切。您现在将使用下一个清单中的命令构建名为 kubia:latest 的映像:

  1. Listing 2.4 Building the image
  2. $ docker build -t kubia:latest .
  3. Sending build context to Docker daemon 3.072kB
  4. Step 1/3 : FROM node:12
  5. 12: Pulling from library/node
  6. 092586df9206: Pull complete
  7. ef599477fae0: Pull complete
  8. 89e674ac3af7: Pull complete
  9. 08df71ec9bb0: Pull complete
  10. Digest: sha256:a919d679dd773a56acce15afa0f436055c9b9f20e1f28b4469a4bee69e0
  11. Status: Downloaded newer image for node:12
  12. ---> e498dabfee1c
  13. Step 2/3 : ADD app.js /app.js
  14. ---> 28d67701d6d9
  15. Step 3/3 : ENTRYPOINT ["node", "app.js"]
  16. ---> Running in a01d42eda116
  17. Removing intermediate container a01d42eda116
  18. ---> b0ecc49d7a1d
  19. Successfully built b0ecc49d7a1d
  20. Successfully tagged kubia:latest

-t 选项指定所需的镜像名称和标记,末尾的点指定包含 Dockerfile 和构建上下文(构建过程所需的所有资源)的目录的路径。

构建过程完成后,新创建的镜像可在您计算机的本地镜像存储中使用。您可以通过列出本地镜像来查看它,如下面的清单所示。

  1. Listing 2.5 Listing locally stored images
  2. $ docker images
  3. REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
  4. kubia latest b0ecc49d7a1d 1 minute ago 908 MB

了解镜像是如何构建的

下图显示了构建过程中发生的情况。你告诉 Docker 根据当前目录的内容构建一个名为 kubia 的镜像。 Docker读取目录中的 Dockerfile 并根据文件中的指令构建镜像。
image.png
构建本身不是由 docker CLI 工具执行的。而是将整个目录的内容上传到 Docker 守护程序,并由它构建镜像。您已经了解到 CLI 工具和守护程序不一定在同一台计算机上。如果您在 macOS 或 Windows 等非 Linux 系统上使用 Docker,则客户端在您的主机操作系统中,但守护程序在 Linux VM 中运行,并且它也可以在远程计算机上运行。

不要将不必要的文件添加到构建目录中,因为它们会减慢构建过程——尤其是如果 Docker 守护进程位于远程系统上。

为了构建镜像,Docker 首先从公共镜像存储库(在本例中为 Docker Hub)中拉取基础镜像(node:12),除非该镜像已经存储在本地。然后它从镜像创建一个新容器并执行 Dockerfile 中的下一个指令。容器的最终状态会生成一个具有自己 ID 的新镜像。构建过程继续处理 Dockerfile 中的剩余指令。每个人都会创建一个新镜像,然后使用您在 docker build 命令中使用 -t 标志指定镜像的tag

了解镜像中的层是什么

镜像是由多个层组成,有人可能会认为每个镜像仅由基础镜像的层和顶部的一个新层组成,但事实并非如此。构建镜像时,会为 Dockerfile 中的每个单独指令创建一个新层。

在构建 kubia 镜像的过程中,在拉取基础镜像的所有层之后,Docker 会创建一个新层并将 app.js 文件添加到其中。然后它会创建另一个层,该层仅包含执行镜像时要运行的命令。然后将最后一层标记为 kubia:latest。

您可以通过运行 docker history 查看镜像的层及其大小,如下面的清单所示。首先打印顶层。

  1. Listing 2.6 Displaying the layers of a container image
  2. $ docker history kubia:latest
  3. IMAGE CREATED CREATED BY SIZE
  4. b0ecc49d7a1d 7 min ago /bin/sh -c #(nop) ENTRYPOINT ["node"… 0B
  5. 28d67701d6d9 7 min ago /bin/sh -c #(nop) ADD file:2ed5d7753… 367B
  6. e498dabfee1c 2 days ago /bin/sh -c #(nop) CMD ["node"] 0B
  7. <missing> 2 days ago /bin/sh -c #(nop) ENTRYPOINT ["docke… 0B
  8. <missing> 2 days ago /bin/sh -c #(nop) COPY file:23873730… 116B
  9. <missing> 2 days ago /bin/sh -c set -ex && for key in 6A0 5.4MB
  10. <missing> 2 days ago /bin/sh -c #(nop) ENV YARN_VERSION=… 0B
  11. <missing> 2 days ago /bin/sh -c ARCH= && dpkgArch="$(dpkg… 67MB
  12. <missing> 2 days ago /bin/sh -c #(nop) ENV NODE_VERSION=… 0B
  13. <missing> 3 weeks ago /bin/sh -c groupadd --gid 1000 node … 333kB
  14. <missing> 3 weeks ago /bin/sh -c set -ex; apt-get update;… 562MB
  15. <missing> 3 weeks ago /bin/sh -c apt-get update && apt-get… 142MB
  16. <missing> 3 weeks ago /bin/sh -c set -ex; if ! command -v… 7.8MB
  17. <missing> 3 weeks ago /bin/sh -c apt-get update && apt-get… 23.2MB
  18. <missing> 3 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
  19. <missing> 3 weeks ago /bin/sh -c #(nop) ADD file:9788b61de… 101MB

您看到的大多数层都来自 node:12 镜像(它们还包括该镜像自己的基础镜像的层)。最上面的两个层对应于 Dockerfile 中的第二个和第三个指令(ADD 和 ENTRYPOINT)。

正如您在 CREATED BY 列中看到的,每个层都是通过在容器中执行命令来创建的。除了使用 ADD 指令添加文件外,您还可以在 Dockerfile 中使用其他指令。例如,RUN 指令在构建期间执行容器中的命令。在上面的清单中,您将找到执行 apt-get update 和一些其他 apt-get 命令的层。 apt-get 是用于安装软件包的 Ubuntu 软件包管理器的一部分。清单中显示的命令将一些包安装到镜像的文件系统中。

每个指令创建一个新层。我已经提到,当你删除一个文件时,它只是在新层中被标记为已删除,而不是从下面的层中删除。因此,使用后续指令删除文件不会减小镜像的大小。如果您使用 RUN 指令,请确保它执行的命令会删除它在终止之前创建的所有临时文件。

运行容器镜像

构建并准备好镜像后,您现在可以使用以下命令运行容器:

  1. $ docker run --name kubia-container -p 1234:8080 -d kubia
  2. 9d62e8a9c37e056a82bb1efad57789e947df58669f94adc2006c087a03c54e02

这告诉 Docker 从 kubia 镜像运行一个名为 kubia-container 的新容器。容器与控制台分离(-d 标志)并在后台运行。主机上的 1234 端口映射到容器中的 8080 端口(由 -p 1234:8080 选项指定),因此您可以通过 http://localhost:1234 访问应用程序。

下图应该可以帮助您直观地了解所有内容如何组合在一起。请注意,Linux VM 仅在您使用 macOS 或 Windows 时才存在。如果直接使用 Linux,则没有 VM,并且描绘端口 1234 的框位于本地计算机的边缘。
image.png

访问你的app

现在使用 curl 或 Internet 浏览器在 http://localhost:1234 访问应用程序:

  1. $ curl localhost:1234
  2. Hey there, this is 44d76963e8e1. Your IP is ::ffff:172.17.0.1.

如果 Docker 守护程序在不同的机器上运行,您必须将 localhost 替换为该机器的 IP。您可以在 DOCKER_HOST 环境变量中查找它。

如果一切顺利,您应该会看到应用程序发送的响应。就我而言,它返回 44d76963e8e1 作为其主机名。在您的情况下,您会看到一个不同的十六进制数字。这是列出它们时显示的容器的 ID。

列出所有正在运行的容器

要列出您计算机上运行的所有容器,请运行以下列表中显示的命令。

  1. $docker ps
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  3. c57818b58dec kubia "node app.js" 11 minutes ago Up 11 minutes 0.0.0.0:1234->8080/tcp kubia-container

对于每个容器,Docker 会打印其 ID 和名称、它使用的镜像以及它执行的命令。它还显示容器的创建时间、状态以及映射到容器的主机端口。

获取有关容器的其他信息

docker ps 命令显示有关容器的最基本信息。要查看更多信息,您可以使用 docker inspect:

  1. $ docker inspect kubia-container

Docker 打印一个长的 JSON 格式的文档,其中包含有关容器的大量信息,例如它的状态、配置和网络设置,包括它的 IP 地址。

检查应用程序日志

Docker 捕获并存储应用程序写入标准输出和错误流的所有内容。这通常是应用程序写入日志的地方。您可以使用 docker logs 命令查看输出,如下面的清单所示。

  1. $ docker logs kubia-container
  2. Kubia server starting...
  3. Local hostname is 44d76963e8e1
  4. Listening on port 8080
  5. Received request for / from ::ffff:172.17.0.1

您现在知道了在容器中执行和检查应用程序的基本命令。接下来,您将学习如何分发它。

分发容器镜像

您构建的映像目前仅在本地可用。要在其他计算机上运行它,您必须首先将其推送到外部镜像仓库。让我们将它推送到公共 Docker Hub 仓库,这样您就不需要设置私有仓库了。您还可以使用其他仓库,例如我已经提到的 Quay.io 或 Google Container Registry。

在推送镜像之前,您必须根据 Docker Hub 的镜像命名模式重新标记它。镜像名称必须包含您在 http://hub.docker.com 注册时选择的 Docker Hub ID。我将在以下示例中使用我自己的 ID (betnevs),因此请记住在自己尝试命令时将其替换为您的 ID。

给镜像打tag

获得 ID 后,您就可以为镜像添加额外的标签了。它的当前名称是 kubia,您现在将其标记为 yourid/kubia:1.0(将 yourid 替换为您的实际 Docker Hub ID)。这是我使用的命令:

  1. $ docker tag kubia luksa/kubia:1.0

通过再次列出镜像来确认您的镜像现在有两个名称,如下面的清单所示。

  1. $ docker images | head
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. betnevs/kubia 1.0 f12f0b88d970 7 days ago 864MB
  4. kubia latest f12f0b88d970 7 days ago 864MB
  5. busybox latest 71a676dd070f 2 weeks ago 1.41MB
  6. redis alpine 5d44f444e409 7 weeks ago 32.4MB

如您所见,kubia 和 luksa/kubia:1.0 都指向同一个镜像 ID,这意味着它们不是两个镜像,而是一个具有两个名称的镜像。

将镜像推送到 Docker Hub

在将镜像推送到 Docker Hub 之前,您必须使用您的用户 ID 使用 docker login 命令登录,如下所示:

  1. $ docker login -u yourid -p yourpassword docker.io

登录后,使用以下命令将 yourid/kubia:1.0 镜像推送到 Docker Hub:

  1. $ docker push yourid/kubia:1.0

在其他主机上运行镜像

推送到 Docker Hub 完成后,该镜像可供所有人使用。您现在可以通过运行以下命令在任何启用 Docker 的主机上运行镜像:

  1. $ docker run -p 1234:8080 -d luksa/kubia:1.0

如果容器在您的计算机上正确运行,它应该可以在任何其他 Linux 计算机上运行,前提是 Node.js 二进制文件不需要任何特殊的内核功能(它不需要)。

停止和删除容器

如果您在另一台主机上运行了容器,您现在可以终止它,因为您只需要本地计算机上运行一个容器即可进行后续练习。

停止容器

使用以下命令指示 Docker 停止容器:

  1. $ docker stop kubia-container

这会向容器中的主进程发送终止信号,以便它可以正常关闭。如果进程没有响应终止信号或没有及时关闭,Docker 会杀死它。当容器中的顶级进程终止时,容器中没有其他进程运行,因此容器停止。

删除容器

容器不再运行,但它仍然存在。 Docker 会保留它,以防您决定重新启动它。您可以通过运行 docker ps -a 查看已停止的容器。 -a 选项打印所有容器——那些正在运行的和那些已经停止的。作为练习,您可以通过运行 docker start kubia-container 再次启动容器。

您可以安全地删除另一台主机上的容器,因为您不再需要它。要删除它,请运行以下 docker rm 命令:

  1. $ docker rm kubia-container

这将删除容器。它的所有内容都已删除,并且无法再启动。但是,镜像仍然存在。如果您决定再次创建容器,则无需再次下载镜像。如果您还想删除镜像,请使用 docker rmi 命令:

  1. $ docker rmi kubia:latest

要删除所有镜像,您还可以使用 docker image prune 命令。