Python-Docker-实践教程-一-

197 阅读29分钟

Python Docker 实践教程(一)

原文:Practical Docker with Python

协议:CC BY-NC-SA 4.0

一、容器化介绍

本章介绍 Docker 是什么,容器化是什么,它与虚拟化有什么不同。其他涉及的副题包括容器化的历史、容器运行时间和容器编排。

Docker 是什么?

为了回答这个问题,我们需要澄清“Docker”这个词,因为 Docker 已经成为容器的同义词。

Docker Inc .是 Docker 背后的公司。它由所罗门·海克斯于 2010 年创立,名为 dotCloud Inc .dotCloud 工程师为 Linux 容器构建了抽象和工具,并使用了 Linux 内核特性cgroups和名称空间,目的是降低使用 Linux 容器的复杂性。dotCloud 将其工具开源,并将重点从平台即服务(PaaS)业务转向容器化。Docker Inc .将 dotCloud 出售给 cloudControl,后者最终申请破产。

Docker 是提供操作系统级虚拟化的技术,称为容器。需要注意的是,这不同于硬件虚拟化。我们将在本章的后面探讨这一点。Docker 使用 Linux 内核的资源隔离特性,如cgroups、内核名称空间和 OverlayFS,所有这些都在同一个物理或虚拟机内。OverlayFS 是一个支持 union 的文件系统,它将几个文件和目录合并成一个文件和目录,以便在同一个物理机或虚拟机中运行多个相互隔离和包含的应用。

了解 Docker 解决的问题

在很长一段时间里,设置开发人员的工作站对于系统管理员来说是一项非常麻烦的任务。即使开发人员工具的安装完全自动化,当您混合了不同的操作系统、不同版本的操作系统以及不同版本的库和编程语言时,建立一个一致并提供统一体验的工作空间几乎是不可能的。Docker 通过减少移动部件解决了这个问题。现在的目标不再是操作系统和编程版本,而是 Docker 引擎和运行时。Docker 引擎提供了底层系统的统一抽象,使得开发人员可以非常容易地测试他们的代码。

生产领域的事情变得更加复杂。假设您有一个 Python web 应用,运行在 Amazon Web Services EC2 实例上的 Python 2.7 上。为了使代码库现代化,该应用进行了一些重大升级,包括对 Python 版的更改。假设当前运行现有代码库的 Linux 发行版所提供的包中没有这个版本的 Python。要部署这个新应用,您可以选择以下任一选项:

  • 替换现有实例

  • 通过以下方式设置 Python 解释器

    • 将 Linux 发行版本更改为包含较新 Python 包的版本。

    • 添加第三方渠道,提供较新 Python 版本的打包版本。

    • 进行就地升级,保留现有版本的 Linux 发行版。

    • 从源代码编译 Python 3.5,这带来了额外的依赖性。

    • 或者使用类似于virtualenv的东西,它有自己的一套权衡。

无论从哪个角度看,应用代码的新版本部署都会带来很多不确定性。作为操作工程师,限制对配置的更改至关重要。考虑到操作系统的变化、Python 版本的变化和应用代码的变化,会产生很多不确定性。

Docker 通过显著减少不确定性的表面积来解决这个问题。您的应用正在现代化?没问题。用新的应用代码和依赖项构建一个新的容器,并交付它。现有的基础设施保持不变。如果应用的行为不符合预期,那么回滚就像重新部署旧容器一样简单——将所有生成的 Docker 映像存储在 Docker 注册表中并不少见。拥有一种简单的回滚方法而不干扰当前的基础架构,可以大大减少响应故障所需的时间。

多年来的容器化

虽然容器化在过去的几年里流行起来,但容器化的概念实际上可以追溯到 20 世纪 70 年代。

1979 年:克鲁特

系统调用chroot是在 1979 年的 UNIX 版本 7 中引入的。chroot的前提是它为当前运行的进程及其子进程改变了明显的根目录。在chroot中启动的进程不能访问指定目录树之外的文件。这种环境被称为 chroot 监狱。

2000 年:自由监狱

chroot概念的基础上,FreeBSD 增加了对一个特性的支持,该特性允许将 FreeBSD 系统划分成几个独立、隔离的系统,称为监狱。每个监狱都是主机系统上的一个虚拟环境,有自己的一组文件、进程和用户帐户。虽然chroot只将进程限制在文件系统的视图中,但 FreeBSD 将被监禁进程的活动限制在整个系统中,包括绑定到它的 IP 地址。这使得 FreeBSD jails 成为测试互联网连接软件的新配置的理想方式,可以很容易地试验不同的配置,同时不允许来自监狱的更改影响外部的主系统。

2005 年:OpenVZ

OpenVZ 在为低端虚拟专用服务器(VPS)提供商提供操作系统虚拟化方面非常受欢迎。OpenVZ 允许一个物理服务器运行多个独立的操作系统实例,称为容器。OpenVZ 使用了一个打了补丁的 Linux 内核,与所有容器共享它。每个容器充当一个独立的实体,拥有自己的一组虚拟化的文件、用户、组、进程树和虚拟网络设备。

2006 年:群体

最初被称为流程容器cgroups(控制组的简称)是由 Google 工程师启动的。cgroups是一个 Linux 内核特性,它将资源使用(如 CPU、内存、磁盘 I/O 和网络)限制并隔离给一组进程。cgroups已经被重新设计了多次,每一次重新设计都考虑到了它不断增长的用例数量和所需的特性。

2008: LXC

LXC 通过结合 Linux 内核的cgroups和对隔离名称空间的支持来提供操作系统级的虚拟化,从而为应用提供一个隔离的环境。Docker 最初使用 LXC 来提供隔离特性,但后来改用了自己的库。

容器和虚拟机

许多人认为既然容器隔离了应用,它们就和虚拟机一样。乍一看,它看起来很像,但根本的区别是容器与主机共享同一个内核。

Docker 只隔离单个进程(或者一组进程,这取决于映像是如何构建的),所有容器都运行在同一个主机系统上。因为隔离是在内核级应用的,所以与虚拟机相比,运行容器不会给主机带来很大的开销。当容器启动时,选定的进程或进程组仍在同一主机上运行,无需虚拟化或模拟任何东西。图 1-1 显示了在单个物理主机上的三个不同容器上运行的三个应用。

img/463857_2_En_1_Fig1_HTML.jpg

图 1-1

在三个不同容器上运行的三个应用的表示

相比之下,当虚拟机启动时,虚拟机管理程序会虚拟化整个系统,从 CPU 到 RAM 再到存储。为了支持这个虚拟化系统,需要安装整个操作系统。出于所有实际目的,虚拟化系统是在计算机中运行的整个计算机。现在,如果你能想象运行一个操作系统需要多少开销,想象一下如果你运行一个嵌套的操作系统会是什么样子!图 1-2 展示了在一台物理主机上的三个不同虚拟机上运行的三个应用。

img/463857_2_En_1_Fig2_HTML.jpg

图 1-2

在三个不同的虚拟机上运行的三个应用的表示

图 1-1 和 1-2 给出了在单个主机上运行的三个不同应用的指示。对于 VM 来说,不仅需要应用的依赖库,还需要操作系统来运行应用。相比之下,使用容器,与应用共享主机操作系统的内核意味着消除了额外操作系统的开销。这不仅大大提高了性能,还让您提高了资源利用率,并最大限度地减少了计算能力的浪费。

容器运行时

容器映像在启动和运行时成为一个容器。但是要做到这一点,必须有一个软件来引导运行容器所需的资源。这个软件叫做容器运行时。Docker 使用 containerd 项目实现了一个容器运行时,该项目现在是云本地计算基金会的毕业项目列表的一部分。

然而,containerd并不是惟一的容器运行时。还有其他的容器运行时项目,比如 cri-orkt (已经不在活跃开发中了)、 runC 等等。

OCI 和国际广播电台

随着更多容器运行时的开发,需要一个标准来定义什么是容器映像,即运行时的规范。这就是开放容器倡议(OCI)的由来。

OCI 是一个开放的治理结构,用于创建容器映像和运行时的行业标准规范,不受特定于供应商的特性的限制,以促进开放的生态系统。OCI 目前有两个规范:运行时规范和映像规范。

运行时规范定义了容器运行时应该如何将容器映像解包到文件系统中,以及运行容器的步骤。这确保了无论使用哪个容器运行时,容器都将按预期准确运行。

图像规范定义了一种 OCI 图像格式,其中包含了关于如何创建 OCI 图像的必要定义。OCI 映像包括映像清单、文件系统定义和映像配置。映像清单包含有关映像内容和依赖关系的元数据。映像配置包括应用参数和环境变量等数据。

容器运行时接口(CRI)是 Kubernetes 特有的术语,它定义了 Kubernetes 如何与多个容器运行时交互并引导容器。在 CRI 之前,Kubernetes 只支持 Docker 运行时。随着来自社区的支持更多容器运行时的请求,Kubernetes 团队为容器运行时实现了一个插件接口。这个插件接口允许 Kubernetes 支持可互换的容器运行时,允许来自社区的简单贡献。

Docker 工人和库柏工人

随着 Kubernetes 在行业中的使用越来越多,一个经常出现的问题是 Docker 和 Kubernetes 之间的区别。

Kubernetes 是运行容器和维护其生命周期的协调器。Docker 是多用途软件,不仅可以构建容器映像,还可以运行容器。虽然 Docker 不仅可以在单个节点上运行和维护容器的生命周期,还可以使用 Docker Compose 和 Docker Swarm 在多个节点上运行和维护容器的生命周期,但 Kubernetes 已经成为容器编排的事实标准。

Docker 和 Kubernetes 是互补的——Docker 构建容器映像,而 Kubernetes 编排这些容器的运行。Kubernetes 还可以调度容器在许多节点上的运行副本。

第八章对容器编排进行了更深入的研究。

摘要

在本章中,您了解了一些关于 Docker 公司、Docker 容器、容器与虚拟机的比较,以及容器试图解决的现实问题。您还简要了解了什么是容器运行时,以及 Docker 和 Kubernetes 是如何相互补充的。在接下来的章节中,您将对 Docker 进行一次介绍性的浏览,并就构建和运行容器进行几次实际操作。

二、Docker 101

现在你对 Docker 的工作原理和它的流行程度有了一点了解,在这一章中,你将学习一些与 Docker 相关的不同术语。您还将学习如何安装 Docker 并理解 Docker 术语,如图像、容器、Docker 文件和 Docker 合成。您还可以使用一些简单的 Docker 命令来创建、运行和停止 Docker 容器。

安装 Docker

Docker 支持 Linux、macOS 和 Windows 平台。在大多数平台上安装 Docker 很简单,我稍后会讲到。Docker Inc .提供 Docker 平台的社区版和企业版。

企业版具有与社区版相同的功能,但是它提供了额外的支持和认证的容器、插件和基础设施。对于本书的目的以及大多数一般的开发和生产用途,社区版是合适的,因此我将在本书中使用它。

在 Windows 上安装 Docker

Windows 上的 Docker 需要满足一些先决条件,然后才能安装它。其中包括:

  • Hyper-V 支持

  • 硬件虚拟化支持:这通常是从系统 BIOS 中启用的

  • 目前仅支持 64 位版本的 Windows 10(专业版/教育版/企业版,周年更新版本为 v1607)

如果您查看这些先决条件,您会注意到这看起来像是虚拟化设置所需要的,但是您在前一章中已经了解到 Docker 不是虚拟化。那么为什么 Docker for Windows 需要虚拟化所需的特性呢?

简短的回答是 Docker 依赖于众多特性,比如名称空间和cgroups,而这些在 Windows 上是没有的。为了绕过这个限制,Docker for Windows 创建了一个运行 Linux 内核的轻量级 Hyper-V 容器。如果你的电脑安装了 Windows 10 家庭版,你应该安装带有 WSL 2 后端的 Docker Desktop。这将在下一节中解释。

让我们把重点放在安装 Docker CE for Windows 上。本节假设所有先决条件都已满足,并且 Hyper-V 已启用。前往 https://store.docker.com/editions/community/docker-ce-desktop-windows 下载 Docker CE。

Note

确保您选择了稳定的频道,并单击“获取 Docker CE”按钮。

作为安装的一部分,可能会提示您启用 Hyper-V 和容器支持(参见图 2-1 )。

img/463857_2_En_2_Fig1_HTML.png

图 2-1

启用 Hyper-V 和容器功能

