Python-Docker-实践教程-二-

225 阅读18分钟

Python Docker 实践教程(二)

原文:Practical Docker with Python

协议:CC BY-NC-SA 4.0

五、了解 Docker 卷

在前几章中,您了解了 Docker 及其相关术语,并深入了解了如何使用 Docker 文件构建 Docker 映像。在本章中,您将看到 Docker 容器的数据持久性策略,并了解为什么您需要特殊的数据持久性策略。

数据持久性

传统上,大多数计算解决方案都附带了持久化和保存数据的相关方法。对于虚拟机,会模拟一个虚拟磁盘,保存到该虚拟磁盘的数据会作为文件保存在主机上。亚马逊网络服务(AWS)等云提供商提供不同的服务,如亚马逊弹性块存储(EBS)和亚马逊弹性文件系统(EFS)。这些服务提供了可以安装在主机虚拟机上的端点;保存到这些装载点的数据被持久化和复制。

说到容器,情况就不一样了。容器是为无状态工作负载而设计的,容器层的设计表明了这一点。第二章解释了 Docker 映像是由各种层组成的只读模板。当映像作为容器运行时,会创建一个包含少量只写数据层的容器。这意味着

  • 数据被紧紧地锁定在主机上,这使得运行跨多个容器和应用共享数据的应用变得困难。

  • 当一个容器被终止时,数据不会持久,并且不可能以一种简单的方式从容器中提取数据。

  • 写入容器的写入层需要存储驱动程序来管理文件系统。存储驱动程序在读/写速度方面不能提供可接受的性能水平,并且写入容器写层的大量数据会导致容器和 Docker 守护程序耗尽内存。

Docker 容器中的数据丢失示例

为了演示 write 层的特性,让我们从 Ubuntu 基础映像中调出一个容器。您将在 Docker 容器中创建一个文件,停止容器,并查看容器的行为。

  1. 首先创建一个nginx容器:

  2. 打开容器内的终端:

        docker run -d --name nginx-test  nginx

  1. nginxdefault.conf复制到一个新的配置文件:
        docker exec -it nginx-test bash

  1. 你不会修改nginx-test.conf的内容,因为它无关紧要。现在你需要停止容器。在 Docker 主机终端上,键入以下命令:
        cd /etc/nginx/conf.d
        cp default.conf nginx-test.conf

  1. 再次启动容器:
        docker stop nginx-test

  1. 打开容器内的终端:
        docker start nginx-test

  1. 让我们看看变化是否还在:
        docker exec -it nginx-test bash

  1. 因为容器只是被停止了,所以数据是持久的。让我们停止它,移除容器,然后调出一个新的,观察会发生什么:
        cd /etc/nginx/conf.d
        ls
        default.conf  nginx-test.conf

  1. 启动一个新容器:
        docker stop nginx-test

        docker rm nginx-test

  1. 现在,一个新的容器已经启动并运行,连接到容器的终端:
        docker run -d --name nginx-test  nginx

  1. 检查nginxconf.d目录的内容:
        docker exec -it nginx-test bash

        cd /etc/nginx/conf.d
        ls
        default.conf

由于容器被移除,与容器相关联的只写层也被移除,并且所创建的文件不再可访问。对于容器化的有状态应用,比如需要数据库的应用,这意味着当一个现有的容器被移除或者一个新的容器被添加时,来自前一个容器的数据不再可访问。为了减轻这种情况,Docker 提供了各种策略来持久化数据。

  • tmpfs 安装

  • 绑定安装

tmpfs 装载

顾名思义,tmpfs 在 tmpfs 中创建一个挂载,这是一个临时文件存储工具。tmpfs 中挂载的目录显示为挂载的文件系统,但存储在内存中,而不是存储在磁盘驱动器之类的永久存储中。

tmpfs 挂载仅限于 Linux 上的 Docker 容器。tmpfs 挂载是临时的,数据存储在 Docker 的主机内存中。一旦容器停止,tmpfs 挂载将被删除,写入 tmpfs 挂载的文件将丢失。

要创建 tmpfs 挂载,可以在运行容器时使用--tmpfs标志,如下所示:

docker run -it --name docker-tmpfs-test --tmpfs /tmpfs-mount ubuntu bash

让我们检查一下容器:

docker inspect docker-tmpfs-test | jq ".[0].HostConfig.Tmpfs"
{
 "/tmpfs-mount": ""
}

这个输出告诉您有一个 tmpfs 配置映射到容器的/tmpfs-mount目录。

tmpfs 挂载最适合于生成不需要持久化和不必写入容器可写层的数据的容器。

绑定安装

在绑定挂载中,主机上的文件/目录被挂载到容器中。相反,当使用 Docker 卷时,会在 Docker 主机上的 Docker 存储目录中创建一个新目录,并且该目录的内容由 Docker 管理。

让我们看看如何使用绑定坐骑。您将尝试将 Docker 主机的主目录挂载到容器内名为host-home的目录中。为此,请键入以下命令:

docker run -it --name bind-mount-container -v $HOME:/host-home ubuntu bash

检查创建的容器揭示了关于装载的不同特征。

docker inspect bind-mount-container | jq ".[0].Mounts"

[
  {
    "Type": "bind",
    "Source": "/home/sathya",
    "Destination": "/host-home",
    "Mode": "",
    "RW": true,
    "Propagation": "rprivate"
  }
]

这个输出表明挂载是绑定类型的,源(即被挂载的 Docker 主机的目录)是/home/sathya(即主目录),挂载的目的地是/host-home。“Propagation”属性指绑定传播——该属性指示为绑定挂载创建的挂载是否被反映到该挂载的副本上。绑定传播仅适用于 Linux 主机。对于绑定装载,该属性通常不需要修改。RW 标志表示可以写入挂载的目录。让我们检查一下host-home的内容,看看挂载是否正确。

  1. 使用以下命令打开容器的交互式终端:

  2. 在容器的终端中,键入以下内容:

        docker run -it -v $HOME:/host-home ubuntu bash

  1. 该命令的输出应该是 Docker 主机主目录的列表。尝试在host-home目录中创建一个文件。为此,请键入以下命令:
        cd  /host-home
        ls

        cd /host-home
        echo "This is a file created from container having kernel `uname -r`" > host-home-file.txt

该命令创建一个名为host-home-file.txt的文件,该文件包含容器的/host-home目录中的文本"This is a file created from container having kernel 4.9.87-linuxkit-aufs"。请注意,内容会因主机操作系统和内核版本而异。

因为这是 Docker 主机主目录的绑定挂载,所以也应该在 Docker 主机的主目录中创建该文件。你可以看看是否确实如此。

  1. 在 Docker 主机中打开一个新的终端窗口,并键入以下命令:

  2. 您应该会看到以下输出,表明文件的存在:

        cd ~
        ls host-home-file.txt

  1. 现在检查文件的上下文:
        ls host-home-file.txt
        host-home-file.txt

        cat host-home-file.txt

这个文件应该与您在上一节中看到的内容相同。这证实了在容器中创建的文件在容器外确实是可用的。因为您关心的是容器停止、移除和重新启动后的数据持久性,所以让我们看看会发生什么。

通过在 Docker 主机终端中输入以下命令来停止容器。

docker stop bind-mount-container
docker rm bind-mount-container

确认 Docker 主机上的文件仍然存在:

cat ~/host-home-file.txt
This is a file created from container having kernel 4.9.87-linuxkit-aufs

绑定挂载非常有用,并且在应用的开发阶段最常用。通过使用绑定装载,您可以在将源目录装载为绑定装载时,使用与生产相同的容器来为生产准备应用。这允许开发人员拥有快速的代码测试迭代周期,而不需要重新构建 Docker 映像。

Caution

记住,对于绑定装载,数据流在 Docker 主机和容器上是双向的。任何破坏性的操作(比如删除目录)也会对 Docker 主机产生负面影响。

注意,在将主机操作系统目录作为绑定装载装载到容器中时要格外小心。如果挂载的目录范围很广,比如主目录(如前所示)或根目录,这就更重要了。一个失控的脚本或一个错误的rm -rf命令可以完全瘫痪 Docker 主机。为了减轻这种情况,您可以创建一个带有只读选项的绑定装载,以便以只读方式装载目录。

为此,您可以使用docker run命令提供一个只读参数。这些命令如下所示:

docker run -it --name read-only-bind-mount -v $HOME:/host-home:ro ubuntu bash

现在检查创建的容器:

docker inspect read-only-bind-mount | jq ".[0].Mounts"
[
  {
    "Type": "bind",
    "Source": "/home/sathya",
    "Destination": "/host-home",
    "Mode": "ro",
    "RW": false,
    "Propagation": "rprivate"
  }
]

您可以看到“RW”标志现在为假,Mode被设置为只读(ro)。让我们像前面一样尝试写入文件。

打开容器 Docker:

docker run -it --name read-only-bind-mount -v $HOME:/host-home:ro ubuntu bash

键入以下命令在容器中创建一个文件:

echo "This is a file created from container having kernel `uname -r`" > host-home-file.txt
bash: host-home-file.txt: Read-only file system

写操作失败,bash 告诉您这是因为文件系统是以只读方式挂载的。任何破坏性操作也会遇到同样的错误:

rm host-home-file.txt
rm: cannot remove 'host-home-file.txt': Read-only file system

Docker 卷

Docker volulmes 是当前推荐的保存容器中数据的方法。卷完全由 Docker 管理,与绑定装载相比有许多优势:

  • 卷比绑定装载更容易备份或传输。

  • 卷在 Linux 和 Windows 容器上都可以工作。

  • 卷可以在多个容器之间共享,没有问题。