单击确定并完成安装。您可能需要重新启动系统,因为启用 Hyper-V 是 Windows 系统的一项功能。安装此功能需要重新启动才能启用。

安装完成后,打开一个命令提示符窗口(或者 PowerShell,如果您喜欢的话),键入以下命令检查 Docker 是否已安装并正常工作。

docker run --rm hello-world

如果安装顺利,您应该会看到清单 2-1 中所示的响应。

docker run --rm hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1\. The Docker client contacted the Docker daemon.
 2\. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3\. The Docker daemon created a new container from that image which runs the
 executable that produces the output you are currently reading.
 4\. The Docker daemon streamed that output to the Docker client, which sent it
 to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
...

Listing 2-1Response from the docker run command on Windows

我们稍后将深入研究这些命令的含义,所以不要担心理解它们。如果您看到“安装似乎工作正常”的消息,您现在应该没事了。

使用 WSL2 后端在 Windows 上安装 Docker

关于 WSL

在 2016 年 Windows 周年更新中宣布,Windows Subsystem for Linux (WSL)是开发人员从 Windows 中运行 GNU/Linux 应用的一种方式,无需第三方虚拟机设置或必须双重启动到 Linux。WSL 支持大多数命令行应用,对 GUI 应用的支持仍处于早期预览模式。

在 WSL 的第一个版本中,微软捆绑了一个定制的兼容层,用于在 Windows 中运行 Linux 二进制可执行文件,而无需重写或重新编译应用的源代码。微软使用一个转换层来实现这一点,该转换层从 Linux 应用中截取 Linux 系统调用,并将它们转换成 Windows 系统调用。

对于 WSL2,微软通过发布一个带有 Linux 内核的轻量级虚拟机(VM ),完全重新构建了 WSL 的工作方式。这个轻量级 VM 充当 Linux 应用的执行层。由于 Linux 应用现在在轻量级虚拟机上的 Linux 内核上本地运行,而不是使用转换层,因此与 WSL 的第一版相比,WSL2 支持 Linux 内核的所有功能,并提高了 Linux 应用的性能。

虽然虚拟机带来了大量资源使用的问题,但 Windows 在幕后管理 WSL2 虚拟机,并完成动态内存分配,随着应用请求/释放内存,增加/减少内存消耗。WSL2 仍处于早期阶段,您可能会偶尔遇到一些问题/速度变慢或大量消耗内存。快速重启 Windows 可以缓解这些问题。您也可以关闭并重新启动虚拟机,这将使 Windows 释放 Windows 保留的内存。

安装和启用 WSL2 的要求

在安装 WSL2 之前,请确保您的计算机安装了 Windows 10 64 位版本 1903 或更高版本。WSL2 不能在低于 1903 的版本上工作。你可以在终端提示符下输入 winver 来检查版本,如图 2-2 所示。

img/463857_2_En_2_Fig2_HTML.jpg

图 2-2

检查红色框中突出显示的 Windows 版本

WSL2 的安装步骤详见微软网站 https://docs.microsoft.com/en-us/windows/wsl/install-win10 。按照手动安装步骤下列出的步骤安装 WSL2。我强烈建议您也安装 Windows 终端,就像在前面的链接中提到的那样,因为它使得在 WSL2 中运行 Docker 命令更加容易。

一旦安装了 WSL,运行以下命令以确保 WSL2 被设置为默认版本。

wsl --set-default-version 2

https://desktop.docker.com/win/stable/amd64/Docker%20Desktop%20Installer.exe 下载并运行安装程序,安装 Docker Desktop with WSL2 Backend。安装完成后,打开一个命令提示符窗口(或者 PowerShell,如果您喜欢的话),键入以下命令检查 Docker 是否已安装并正常工作。

docker run –rm hello-world

如果安装顺利,您应该会看到清单 2-2 中的响应。

docker run --rm hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

[...]

Listing 2-2Response from the docker run Command Using WSL

"Hello from Docker!"消息表明 Docker 已安装并正常工作。请注意,实际输出类似于清单 2-1 中的输出,并且在本例中已经进行了调整。

在 macOS 上安装

安装 Docker for Mac 就像安装任何其他应用一样。进入 https://store.docker.com/editions/community/docker-ce-desktop-mac ,点击 Get Docker for CE Mac (stable)链接,双击文件运行下载的安装程序。将 Docker whale 拖到 Applications 文件夹下安装,如图 2-3 所示。

img/463857_2_En_2_Fig3_HTML.png

图 2-3

安装 Docker

安装 Docker 后,打开终端应用并运行此命令以确认安装成功。

docker run --rm hello-world

如果安装顺利,您应该会看到清单 2-3 中所示的响应。

docker run --rm hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

[...]

Listing 2-3Response from the docker run Command on macOS

“Docker 工人向你问好!”消息表明 Docker 已安装并正常工作。请注意,实际的输出类似于清单 2-1 中的输出,在本例中已经进行了调整。

在 Linux 上安装

要在 Linux 上安装 Docker,请访问 https://www.docker.com/community-edition 。选择您正在使用的发行版,按照命令安装 Docker。

以下部分概述了在 Ubuntu 上安装 Docker 所需的步骤。

  1. 更新apt索引:

  2. 安装在 HTTPS 上使用存储库所需的必要软件包:

sudo apt-get update

  1. 安装 Docker 的官方 GPG 密钥:
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common

  1. 添加 Docker 的稳定存储库:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

  1. 更新apt包索引:
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

  1. 安装 Docker:
sudo apt-get update

sudo apt-get install docker-ce

附加步骤

Docker 通过 root 用户拥有的 UNIX 套接字进行通信。您可以通过以下步骤避免键入sudo:

Warning

Docker 组权限仍然等同于 root 用户。

  1. 创建 Docker 工人组:

  2. 将您的用户添加到docker组:

sudo groupadd docker

  1. 注销并重新登录。运行以下命令,确认 Docker 安装正确:
sudo usermod -aG docker $USER

docker run --rm hello-world

如果安装顺利,您应该会看到清单 2-4 中所示的响应。

docker run --rm hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.
[...]

Listing 2-4Response from the docker run Command on Linux

“Docker 工人向你问好!”消息表明 Docker 已安装并正常工作。请注意,实际的输出类似于清单 2-1 中的输出,在本例中已经进行了调整。

理解 Docker 的行话

现在,您已经安装并运行了 Docker,是时候学习与 Docker 相关的不同术语了。

是应用于由 Docker 文件中的指令表示的 Docker 图像的修改。通常,当基础图像发生变化时,会创建一个层。例如,考虑如下所示的 docker 文件:

FROM ubuntu
Run mkdir /tmp/logs
RUN apt-get install vim
RUN apt-get install htop

在这种情况下,Docker 会将ubuntu图像视为基础图像,并添加三层:

  • 一层用于创建/tmp/logs

  • 安装vim的另一层

  • 安装htop的第三层

当 Docker 构建映像时,每一层都堆叠在一起,并使用 union 文件系统合并成一个层。使用 SHA-256 哈希对图层进行唯一标识。这使得重用和缓存它们变得容易。当 Docker 扫描一个基本映像时,它会扫描组成该映像的所有层的 id,并开始下载这些层。如果图层存在于本地缓存中,它会跳过下载缓存的图像。

Docker 工人图像

Docker image 是一个只读模板,它构成了应用的基础。它非常像一个 shell 脚本,为系统准备了所需的状态。更简单地说,它相当于一份烹饪食谱,上面有制作最终菜肴的一步一步的说明。

Docker 映像从基础映像开始,通常选择您最熟悉的操作系统,如 Ubuntu。在这个映像之上,您可以添加构建您的应用堆栈,在需要时添加包。对于一些最常见的应用堆栈,有许多预构建的映像,包括 Ruby on Rails、Django、PHP-FPM 和nginx等等。在高级规模上,为了保持图像大小尽可能小,您还可以从诸如 Alpine 甚至 scratch 之类的超薄包开始,这是 Docker 为构建其他图像保留的最小起始图像。

Docker 映像是使用一系列称为指令的命令在一个称为 Dockerfile 的文件中创建的。在项目存储库的根中出现 Dockerfile 是一个很好的指示,表明程序是容器友好的。您可以从关联的 Dockerfile 文件构建自己的映像,然后将构建的映像发布到注册表。您将在后面的章节中深入了解 Dockerfile。现在,将 Docker 映像视为最终的可执行包,它包含了运行应用所需的一切——源代码、所需的库和依赖项。

Docker 标签

标签是唯一标识 Docker 图像的特定版本的名称。标签是纯文本标签,通常用于标识特定的细节,如版本、映像的基本操作系统或 Docker 映像的架构。

标记 Docker 映像使您能够灵活地唯一引用特定版本,从而在当前映像未按预期工作时更容易回滚到 Docker 映像的以前版本。

Docker 容器

当在主机中运行时,Docker 映像产生一个具有自己的名称空间的进程,并被称为 Docker 容器。Docker 映像和容器之间的主要区别是存在一个称为容器层的薄读写层。对容器的文件系统所做的任何更改——比如写入新文件或修改现有文件——都是对这个可写容器层进行的。

需要把握的一个重要方面是,当容器运行时,更改会应用到容器层,而当容器停止/终止时,容器层不会被保存。因此,所有更改都将丢失。容器的这一方面还没有被很好地理解,因此,有状态的应用和那些需要持久数据的应用最初不被推荐作为容器化的应用。然而,有了 Docker 卷,就有办法绕过这个限制。第五章更详细地介绍了 Docker 卷。

绑定装载和卷

回想一下,当容器运行时,对容器的任何更改都会出现在文件系统的容器层中。在容器被终止的情况下,更改会丢失,并且数据不再可访问。即使容器正在运行,从容器中获取数据也不是很简单。此外,写入容器的可写层需要一个存储驱动程序来管理文件系统。存储驱动程序在文件系统上提供了一个抽象,可用于保存更改,这种抽象通常会降低性能。

出于这些原因,Docker 提供了不同的方式将数据从 Docker 主机装载到容器中:卷、绑定装载或 tmpfs 卷。虽然 tmpfs 卷仅存储在主机系统的内存中,但绑定装载和卷存储在主机文件系统中。

第五章详细探讨了 Docker 卷。

Docker 存储库

您之前了解到可以利用常见应用堆栈的现有映像,您是否想过这些映像存储在哪里,以及如何在构建应用时使用它们?Docker 存储库是一个你可以上传和存储 Docker 图片的地方。这些存储库允许在您的公司内或向公众轻松分发 Docker 图像。

Docker 注册表

Docker 存储库需要一个中心位置来存储数据——这个中心位置是一个 Docker 注册表。Docker 注册中心是各种 Docker 存储库的集合。Docker 注册中心由第三方公司托管,如果您需要满足更严格的合规性要求,也可以自行托管。Docker Hub 是一个常用的 Docker 注册表。其他一些流行的 Docker 注册表包括:

  • 谷歌容器注册

  • 亚马逊弹性容器注册中心

  • JFrog Artifactory

这些注册表中的大多数还允许将您推送的图像的可见性级别设置为公共/私有。私人注册将防止您的 Docker 图像被公众访问,允许您设置访问控制,以便只有授权用户才能使用您的 Docker 图像。

Dockerfile

一个 Dockerfile 是一组指令,告诉 Docker 如何构建一个映像。典型的 Dockerfile 文件包括以下内容:

  • 一个FROM指令,指示 Docker 什么是基本图像

  • 传递环境变量的ENV指令

  • 运行一些 shell 命令的RUN指令(例如,安装基础映像中没有的相关程序)

  • 一个CMD或一个ENTRYPOINT指令,告诉 Docker 当一个容器启动时要运行什么可执行文件

正如你所看到的,Dockerfile 指令集有一个清晰简单的语法,这使得它很容易理解。在本书的后面,您将更深入地了解 Dockerfiles。

Docker 工人引擎

Docker 引擎是 Docker 的核心部分。Docker Engine 是一个客户端-服务器应用,它提供了平台、运行时和工具,用于构建和管理 Docker 映像、Docker 容器等等。Docker 引擎提供以下功能:

  • docker daemon(Docker 守护程序)

  • CLI Docker

  • Docker API

docker daemon(Docker 守护程序)

Docker 守护进程是一种在主机后台运行的服务,它处理大部分 Docker 命令的繁重工作。这个守护进程监听 API 创建和管理 Docker 对象(如容器、网络和卷)的请求。Docker 守护进程还可以与其他守护进程对话,以管理和监控 Docker 容器。守护进程间通信的一些例子包括用于容器度量监控的通信数据狗和用于容器安全监控的 Aqua。

CLI Docker

Docker CLI 是您与 Docker 交互的主要方式。Docker CLI 公开了一组您可以提供的命令。Docker CLI 将请求转发给 Docker 守护进程,后者执行必要的工作。

虽然 Docker CLI 包括大量的命令和子命令,但本书中提到的最常见的命令如下:

docker build
docker pull
docker run
docker exec

Tip

Docker 在其文档页面上的 https://docs.docker.com/engine/reference/commandline/cli/ 维护了所有 Docker 命令的广泛引用。

在任何时间点,在命令前面加上help将会打印出关于该命令的所需文档。例如,如果您不确定从哪里开始使用 Docker CLI,您可以键入以下内容:

docker help

Usage:  docker COMMAND

A self-sufficient runtime for containers

Options:
      --config string      Location of client config files (default
                           ".docker")
  -D, --debug              Enable debug mode
  -H, --host list          Daemon socket(s) to connect to
  -l, --log-level string   Set the logging level
                           ("debug"|"info"|"warn"|"error"|"fatal")
                           (default "info")
[..]

如果您想了解关于 Docker pull的更多信息,请键入以下内容:

docker help pull

Usage:  docker pull [OPTIONS] NAME[:TAG|@DIGEST]

Pull an image or a repository from a registry

Options:
  -a, --all-tags                Download all tagged images in the repository
      --disable-content-trust   Skip image verification (default true)
      --platform string         Set platform if server is multi-platform
                                capable

Docker API

Docker 还提供了与 Docker 引擎交互的 API。如果需要从应用内部创建或管理容器,这将非常有用。Docker CLI 支持的几乎所有操作都可以通过 API 来完成。

开始使用 Docker API 最简单的方法是使用curl发送一个 API 请求。Windows Docker 主机可以访问 TCP 端点:

curl http://localhost:23img/json
[{"Containers":-1,"Created":1511223798,"Id":"sha256:f2a91732366c0332ccd7afd2a5c4ff2b9af81f549370f7a19acd460f87686bc7","Labels":null,"ParentId":"","RepoDigests":["hello-world@sha256:66ef312bbac49c39a89aa9bcc3cb4f3c9e7de3788c944158df3ee0176d32b751"],"RepoTags":["hello-world:latest"],"SharedSize":-1,"Size":1848,"VirtualSize":1848}]

在 Linux 和 Mac 上,同样可以通过使用curl向 UNIX 套接字发送请求来实现:

curl --unix-socket /var/run/docker.sock -X POST httpimg/json

[{"Containers":-1,"Created":1511223798,"Id":"sha256:f2a91732366c0332ccd7afd2a5c4ff2b9af81f549370f7a19acd460f87686bc7","Labels":null,"ParentId":"","RepoDigests":["hello-world@sha256:66ef312bbac49c39a89aa9bcc3cb4f3c9e7de3788c944158df3ee0176d32b751"],"RepoTags":["hello-world:latest"],"SharedSize":-1,"Size":1848,"VirtualSize":1848}]

Docker Compose

Docker Compose 是一个定义和运行多容器应用的工具。就像 Docker 允许您为您的应用构建一个映像并在您的容器中运行它一样,Compose 使用相同的映像结合一个定义文件(称为 compose file )来构建、启动和运行多容器应用,包括依赖容器和链接容器。

Docker Compose 最常见的用例是以与运行单个容器应用相同的简单、简化的方式运行应用及其依赖的服务(如数据库和缓存提供者)。第七章深入了解 Docker Compose。

Docker 机器

Docker Machine 是一个用于在多个虚拟主机上安装 Docker 引擎和管理主机的工具。Docker Machine 允许在本地和远程系统上创建 Docker 主机,包括 Amazon Web Services、DigitalOcean 或 Microsoft Azure 等云平台。

动手 Docker 工人

你现在可以尝试一下你在本章中读到的一些东西。在您开始研究各种可用的命令之前,请确保您的 Docker 安装是正确的,并且它按预期工作。

Tip

为了便于阅读和理解,我们使用了一个名为jq的工具来处理 Docker 的 JSON 输出。可以从 https://stedolan.github.io/jq/ 下载安装jq

打开终端窗口,键入以下命令:

docker info

您应该会看到这样的结果:

docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 1
Server Version: 17.12.0-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host ipvlan macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 89623f28b87a6004d4b785663257362d1658a729
runc version: b2567b37d7b75eb4cf325b77297b140ea686ce8f
init version: 949e6fa
Security Options:
 seccomp
  Profile: default
Kernel Version: 4.9.60-linuxkit-aufs
Operating System: Docker for Windows
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 1.934GiB
Name: linuxkit-00155d006303
ID: Y6MQ:YGY2:VSAR:WUPD:Z4DA:PJ6P:ZRWQ:C724:6RKP:YCCA:3NPJ:TRWO
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): true
 File Descriptors: 19
 Goroutines: 35
 System Time: 2018-02-11T15:56:36.2281139Z
 EventsListeners: 1
Registry: https://index.docker.io/v1/
Labels:
Experimental: true
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

如果您没有看到类似的消息,请参考前面的章节来安装和验证您的 Docker 安装。

使用 Docker 图像

现在,您可以尝试查看可用的 Docker 图像。为此,请键入以下命令:

docker image ls

这里有一个本地可用图像的列表。

REPOSITORY     TAG       IMAGE ID       CREATED          SIZE
hello-world    latest    f2a91732366c   2 months ago     1.85kB

如果您提取更多的图像或运行更多的容器,您会看到一个更大的列表。再来看hello-world图。为此,请键入以下内容:

docker image inspect hello-world

 [
    {
        "Id": "sha256:f2a91732366c0332ccd7afd2a5c4ff2b9af81f549370f7a19acd460f87686bc7",
        "RepoTags": [
            "hello-world:latest"
        ],
        "RepoDigests": [
            "hello-world@sha256:66ef312bbac49c39a89aa9bcc3cb4f3c9e7de3788c944158df3ee0176d32b751"
        ],
        "Parent": "",
        "Comment": "",
        "Created": "2017-11-21T00:23:18.797567713Z",
        "Container": "fb0b4536aac3a96065e1bedb2b637a6019feec666c7699592206956c9d3adf5f",
        "ContainerConfig": {
            "Hostname": "fb0b4536aac3",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"/hello\"]"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:2243ee460b69c4c036bc0e42a48eaa59e82fc7737f7c9bd2714f669ef1f8370f",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {}
        },
        "DockerVersion": "17.06.2-ce",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/hello"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:2243ee460b69c4c036bc0e42a48eaa59e82fc7737f7c9bd2714f669ef1f8370f",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": null
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 1848,
        "VirtualSize": 1848,
        "GraphDriver": {
            "Data": {
                "MergedDir": "/var/lib/docker/overlay2/5855bd20ab2f521c39e1157f98f235b46d7c12c9d8f69e252f0ee8b04ac73d33/merged",
                "UpperDir": "/var/lib/docker/overlay2/5855bd20ab2f521c39e1157f98f235b46d7c12c9d8f69e252f0ee8b04ac73d33/diff",
                "WorkDir": "/var/lib/docker/overlay2/5855bd20ab2f521c39e1157f98f235b46d7c12c9d8f69e252f0ee8b04ac73d33/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:f999ae22f308fea973e5a25b57699b5daf6b0f1150ac2a5c2ea9d7fecee50fdf"
            ]
        },
        "Metadata": {
            "LastTagTime": "0001-01-01T00:00:00Z"
        }
    }
]

docker inspect提供了大量关于图像的信息。重要的是图像属性EnvCmdLayers,它们告诉您环境变量、容器启动时运行的可执行文件以及与之相关的层。

环境变量如下:

docker image inspect hello-world | jq .[].Config.Env
[
  "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
]

容器上的启动命令如下:

docker image inspect hello-world | jq .[].Config.Cmd
[
  "/hello"
]

与图像相关联的层如下:

docker image inspect hello-world | jq .[].RootFS.Layers
[
  "sha256:f999ae22f308fea973e5a25b57699b5daf6b0f1150ac2a5c2ea9d7fecee50fdf"
]

使用真实的 Docker 图像

让我们看一个更复杂的图像。Nginx 是一个非常流行的 HTTP/S 反向代理服务器,也是一个负载平衡器和 web 服务器。

要下拉nginx图像,请键入以下内容:

docker pull nginx

Using default tag: latest
latest: Pulling from library/nginx
e7bb522d92ff: Pull complete
6edc05228666: Pull complete
cd866a17e81f: Pull complete
Digest: sha256:285b4
Status: Downloaded newer image for nginx:latest

注意第一行:

Using default tag: latest

由于您没有提供标签,Docker 使用名为latest的默认标签。Docker Store 列出了与图像相关的不同标签——因此,如果您正在寻找特定的标签/版本,最好在 Docker Store 上查看。图 2-4 显示了一个图像的典型标签列表。

img/463857_2_En_2_Fig4_HTML.png

图 2-4

Docker 存储 nginx 和可用标签的列表

让我们尝试拉一个带有特定标签的图像,称为stable。命令与以前一样。您必须在标签后添加一个冒号,以明确提及该标签:

docker pull nginx:stable
stable: Pulling from library/nginx
b4d181a07f80: Already exists
e929f62bc938: Pull complete
ca8370516c99: Pull complete
6af693de7b22: Pull complete
c8fe6ce83489: Pull complete
7aa1fe8b4a84: Pull complete
Digest: sha256:a7c7c13
Status: Downloaded newer image for nginx:stable
docker.io/library/nginx:stable

您看到的不同十六进制数字是图像的相关层。默认情况下,Docker 从 Docker Hub 获取图像。您可以手动指定不同的注册表。如果 Docker 映像在 Docker Hub 上不可用,而是存储在其他地方,如本地托管的 artifactory,这将非常有用。要指定不同的注册表,您必须在映像名称前添加注册表路径。因此,如果注册表托管在docker-private-docker-registry.example.com上,那么pull命令现在将是:

docker pull private-docker-registry.example.com/nginx

如果注册中心需要认证,您可以通过输入凭证docker login来登录,如下所示:

docker login -u <username> -p <password> private-docker-registry.example.com

这样做的一个不利的副作用是,输入的密码会被记录下来,并以明文形式保存在 shell 历史记录中。Docker 会提醒你这条消息。

为了防止这种情况,您可以使用下面的命令将密码从一个文件传输到 Docker 的标准输入中,假设密码存储在一个名为docker_password的文件中

docker login -u <username> --password-stdin private-docker-registry.example.com < docker_password

使用 PowerShell 的 Windows 用户可以使用Get-Content cmdlet 实现相同的功能,如下所示:

Get-Content docker_password | docker login -u <username> --password-stdin private-docker-registry.example.com

现在有了图像,试着启动一个容器。要启动一个容器并运行相关的映像,请键入docker run

docker run -p 80:80 nginx

尝试发出一个 curl 请求,看看nginxweb 服务器是否正在运行:

curl http://localhost:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

这证实了nginx容器确实已经启动并正在运行。在这里,您会看到一个额外的标志,-p。这个标志告诉 Docker 将暴露的端口从 Docker 容器发布到主机。

标志后的第一个参数是 Docker 主机上必须发布的端口,第二个参数是指容器内的端口。您可以使用docker inspect:确认镜像发布端口

docker image inspect nginx | jq .[].Config.ExposedPorts
{
  "80/tcp": {}
}

您可以通过更改-p标志后的第一个参数来更改 Docker 主机上发布服务的端口,如下所示:

docker run -p 8080:80 nginx

现在尝试向 8080 端口发出一个curl请求:

curl http://localhost:8080

您应该会看到相同的响应。要列出所有正在运行的容器,请键入docker ps:

docker ps

docker ps
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
fac5e92fdfac  nginx  "nginx -g 'daemon of…"  5 seconds ago       Up 3 seconds  0.0.0.0:80->80/tcp     elastic_hugle
3ed1222964de  nginx  "nginx -g 'daemon of…"  16 minutes agoUp 16 minutes 0.0.0.0:8080->80/tcp   clever_thompson

需要注意的一点是names列。Docker 会在容器启动时自动分配一个随机名称。由于您应该使用更有意义的名称,您可以通过提供-n required-name作为参数来为容器提供一个名称。

Tip

Docker 名称的格式为adjective_surname,并且是随机生成的,例外情况是,如果选择的形容词是boring并且姓氏是Wozniak,Docker 会重试名称生成。