Docker 卷子命令

Docker 将卷 API 公开为一系列子命令。这些命令如下所示:

  • docker volume create

  • docker volume inspect

  • docker volume ls

  • docker volume prune

  • docker volume rm

卷创建

volume create子命令用于创建命名卷。最常见的用例是生成命名卷。该命令的用法如下:

docker volume create --name=<name of the volume> --label=<any extra metadata>

Tip

Docker 对象标签在第四章中讨论。

例如,该命令创建一个名为nginx-volume的命名卷:

docker volume create --name=nginx-volume

体积检查

volume inspect命令显示卷的详细信息。该命令的用法如下:

docker volume inspect <volume-name>

nginx-volume名称为例,您可以通过键入以下内容找到更多详细信息:

docker volume inspect nginx-volume

这将产生以下结果:

docker volume inspect nginx-volume
[
    {
        "CreatedAt": "2018-04-17T13:51:02Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/nginx-volume/_data",
        "Name": "nginx-volume",
        "Options": {},
        "Scope": "local"
    }
]

当您想要复制/移动/备份卷时,此命令非常有用。mount path 属性列出了 Docker 主机上保存包含卷数据的文件的位置。

列出卷

volume ls命令显示主机中存在的所有卷。用法如下:

docker volume ls

清理卷

volume prune命令删除所有未使用的本地卷。用法如下:

docker volume prune

Docker 认为至少有一个容器未使用的卷是未使用的。由于未使用的卷最终会占用大量的磁盘空间,所以定期运行prune命令并不是一个坏主意,尤其是在本地开发机器上。您可以将--force附加到命令的末尾,当命令运行时,它不会要求确认删除。

移除卷

volume rm命令删除其名称作为参数提供的卷。用法如下:

docker volume rm <name>

对于之前创建的卷,命令如下:

docker volume rm nginx-volume

Docker 不会删除正在使用的卷,并将返回一个错误。例如,如果您尝试删除连接到容器的nginx-volume卷,您将得到以下错误消息:

docker volume rm nginx-volume

Error response from daemon: unable to remove volume: remove nginx-volume: volume is in use - [6074757a]

Note

即使容器被停止,Docker 也会认为该卷正在使用中。

长标识符是与卷相关联的容器的 ID。如果卷与多个容器相关联,将列出所有容器 id。使用docker inspect命令可以找到相关容器的更多细节,如下所示:

docker inspect 6074757a

启动容器时使用卷

创建附加了卷的容器的命令如下所示:

docker run --name container-with-volume -v data:/data ubuntu

在本例中,创建了一个名为container-with-volume的容器,其中一个名为data的卷被映射到容器内的/data目录。使用卷时,不提供主机目录的完整路径,而是提供存储数据的卷名。在后台,Docker 将通过将这个卷映射到主机上的一个目录来创建和管理它。

让我们检查使用以下命令创建的容器:

docker inspect container-with-volume | jq ".[0].Mounts"
[
  {
    "Type": "volume",
    "Name": "data",
    "Source": "/var/lib/docker/volumes/data/_data",
    "Destination": "/data",
    "Driver": "local",
    "Mode": "z",
    "RW": true,
    "Propagation": ""
  }
]

查看mounts部分,您可以得出结论,Docker 在/var/lib/docker/volumes/data/_data的主机目录中创建了一个名为data的新卷,该卷的内容由 Docker 管理。这个卷被安装到容器的/data目录中。

也可以使用以下命令提前生成这些卷:

docker volume create info

您可以使用docker volume inspect来检查卷的属性:

docker volume inspect info
[
    {
        "CreatedAt": "2021-07-27T19:23:00Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/info/_data",
        "Name": "images",
        "Options": {},
        "Scope": "local"
    }
]

现在,您可以在创建/运行容器时引用该卷,如下所示:

docker run -it --name info-container -v info:/container-info ubuntu bash

让我们尝试创建与前面相同的文件。从容器内的终端,键入以下内容:

echo "This is a file created from container having kernel `uname -r`" > /container-info/docker_kernel_info.txt

退出容器,然后使用以下命令停止并移除容器:

exit
docker stop info-container
docker rm info-container

在没有卷的情况下,当容器被删除时,其可写层也将被删除。让我们看看当您启动一个附加了卷的新容器时会发生什么。请记住,这不是一个绑定挂载,因此您没有从 Docker 主机显式转发任何目录。下面的命令将在名为new-info-container的容器上启动一个 shell,其中一个名为info的卷被挂载到容器的/container-info目录中。

docker run -it --name new-info-container -v info:/container-info ubuntu bash

检查容器的/data-volume目录的内容,如下所示:

cd /container-info/
ls
docker-kernel-info.txt

检查docker-kernel-info.txt的内容,如下所示:

cat docker_kernel_info.txt
This is a file created from container having kernel 4.9.87-linuxkit-aufs.

当您将文件写入装载并映射到卷的目录中时,数据将保存在卷中。当您启动一个新的容器时,提供卷名和run命令会将卷附加到容器上,使得任何以前保存的数据都可以用于新启动的容器。

docker 文件中的卷指令

VOLUME指令将指令后面提到的路径标记为 Docker 管理的外部存储数据卷。语法如下所示:

VOLUME ["/data-volume"]

指令后面提到的路径可以是 JSON 数组,也可以是用空格分隔的路径数组。

Note

docker 文件中的VOLUME指令不支持命名卷。因此,当容器运行时,卷名将是自动生成的名称。

练习

Building and Running an Nginx Container with Volumes and Bind Mounts

在本练习中,您将构建一个附加了 Docker 卷的nginx Docker 映像,其中包含一个自定义的nginx配置。在练习的第二部分,您将附加一个绑定挂载和一个包含静态 web 页面和自定义nginx配置的卷。本练习的目的是帮助您了解如何利用卷和绑定装载来简化本地开发。

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

从 Dockerfile 文件开始,如下所示。

FROM nginx:alpine
COPY default.conf /etc/nginx/conf.d
VOLUME ["/var/lib"]
EXPOSE 80

这个 Dockerfile 获取一个基本的nginx映像,用定制的default.conf nginx配置文件覆盖default.conf nginx配置文件,并将/var/lib声明为一个卷。您可以使用 repo 中的docker-volume-bind-mount目录中的以下命令来构建它:

docker build -t sathyabhat/nginx-volume .

[+] Building 0.9s (7/7) 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/nginx:alpine   0.8s
 => [internal] load build context   0.0s
 => => transferring context: 34B 0.0s
 => [1/2] FROM docker.io/library/nginx:alpine@sha256:ad14f34   0.0s
 => CACHED [2/2] COPY default.conf /etc/nginx/conf.d  0.0s
 => exporting to image  0.0s
 => => exporting layers 0.0s
 => => writing image sha256:f6f3af7 0.0s
 => => naming to docker.io/sathyabhat/nginx-volume 0.0s

在运行该图像之前,请查看定制的nginx default.conf内容:

server {
    listen       80;
    server_name  localhost;

    location / {
        root   /srv/www/starter;
        index  index.html index.htm;
    }
    access_log  /var/log/nginx/access.log;
    access_log  /var/log/nginx/error.log;

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

}

nginx配置是一个简单的配置;它告诉nginx/srv/www/starter/.提供一个名为index.html的默认文件,让我们运行 Docker 容器。由于nginx正在监听端口 80,您需要告诉 Docker 使用-p标志发布端口:

docker run -d --name nginx-volume  -p 8080:80 sathyabhat/nginx-volume

请注意,您是从 Docker 主机的端口 8080 发布到容器的端口 80。尝试通过导航至http://localhost:8080加载网页。

img/463857_2_En_5_Fig1_HTML.png

图 5-1

未装载源目录时出现 404 错误

当你加载网站时,你会看到一个 HTTP 404 - Page Not Found 错误(见图 5-1 )。这是因为在nginx config档中,你指挥nginxindex.html服务。但是,您还没有将index.html文件复制到容器中,也没有将index.html的位置作为绑定挂载挂载到容器中。结果,nginx找不到index.htm l 文件。

您可以通过将网站文件复制到容器中来纠正这个错误,正如您在上一章中看到的那样。在本练习中,您将利用之前学习的绑定挂载特性,并挂载包含源代码的整个目录。所需要做的就是使用您在前面学到的绑定挂载标志。您不必对 docker 文件进行更改。

使用以下命令停止现有容器:

docker stop nginx-volume

现在,使用绑定挂载启动一个新容器,如以下命令所示:

docker run -d --name nginx-volume-bind -v "$(pwd)"/:/srv/www  -p 8080:80 sathyabhat/nginx-volume

使用以下命令确认容器正在运行:

docker ps

您应该会看到正在运行的容器列表,如下所示:

CONTAINER ID     IMAGE       COMMAND         CREATED        STATUS      PORTS       NAMES
54c857ca065b    sathyabhat/nginx-volume   "nginx -g 'daemon of..."6 minutes ago       Up 6 minutes        0.0.0.0:8080->80/tcpnginx-volume-bind

使用以下命令确认卷和装载是否正确:

docker inspect nginx-volume-bind | jq ".[].Mounts"
[
  {
    "Type": "bind",
    "Source": "/code/practical-docker-with-python/docker-volume-bind-mount/",
    "Destination": "/srv/www",
    "Mode": "",
    "RW": true,
    "Propagation": "rprivate"
  },
  {
    "Type": "volume",
    "Name": "c069ba7",
    "Source": "/var/lib/docker/volumes/c069ba7/_data",
    "Destination": "/var/lib",
    "Driver": "local",
    "Mode": "",
    "RW": true,
    "Propagation": ""
  }
]