需要注意的另一点是,当您创建第二个容器,并将其端口发布到端口 8080 时,另一个容器将继续运行。要停止容器,您必须键入以下命令:

docker stop <container-id>

其中container-id可从该列表中获得。如果停靠成功,Docker 将回显容器 ID。如果容器拒绝停止,您可以发出kill命令强制停止并杀死容器:

docker kill <container-id>

让我们试着停止一个容器。键入以下内容:

docker stop fac5e92fdfac
fac5e92fdfac

现在,让我们试着杀死另一个容器:

docker kill 3ed1222964de
3ed1222964de

让我们确认容器不再运行,为此,键入:

docker ps

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

那么,被拦下的容器呢——它们在哪里?默认情况下,docker ps只显示活动的、正在运行的容器。要列出所有容器,请键入:

docker ps -a
CONTAINER ID        IMAGE         COMMAND          CREATEDSTATUS              PORTS         NAMES
fac5e92fdfac        nginx         "nginx -g 'daemon of…"6 minutes ago       Exited (0) 4 minutes ago    elastic_hugle
3ed1222964de        nginx         "nginx -g 'daemon of…"22 minutes ago      Exited (137) 3 minutes ago  clever_thompson
febda50b0a80        nginx         "nginx -g 'daemon of…"28 minutes ago      Exited (137) 24 minutes agoobjective_franklin
dc0c33a79fb7        nginx         "nginx -g 'daemon of…"33 minutes ago      Exited (137) 28 minutes ago   vigorous_mccarthy
179f16d37403        nginx         "nginx -g 'daemon of…"34 minutes ago      Exited (137) 34 minutes ago     nginx-test

即使容器已经被停止和/或终止,这些容器仍然存在于本地文件系统中。您可以通过键入以下命令来删除容器:

docker rm <container-id>
docker rm fac5e92fdfac
fac5e92fdfac

让我们确认容器确实被移走了:

docker ps -a
CONTAINER ID        IMAGE         COMMAND             CREATED             STATUS              PORTS         NAMES
3ed1222964de        nginx         "nginx -g 'daemon of…"28 minutes ago      Exited (137) 9 minutes ago  clever_thompson
febda50b0a80        nginx         "nginx -g 'daemon of…"34 minutes ago      Exited (137) 30 minutes ago      objective_franklin
dc0c33a79fb7        nginx         "nginx -g 'daemon of…"39 minutes ago      Exited (137) 34 minutes agovigorous_mccarthy
179f16d37403        nginx         "nginx -g 'daemon of…"40 minutes ago      Exited (137) 40 minutes ago     nginx-test

从这个表中可以看到,ID 为fac5e92fdfac的容器不再显示,因此已经被删除。

同样,您可以通过键入以下命令列出系统中存在的所有图像:

docker image ls
REPOSITORY         TAG      IMAGE ID        CREATED        SIZE
nginx              1.12-alpine-perl          b6a456f1d7ae 4 weeks ago        57.7MB
nginx              latest   3f8a4339aadd    6 weeks ago   108MB
hello-world        latest   f2a91732366c    2 months ago  1.85kB
kitematic/hello-world-nginx   latest        03b4557ad7b9 2 years ago        7.91MB

让我们试着移除nginx图像:

docker rmi 3f8a4339aadd
Error response from daemon: conflict: unable to delete 3f8a4339aadd (must be forced) - image is being used by stopped container dc0c33a79fb7

在这种情况下,Docker 拒绝删除图像,因为另一个容器中存在对该图像的引用。在移除所有使用特定图像的容器之前,您不能完全移除该图像。

摘要

在本章中,你学习了如何在不同的操作系统上安装 Docker。您还了解了如何验证 Docker 是否已安装并正常工作,以及一些与 Docker 相关的常用术语。最后,您在 Docker 上运行了一些实践练习,包括如何提取图像、运行容器、列出正在运行的容器,以及最后如何停止和删除容器。

下一章简要介绍 telegram,包括如何使用 telegram 创建和注册一个 bot,以及如何运行基于 Python 的 Telegram 消息 bot,它将从 Reddit 获取帖子。

三、构建 Python 应用

对于许多开始编程的人来说,他们的首要问题之一是弄清楚他们能构建什么。仅仅通过阅读很少能学会编程。许多人认为他们可以阅读几本指南,看看语法,然后轻松地学习如何编程。但是编程需要动手实践。

因此,本书包含了一个示例 Python 项目。该项目在开始时并不复杂,但随着经验的积累,很容易对项目进行进一步的扩展和定制。

关于项目

Note

本书假设你具备 Python 的基础知识,并且已经安装了 Python 3.6 或更高版本。

为了帮助您熟悉 Docker,这本书将教您如何使用现有的 Python 应用,从 Python 命令行运行它,介绍不同的 Docker 组件,然后将应用转换为容器化的映像。

Python 应用是一个简单的应用,具有 bot 接口,使用 Telegram Messenger 从 Reddit 获取最近 10 篇文章。使用 Telegram,您可以订阅子编辑列表。web 应用将检查新帖子的订阅子编辑,如果发现新主题,它会将主题发布到 bot 接口。当用户请求时,该接口将把消息传送到电报信使。

最初,您不会保存首选项(即 subreddit 订阅),而是将重点放在启动和运行机器人上。一旦一切正常,您将学习如何将首选项保存到文本文件中,并最终保存到数据库中。

设置电报信使

在您继续之前,您需要一个电报信使帐户。要注册,请转到 https://telegram.org ,为您选择的平台下载应用并安装。一旦它运行,你将被要求提供一个手机号码。Telegram 用这个来验证你的帐户。输入您的手机号码,如图 3-1 所示。

img/463857_2_En_3_Fig1_HTML.png

图 3-1

电报注册页面

输入您的号码后,您应该会获得一个一次性密码来登录。输入一次性密码并登录,如图 3-2 所示。

img/463857_2_En_3_Fig2_HTML.png

图 3-2

电报的一次性密码

BotFather: Telegram 的机器人创建接口

Telegram 使用一个名为“机器人父亲”的机器人作为其创建新机器人和更新它们的接口。要开始使用 BotFather,请在搜索面板中键入BotFather。在聊天窗口中,输入/start

这将触发 BotFather 提供一组介绍性的消息,如图 3-3 所示。

img/463857_2_En_3_Fig3_HTML.png

图 3-3

父亲的选择

用机器人父亲创建机器人

您将使用 BotFather 生成一个新的机器人。首先在电报信使中输入/newbot。这会引发一系列你需要回答的问题(大部分都很直白)。由于 Telegram 的限制,机器人的用户名必须总是以bot结尾。这意味着你可能得不到你想要的用户名(见图 3-4 )。

img/463857_2_En_3_Fig4_HTML.png

图 3-4

电报机器人准备行动

除了文档的链接,您还会注意到 Telegram 发布了一个令牌。HTTP 是一种无状态协议——web 服务器不知道也不跟踪谁在请求资源。客户机需要标识自己,以便 web 服务器可以标识客户机、授权客户机并为请求提供服务。Telegram 使用 API 令牌(以下简称为<token>,包括在代码示例中)作为一种识别和授权机器人的方式。

Caution

令牌极其敏感。如果它被泄露到网上,任何人都可以作为你的机器人发布消息。不要将其签入您的版本控制或发布到任何地方!

当使用您不熟悉的 API 时,最好使用一个好的工具来测试和探索端点,而不是马上输入代码。REST API 测试工具的一些例子包括失眠邮差卷曲

Telegram 的 Bot API 文档可以在 https://core.telegram.org/bots/api 获得。要提出请求,您必须包含<token>。常规 URL 如下:

https://api.telegram.org/bot<token>/METHOD_NAME

让我们尝试一个样例 API 请求,它确认令牌按预期工作。Telegram Bot API 提供了一个用于测试auth令牌的/getMe端点。您可以尝试一下,首先不用令牌,如清单 3-1 所示。

curl https://api.telegram.org/bot/getMe

{
  "ok": false,
  "error_code": 404,
  "description": "Not Found"
}

Listing 3-1Making a curl Request to Telegram API Without a Token

如果没有机器人令牌,Telegram 就不会接受这个请求。现在试试这个令牌,如清单 3-2 所示。

curl https://api.telegram.org/bot<token>/getMe

{
  "ok": true,
  "result": {
    "id": 495637361,
    "is_bot": true,
    "first_name": "SubRedditFetcherBot",
    "username": "SubRedditFetcher_Bot"
  }
}

Listing 3-2Making a curl Request to Telegram API with a Valid Token

您可以看到,使用适当的令牌,Telegram 可以识别并授权机器人。这确认了 bot 令牌是正确的,您可以继续使用该应用。

新闻机器人:Python 应用

Newsbot 是一个 Python 脚本,在 Telegram bot API 的帮助下与 Bot 进行交互。新闻机器人做以下事情:

  • 持续轮询 Telegram API 以获取发布到 bot 的新更新。

  • 如果检测到获取新更新的关键字,它将从选定的子编辑中获取新闻。

在幕后,Newsbot 处理这些场景:

  • 如果有一条以/start/help开头的新消息,它会显示关于该做什么的简单帮助文本。

  • 如果有一条消息以/sources开头,后跟一个子编辑列表,那么它接受它们作为来自适用的 Reddit 帖子的子编辑。

Newsbot 依赖于几个 Python 库;

  • Praw 或 Python Reddit API 包装器,用于从子编辑中获取帖子。

  • Requests 是最流行的 Python 库之一,它为 HTTP 请求提供了更简单、更干净的 API。

新闻机器人入门

要开始使用 Newsbot,请下载该 bot 的源代码。源代码可以在本书的 GitHub 资源库中找到,在 https://github.com/Apress/practical-docker-with-python

如果您熟悉 Git,可以使用以下命令克隆 repo:

git clone https://github.com/Apress/practical-docker-with-python.git

或者,您可以单击绿色的 Code 按钮,并从 GitHub 存储库页面选择 Download ZIP 来获取源代码。一旦克隆了 repo 或解压了 ZIP 文件,通过键入以下命令切换到包含源代码的目录:

cd practical-docker-with-python/source-code/chapter-3/python-app

现在安装依赖项。为此,请键入以下内容:

pip3 install -r requirements.txt

pip (Pip 安装包)是一个安装 Python 库的包管理器。pip 包含在 Python 2.7.9 和更高版本以及 Python 3.4 和更高版本中。 pip3 表示您正在为 Python 3 安装库。如果没有安装 pip,请在继续之前安装它。

-r标志告诉 pip 从requirements.txt安装所需的包。

pip 将检查、下载和安装依赖项。如果一切顺利,您应该会看到清单 3-3 中的输出。

Collecting praw==3.6.0 (from -r requirements.txt (line 1))
  Downloading praw-3.6.0-py2.py3-none-any.whl (74kB)
Collecting requests==2.18.4 (from -r requirements.txt (line 2))
[...]
Installing collected packages: requests, update-checker, decorator, six, praw
Successfully installed decorator-4.0.11 praw-3.6.0 requests-2.18.4 six-1.10.0 update-checker-0.16

Listing 3-3The Output from a Successful pip Install

如果已经安装了一些包,pip 将不会重新安装它们,并且会用一条"Requirement already satisfied"消息通知您已经安装了依赖项。

运行新闻机器人

让我们启动机器人。bot 需要一个来自您之前创建的 Telegram 的认证令牌(称为<token>)。这需要设置为一个名为NBT_ACCESS_TOKEN的环境变量。没有这个令牌,机器人将不会运行。要设置此令牌,请打开一个终端,并根据您的平台输入以下命令。

Windows 用户:

setx NBT_ACCESS_TOKEN <token>

Linux 和 macOS 用户:

export NBT_ACCESS_TOKEN=<token>

现在,通过键入以下命令启动 Python 脚本:

python newsbot.py

如果一切正常,您应该会看到周期性的 OK 消息,如清单 3-4 所示。这意味着 Newsbot 正在运行,并且正在主动侦听更新。

python newsbot.py
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}

Listing 3-4Output from Newsbot When It Is Running and Listening to Messages from Telegram

向新闻机器人发送消息

在本节中,您将尝试向 Newsbot 发送一条消息,看看它是否接受请求。在“机器人父亲”窗口中,单击指向机器人的链接(或者,您也可以使用机器人用户名进行搜索)。单击开始按钮。这将触发一个/start命令,该命令将被机器人拦截。

注意,日志窗口显示了传入的请求和正在发送的传出消息,如清单 3-5 所示。

INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': [{'update_id': 720594461, 'message': {'message_id': 5, 'from': {'id': 7342383, 'is_bot': False, 'first_name': 'Sathya', 'last_name': 'Bhat', 'username': 'sathyabhat', 'language_code': 'en-US'}, 'chat': {'id': 7342383, 'first_name': 'Sathya', 'last_name': 'Bhat', 'username': 'sathyabhat', 'type': 'private'}, 'date': 1516558659, 'text': '/start', 'entities': [{'offset': 0, 'length': 6, 'type': 'bot_command'}]}}]}
INFO: handle_incoming_messages - Chat text received: /start
INFO: post_message - posting
                    Hi! This is a News Bot which fetches news from subreddits. Use "/source" to select a subreddit source.

 Example "/source programming, games" fetches news from r/programming, r/games.

 Use "/fetch" for the bot to go ahead and fetch the news. At the moment, bot will fetch total of 10 posts from all subreddits
                 to 7342383
INFO: get_updates - received response: {'ok': True, 'result': []}

Listing 3-5The Newsbot Responding to Commands

图 3-5 显示电报信使窗口。

img/463857_2_En_3_Fig5_HTML.png

图 3-5

新闻机器人对开始消息的响应

尝试设置一个源子编辑。在电报信使窗口中,键入以下内容:

/source python

你应该从机器人那里得到一个肯定的确认,说源被选中了(见图 3-6 )。

img/463857_2_En_3_Fig6_HTML.jpg

图 3-6

分配的来源

现在你可以告诉机器人获取一些消息。为此,请键入:

/fetch

机器人应该发送一个关于获取帖子的确认消息。然后它将发布来自 Reddit 的帖子(见图 3-7 )。

img/463857_2_En_3_Fig7_HTML.png

图 3-7

新闻机器人发布来自 Python subreddit 的头条新闻

bot 有效;正如预期的那样,它获得了最高职位。在接下来的一系列章节中,您将学习如何将 Newsbot 移动到 Docker。

摘要

在这一章中,你学习了本书的 Python 项目的细节,它是一个聊天机器人。您还了解了如何安装和配置 Telegram Messenger,如何使用 Telegram 的 BotFather 创建 bot,如何安装 bot 的依赖项,以及如何运行 bot 并确保其正常工作。在下一章中,您将深入了解 Docker,了解关于 Dockerfiles 的更多信息,并通过为其编写 Dockerfile 来将 Newsbot 应用容器化。

四、了解 Dockerfile 文件

现在您对 Docker 及其相关术语有了更好的理解,本章将向您展示如何使用 Docker 将您的项目转换成容器化的应用。在本章中,您将学习 Dockerfile 是什么,包括它的语法,并学习如何编写 Dockerfile。对 Dockerfiles 有了更好的理解,你就可以开始为 Newsbot 应用编写 docker files 的第一步了。

Dockerfile First

对于传统部署的应用,构建和打包应用通常非常繁琐。为了自动化应用的构建和打包,人们求助于不同的工具,如 GNU Make、maven、Gradle 等等,来构建应用包。类似地,在 Docker 世界中,Docker 文件是构建 Docker 映像的自动化方式。

Docker 文件包含特殊指令,告诉 Docker 引擎构建映像所需的步骤。要使用 Docker 调用构建,可以发出Docker build命令。清单 4-1 显示了一个典型的 Dockerfile 文件。

FROM ubuntu:latest
LABEL author="sathyabhat"
LABEL description="An example Dockerfile"
RUN apt-get install python
COPY hello-world.py
CMD python hello-world.py

Listing 4-1A Typical Dockerfile

查看这个 Docker 文件,很容易看到我们告诉 Docker 引擎构建什么。但是,不要让简单性欺骗了你 Docker 文件让你在生成 Docker 图像时构建复杂的条件。当发出一个Docker build命令时,它从 Docker 文件和一个构建上下文构建 Docker 映像。

构建上下文

一个构建上下文是一个或一组在特定路径或 URL 上可用的文件。为了更好地理解这一点,假设您有一些在 Docker 映像构建期间需要的支持文件——例如,之前生成的特定于应用的配置文件,它需要成为容器的一部分。

构建上下文可以是本地的,也可以是远程的——您甚至可以将构建上下文设置为 Git 存储库的 URL,如果源文件与 Docker 守护进程不在同一个主机上,或者如果您想要测试特性分支,这将非常方便。您只需设置分支的上下文。build命令如下所示:

docker build https://github.com/sathyabhat/sample-repo.git#mybranch

类似地,要构建基于 Git 标签的图像,build命令应该是这样的:

docker build https://github.com/sathyabhat/sample-repo.git#mytag

通过拉取请求处理功能?想试试那个拉请求吗?没问题,您甚至可以将上下文设置为 pull 请求:

docker build https://github.com/sathyabhat/sample-repo.git#pull/1337/head

build命令将上下文设置为所提供的路径或 URL,上传 Docker 守护进程可用的文件,并允许它构建映像。您不限于 URL 或路径的构建上下文。如果您将一个 URL 传递给一个远程的 tarball(即一个.tar文件),位于该 URL 的 tarball 将被下载到 Docker 守护进程中,并发出build命令,以此作为构建上下文。

Caution

如果您在根(/)目录下提供 Docker 文件并将其设置为上下文,那么这样做会将您的硬盘内容传输到 Docker 守护进程。

Dockerignore

现在您应该明白,在构建过程中,构建上下文将当前目录的内容转移到 Docker 守护进程。考虑这样一种情况,其中上下文目录有许多与构建过程无关的文件/目录。上传这些文件/目录会导致网络流量显著增加。Dockerignore 文件很像 gitignore ,允许您定义在构建过程中免于传输的文件。