让我们再次导航到同一个 URL。如果 mounts 部分看起来不错,那么您应该会看到图 5-2 中的页面。

img/463857_2_En_5_Fig2_HTML.jpg

图 5-2

nginx 服务网页成功

成功!

Adding Volumes to Newsbot

在上一章的练习中,您为 Newsbot 编写了一个 docker 文件。然而,正如您可能已经注意到的,终止容器会重置 Newsbot 的状态,并且您需要重新定制该 bot。要解决这个问题,您将添加一个 SQLite 数据库,该数据库的数据文件将保存到 Docker 卷中。通过完成本练习,您将知道可以通过将容器中的数据保存到卷中,然后将该卷重新附加到新容器中来持久保存数据。

Newsbot 源代码已经从代码库进行了轻微的修改,以便将首选项(即新闻应该从哪个子编辑中获取)保存到 SQLite 数据库中。

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

Dockerfile 文件修改如下:

FROM python:3-alpine

RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev
WORKDIR /apps/subredditfetcher/
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "newsbot.py"]

在这个 Dockerfile 文件中,从python:3-alpine开始作为基本图像。您添加了RUN步骤来安装 Python 包所需的一些库依赖项。然后将源代码复制到容器中,并安装所需的 Python 包。另一个显著的变化是增加了VOLUME指令。正如您在前面了解到的,这是为了告诉 Docker 将指定管理的目录标记为卷,即使您没有在docker run命令中指定所需的卷名。

使用以下命令构建映像:

docker build -t sathyabhat/newsbot-sqlite .

构建日志如下所示:

[+] Building 9.5s (11/11) FINISHED
 => [internal] load build definition from Dockerfile 0.1s
 => => transferring dockerfile: 38B   0.0s
 => [internal] load .dockerignore  0.1s
 => => transferring context: 2B 0.0s
 => [internal] load metadata for docker.io/library/python:3-alpine  2.3s
 => [auth] library/python:pull token for registry-1.docker.io 0.0s
 => [internal] load build context  0.1s
 => => transferring context: 6.23kB   0.0s
 => [1/5] FROM docker.io/library/python:3-alpine@sha256:eb31d7f  0.0s
 => CACHED [2/5] RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev 0.0s
 => CACHED [3/5] WORKDIR /apps/subredditfetcher/  0.0s
 => [4/5] COPY . .  0.1s
 => [5/5] RUN pip install -r requirements.txt  6.3s
 => exporting to image 0.4s
 => => exporting layers   0.3s
 => => writing image sha256:6605a7a   0.0s
 => => naming to docker.io/sathyabhat/newsbot-sqlite 0.0s

现在使用docker run命令运行机器人。注意,您通过-v标志提供了卷名。不要忘记将第三章中生成的 Newsbot API 密匙传递给NBT_ACCESS_TOKEN环境变量。

docker run --rm --name newsbot-sqlite -e NBT_ACCESS_TOKEN -v newsbot-data:/data sathyabhat/newsbot-sqlite

run命令创建一个名为newsbot-sqlite的新容器,一个名为newsbot-data的卷被附加到该容器,并被挂载到容器内的/data目录中。--rm标志确保容器停止时被移走。

如果 bot 启动良好,您应该开始看到这些日志:

docker run --rm --name newsbot-sqlite -e NBT_ACCESS_TOKEN=<token> -v newsbot-data:/data sathyabhat/newsbot-sqlite

INFO: <module> - Starting newsbot
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': []}

比如说 Python,试着设置一个机器人应该从中获取数据的子编辑器。要做到这一点,从电报,找到机器人和类型/source python

来自应用的日志应该确认收到了命令:

INFO: - handle_incoming_messages - Chat text received: /source python
INFO: - handle_incoming_messages - Sources set for nnn to  python
INFO: - handle_incoming_messages - nnn
INFO: - post_message - posting Sources set as  python! to nnn

电报窗口现在应该如图 5-3 所示。

img/463857_2_En_5_Fig3_HTML.png

图 5-3

子编辑源的确认

现在您可以获取一些内容。为此,在 bot 窗口中键入/fetch。应用应该用一个加载消息和另一个聊天内容来响应(见图 5-4 )。

img/463857_2_En_5_Fig4_HTML.png

图 5-4

机器人正在从子编辑中获取内容

现在,您可以通过停止 bot、移除容器并创建一个新容器来测试数据持久性。首先通过按 Ctrl+C 来停止 Newsbot。由于您使用--rm标志启动了容器,Docker 将自动移除容器。通过键入之前启动容器时使用的相同命令,创建一个新容器:

docker run --rm --name newsbot-sqlite -e NBT_ACCESS_TOKEN -v newsbot-data:/data sathyabhat/newsbot-sqlite

现在,在电报聊天窗口中,再次键入/fetch。由于子编辑源已经保存到数据库中,您应该可以看到之前配置的子编辑的内容(见图 5-5 )。

img/463857_2_En_5_Fig5_HTML.png

图 5-5

移除并启动新容器后,Newsbot 从 subreddit 获取内容

再次查看内容 Docker 音量设置工作正常。恭喜你!您已成功为此项目设置了数据持久性。

摘要

在本章中,您了解了为什么数据持久性在容器中是一个问题,以及 Docker 为管理数据持久性提供的不同策略。您还深入了解了如何配置,并了解了它们与*绑定挂载的不同之处。*最后,您进行了一些关于如何使用绑定挂载和卷的实践练习,并为 Newsbot 添加了卷支持。在下一章中,你将学习更多关于 Docker 网络的知识,并学习容器如何相互连接。

六、了解 Docker 网络

在前面的章节中,您了解了 Docker 及其相关术语,深入了解了如何使用 Docker 文件构建 Docker 映像,并了解了如何持久存储由容器生成的数据。

在这一章中,你将看到 Docker 中的网络,并了解容器如何在 Docker 的网络特性的帮助下相互对话和发现对方。

为什么我们需要容器网络?

传统上,大多数计算解决方案被认为是单一用途的解决方案,您很少会遇到单个主机(或虚拟机)托管多个工作负载的情况,尤其是生产工作负载。有了容器,情况就变了。随着轻量级容器和先进编排平台(如 Kubernetes 和 DC/OS)的出现,在同一台主机上运行不同工作负载的多个容器,并且应用的不同实例分布在多个主机上是非常常见的。在这种情况下,容器网络有助于允许(或限制)跨容器对话。为了促进这个过程,Docker 提供了不同的网络模式。

Tip

Docker 的联网子系统是通过可插拔驱动实现的;Docker 自带四个驱动程序,更多驱动程序可从 Docker Store 获得,可在 https://store.docker.com/search?category=network&q=&type=plugin 获得。

值得注意的是,Docker 的所有网络模式都是通过软件定义网络 (SDN)实现的。具体来说,在 Linux 系统上,Docker 修改 iptables 规则以提供所需的访问/隔离级别。

默认 Docker 网络驱动程序

对于 Docker 的标准安装,以下网络驱动程序可用:

  • 圣体

  • 覆盖物

  • 麦克法兰

  • 没有人

桥接网络

网络是用户定义的网络,允许连接在同一网络上的所有容器相互通信。好处是在同一网桥网络上的容器可以相互连接、发现和对话,而不在同一网桥上的容器不能直接通信。当在同一主机上运行的容器需要相互通信时,桥接网络是有用的——如果需要通信的容器在不同的 Docker 主机上,那么就需要一个覆盖网络。

安装并启动 Docker 时,会创建一个默认的桥接网络,新启动的容器会连接到该网络。但是,如果您自己创建一个桥接网络,效果会更好。原因有很多:

  • **容器之间更好的隔离。**如您所知,同一个桥接网络上的容器是可发现的,并且可以相互通信。它们自动向对方公开所有端口,没有端口向外界公开。为每个应用提供一个单独的用户定义的桥接网络可以在不同应用的容器之间提供更好的隔离。

  • **跨容器的简单名称解析。**对于加入同一个桥接网络的服务,容器可以通过名称相互连接。对于默认桥接网络上的容器,容器相互连接的唯一方式是通过 IP 地址或使用--link标志,这已被否决。

  • **在用户定义的网络上轻松连接/分离容器。**对于默认网络上的容器,分离它们的唯一方法是停止正在运行的容器,并在新网络上重新创建它。

主机网络

顾名思义,有了主机网络,容器就附加到了 Docker 主机上。这意味着任何到达主机的流量都被路由到容器。由于容器的所有端口都直接连接到主机,在这种模式下,发布端口的概念没有意义。当 Docker 主机上只有一个容器运行时,主机模式是最理想的。

覆盖网络

覆盖网络创建了一个跨越多个 docker 主机的网络。这种类型的网络称为覆盖网络,因为它位于现有主机网络之上,允许连接到覆盖网络的容器跨多个主机进行通信。覆盖网络是一个高级主题,主要用于以群模式建立 Docker 主机集群的情况。覆盖网络还允许您加密通过它们的应用数据流量。

Macvlan 网络

Macvlan 网络利用 Linux 内核的能力,将基于 MAC 的多个逻辑地址分配给单个物理接口。这意味着您可以将 MAC 地址分配给容器的虚拟网络接口,使其看起来好像容器具有连接到网络的物理网络接口。这带来了独一无二的机会,尤其是对于那些希望物理接口存在并连接到物理网络的遗留应用。

Macvlan 网络需要对网络接口卡 (NIC)的额外依赖,以支持所谓的“混杂”模式,这是一种特殊的模式,允许 NIC 接收所有流量并将其定向到控制器,而不是仅接收 NIC 预期接收的流量。

无网络

当容器启动时,Docker 将容器连接到默认的桥接网络。桥接网络允许容器发出网络请求。尽管容器网络绝对是一个特性和亮点,但在许多情况下,应用必须完全隔离,不允许传入或传出请求——特别是对于安全性和合规性要求高的应用。在这种情况下,无网络就派上了用场。

顾名思义,无网络是指容器没有连接到任何网络接口,也没有接收或发送任何网络流量。在这种网络模式下,仅创建环回接口,允许容器与自身对话,但不能与外界或其他容器对话。

使用此处显示的命令,可以在无网络的情况下启动容器:

docker run -d --name nginx --network=none -p 80:80 nginx

尝试curl端点导致瞬间Connection Refused,表明容器不接受连接。

curl localhost
curl: (7) Failed to connect to localhost port 80 after 1 ms: Connection refused

如果您使用容器打开一个交互式终端,并尝试使用curl发出网络请求,如下所示:

docker exec -it nginx sh
curl google.com
curl: (6) Could not resolve host: google.com

您将看到没有配置网络。容器无法接收或发送网络流量。

使用 Docker 网络

现在您已经从概念上理解了不同的网络模式,您可以尝试其中的一些模式。本章只看桥接网络,因为它是最常用的驱动程序。与其他子系统非常相似,Docker 附带了一个用于处理 Docker 网络的子命令。要开始,请尝试以下命令:

docker network

您应该会看到可用选项的说明:

docker network

Usage:   docker network COMMAND

Manage networks

Options:

Commands:
  connect     Connect a container to a network
  create      Create a network
  disconnect  Disconnect a container from a network
  inspect     Display detailed information on one or more networks
  ls          List networks
  prune       Remove all unused networks
  rm          Remove one or more networks

现在看看哪些网络可用。为此,请键入以下内容:

docker network ls

至少,您应该看到列出了这些网络:

docker network ls
NETWORK ID NAME DRIVER SCOPE
8ea951d9f963 bridge bridge local
790ed54b21ee host host local
38ce4d23e021 none null local

其中每一种都对应于前面提到的三种不同类型的网络——网桥、主机和无类型网络。您可以通过键入以下命令来检查网络的详细信息:

docker network inspect <network id or name>

例如,如果您想要检查默认桥接网络,请键入以下命令:

docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "c540708",
        "Created": "2018-04-17T13:10:43.002552762Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

除其他外,您可以看到:

  • Options下的com.docker.network.bridge.default_bridge键表示网桥是默认的。

  • "EnableIPv6": false表示此网桥禁用了 IPv6。

  • IPAM – Config下的"Subnet"键表示 Docker 网络子网的 CIDR 为 172.17.0.0/16。这意味着多达 65,536 个容器可以连接到这个网络(这是从/16的 CIDR 区块得出的)。

  • Options下的com.docker.network.bridge.enable_ip_masquerade表示网桥启用了 IP 伪装。这意味着外界看不到容器的私有 IP,看起来好像请求来自 Docker 主机。

  • com.docker.network.bridge.host_binding_ipv4表示主机绑定为0.0.0.0。网桥绑定到主机上的所有接口。

相反,如果您检查无网络:

docker network inspect none
[
    {
        "Name": "none",
        "Id": "d30afbe",
        "Created": "2017-05-10T10:37:04.125762206Z",
        "Scope": "local",
        "Driver": "null",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": []
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

驱动程序null表示不会为此处理任何联网。

桥接网络

在创建桥接网络之前,您需要创建两个运行的容器:

  • MySQL 数据库服务器

  • 用于管理 MySQL 数据库的网络门户

要创建 MySQL 容器,请运行以下命令:

docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=dontusethisinprod mysql:8

因为您是在分离模式下启动的(由-d标志指定),所以请跟踪日志,直到您确定容器已启动:

docker logs -f mysql

结果应该是以下几行:

Initializing database
[...]
Database initialized
[...]
MySQL init process in progress...
[...]
MySQL init process done. Ready for start-up.
[...]
[Note] mysqld: ready for connections.
Version: '8.0.26'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
[...]

如果您看到最后一行,MySQL 数据库容器就准备好了。创建adminer容器:

docker run -d --name adminer -p 8080:8080 adminer

以下是adminer的日志:

docker logs -f adminer
PHP 7.4.22 Development Server started

这意味着adminer准备好了。现在看看这两个容器——特别是它们的网络方面。

docker inspect mysql | jq ".[0].NetworkSettings.Networks"
{
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8ea951d",
    "EndpointID": "c33e38",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:03",
    "DriverOpts": null
  }
}

从这个输出中,您知道 MySQL 容器在默认的桥接网络上被分配了一个 IP 地址172.17.0.2。现在检查adminer容器:

docker inspect adminer | jq ".[0].NetworkSettings.Networks"
{
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8ea951d",
    "EndpointID": "a26bcc",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.3",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:04",
    "DriverOpts": null
  }
}

adminer容器与桥接网络内的172.17.0.3的 IP 地址相关联。但是,由于两个容器都绑定到了0.0.0.0的主机 IP,转换为 Docker 主机的所有接口,您应该能够通过它的端口进行连接。

在桥接网络中,无论是默认的 Docker 桥接网络还是您创建的自定义桥接网络(您将在本章的练习中看到这一点),所有的容器都可以使用及其容器名称来访问。但是,只有当这些容器的端口已经暴露时,才能从主机访问它们。为了演示这一点,尝试通过adminer连接到数据库。导航至http://localhost:8080

mysql的身份进入服务器并尝试登录。你会注意到登录会失败(见图 6-1 )。

img/463857_2_En_6_Fig1_HTML.jpg

图 6-1

与指定主机的连接失败

尝试再次登录,这次是在服务器框中。输入 MySQL 容器的 IP 地址,如图 6-2 所示。

img/463857_2_En_6_Fig2_HTML.jpg

图 6-2

尝试使用容器的 IP 地址登录

当你尝试登录时,应该会成功(见图 6-3 )。

img/463857_2_En_6_Fig3_HTML.jpg

图 6-3

使用 IP 地址登录成功

登录成功。虽然当只有一个依赖容器时,输入 IP 是一种可以接受的变通方法,但是许多应用有多个依赖项。这种方法在这些情况下就失效了。

创建命名桥接网络

在本节中,您将创建一个数据库网络,并尝试将 MySQL 和adminer容器连接到网络。您可以通过键入以下命令来创建桥接网络:

docker network create <network name>

Docker 在指定子网方面给了你更多的选择,但是大部分情况下缺省值是好的。请注意,桥接网络只允许您创建一个子网。

使用以下命令创建一个名为database的网络:

docker network create database

现在检查您创建的网络:

docker network inspect database
[
    {
        "Name": "database",
        "Id": "8574145",
        "Created": "2021-07-31T15:58:11.4652433Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

请注意,创建的网络有一个子网172.18.0.0/16.,使用以下命令停止并删除现有容器:

docker stop adminer
docker rm adminer
docker stop mysql
docker rm mysql

现在启动 MySQL 容器,这次连接到数据库网络。该命令如下所示:

docker run -d --network database --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=dontusethisinprod mysql:8

注意附加的--network标志,它告诉 Docker 应该将容器附加到哪个网络。等待容器初始化。您还可以检查日志,确保容器准备就绪:

docker logs -f mysql

结果应该是以下几行:

Initializing database
[...]
Database initialized
[...]
MySQL init process in progress...
[...]
MySQL init process done. Ready for start up.
[...]
[Note] mysqld: ready for connections.
Version: '8.0.26'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
[...]

现在检查容器:

docker inspect mysql | jq ".[0].NetworkSettings.Networks"
{
  "database": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": [
      "6149cb2453da"
    ],
    "NetworkID": "8574145",
    "EndpointID": "3343960402",
    "Gateway": "172.18.0.1",
    "IPAddress": "172.18.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:12:00:02",
    "DriverOpts": null
  }
}

注意,容器是数据库网络的一部分。您也可以通过检查数据库网络来确认这一点。

docker network inspect database | jq ".[0].Containers"
{
  "6149cb2": {
    "Name": "mysql",
    "EndpointID": "3343960",
    "MacAddress": "02:42:ac:12:00:02",
    "IPv4Address": "172.18.0.2/16",
    "IPv6Address": ""
  }
}

注意,数据库网络中的 containers 键包含 MySQL 容器。启动adminer容器。键入以下命令:

docker run -d --name adminer -p 8080:8080 adminer

请注意,--network命令已被省略。这意味着adminer将连接到默认的桥接网络:

docker inspect adminer | jq ".[0].NetworkSettings.Networks"
{
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8ea951d",
    "EndpointID": "c1a5df0",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:02",
    "DriverOpts": null
  }
}

将容器连接到命名的桥接网络

Docker 让你可以很容易地将一个容器连接到另一个网络上。为此,请键入以下命令:

dockr network connect <network name> <container name>

您需要将adminer容器连接到数据库网络,如下所示:

docker network connect database adminer

现在检查adminer容器:

docker inspect adminer | jq ".[0].NetworkSettings.Networks"
{
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8ea951d",
[...]
    "DriverOpts": null
  },
  "database": {
    "IPAMConfig": {},
    "Links": null,
    "Aliases": [
      "2a7363ec1888"
    ],
    "NetworkID": "8574145",
    [...]
    "DriverOpts": {}
  }
}