忽略列表由名为.dockerignore的文件提供,当 Docker CLI 找到该文件时,它会修改上下文以排除文件中提供的文件/模式。任何以散列(#)开头的内容都被视为注释并被忽略。下面的代码片段显示了一个示例.dockerignore文件,它不包括temp.git.DS_Store目录:

*/temp*
.DS_Store
.git

构建工具包

随着 Docker 引擎 18.09 版本的发布,Docker 使用 BuildKit 彻底检查了他们的容器构建系统。BuildKit 现在是 Docker 的默认构建系统。对于大多数用户来说,BuildKit 与遗留构建系统完全一样。BuildKit 为 Docker 映像构建提供了一个新的命令输出,因此提供了关于构建过程的更详细的反馈。

如果您看到与其他学习资源不同的输出,这意味着它们可能没有使用 BuildKit 的输出进行更新。BuildKit 还试图尽可能并行化构建步骤,因此您可以期待更快的构建速度,尤其是对于具有大量 Dockerfile 指令的容器。对于高级用户,BuildKit 还引入了将秘密传递到构建阶段的能力,而秘密不在最终层中。当使用 BuildKit 时,构建输出如清单 4-2 所示。(注意,由于空间限制,sha输出已被截断。)

docker build .
[+] Building 11.6s (6/6) FINISHED
 => [internal] load build definition from Dockerfile 0.1s
 => => transferring dockerfile: 84B   0.0s
 => [internal] load .dockerignore  0.1s
 => => transferring context: 2B 0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest 8.7s
 => [auth] library/ubuntu:pull token for registry-1.docker.io 0.0s
 => [1/1] FROM docker.io/library/ubuntu:latest@sha256:aba80b7 2.7s
 => => resolve docker.io/library/ubuntu:latest@sha256:aba80b7 0.0s
 => => sha256:aba80b7 1.20kB / 1.20kB 0.0s
 => => sha256:376209 529B / 529B  0.0s
 => => sha256:987317 1.46kB / 1.46kB 0.0s
 => => sha256:c549ccf8 28.55MB / 28.55MB  1.1s
 => => extracting sha256:c549ccf   1.2s
 => exporting to image 0.0s
 => => exporting layers   0.0s
 => => writing image sha256:f2afdc

Listing 4-2Build Output When BuildKit Is Enabled

在撰写本章时,仍然可以通过设置DOCKER_BUILDKIT标志切换回遗留构建过程,如清单 4-3 所示。

DOCKER_BUILDKIT=0 docker build .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM ubuntu:latest
latest: Pulling from library/ubuntu
c549ccf8d472: Already exists
Digest: sha256:aba80b77e27148d99c034a987e7da3a287ed455390352663418c0f2ed40417fe
Status: Downloaded newer image for ubuntu:latest
 ---> 9873176a8ff5
Step 2/2 : CMD echo Hello World!
 ---> Running in d5ca2635eecd
Removing intermediate container d5ca2635eecd
 ---> 77711564634f
Successfully built 77711564634f

Listing 4-3Switching Back to the Legacy Build Process

除非您遇到任何问题,否则我不建议切换回遗留构建过程。坚持使用 Docker 构建工具包。如果您没有看到新的构建输出,请确保您已经更新到 Docker 的最新版本。

使用 Docker Build 构建

稍后您将返回到示例 docker 文件。让我们先从一个简单的 Dockerfile 开始。将以下代码片段复制到一个文件中,并另存为Dockerfile:

FROM ubuntu:latest
CMD echo Hello World!

现在使用docker build命令构建这个图像。您将看到如清单 4-4 所示的响应。(请注意,sha输出已被截断。)

 docker build .
[+] Building 11.6s (6/6) FINISHED
 => [internal] load build definition from Dockerfile0.1s
 => => transferring dockerfile: 84B  0.0s
 => [internal] load .dockerignore 0.1s
 => => transferring context: 2B0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest 8.7s
 => [auth] library/ubuntu:pull token for registry-1.docker.io 0.0s
 => [1/1] FROM docker.io/library/ubuntu:latest@sha256:aba80b7 2.7s
 => => resolve docker.io/library/ubuntu:latest@sha256:aba80b7 0.0s
 => => sha256:aba80b7 1.20kB / 1.20kB 0.0s
 => => sha256:376209 529B / 529B 0.0s
 => => sha256:987317 1.46kB / 1.46kB 0.0s
 => => sha256:c549ccf8 28.55MB / 28.55MB 1.1s
 => => extracting sha256:c549ccf  1.2s
 => exporting to image0.0s
 => => exporting layers  0.0s
 => => writing image sha256:f2afdc

Listing 4-4Response from Docker Engine as it Builds the Dockerfile

您可以看到 Docker 构建是分步进行的,每一步都对应于 Docker 文件中的一条指令。现在再次尝试构建过程。

docker build .
[+] Building 0.1s (5/5) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 37B   0.0s
=> [internal] load .dockerignore  0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:latest 0.0s
=> CACHED [1/1] FROM docker.io/library/ubuntu:latest   0.0s
=> exporting to image 0.0s
=> => exporting layers   0.0s
=> => writing image sha256:f2afdcc   0.0s

请注意第二次构建过程的速度有多快。Docker 已经缓存了层,不用再拉了。要运行这个映像,使用docker run命令,后跟映像 ID f2afdcc:

docker run f2afdcc
Hello World!

因此,Docker 运行时能够启动一个容器并运行由CMD指令定义的命令;因此,您得到了输出。现在,通过键入图像 ID 从图像启动容器变得很快。您可以用一个容易记住的名称来标记图像,这样会更容易。您可以通过使用docker tag命令来做到这一点,如下所示:

docker tag <image id> <tag name>
docker tag f2afdcc sathyabhat/hello-world

在下一节中,您将更深入地了解标记。Docker 还验证 docker 文件是否有有效的指令,以及它们的语法是否正确。考虑前面的 Dockerfile 文件,如清单 4-5 所示。

FROM ubuntu:latest
LABEL author="sathyabhat"
LABEL description="An example Dockerfile"
RUN apt-get install python
COPY hello-world.py
CMD python hello-world.py

Listing 4-5Dockerfile for Python with an Invalid Instruction

如果您尝试构建这个 Docker 文件,Docker 会报错,如下所示:

docker build -f Dockerfile.invalid .
[+] Building 0.1s (2/2) FINISHED
=> [internal] load build definition from Dockerfile.invalid  0.0s
=> => transferring dockerfile: 336B  0.0s
=> [internal] load .dockerignore  0.0s
=> => transferring context: 2B 0.0s
failed to solve with frontend dockerfile.v0: failed to create LLB definition: dockerfile parse error line 6:
COPY requires at least two arguments, but only one was provided. Destination could not be determined.

在本章的稍后部分,您将回到解决这个问题。现在,是时候看看一些常用的 Dockerfile 指令和标记图像了。

标签

标签是唯一标识 Docker 图像的特定版本的名称。标签是纯文本标签,通常用于标识特定的细节,如版本、映像的基本操作系统或 Docker 映像的架构。标记 Docker 映像使您能够灵活地引用特定版本,这样,如果当前映像没有按预期工作,就可以更容易地回滚到 Docker 映像的以前版本。

如果没有指定标签,Docker 将应用一个名为"latest"的字符串作为默认标签。标签通常是许多问题的来源,尤其是对于 Docker 的新用户。许多人认为将"latest"作为标签意味着 Docker 图像是图像的最新版本,并且会一直更新到最新版本。这是不正确的— latest被选为惯例,但并没有任何特殊的意义。

我不建议使用latest作为标签,尤其是对于生产工作负载。在开发阶段,省略标签将导致"latest"标签被应用到每个构建中。如果有重大变化,由于标签是通用的,先前的图像将被覆盖。这使得回滚到映像的前一个版本非常困难,除非您注意到了映像的 SHA-hash。使用特定的标签可以更容易地确定容器上运行的是哪个标签或 Docker image 的哪个版本。使用特定的标签也减少了破坏性改变被传播的机会,特别是如果你把你的图像标记为latest,并且有一个破坏性的改变或错误。下次您的容器崩溃或重启时,它可能会提取带有重大更改或错误的图像。

可以使用docker tag命令标记和重新标记 Docker 图像:

docker tag <image id> <tag name>
docker tag f2afdcc sathyabhat/hello-world

标记名通常会有 Docker 注册表作为标记名的前缀。如果没有指定注册表名称,Docker 将假设该映像是 Docker Hub 的一部分,并将尝试从那里提取它。标签可以作为构建过程的一部分通过传递-t标志来分配,如清单 4-6 所示。

docker build -t sathyabhat/helloworld .

[+] Building 0.2s (5/5) FINISHED
=> [internal] load build definition from Dockerfile0.0s
=> => transferring dockerfile: 37B  0.0s
=> [internal] load .dockerignore 0.1s
=> => transferring context: 2B0.0s
=> [internal] load metadata for docker.io/library/ubuntu:latest0.0s
=> CACHED [1/1] FROM docker.io/library/ubuntu:latest  0.0s
=> exporting to image 0.0s
=> => exporting layers  0.0s
=> => writing image sha256:f2afdcc 0.0s
=> => naming to docker.io/sathyabhat/helloworld

Listing 4-6Adding a Tag When Building the Image

请注意,尽管您没有将docker.io作为标记的一部分,但是它被添加到了标记名的前面。最后一行告诉您该图像已被成功标记。您可以通过搜索docker images来验证这一点:

docker images sathyabhat/helloworld
REPOSITORY              TAG      IMAGE ID        CREATED      SIZE
sathyabhat/helloworld   latest   f2afdccf8eeb   3 weeks ago   72.7MB

dockerfile instructions(Docker 文件说明)

当查看 Dockerfile 文件时,您很可能会遇到以下指令。

  • FROM

  • ADD

  • COPY

  • RUN

  • CMD

  • ENTRYPOINT

  • ENV

  • VOLUME

  • LABEL

  • EXPOSE

让我们看看他们怎么做。

正如您之前了解到的,每个图像都需要从基础图像开始。FROM指令告诉 Docker 引擎用于后续指令的基本映像。每个有效的 Dockerfile 必须以一个FROM指令开始。语法如下:

FROM <image> [AS <name>]

运筹学

FROM <image>[:<tag>] [AS <name>]

运筹学

FROM <image>[@<digest>] [AS <name>]

其中<image>是来自任何公共/私有存储库的有效 Docker 映像的名称。如上所述,如果标签被跳过,Docker 将获取标签为latest的图像。

工作目录

WORKDIR指令为RUNCMDENTRYPOINTCOPYADD指令设置当前工作目录。当您在源代码中有多个目录,并且您希望在这些特定的目录中完成一些特定的操作时,WORKDIR非常有用。WORKDIR也常用来为应用在容器中运行设置一个单独的位置。语法如下:

WORKDIR /path/to/directory

WORKDIR可以在 Dockerfile 文件中多次设置,如果相对目录在前面的WORKDIR指令之后,它将相对于前面设置的工作目录。让我们看一个例子来证明这一点。

考虑这个 Dockerfile 文件:

FROM ubuntu:latest
WORKDIR /app
CMD pwd

Dockerfile 从 Ubuntu 获取latest标记的图像作为基础图像,将当前工作目录设置为/app,并在图像运行时运行pwd命令。pwd命令打印当前工作目录。

让我们试着构建并运行它,并检查输出:

docker build -t sathybhat/workdir .
[+] Building 0.7s (6/6) FINISHED
 => [internal] load build definition from Dockerfile  0.0s
 => => transferring dockerfile: 36B 0.0s
 => [internal] load .dockerignore0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest  0.6s
 => [1/2] FROM docker.io/library/ubuntu:latest@sha256:b3e2e4  0.0s
 => CACHED [2/2] WORKDIR /app 0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:f8853df 0.0s
 => => naming to docker.io/sathybhat/workdir

现在您运行新构建的映像:

docker run sathybhat/workdir
/app

pwd的结果表明,通过WORKDIR指令将当前工作目录设置为/app。修改 Dockerfile 文件以添加几条WORKDIR指令,如下所示:

FROM ubuntu:latest
WORKDIR /usr
WORKDIR src
WORKDIR app
CMD pwd

让我们构建并运行新的映像:

docker build -t sathybhat/workdir .

[+] Building 0.7s (8/8) FINISHED
 => [internal] load build definition from Dockerfile  0.0s
 => => transferring dockerfile: 121B  0.0s
 => [internal] load .dockerignore 0.0s
 => => transferring context: 2B 0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest  0.6s
 => [1/4] FROM docker.io/library/ubuntu:latest@sha256:b3e2e47  0.0s
 => CACHED [2/4] WORKDIR /usr 0.0s
 => CACHED [3/4] WORKDIR src  0.0s
 => CACHED [4/4] WORKDIR app  0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:207b405  0.0s
 => => naming to docker.io/sathyabhat/workdir

请注意,图像 ID 已经更改,因此这是一个使用相同标记构建的新图像:

docker run sathybhat/workdir
/usr/src/app

不出所料,相对目录的WORKDIR指令已经附加到初始绝对目录集。默认情况下,WORKDIR被设置为/,所以任何具有相对目录的WORKDIR指令将被附加到/。这里有一个例子来说明这一点。让我们按如下方式修改 Dockerfile 文件:

FROM ubuntu:latest
WORKDIR var
WORKDIR log/nginx
CMD pwd

建立形象:

docker build -t sathyabhat/workdir .

[+] Building 1.8s (8/8) FINISHED
 => [internal] load build definition from Dockerfile   0.0s
 => => transferring dockerfile: 115B   0.0s
 => [internal] load .dockerignore   0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest  1.6s
 => [auth] library/ubuntu:pull token for registry-1.docker.io  0.0s
 => CACHED [1/3] FROM docker.io/library/ubuntu:latest@sha256:b3e2e47 0.0s
 => [2/3] WORKDIR var   0.0s
 => [3/3] WORKDIR log/nginx   0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:e7ded5d 0.0s
 => => naming to docker.io/sathyabhat/workdir

现在运行它:

docker run sathyabhat/workdir
/var/log/nginx

注意,您没有在 Dockerfile 文件中设置任何绝对工作目录——相对目录被附加到默认目录中。

添加并复制

乍一看,ADDCOPY指令似乎是相同的——它们允许您将文件从主机传输到容器的文件系统。COPY支持将文件基本复制到容器,而ADD支持 tarball 自动提取(即 Docker 将自动提取从本地目录添加的压缩文件)和远程 URL 支持(即 Docker 将从远程 URL 下载资源)等功能。

两者的语法非常相似:

ADD <source> <destination>
COPY  <source> <destination>

当您从远程 URL 添加文件或者您有来自本地文件系统的压缩文件需要自动提取到容器文件系统中时,ADD指令很有用。

例如,下面的COPY指令将一个名为hugo的文件复制到容器中的/app目录:

COPY hugo /app/

下面的ADD指令从 URL 获取一个名为hugo_0.88.0_Linux-64bit.tar.gz的压缩文件,但不会自动解压缩该文件:

ADD https://github.com/gohugoio/hugo/releases/download/v0.88.0/hugo_0.88.0_Linux-64bit.tar.gz /app/

而下面的ADD指令会将压缩文件的内容复制并自动提取到容器中的/app目录。

ADD hugo_0.88.0_Linux-64bit.tar.gz /app/

对于用于构建 Linux 容器的 owner 文件,这两条指令都允许您更改添加到容器中的文件的所有者/组。这是使用--chown标志完成的,如下所示:

ADD --chown=<user>:<group> <source> <destination>
COPY --chown=<user>:<group> <source> <destination>

例如,如果您想将当前工作目录中的requirements.txt添加到/usr/share/app目录中,指令如下:

ADD requirements.txt /usr/share/app
COPY  requirements.txt /usr/share/app

在指定模式时,ADDCOPY都支持通配符。例如,在 docker 文件中包含以下指令将使用。py扩展到/apps/目录的镜像。

ADD *.py /apps/

COPY *.py /apps/

Note

Docker 建议使用COPY而不是ADD,尤其是当它是一个正在被复制的本地文件时。

选择COPY还是ADD有几点需要考虑。在COPY指令的情况下:

  • 如果<destination>在图像中不存在,它将被创建。

  • 所有新文件/目录都是以 UID 和 GID 作为0创建的,即作为 root 用户。要改变这一点,您可以使用--chown标志。

  • 如果文件/目录包含特殊字符,需要对它们进行转义。

  • <destination>可以是绝对或相对路径。在相对路径的情况下,相对性将从WORKDIR指令设置的路径中推断出来。

  • 如果<destination>不以斜杠结尾,它将被认为是一个文件,并且<source>的内容将被写入<destination>

  • 如果将<source>指定为通配符模式,则<destination>必须是一个目录,并且必须以尾随斜杠结束;否则,构建过程将会失败。

  • <source>必须在构建上下文中。它不能是构建上下文之外的文件/目录,因为 Docker 构建过程的第一步涉及到将上下文目录发送到 Docker 守护进程。

ADD指令的情况下:

  • 如果<source>是一个 URL,而<destination>不是一个目录,并且不以斜杠结尾,那么文件从 URL 下载并复制到<destination>

  • 如果<source>是一个 URL,而<destination>是一个目录并以斜杠结尾,则从 URL 中推断出文件名,并将文件下载并复制到<destination>/<filename>

  • 如果<source>是一个已知压缩格式的本地 tarball,tarball 被解压为一个目录。然而,远程 tarballs 不是未压缩的。

奔跑

RUN指令将在容器的构建步骤中执行任何命令。这将创建一个新层,可用于 docker 文件中的后续步骤。值得注意的是,跟随RUN指令的命令仅在构建映像时运行。当一个容器已经启动并正在运行时,RUN指令没有任何意义。

RUN有两种形式,shell 形式和 exec 形式。在 shell 形式中,该命令以空格分隔,如下所示:

RUN <command>

这种形式使得在RUN指令本身中使用外壳变量、子命令、命令管道和命令链成为可能。

考虑一个场景,您想要将内核发布版本嵌入到 Docker 映像的home目录中。您可以使用uname –rv命令获得内核发布和版本。这个输出可以使用 echo 打印出来,然后重定向到映像的home目录中一个名为 kernel-info 的文件。您可以使用 shell 形式的 RUN 指令来实现这一点,如下所示:

RUN echo `uname -rv` > $HOME/kernel-info

在 exec 格式中,该命令用逗号分隔并用引号括起来,如下所示:

RUN ["executible", "parameter 1", " parameter 2"] (the exec form)

除非您需要使用链接和重定向之类的 shell 特性,否则建议对RUN指令使用 exec 格式。

图层缓存

构建图像时,Docker 将缓存它提取的层。从构建日志中可以明显看出这一点。考虑以下 Dockerfile 文件:

FROM ubuntu:latest
RUN apt-get update

运行docker build时的构建日志如下所示:

docker build -f Dockerfile .
[+] Building 8.1s (7/7) FINISHED
 => [internal] load build definition from Dockerfile  0.1s
 => => transferring dockerfile: 96B 0.0s
 => [internal] load .dockerignore   0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest  1.8s
 => [auth] library/ubuntu:pull token for registry-1.docker.io  0.0s
 => CACHED [1/2] FROM docker.io/library/ubuntu:latest@sha256:b3e2e47 0.0s
 => [2/2] RUN apt-get update  6.0s
 => exporting to image  0.2s
 => => exporting layers 0.1s
 => => writing image sha256:a9824f6

日志表明,Docker 使用保存到磁盘的缓存层,而不是重新下载基本 Ubuntu 映像的层。这适用于所有创建的层——每当 Docker 遇到RUNCOPYADD指令时,它都会创建一个新层。拥有正确的指令顺序可以大大提高 Docker 是否会重用这些层。这不仅可以提高映像构建速度,还可以通过减少下载层数来减少容器启动时间。

由于层缓存的工作方式,最好将包更新和包安装链接成一条RUN指令。考虑一个 Dockerfile 文件,其中的运行指令如下所示:

RUN apt-get update
RUN apt-get install pkg1
RUN apt-get install pkg2
RUN apt-get install pkg3

当 Docker 构建这个映像时,它缓存由四个RUN命令创建的四个层。为了减少层数,并防止由于软件包缓存过期而无法安装软件包,最好将更新和安装链接起来,如下所示:

RUN apt-get update && apt-get install -y \
   pkg1 \
   pkg2 \
   pkg3 \
   pkg4

这将创建一个包含要安装的软件包的层,任何软件包中的任何更改都将使缓存无效,并导致使用更新的软件包创建一个新层。如果你想明确地指示 Docker 避免使用缓存,那么将--no-cache标志传递给docker build命令将跳过使用缓存。

CMD 和入口点

CMDENTRYPOINT指令定义运行容器时执行哪个命令。两者的语法如下所示:

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

当您希望您的容器像可执行文件一样运行时,ENTRYPOINT指令是最好的,并且CMD指令为正在执行的容器提供缺省值。考虑下面显示的 Dockerfile 文件:

FROM ubuntu:latest
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*
CMD ["curl"]

在这个 Docker 镜像中,Ubuntu 是基础镜像,curl安装在其上,curlCMD指令的参数。这意味着当容器被创建并运行时,它将不带任何参数地运行curl。让我们为 docker 文件构建如下所示的图像:

docker build –t sathyabhat/curl .

[+] Building 11.8s (6/6) FINISHED
 => [internal] load build definition from Dockerfile 0.0s
 => => transferring dockerfile: 50B  0.0s
 => [internal] load .dockerignore 0.0s
 => => transferring context: 2B   0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest   0.7s
 => CACHED [1/2] FROM docker.io/library/ubuntu:latest@sha256:b3e2e47  0.0s
 => [2/2] RUN apt-get update &&   apt-get install -y curl 10.7s
 => exporting to image   0.3s
 => => exporting layers  0.3s
 => => writing image sha256:8a9fc4b  0.0s
 => => naming to docker.io/sathyabhat/curl

您可以在运行容器时看到结果:

docker run sathyabhat/curl
curl: try 'curl --help' or 'curl --manual' for more information

这是因为curl期望传递一个参数。您可以通过向docker run命令传递参数来覆盖CMD指令。例如,尝试curl wttr.in,它获取当前天气。

docker run sathyabhat/curl wttr.in
docker: Error response from daemon: OCI runtime create failed: container_linux.go:296: starting container process caused "exec: \"wttr.in\": executable file not found in $PATH": unknown.

啊哦,一个错误。如上所述,docker run之后的参数用于覆盖CMD指令。然而,您只传递了wttr.in作为参数,而不是可执行文件本身。为了让覆盖正常工作,您需要传入可执行文件,也就是curl:

docker run sathyabhat/curl -s wttr.in
Weather report: Gurgaon, India

               Haze
  _ - _ - _ -  24-25 °C
   _ - _ - _   ↖ 13 km/h
  _ - _ - _ -  3 km
               0.0 mm

每次传递一个可执行文件来覆盖一个参数是非常乏味的。这就是ENTRYPOINTCMD组合的闪光之处。您可以将ENTRYPOINT设置为可执行文件,同时该参数可以从命令行传递并将被覆盖。

按如下方式修改 Dockerfile 文件:

FROM ubuntu:latest
RUN apt-get update && \
apt-get install -y curl
ENTRYPOINT ["curl", "-s"]

再次构建图像:

docker build -t sathyabhat/curl .

[+] Building 0.7s (6/6) FINISHED
 => [internal] load build definition from Dockerfile.listing-4-x-5 0.0s
 => => transferring dockerfile: 157B 0.0s
 => [internal] load .dockerignore 0.0s
 => => transferring context: 2B   0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest   0.6s
 => [1/2] FROM docker.io/library/ubuntu:latest@sha256:b3e2e47   0.0s
 => CACHED [2/2] RUN apt-get update &&   apt-get install -y curl 0.0s
 => exporting to image   0.0s
 => => exporting layers  0.0s
 => => writing image sha256:7e31728  0.0s
 => => naming to docker.io/sathyabhat/curl

现在,您可以通过将 URL 作为参数传递来curl任何 URL,而不必添加可执行文件。

docker run sathyabhat/curl wttr.in
Weather report: Gurgaon, India

               Haze
  _ - _ - _ -  24-25 °C
   _ - _ - _   ↖ 13 km/h
  _ - _ - _ -  3 km
               0.0 mm

当然,curl在这里只是一个例子。您可以用任何其他接受参数的程序来替换curl(比如负载测试工具、基准测试工具等。)并且CMDENTRYPOINT的组合使得分发图像变得容易。

请注意,ENTRYPOINT必须以 exec 形式提供——以 shell 形式编写意味着参数没有被正确传递,不会按预期工作。表 4-1 来自*Docker 工人参考指南。*解释了允许的ENTRYPOINT / CMD组合的矩阵,假设p1_cmdp1_entryp2_cmdp2_entry是您想要在容器中运行的p1p2命令的CMDENTRYPOINT变体。

表 4-1

ENTRYPOINT / CMD组合命令

|   |

ENTRYPOINT

|

ENTRYPOINT exec_entry p1_entry

|

ENTRYPOINT ["exec_entry", "p1_entry"]

| | --- | --- | --- | --- | | 否CMD | 错误,不允许 | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry | | CMD ["exec_cmd", "p1_cmd"] | exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry exec_cmd p1_cmd | | CMD ["p1_cmd", "p2_cmd"] | p1_cmd p2_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry p1_cmd p2_cmd | | CMD exec_cmd p1_cmd | /bin/sh -c exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd |

关于 shell 和 exec 表单,需要记住以下几点:

  • 如前所述,您可以在 shell 形式和 exec 形式中指定RUNCMDENTRYPOINT。应该使用哪一种将完全取决于需求。但是作为一般指南:

    • 在 shell 形式中,命令在 shell 中运行,并将命令作为参数。这种形式提供了一个外壳,其中外壳变量、子命令、命令管道和链接都是可能的。

    • 在 exec 形式下,命令不调用命令外壳。这意味着正常的壳体加工(如$VARIABLE替换、配管等。)就不行了。

  • 以 shell 形式启动的程序将作为/bin/sh -c的子命令运行。这意味着可执行文件不会作为 PID 运行,也不会接收 UNIX 信号。因此,发送SIGTERM的 Ctrl+C 不会被转发到容器,应用可能无法正确退出。

包封/包围(动词 envelop 的简写)

ENV指令为图像设置环境变量。ENV指令有两种形式:

ENV <key> <value>
ENV <key>=<value> ...

在第一种形式中,<key>之后的整个字符串被认为是值,包括空白字符。在此表单中,每行只能设置一个变量。

在第二种形式中,可以一次设置多个变量,用等号(=)字符给键赋值。

环境变量集在容器运行时保持不变。可以使用docker inspect查看它们。

考虑这个 Dockerfile 文件:

FROM ubuntu:latest
ENV LOGS_DIR="/var/log"
ENV APPS_DIR /apps/

构建 Docker 映像:

docker build  -t sathyabhat/env .
[+] Building 1.7s (6/6) FINISHED
 => [internal] load build definition from Dockerfile.listing-4-x-6   0.0s
 => => transferring dockerfile: 50B 0.0s
 => [internal] load .dockerignore   0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest  1.6s
 => [auth] library/ubuntu:pull token for registry-1.docker.io  0.0s
 => CACHED [1/1] FROM docker.io/library/ubuntu:latest@sha256:b3e2e47 0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:23eb815 0.0s
 => => naming to docker.io/sathyabhat/env

您可以使用以下命令检查环境变量:

docker inspect sathyabhat/env | jq ".[0].Config.Env"

输出如下所示:

[
 "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  "LOGS_DIR=/var/log",
  "APPS_DIR=/apps/"
]

当运行带有-e标志的容器时,可以更改为容器定义的环境变量。在前面的示例中,将容器的LOGS_DIR值更改为/logs。这可以通过键入以下命令来实现:

docker run -it -e LOGS_DIR="/logs" sathyabhat/env

您可以通过在终端键入以下命令来确认更改后的值:

printenv | grep LOGS
LOGS_DIR=/logs

键入exit关闭容器的交互终端。要分配多个环境变量,使用-e标志传递额外的环境变量,就像传递第一个环境变量一样。在前面的例子中,如果您想覆盖LOGS_DIRAPPS_DIR,可以使用下面的命令:

docker run -it -e LOGS_DIR="/logs" -e APPS_DIR="/opt" sathyabhat/env

printenv | grep DIR
LOGS_DIR=/logs
APPS_DIR=/opt

键入exit关闭容器的交互终端。

VOLUME指令告诉 Docker 在容器上创建一个挂载点,并从主机外部挂载它。例如,这样的指令:

VOLUME /var/logs/nginx

告诉 Docker 将/var/logs/nginx目录标记为挂载点,从 Docker 主机挂载数据。当与 Docker run命令上的卷标志结合使用时,这将导致数据作为卷保存在 Docker 主机上。然后,可以使用 Docker CLI 命令备份、移动或转移该卷。在本书后面的章节中,你会学到更多关于卷的知识。

揭露

EXPOSE指令告诉 Docker 容器在运行时监听指定的网络端口。语法如下:

EXPOSE <port> [<port>/<protocol>...]

例如,如果你想暴露端口 80,EXPOSE指令如下:

EXPOSE 80

如果要在 TCP 和 UDP 上公开端口 53,Dockerfile 指令如下:

EXPOSE 53/tcp
EXPOSE 53/udp

您还可以包括端口号以及端口是侦听 TCP/UDP 还是同时侦听两者。如果没有指定,Docker 假定协议是 TCP。

Note

一个EXPOSE指令不发布端口。对于要向主机发布的端口,您需要使用带有docker run-p标志来发布和映射端口。

这里有一个示例 Dockerfile 文件,它使用了端口 80 暴露在容器中的nginx图像。

FROM nginx:alpine
EXPOSE 80

构建容器:

[+] Building 0.4s (5/5) FINISHED
 => [internal] load build definition from Dockerfile  0.0s
 => => transferring dockerfile: 50B 0.0s
 => [internal] load .dockerignore   0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/nginx:alpine   0.2s
 => CACHED [1/1] FROM docker.io/library/nginx:alpine@sha256:9152859  0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:33fcd52 0.0s
 => => naming to docker.io/sathyabhat/web

要运行这个容器,您必须提供它要映射到的主机端口。将其映射到主机上的端口 8080,再映射到容器的端口 80。为此,请键入以下命令:

docker run -d -p 8080:80 sathyabhat:web

-d标志使nginx容器在后台运行,-p标志进行端口映射。确认容器正在运行:

curl http://localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

标签

LABEL指令将元数据作为键值对添加到图像中。

LABEL <key>=<value> <key>=<value> <key>=<value> …

一个图像可以有多个标签,通常用于添加一些元数据,以帮助搜索和组织图像和其他 Docker 对象。Docker 推荐以下指南。

  • 对于钥匙:

    • 第三方工具的作者应该用他们拥有的域名的反向 DNS 符号作为每个密钥的前缀:例如,com.sathyasays.my-image

    • com.docker.*, io.docker.*,org.dockerproject.*由 Docker 保留供内部使用。

    • 标签键应该以小写字母开始和结束,并且应该只包含小写字母数字字符和句点(。)和连字符(-)。不允许使用连续的连字符和句点。

    • 句号(。)分隔名称空间字段。

  • 对于值:

    • 标签值可以包含任何可以表示为字符串的数据类型,包括 JSON、XML、YAML 和 CSV 类型。

撰写文档的指南和建议

以下是 Docker 推荐的一些编写 Docker 文件的指导原则和最佳实践。

  • 容器应该是短暂的。Docker 建议由 docker 文件生成的图像应该尽可能短暂。您应该能够在任何时候停止、销毁和重启容器,只需对容器进行最少的设置和配置。理想情况下,容器不应该将数据写入容器文件系统,任何持久数据都应该写入 Docker 卷或容器外部管理的数据存储(例如,使用像亚马逊 S3 这样的块存储)。

  • 保持构建环境最小化。您在本章前面已经了解了构建上下文。重要的是尽可能保持最小的构建上下文,以减少构建时间和图像大小。这可以通过有效利用.dockerignore文件来实现。

  • 使用多阶段构建。多阶段构建有助于大幅减少映像的大小,而不必编写复杂的脚本来传输/保留所需的工件。下一节将介绍多阶段构建。

  • 跳过不想要的包裹。拥有不需要的或有用的包会增加映像的大小,引入不需要的依赖包,并增加攻击的表面积。

  • 尽量减少层数。虽然不像以前那样令人担忧,但减少图像中的图层数量仍然很重要。从 Docker 1.10 及以上版本开始,只有RUNCOPYADD指令创建层。考虑到这一点,使用最少的指令或者组合多行相应的指令可以减少层数,最终减小图像的大小。

使用多阶段构建

从 17.05 及更高版本开始,Docker 增加了对多阶段构建的支持,允许执行复杂的映像构建,而不会使 Docker 映像不必要地臃肿。当您构建需要一些额外的构建时依赖项但在运行时不需要的应用映像时,多阶段构建特别有用。最常见的例子是使用编程语言(如 Go 或 Java)编写的应用,在多阶段构建之前,通常有两个不同的 docker 文件。一个用于构建,另一个用于从构建时映像到运行时映像的工件的发布和编排。

通过多阶段构建,可以利用单个 docker 文件来构建和部署映像——构建映像可以包含生成二进制文件或工件所需的构建工具。在第二个阶段,工件可以被复制到运行时映像,从而大大减小运行时映像的大小。对于一个典型的多阶段构建,一个构建阶段有几层—每一层用于安装构建应用、生成依赖项和生成应用所需的工具。在最终层中,从构建阶段构建的应用被复制到最终层,并且只有该层被考虑用于构建映像。构建层被丢弃,大大减小了最终图像的大小。

虽然这本书没有详细介绍多阶段构建,但您将尝试一个如何创建多阶段构建的练习,看看使用多阶段构建的苗条图像会使最终图像变得多小。有关多阶段构建的更多详细信息,请访问 Docker 网站 https://docs.docker.com/develop/develop-images/multistage-build/

练习

Building a Simple Hello World Docker Image

本章开头介绍了一个简单的 docker 文件,由于语法错误,该文件没有构建。在本练习中,您将看到如何修复 docker 文件并添加您在本章中学到的一些指令。

提示源代码和相关的 Dockerfile 可以在本书的 GitHub repo 上获得,在 https://github.com/Apress/practical-docker-with-pythonsource-code/chapter-4/exercise-1目录下。

原始 Dockerfile 文件如下:

FROM ubuntu:latest
LABEL author="sathyabhat"
LABEL description="An example Dockerfile"
RUN apt-get install python
COPY hello-world.py
CMD python hello-world.py

尝试构建它会导致错误,因为hello-world.py丢失了。让我们修复构建错误。为此,您需要添加一个读取环境变量NAME并打印Hello, $NAME!hello-world.py。如果没有定义环境变量,它将打印"Hello, World!"

hello-world.py的内容如下:

#!/usr/bin/env python3
from os import getenv

if getenv('NAME') is None:
        name = 'World'
else:
        name = getenv('NAME')
print(f"Hello, {name}!")

更正后的 Dockerfile 文件如下:

FROM python:3-alpine
LABEL description="Dockerfile for Python script which prints Hello, Name"
COPY hello-world.py /app/
ENV NAME=Readers
CMD python3 /app/hello-world.py

构建 Dockerfile 文件:

docker build -t sathyabhat/chap04-ex1 .
[+] Building 1.9s (8/8) FINISHED
 => [internal] load build definition from Dockerfile  0.0s
 => => transferring dockerfile: 37B 0.0s
 => [internal] load .dockerignore   0.0s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/python:3-alpine   1.7s
 => [auth] library/python:pull token for registry-1.docker.io  0.0s
 => [internal] load build context   0.0s
 => => transferring context: 36B 0.0s
 => [1/2] FROM docker.io/library/python:3-alpine@sha256:3998e97  0.0s
 => CACHED [2/2] COPY hello-world.py /app/   0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:538be87 0.0s
 => => naming to docker.io/sathyabhat/chap04-ex1

确认图像名称和大小:

docker images sathyabhat/chap04-ex1
REPOSITORY             TAG     IMAGE ID      CREATED      SIZE
sathyabhat/chap04-ex1  latest  538be873d192  3 hours ago  45.1MB

运行 Docker 映像:

docker run sathyabhat/chap04-ex1
Hello, Readers!

尝试在运行时覆盖环境变量。您可以通过为-e参数提供docker run来做到这一点:

docker run -e NAME=all sathyabhat/chap04-ex1
Hello, all!

恭喜你!您已经成功编写了您的第一个 Docker 文件,并构建了您的第一个 Docker 映像。

A Look at Slim Docker Release Image (Using Multi-Stage Builds)

在本练习中,您将构建两个 Docker 映像。第一个图像使用标准构建,以python:3作为基础图像,而第二个图像给出了如何利用多阶段构建的概述。

提示https://github.com/Apress/practical-docker-with-python ,在source-code/chapter-4/exercise-2/ directory的 GitHub repo 上可以获得该书的源代码和相关 Dockerfile。

使用标准构建构建 Docker 映像

用以下内容创建一个requirements.txt file:

praw==3.6.0

创建一个包含以下内容的 Dockerfile 文件:

FROM python:3
COPY requirements.txt .
RUN pip install -r requirements.txt

现在构建 Docker 映像:

[+] Building 7.2s (8/8) FINISHED
 => [internal] load build definition from Dockerfile   0.3s
 => => transferring dockerfile: 114B 0.0s
 => [internal] load .dockerignore 0.3s
 => => transferring context: 2B   0.0s
 => [internal] load metadata for docker.io/library/python:3  0.0s
 => [internal] load build context 0.6s
 => => transferring context: 54B  0.0s
 => [1/3] FROM docker.io/library/python:3  1.6s
 => [2/3] COPY requirements.txt . 0.2s
 => [3/3] RUN pip install -r requirements.txt 3.3s
 => exporting to image   1.6s
 => => exporting layers  1.5s
 => => writing image sha256:03191af  0.0s
 => => naming to docker.io/sathyabhat/base-build

映像构建成功!让我们确定图像的大小:

|

贮藏室ˌ仓库

|

标签

|

图像 ID

|

创造

|

大小

| | --- | --- | --- | --- | --- | | sathyabhat/base-build | latest | 03191af | 大约一分钟前 | 895MB |

docker images sathyabhat/base-build

Docker 映像有相当大的 895MB,尽管您没有添加任何应用代码,只是添加了一个依赖项。让我们把它重写为一个多阶段构建。

使用多阶段构建构建 Docker 映像

FROM python:3 as python-base
COPY requirements.txt .
RUN pip install -r requirements.txt

FROM python:3-alpine
COPY --from=python-base /root/.cache /root/.cache
COPY --from=python-base requirements.txt .
RUN pip install -r requirements.txt && rm -rf /root/.cache

Dockerfile 文件的不同之处在于有多个FROM语句,表示不同的阶段。在第一阶段,您使用python:3映像构建所需的包,它具有必要的构建工具。

在第二阶段,您复制第一阶段安装的文件,重新安装它们(注意这一次 pip 获取缓存的文件,不再构建它们),然后删除缓存的安装文件。构建日志如下所示:

[+] Building 0.6s (13/13) FINISHED
 => [internal] load build definition from Dockerfile  0.2s
 => => transferring dockerfile: 35B 0.0s
 => [internal] load .dockerignore .1s
 => => transferring context: 2B  0.0s
 => [internal] load metadata for docker.io/library/python:3-alpine .2s
 => [internal] load metadata for docker.io/library/python:3 0.0s
 => [internal] load build context .1s
 => => transferring context: 37B 0.0s
 => [stage-1 1/4] FROM docker.io/library/python:3-alpine@sha256:3998e97 0.0s
 => [python-base 1/3] FROM docker.io/library/python:3 0.0s
 => CACHED [python-base 2/3] COPY requirements.txt .  0.0s
 => CACHED [python-base 3/3] RUN pip install -r requirements.txt  0.0s
 => CACHED [stage-1 2/4] COPY --from=python-base /root/.cache /root/.cache 0.0s
 => CACHED [stage-1 3/4] COPY --from=python-base requirements.txt .  0.0s
 => CACHED [stage-1 4/4] RUN pip install -r requirements.txt && rm -rf /root/.cache 0.0s
 => exporting to image  0.1s
 => => exporting layers 0.0s
 => => writing image sha256:35c85a8 0.0s
 => => naming to docker.io/sathyabhat/multistage-build

使用docker images检查图像的大小,您会发现使用多阶段构建已经大大减小了图像的大小。这意味着图像尺寸减小,应用启动更快,甚至成本降低,因为您节省了提取容器图像所需的带宽。

|

贮藏室ˌ仓库

|

标签

|

图像标识

|

创造

|

大小

| | --- | --- | --- | --- | --- | | sathyabhat/multistage-build | latest | 35c85a8497b5 | 大约一分钟前 | 54.2 兆字节 |

docker images sathyabhat/multistage-build

Writing a Dockerfile For Newsbot

在本练习中,您将为 Newsbot(电报聊天机器人项目)编写 Dockerfile。

提示源代码和相关的 docker 文件可以在 https://github.com/Apress/practical-docker-with-python 的 GitHub repo 上的source-code/chapter-4/exercise-3/目录下获得。

让我们回顾一下您对这个项目的需求:

  • 一个基于 Python 3 的 Docker 图像

  • requirements.txt中列出的项目依赖关系

  • 名为NBT_ACCESS_TOKEN的环境变量

现在您已经有了您需要的东西,您可以编写 Dockerfile 文件了。编写 Dockerfile 文件的一般步骤如下

  1. 从一个合适的基础图像开始。

  2. 列出应用所需的文件列表。

  3. 列出应用所需的环境变量。

  4. 使用COPY指令将应用文件复制到映像。

  5. ENV指令指定环境变量。

结合这些步骤,您将得到这个 Dockerfile 文件。

FROM python:3-alpine
WORKDIR /apps/subredditfetcher/
COPY . .
RUN ["pip", "install", "-r", "requirements.txt"]
CMD ["python", "newsbot.py"]

现在构建图像:

[+] Building 0.9s (9/9) FINISHED
 => [internal] load build definition from Dockerfile   0.1s
 => => transferring dockerfile: 182B 0.0s
 => [internal] load .dockerignore 0.2s
 => => transferring context: 2B   0.0s
 => [internal] load metadata for docker.io/library/python:3-alpine 0.4s
 => [1/4] FROM docker.io/library/python:3-alpine@sha256:3998e97 0.0s
 => [internal] load build context 0.1s
 => => transferring context: 392B 0.0s
 => CACHED [2/4] WORKDIR /apps/subredditfetcher/ 0.0s
 => CACHED [3/4] COPY . .   0.0s
 => CACHED [4/4] RUN ["pip", "install", "-r", "requirements.txt"]  0.0s
 => exporting to image   0.1s
 => => exporting layers  0.0s
 => => writing image sha256:783b4c0  0.0s
 => => naming to docker.io/sathyabhat/newsbot

现在运行容器。注意用你在第三章中创建的电报机器人 API 密匙替换<token>

docker run –e NBT_ACCESS_TOKEN=<token> sathyabhat/newsbot

您应该会看到 bot 的日志,以确保它正在运行:

INFO: <module> - Starting up
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}

如果你看到这些日志,恭喜你!您不仅为 Newsbot 编写了 Dockerfile,而且还成功地构建并运行了它。

摘要

在本章中,通过回顾 Dockerfile 的语法,您对它有了更好的理解。你现在离掌握为 Newsbot 编写 docker 文件又近了一步。