请注意,networks 键有两个网络,默认网桥网络和您刚刚连接到的数据库网络。因为容器不需要连接到默认的桥接网络,所以您可以断开它。为此,命令如下:

docker network disconnect bridge adminer

现在使用下面的命令检查adminer容器,您可以只看到连接的数据库网络。

docker inspect adminer | jq ".[0].NetworkSettings.Networks"
{
  "database": {
    "IPAMConfig": {},
    "Links": null,
    "Aliases": [
      "2a7363ec1888"
    ],
    "NetworkID": "8574145",
[...]
    "DriverOpts": {}
  }
}

桥接网络不再连接到adminer网络。通过导航到http://localhost:8080启动adminer。在服务器字段中,输入想要连接的容器名称,即数据库容器名称mysql,如图 6-4 所示。

img/463857_2_En_6_Fig4_HTML.jpg

图 6-4

通过命名主机连接到容器

输入详细信息,然后单击登录。登录应该成功,您应该会看到如图 6-5 所示的屏幕。

img/463857_2_En_6_Fig5_HTML.jpg

图 6-5

命名主机解析为 IP 并成功连接

因此,用户定义的桥接网络使得连接服务非常容易;你不必去寻找 IP 地址。Docker 通过让您使用容器的名称作为主机来连接到服务,使这变得很容易。Docker 处理容器名到 IP 地址的幕后翻译。

主机网络

在主机网络中,Docker 不为容器创建虚拟网络;相反,Docker 主机的网络接口被绑定到容器。

当您只有一个容器在主机上运行并且不需要任何桥接网络或网络隔离时,主机网络是非常好的。现在您将创建一个在主机模式下运行的nginx容器,看看如何运行它。

前面你看到已经有一个网络叫host。这不是控制网络是否是主机网络的名称;是司机。回想一下,主机网络有一个主机驱动程序,因此任何连接到主机网络的容器都将以主机网络模式运行。

要启动容器,只需传递--network host参数。尝试以下命令来启动一个nginx容器,并将容器的端口 80 发布到主机的 8080 端口。

docker run -d --network host -p 8080:80 nginx:alpine
WARNING: Published ports are discarded when using host network mode

注意 Docker 警告您没有使用端口发布。因为容器的端口直接绑定到 Docker post,所以发布端口的概念不会出现。实际的命令应该如下所示:

docker run -d --network host nginx:alpine

练习

Connecting a Mysql Container to a Newsbot Container

在前一章的练习中,您为 Newsbot 编写了一个 Dockerfile 并构建了容器。然后使用 Docker 卷跨容器持久化数据库。在本练习中,您将修改 Newsbot,以便数据持久保存到 MySQL 数据库,而不是保存到 SQLite DB。然后,您将创建一个定制的桥接网络来连接项目容器和 MySQL 容器。

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

考虑下面的 Dockerfile 文件。它看起来,实际上,与你在第五章的练习 2 中使用的 Dockerfile 非常相似。唯一需要改变的是 Newsbot 的代码,使它连接到 MySQL 服务器,而不是从 SQLite 数据库读取。

FROM python:3-alpine

RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev
WORKDIR /apps/subredditfetcher/
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "newsbot.py"]

现在使用以下命令构建容器:

docker build -t sathyabhat/newsbot-mysql .
[+] Building 2.9s (11/11) FINISHED
 => [internal] load build definition from Dockerfile   0.1s
 => => transferring dockerfile: 38B  0.0s
 => [internal] load .dockerignore 0.1s
 => => transferring context: 2B   0.0s
 => [internal] load metadata for docker.io/library/python:3-alpine 2.6s
 => [auth] library/python:pull token for registry-1.docker.io   0.0s
 => [1/5] FROM docker.io/library/python:3-alpine@sha256:1e8728b 0.0s
 => => resolve docker.io/library/python:3-alpine@sha256:1e8728b 0.0s
 => [internal] load build context 0.0s
 => => transferring context: 309B 0.0s
 => CACHED [2/5] RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev cargo   0.0s
 => CACHED [3/5] WORKDIR /apps/subredditfetcher/ 0.0s
 => CACHED [4/5] COPY . .   0.0s
 => CACHED [5/5] RUN pip install --upgrade pip && pip install -r requirements.txt 0.0s
 => exporting to image   0.0s
 => => exporting layers  0.0s
 => => writing image sha256:44cd813  0.0s
 => => naming to docker.io/sathyabhat/newsbot-mysql 0.0s

创建一个名为newsbot的新网络,容器将连接到这个网络。为此,请键入以下内容:

docker network create newsbot

现在,您将打开一个新的 MySQL 容器,并将其连接到您之前创建的网络。因为您希望数据持久化,所以您还将 MySQL 数据库挂载到一个名为newsbot-db的卷上。这个练习使用root作为用户名,使用dontusethisinprod作为密码。这些凭证非常脆弱,我们强烈建议您不要在现实世界中使用它们。

键入以下命令启动 MySQL 容器:

docker run -d --name mysql --network newsbot -v newsbot-db:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=dontusethisinprod mysql:8

注意--network标志,它告诉 Docker 将mysql容器连接到名为newsbot的网络。MySQL 将所有与数据库相关的文件保存在/var/lib/mysql目录中,-v newsbot-db:/var/lib/mysql标志指示 Docker 将容器中/var/lib/mysql目录的内容保存到名为newsbot-db的卷中。这样,即使在容器被移除后,内容仍然存在。

遵循日志并验证 MySQL 数据库是否启动:

docker logs mysql

Initializing database
[...]
Database initialized
[...]
MySQL init process in progress
[...]
MySQL init process done. Ready for start up.
[...]
2021-08-01T12:41:15.295013Z 0 [Note] mysqld: ready for connections.
Version: '8.0.26'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)

最后几行表示 MySQL 数据库启动了。现在启动 Newsbot 容器,同时将其连接到您创建的newsbot网络。为此,请键入以下命令:

docker run --rm --network newsbot --name newsbot-mysql -e NBT_ACCESS_TOKEN=<token> sathyabhat/newsbot-mysql

注意用第三章中生成的 Newsbot API 键的值替换<token>

您应该会看到以下日志:

INFO: <module> - Starting up
INFO: <module> - Waiting for 60 seconds for db to come up
INFO: <module> - Checking on dbs
INFO: get_updates - received response: {'ok': True, 'result': []}
INFO: get_updates - received response: {'ok': True, 'result': []}

因为创建了新卷,所以上一章设置的源不可用。

再次设置机器人应该从中获取数据的 subreddit,比如 Docker。要做到这一点,从电报,找到机器人和类型/source docker。来自应用的日志应该确认收到了命令:

INFO: handle_incoming_messages - Chat text received: /source docker
INFO: handle_incoming_messages - Sources set for 7342383 to  docker
INFO: handle_incoming_messages - 7342383
INFO: post_message - posting Sources set as  docker! to 7342383
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': []}

您的电报窗口应该如图 6-6 所示。

img/463857_2_En_6_Fig6_HTML.jpg

图 6-6

子编辑源的确认

现在您可以获取一些内容。为此,在 bot 窗口中键入/fetch。应用应该通过加载一个消息和另一个聊天内容来响应,如图 6-7 所示。

img/463857_2_En_6_Fig7_HTML.png

图 6-7

机器人正在从子编辑中获取内容

现在,您将确认 Newsbot 确实正在将源保存到数据库中。为此,使用以下命令连接到正在运行的mysql容器:

docker exec --it mysql sh

现在,在容器外壳中,键入以下命令以连接到 MySQL 服务器:

mysql –p

输入密码(如前所述)进行连接。如果您输入了正确的密码,将会收到以下消息:

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 32
Server version: 8.0.26 MySQL Community Server - GPL
Copyright (c) 2000, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>

在 MySQL 提示符下,键入以下命令以确保 Newsbot 数据库存在:

show databases;

您应该会看到类似于以下列表的数据库列表:

show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| newsbot            |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.03 sec)

键入以下命令选择数据库,然后获取名为source的表的内容:

use newsbot
select * from source;
+-----------+------------+
| person_id | fetch_from |
+-----------+------------+
|   7342383 |  docker    |
+-----------+------------+
1 row in set (0.00 sec)

这向您展示了 Newsbot 可以成功地连接到 MySQL 容器并将数据保存到数据库中。

摘要

在这一章中,你学习了容器网络的基础知识和 Docker 网络的不同模式。您还学习了如何创建和使用定制的 Docker 桥接网络,并了解了对 Docker 主机网络的见解。最后,您运行了一些关于创建单独的数据库容器(使用 MySQL)的实践练习,并学习了如何将数据库容器连接到 Newsbot 项目。在下一章中,您将了解 Docker Compose,以及 Docker Compose 如何让您轻松运行多个相互依赖的容器。

七、了解 Docker Compose

在前面的章节中,您了解了 Docker 及其相关术语,深入了解了如何使用 Docker 文件构建 Docker 映像,了解了如何持久存储容器生成的数据,并借助 Docker 的网络功能链接了各种运行中的容器。

在这一章中,你将会看到 Docker Compose,这是一个运行多容器应用的工具,可以调出各种链接的、相关的容器等等——所有这些都只需要一个配置文件和一个命令。

Docker 编写概述

随着软件变得越来越复杂,以及您越来越倾向于微服务架构,需要部署的组件数量也会大大增加。虽然微服务可能通过鼓励松散耦合的服务来帮助保持整个系统的流动性,但从运营的角度来看,事情会变得更加复杂。当您有依赖的应用时,这尤其具有挑战性。例如,为了让 web 应用正常工作,它需要其数据库在 web 层开始响应请求之前工作。

Docker 可以很容易地将每个微服务绑定到一个容器。Docker Compose 使得编排所有这些容器变得非常容易。如果没有 Docker Compose,容器编排步骤将涉及构建各种映像、创建所需的网络,然后按照必要的顺序使用一系列docker run命令运行应用。随着容器数量的增加和部署目标的增加,手动运行这些步骤变得不合理,您将需要走向自动化。

从本地开发的角度来看,手动启动多个相互关联的服务非常繁琐和痛苦。Docker Compose 大大简化了这一点。Docker Compose 只需提供一个描述所需容器和容器间关系的 YAML 文件,就可以用一个命令显示所有容器。Docker Compose 不仅可以打开容器,还可以让您完成以下任务:

  • 构建、停止和启动与应用相关联的容器。

  • 跟踪正在运行的容器的日志,省去为每个容器打开多个终端会话的麻烦。

  • 查看每个容器的状态。

Docker Compose 帮助您实现持续集成。通过提供多个可处理的、可复制的环境,Docker Compose 允许您独立地运行集成测试,允许对这些自动化测试用例采用一种干净的方法。它允许您运行测试,验证结果,然后彻底地拆除环境。

正在安装 Docker Compose

Docker Compose 作为 Docker 安装的一部分预先安装,不需要任何额外的步骤就可以在 macOS 和 Windows 系统上开始使用。在 Linux 系统上,你可以从它的 GitHub 发布页面下载 Docker Compose 二进制文件,可以在 https://github.com/docker/compose/releases 下载。或者,您可以运行下面的curl命令来下载正确的二进制文件。

sudo curl -L https://github.com/docker/compose/releases/download/1.21.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose

如果您已经安装了 Python 和 pip,您也可以使用以下命令使用 pip 来安装docker-compose:

pip install docker-compose

Note

确保pip install docker-compose命令中的版本号与 GitHub 发布页面上 Docker Compose 的最新版本相匹配。否则,你最终会得到一个过时的版本。

下载完二进制文件后,更改权限,以便可以使用以下命令执行它:

sudo chmod +x /usr/local/bin/docker-compose

如果文件是手动下载的,请在运行命令之前将下载的文件复制到/usr/local/bin目录。要确认安装成功且运行正常,请运行以下命令:

docker-compose version

结果应该是 Docker Compose 的版本,如下所示:

docker-compose version 1.29.1, build 5becea4c
docker-py version: 5.0.0
CPython version: 3.9.0
OpenSSL version: OpenSSL 1.1.1g 21 Apr 2020

Docker 合成基础知识

与 Docker 文件(Docker 引擎关于如何构建 Docker 映像的一组指令)不同,撰写文件是一个 YAML 配置文件,它定义了启动应用所需的服务、网络和卷。Docker 希望合成文件出现在调用docker-compose命令的同一路径中,并被称为docker-compose.yaml(或docker-compose.yml)。这可以使用-f标志,后跟撰写文件名的路径来覆盖。

Docker 合成版本概述

在 Docker 桌面 3.4 版中,Docker 推出了一个新版本的 Docker Compose,称为 Compose V2。Compose V2 被认为是旧版本 Compose 的替代产品。Docker 提取了 Compose 文件的 YAML 文件模型,围绕它创建了一个社区,并将其作为一个规范提交,称为 Compose 规范。撰写 V2 实现撰写规范。然而,它的功能还不能与 Compose V1 媲美,可以从 Docker 桌面设置的实验设置中启用。鉴于缺乏功能对等,这一章的重点是撰写 V1。如果您需要“合成 V2”中的特定功能,例如对 GPU 设备和配置文件的支持,您可以使用本章的其余部分作为指南。只需将docker-compose命令(带连字符)替换为docker compose(用空格替换连字符),这些命令仍然可以工作。

合成文件版本和合成规范

虽然合成文件是一个 YAML 文件,但 Docker 使用文件开头的版本密钥来确定支持 Docker 引擎的哪些功能。合成文件格式有三种版本。随着 Docker Compose v1.27.0 和 Docker Compose V2 的推出,Docker 统一了 2.x 和 3.x 版本的 Compose 文件格式,并将其作为规范提交给了社区。以下是合成文件格式的前三个版本的简要说明:

  • 版本 1: 版本 1 被认为是遗留格式。如果 Docker 合成文件在 YAML 文件的开头没有版本密钥,Docker 会将其视为版本 1 格式。版本 1 已被否决,不再受支持。

  • 版本 2.x: 由 YAML 文件开头的版本 2.x 密钥标识的版本 2.x。

  • 版本 3.x: 由 YAML 文件开头的版本 3.x 密钥标识的版本 3.x。

  • **Compose spec:**Compose spec 统一了 Compose 文件格式的 2.x 和 3.x 版本,并已作为规范提交给社区。Compose 规范也反对版本键。

以下几节将讨论这三个主要版本之间的差异。

版本 1

在 YAML 文件的根目录下没有版本密钥的 Docker 合成文件被视为版本 1 合成文件。Docker Compose 的未来版本将弃用并删除版本 1,因此我不建议编写版本 1 文件。除了不赞成之外,版本 1 还有以下主要缺点:

  • 版本 1 文件不能声明命名服务、卷或生成参数。

  • 只有使用 links 标志才能启用容器发现。

版本 2

Docker Compose 第 2 版文件具有值为 2 或 2.x 的版本密钥。第 2 版引入了一些更改,这使得第 2 版与以前版本的 Compose 文件不兼容。其中包括:

  • 所有服务都必须存在于 services 键中。

  • 所有容器都位于特定于应用的默认网络上,可以通过主机名发现容器,主机名由服务名指定。

  • 链接变得多余。

  • 引入了depends_on标志,允许您指定相关的容器以及容器出现的顺序。

版本 3

Docker Compose 第 3 版文件有一个值为 3 或 3.x 的版本密钥。第 3 版删除了几个不推荐使用的选项,包括volume_drivervolumes_from等等。版本 3 还增加了一个deploy键,用于 Docker Swarm 上服务的部署和运行。

撰写规范

Docker 统一了 Compose 文件格式的 2.x 和 3.x 版本,并引入了 Compose 规范。在 Docker Compose 及以上版本中,Docker 将 Compose 规范实现为当前最新的格式。Docker 也声明以前的版本是遗留的,尽管它们仍然受支持。合成规范也反对合成文件中的版本密钥。Compose 规范允许您定义不依赖于任何特定云提供商的容器应用,包括多容器应用所需的基本构建块:

  • Services key 定义了计算方面,实现为一个或多个容器。

  • Networks key 定义了服务如何相互通信。

  • Volumes 键定义服务如何存储持久数据。

清单 7-1 中显示了一个示例引用组合文件。

services:
    database:
      image: mysql
      environment:
        MYSQL_ROOT_PASSWORD: dontusethisinprod
      volumes:
        - db-data:/var/lib/mysql
    webserver:
      image: 'nginx:alpine'
      ports:
        - 8080:80
      depends_on:
        - cache
        - database
    cache:
      image: redis

volumes:
    db-data:

Listing 7-1A Sample Docker Compose File

与 docker 文件类似,Compose 文件可读性很强,很容易理解。这个合成文件用于一个典型的 web 应用,该应用包括一个 web 服务器、一个数据库服务器和一个缓存服务器。Compose 文件声明当 Docker Compose 运行时,它将启动三个服务——web 服务器、数据库服务器和缓存服务器。web 服务器依赖于数据库和缓存服务,这意味着除非启动数据库和缓存服务,否则不会启动 web 服务。缓存和数据库关键字表明,对于缓存,Docker 必须为数据库调出 Redis 映像和 MySQL 映像。

要调出所有容器,发出以下命令:

docker-compose up -d

[+] Running 4/4
 ⠿ Network code_default        Created   0.1s
 ⠿ Container code_database_1   Started   1.2s
 ⠿ Container code_cache_1      Started   1.1s
 ⠿ Container code_webserver_1  Started   2.3s

命令发出后,Docker 会在后台调出所有的服务。请注意,尽管合成文件首先定义了数据库,其次是 web 服务器,最后是缓存,Docker 仍然在调用 web 服务器容器之前调用缓存容器和数据库容器。这是因为您为 web 服务器定义了如下的depends_on键:

depends_on:
    - cache
    - database

这告诉 Docker 在启动 web 服务器之前先启动缓存和数据库容器。然而,Docker Compose 不会等待并检查缓存容器是否准备好接受连接,然后打开数据库容器——它只是按照指定的顺序打开容器。

您可以通过键入以下命令来查看日志:

docker-compose logs

webserver_1  | [notice] 1#1: nginx/1.21.1
database_1   | [Note] [Entrypoint]: Switching to dedicated user 'mysql'
cache_1      | # Server initialized
cache_1      | * Ready to accept connections

Docker 将聚合每个容器的STDOUT,并在前台运行时将它们流式传输。默认情况下,docker-compose日志将只显示日志的快照。如果您想让日志连续流式传输,您可以添加-f--follow标志来告诉 Docker 继续流式传输日志。或者,如果您想查看每个容器中最后的 n 个日志,您可以键入:

docker-compose logs --tail=n

其中 n 是你需要看到的行数。停止容器就像发出stop命令一样简单,如下所示:

docker-compose stop

[+] Running 3/3
 ⠿ Container code_webserver_1  Stopped   0.5s
 ⠿ Container code_database_1   Stopped   1.4s
 ⠿ Container code_cache_1      Stopped   0.4s

要恢复停止的容器,发出start命令:

docker-compose start
[+] Running 3/3
 ⠿ Container code_database_1   Started  1.8s
 ⠿ Container code_cache_1      Started  1.9s
 ⠿ Container code_webserver_1  Started  0.7s

要完全拆除容器,请发出以下命令:

docker-compose down

这将停止所有容器,还将删除发出docker-compose up 时创建的相关容器、网络和卷。

[+] Running 4/4
 ⠿ Container code_webserver_1  Removed  0.5s
 ⠿ Container code_cache_1      Removed  0.6s
 ⠿ Container code_database_1   Removed  1.3s
 ⠿ Network code_default        Removed  0.2s

Docker 构成档案参考

回想一下,合成文件是一个 YAML 文件,用于 Docker 读取和设置合成作业的配置。本节解释了 Docker 合成文件中不同键的作用。

服务密钥

服务是组合 YAML 的第一个根键,它是需要创建的容器的配置。

构建密钥

生成密钥包含在生成时应用的配置选项。构建键可以是构建上下文的路径,也可以是由上下文和可选 Dockerfile 位置组成的详细对象:

services:
    app:
        build: ./app

services:
    app:
        build:
            context: ./app
            Dockerfile: dockerfile-app

上下文关键字

上下文键设置构建的上下文。如果上下文是相对路径,则该路径被视为相对于合成文件的位置。

build:
    context: ./app
    Dockerfile: dockerfile-app

图像键

如果图像标签与构建选项一起提供,Docker 将构建图像,然后用提供的图像名称和标签命名和标记图像。

services:
    app:
        build: ./app
        image: sathyabhat:app

环境/env_file 键

environment键为应用设置环境变量,而env_file提供了环境文件的路径,读取该文件以设置环境变量。environmentenv_file都可以接受单个文件或多个文件作为一个数组。

在下面的例子中,对于 app 服务,两个环境变量——PATHAPI_KEY,分别具有值/homethisisnotavalidkey——被设置为 app 服务。

services:
    app:
        image: mysql
        environment:
            PATH: /home
            API_KEY: thisisnotavalidkey

在下面的示例中,从名为.env的文件中获取环境变量,并将这些值分配给 app 服务。

services:
    app:
        image: mysql
        env_file: .env

在下面的示例中,提取了在env_file键下定义的多个环境文件,并将这些值分配给 app 服务。

services:
    app:
        image: mysql
        env_file:
            - common.env
            - app.env
            - secrets.env

依赖键

该键用于设置跨各种服务的依赖性要求。考虑以下配置:

services:
    database:
        image: mysql
    webserver:
        image: nginx:alpine
        depends_on:
            - cache
            - database
    cache:
        image: redis

发出docker-compose up时,Docker 将按照定义的依赖顺序调出服务。在前一个例子中,Docker 在启动 webserver 服务之前启动了缓存和数据库服务。

Caution

使用depends_on键,Docker 将只按定义的顺序调出服务;它不会等待每个服务都准备好,然后调用后续服务。

图像键

此键指定当一个容器被打开时使用的图像的名称。如果映像在本地不存在,Docker 会在构建密钥不存在的情况下尝试提取它。如果构建密钥在合成文件中,Docker 将尝试构建并标记图像。

services:
    database:
        image: mysql

端口键

此键指定将向端口公开的端口。当提供此密钥时,您可以指定两个端口(即,容器端口将向其公开的 Docker 主机端口或仅容器端口),在这种情况下,将选择主机上随机的临时端口号。

services:
    database:
        image: nginx
        ports:
            - "8080:80"

services:
    database:
        image: nginx
        ports:
            - "80"

卷密钥

Volumes 既可以作为顶级键,也可以作为服务的子选项。当volumes被称为顶级键时,它允许您提供将用于底层服务的命名卷。其配置如下所示:

services:
    database:
        image: mysql
        environment:
            MYSQL_ROOT_PASSWORD: dontusethisinprod
        volumes:
            - "dbdata:/var/lib/mysql"
    webserver:
        image: nginx:alpine
        depends_on:
            - cache
            - database
    cache:
        image: redis

volumes:
    dbdata:

如果没有顶级 volumes 键,Docker 将在创建容器时抛出一个错误。考虑以下配置,其中跳过了volumes键:

services:
  database:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: dontusethisinprod
    volumes:
        - "dbdata:/var/lib/mysql"
  webserver:
    image: nginx:alpine
    depends_on:
        - cache
        - database
  cache:
    image: redis

尝试启动容器会引发错误,如下所示:

docker-compose up
service "database" refers to undefined volume dbdata: invalid compose project

也可以使用绑定安装。您只需提供路径,而不是引用指定的卷。考虑这种配置:

services:
    database:
        image: mysql
        environment:
            MYSQL_ROOT_PASSWORD: dontusethisinprod
        volumes:
            - ./dbdir:/var/lib/mysql
    webserver:
        image: nginx:alpine
        depends_on:
            - cache
            - database
    cache:
        image:redis

volume键的值为./dbdir:/var/lib/mysql,这意味着 Docker 将把当前目录中的dbdir挂载到容器的/var/lib/mysql目录中。相对路径是相对于合成文件的目录来考虑的。

重新启动键

重启键为容器提供重启策略。默认情况下,重启策略设置为no,这意味着无论如何 Docker 都不会重启容器。以下是可用的重启策略:

  • no:容器永远不会重启

  • always:容器在退出后总是会重新启动

  • on-failure:如果由于错误退出,容器将重新启动

  • unless-stopped:除非明确退出或者 Docker 守护进程停止,否则容器将总是重新启动

docker 由 CLI 参考组成

命令自带一组子命令。下面几节将对它们进行解释。

build 子命令

build命令读取合成文件,扫描构建密钥,然后继续构建和标记图像。图像被标记为project_service。如果合成文件没有构建键,Docker 将跳过构建任何图像。用法如下:

docker-compose build <options> <service...>

如果提供了服务名,Docker 将继续为该服务构建映像。否则,它将为所有服务构建映像。一些常用的选项如下:

--compress: Compresses the build context
--no-cache Ignore the build cache when building the image

down 子命令

down命令停止容器,并将继续移除容器、卷和网络。其用法如下:

docker-compose down

exec 子命令

compose exec命令相当于docker exec命令。它允许您在任何容器上运行特定的命令。其用法如下:

docker-compose exec  <service> <command>

logs 子命令

logs命令显示所有服务的日志输出。其用法如下:

docker-compose logs <options> <service>

默认情况下,logs将只显示所有服务的最后日志。通过提供服务名,您可以只显示一个服务的日志。-f选项跟随日志输出。

停止子命令

stop命令停止容器。其用法如下:

docker-compose stop

练习

Building and Running a Mysql Database Container With a Web UI for Managing the Database

在本练习中,您将构建一个多容器应用,其中包含一个用于 MySQL 数据库的容器和另一个用于 MySQL 的流行 Web UIadminer的容器。因为您已经有了 MySQL 和adminer的预构建映像,所以您不需要构建它们。

提示与本练习相关的源代码、Dockerfile 和docker-compose文件可以在本书的 GitHub repo 上 https://github.com/Apress/practical-docker-with-pythonsource-code/chapter-7/exercise-1目录中找到。

您可以从 Docker 合成文件开始,如下所示:

services:
  mysql:
    image: mysql
    environment:
        MYSQL_ROOT_PASSWORD: dontusethisinprod
    ports:
        - 3306:3306
    volumes:
        - dbdata:/var/lib/mysql
  adminer:
    image: adminer
    ports:
        - 8080:8080

volumes:
    dbdata:

这个组合文件将您在本书中学到的所有内容合并到一个简洁的文件中。因为您的目标是 Compose 规范,所以可以省略 version 标记。在 Services 下,定义两个服务——一个用于数据库,它拉入一个名为mysql的 Docker 映像。创建容器时,环境变量MYSQL_ROOT_PASSWORD为数据库设置 root 密码,容器的端口 3306 被发布到主机。

MySQL 数据库中的数据存储在一个名为dbdata的卷中,并挂载到容器的/var/lib/mysql目录中。这是 MySQL 存储数据的地方。换句话说,保存到容器中数据库的任何数据都由名为dbdata的卷处理。另一个名为adminer的服务只是拉入一个名为adminer的 Docker 映像,并将端口 8080 从容器发布到主机。

通过键入以下命令来验证合成文件:

docker-compose config

如果一切正常,Docker 将打印出解析后的合成文件;它应该是这样的:

services:
  adminer:
    image: adminer
    networks:
      default: null
    ports:
    - mode: ingress
      target: 8080
      published: 8080
      protocol: tcp
  mysql:
    environment:
      MYSQL_ROOT_PASSWORD: dontusethisinprod
    image: mysql
    networks:
      default: null
    ports:
    - mode: ingress
      target: 3306
      published: 3306
      protocol: tcp
    volumes:
    - type: volume
      source: dbdata
      target: /var/lib/mysql
      volume: {}
networks:
  default:
    name: docker-compose-adminer_default
volumes:
  dbdata:

通过键入如下命令运行所有容器:

docker-compose up -d

容器将在后台启动,如下所示:

docker-compose up -d
[+] Running 3/3
 ⠿ Network docker-compose-adminer_default      Created   0.1s
 ⠿ Container docker-compose-adminer_adminer_1  Started   1.0s
 ⠿ Container docker-compose-adminer_mysql_1    Started   1.1s

现在看一下日志。键入以下命令:

docker-compose logs
adminer_1  | PHP 7.4.22 Development Server (http://[::]:8080) started
mysql_1    | [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.26-1debian10 started.
mysql_1    | [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.26'

这告诉你adminer UI 和 MySQL 数据库已经准备好了。导航至http://localhost:8080尝试登录。应当加载adminer登录页面(见图 7-1 )。

img/463857_2_En_7_Fig1_HTML.jpg

图 7-1

管理员登录页面

注意,服务器已经填充了值db。因为docker-compose为应用创建了自己的网络,所以每个容器的主机名就是服务名。在这种情况下,MySQL 数据库服务名是mysql,数据库将可以通过mysql主机名访问。输入用户名root和在MYSQL_ROOT_PASSWORD环境变量中输入的密码(见图 7-2 )。

img/463857_2_En_7_Fig2_HTML.jpg

图 7-2

管理员登录详细信息

如果细节正确,您应该会看到如图 7-3 所示的数据库页面。

img/463857_2_En_7_Fig3_HTML.jpg

图 7-3

登录后即可获得数据库详细信息

Converting Newsbot to a Docker Compose Project

在第六章的练习中,您向 Newsbot 添加了卷,数据被保存到 MySQL 容器中。您还分别启动了newsbotmysql容器,并将它们连接到公共桥接网络。在本练习中,您将编写一个包含 Newsbot 容器和 MySQL 容器的 Docker 合成文件,并附加一个卷来保存数据。在本练习中,您将看到 Docker Compose 如何轻松地打开多个容器,每个容器都有其相关的属性。

提示与本练习相关的源代码、Dockerfile 和docker-compose文件可以在本书的 GitHub repo 上 https://github.com/Apress/practical-docker-with-pythonsource-code/chapter-7/exercise-2目录中找到。

让我们创建一个新的 Docker 合成文件,并添加以下内容:

services:
  newsbot:
    build: .
    depends_on:
      - mysql
    restart: "on-failure"
    environment:
      - NBT_ACCESS_TOKEN=${NBT_ACCESS_TOKEN}
    networks:
      - newsbot

  mysql:
    image: mysql
    volumes:
        - newsbot-db:/var/lib/mysql
    environment:
        - MYSQL_ROOT_PASSWORD=dontusethisinprod
    networks:
      - newsbot

volumes:
  newsbot-db:

networks:
  newsbot:

因为您需要两个服务,一个用于 Newsbot,一个用于 MySQL 服务器,所以它们都有对应的键。对于 Newsbot,您添加一个值为mysqldepends_on键,表示 MySQL 容器应该在 Newsbot 之前启动。但是正如您之前看到的,Docker 不会等待 MySQL 容器准备好,因此 Newsbot 被修改为在尝试连接到mysql容器之前等待 60 秒。还有一个重启策略,在应用失败时重启newsbot容器。

Newsbot 需要 Telegram bot API 令牌,您可以将它从同一个主机环境变量传递给容器环境变量NBT_ACCESS_TOKEN。这两个服务中的每一个都有一个网络密钥,指示容器将被连接到newsbot网络。最后,添加卷和网络的顶级键,声明为newsbot-db用于保存卷的 MySQL 数据,声明为newsbot用于保存网络。

您可以通过键入如下所示的config命令来验证合成文件是否正确有效:

docker-compose config

Docker 打印您编写的合成的配置,类似于合成文件本身。

services:
  mysql:
    environment:
      MYSQL_ROOT_PASSWORD: dontusethisinprod
    image: mysql
    networks:
      newsbot: null
    volumes:
    - type: volume
      source: newsbot-db
      target: /var/lib/mysql
      volume: {}
  newsbot:
    build:
      context: exercise-2/newsbot-compose
      dockerfile: exercise-2/newsbot-compose/Dockerfile
    depends_on:
      mysql:
        condition: service_started
    environment:
      NBT_ACCESS_TOKEN: ""
    networks:
      newsbot: null
    restart: on-failure
networks:
  newsbot:
    name: newsbot-compose_newsbot
volumes:
  newsbot-db:
    name: newsbot-compose_newsbot-db

现在运行撰写应用。不要忘记传递你在第三章中生成的 Newsbot API 键的值。

NBT_ACCESS_TOKEN=<token> docker-compose up

您应该看到容器正在构建和启动,如下所示:

[+] Running 4/4
 ⠿ Network newsbot-compose_newsbot      Created    0.0s
 ⠿ Volume "newsbot-compose_newsbot-db"  Created    0.0s
 ⠿ Container newsbot-compose_mysql_1    Started    1.6s
 ⠿ Container newsbot-compose_newsbot_1  Started    1.8s

Attaching to mysql_1, newsbot_1
newsbot_1  | INFO:  <module> - Starting up
newsbot_1  | INFO:  <module> - Waiting for 60 seconds for db to come up
mysql_1    |  [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
mysql_1    |  [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.26'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
newsbot_1  | INFO:  <module> - Checking on dbs
newsbot_1  | INFO:  get_updates - received response: {'ok': True, 'result': []}
newsbot_1  | INFO:  get_updates - received response: {'ok': True, 'result': []}
newsbot_1  | INFO:  get_updates - received response: {'ok': True, 'result': []}
newsbot_1  | INFO:  get_updates - received response: {'ok': True, 'result': []}

最后一行表示机器人正在工作。尝试通过在电报机器人中输入/sources docker然后输入/fetch来设置一个源并获取数据。如果一切顺利,您应该会在图 7-4 中看到结果。

img/463857_2_En_7_Fig4_HTML.jpg

图 7-4

行动中的 subreddit fetcher 机器人

您可以更进一步,通过修改 Compose 文件来包含adminer服务,这样您就有一个 WebUI 来检查内容是否被保存到数据库中。修改现有的 Docker compose 文件,使其包含如下所示的adminer服务,并将其保存到名为docker-compose.adminer.yml的文件中:

services:
  newsbot:
    build: .
    depends_on:
      - mysql
    restart: "on-failure"
    environment:
      - NBT_ACCESS_TOKEN=${NBT_ACCESS_TOKEN}
    networks:
      - newsbot

  mysql:
    image: mysql
    volumes:
        - newsbot-db:/var/lib/mysql
    environment:
        - MYSQL_ROOT_PASSWORD=dontusethisinprod
    networks:
      - newsbot

  adminer:
    image: adminer
    ports:
        - 8080:8080
    networks:
      - newsbot

volumes:
  newsbot-db:

networks:
  newsbot:

按如下方式键入config命令,确认合成文件有效:

docker-compose -f docker-compose.adminer.yml config
services:
  adminer:
    image: adminer
    networks:
      newsbot: null
    ports:
    - mode: ingress
      target: 8080
      published: 8080
      protocol: tcp
  mysql:
    environment:
      MYSQL_ROOT_PASSWORD: dontusethisinprod
    image: mysql
    networks:
      newsbot: null
    volumes:
    - type: volume
      source: newsbot-db
      target: /var/lib/mysql
      volume: {}
  newsbot:
    build:
      context: exercise-2/newsbot-compose
      dockerfile: exercise-2/newsbot-compose/Dockerfile
    depends_on:
      mysql:
        condition: service_started
    environment:
      NBT_ACCESS_TOKEN: ""
    networks:
      newsbot: null
    restart: on-failure
networks:
  newsbot:
    name: newsbot-compose_newsbot
volumes:
  newsbot-db:
    name: newsbot-compose_newsbot-db

现在,使用以下命令删除现有的合成文件:

docker-compose down

[+] Running 3/3
 ⠿ Container newsbot-compose_newsbot_1  Removed      1.0s
 ⠿ Container newsbot-compose_mysql_1    Removed      0.1s
 ⠿ Network newsbot-compose_newsbot      Removed      0.1s

由于数据保存在卷中,您不必担心数据丢失。

使用以下命令再次启动服务。不要忘记传递你在第三章中生成的 Newsbot API 键的值。

NBT_ACCESS_TOKEN=<token> docker-compose -f docker-compose.adminer.yml up

Running 4/4
 ⠿ Network newsbot-compose_newsbot      Created  0.1s
 ⠿ Container newsbot-compose_adminer_1  Started  7.1s
 ⠿ Container newsbot-compose_mysql_1    Started  7.1s
 ⠿ Container newsbot-compose_newsbot_1  Started  5.1s
Attaching to adminer_1, mysql_1, newsbot_1

mysql_1    | [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.26'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
newsbot_1  | INFO: <module> - Starting up
newsbot_1  | INFO: <module> - Waiting for 60 seconds for db to come up
newsbot_1  | INFO: <module> - Checking on dbs
newsbot_1  | INFO: get_updates - received response: {'ok': True, 'result': []}

通过前往http://localhost:8080导航至adminer。使用root用户名登录,密码设置为MYSQL_ROOT_PASSWORD值,服务器值为mysql。单击 Newsbot 数据库中的source作为表,然后选择选择数据。您应该会看到之前设置为source的子编辑(参见图 7-5 )。

img/463857_2_En_7_Fig5_HTML.jpg

图 7-5

项目运行时将数据保存到数据库中

成功!应用正在运行,数据被保存到 MySQL 数据库并被持久化,尽管删除并重新创建了容器。

摘要

在本章中,您了解了 Docker Compose,包括如何安装它以及为什么使用它。您还深入研究了 Docker 编写文件和 CLI。您运行了一些关于使用 Docker Compose 构建多容器应用的练习。您还了解了如何使用 Docker Compose 将 Newsbot 项目扩展到多容器应用,方法是添加一个链接数据库和一个 Web UI 来编辑数据库。