《Docker 实战:打造可靠的生产环境容器化应用》第八章:探索Docker Compose

314 阅读9分钟

到目前为止,你应该已经对docker命令有了很好的了解,并知道如何使用它来构建、启动、监视和调试你的应用程序。一旦你熟悉了单个容器的工作方式,你很快就会想要分享你的项目,并开始构建更复杂的项目,这些项目需要多个容器才能正常运行。特别是在开发环境中,运行一整套容器可以在你的本地机器上轻松模拟许多生产环境。

然而,如果你运行一整套容器,每个容器都需要以正确的设置运行,以确保底层应用程序被正确配置并按预期运行。每次都正确设置这些参数可能会很有挑战性,特别是当你不是最初编写应用程序的人时。为了在开发过程中解决这个问题,人们通常会尝试编写能够一致构建和运行多个容器的Shell脚本。虽然这样做是可行的,但对于新手来说可能很难理解,并且随着项目的变化,难以维护。而且,并不一定在不同的项目之间可重复使用。

为了解决这个问题,Docker公司推出了一个主要面向开发者的工具,名为Docker Compose。这个工具已包含在Docker Desktop中,但你也可以按照在线安装指南进行安装。

Docker Compose是一个非常有用的工具,可以简化各种传统上非常繁琐且容易出错的开发任务。它可以帮助开发者快速启动复杂的应用程序堆栈,编译应用程序而无需设置复杂的本地开发环境,以及其他很多功能。

在本章中,我们将演示如何充分利用Compose。在接下来的所有示例中,我们将使用一个GitHub存储库。如果你想在我们逐步进行示例时运行这些示例,请运行以下命令下载代码,如果你在第6章中尚未执行此操作:

$ git clone https://github.com/spkane/rocketchat-hubot-demo.git \
    --config core.autocrlf=input

配置Docker Compose

在深入使用docker compose命令之前,先了解一下它所取代的临时工具可能会很有用。让我们花一些时间来查看一个用于构建和部署本地服务副本的Shell脚本,以便通过Docker进行开发和本地测试。以下输出可能会很长和详细,但它很重要,可以证明Docker Compose相对于Shell脚本的巨大优势。

#!/bin/bash

# This is here just to keep people from really running this.
exit 1

# The actual script
#
# Note: This has not been updated to directly mirror the docker-compose file
#       since it is just intended to make a point.

set -e
set -u

if [ $# -ne 0 ] && [ ${1} == "down" ]; then
  docker rm -f hubot || true
  docker rm -f zmachine || true
  docker rm -f rocketchat || true
  docker rm -f mongo-init-replica || true
  docker rm -f mongo || true
  docker network rm botnet || true
  echo "Environment torn down…"
  exit 0
fi

# Global Settings
export PORT="3000"
export ROOT_URL="http://127.0.0.1:3000"
export MONGO_URL="mongodb://mongo:27017/rocketchat"
export MONGO_OPLOG_URL="mongodb://mongo:27017/local"
export MAIL_URL="smtp://smtp.email"
export RESPOND_TO_DM="true"
export HUBOT_ALIAS=". "
export LISTEN_ON_ALL_PUBLIC="true"
export ROCKETCHAT_AUTH="password"
export ROCKETCHAT_URL="rocketchat:3000"
export ROCKETCHAT_ROOM=""
export ROCKETCHAT_USER="hubot"
export ROCKETCHAT_PASSWORD="bot-pw!"
export BOT_NAME="bot"
export EXTERNAL_SCRIPTS="hubot-help,hubot-diagnostics,hubot-zmachine"
export HUBOT_ZMACHINE_SERVER="http://zmachine:80"
export HUBOT_ZMACHINE_ROOMS="zmachine"
export HUBOT_ZMACHINE_OT_PREFIX="ot"

docker build -t spkane/mongo:4.4 ./mongodb/docker

docker push spkane/mongo:4.4
docker pull spkane/zmachine-api:latest
docker pull rocketchat/rocket.chat:5.0.4
docker pull rocketchat/hubot-rocketchat:latest

docker rm -f hubot || true
docker rm -f zmachine || true
docker rm -f rocketchat || true
docker rm -f mongo-init-replica || true
docker rm -f mongo || true

docker network rm botnet || true

docker network create -d bridge botnet

docker container run-d \
  --name=mongo \
  --network=botnet \
  --restart unless-stopped \
  -v $(pwd)/mongodb/data/db:/data/db \
  spkane/mongo:4.4 \
  mongod --oplogSize 128 --replSet rs0
sleep 5
docker container run-d \
  --name=mongo-init-replica \
  --network=botnet \
  spkane/mongo:4.4 \
  'mongo mongo/rocketchat --eval "rs.initiate({ _id: ''rs0'', members: [ { … '
sleep 5
docker container run-d \
  --name=rocketchat \
  --network=botnet \
  --restart unless-stopped  \
  -v $(pwd)/rocketchat/data/uploads:/app/uploads \
  -p 3000:3000 \
  -e PORT=${PORT} \
  -e ROOT_URL=${ROOT_URL} \
  -e MONGO_URL=${MONGO_URL} \
  -e MONGO_OPLOG_URL=${MONGO_OPLOG_URL} \
  -e MAIL_URL=${MAIL_URL} \
  rocketchat/rocket.chat:5.0.4
docker container run-d \
  --name=zmachine \
  --network=botnet \
  --restart unless-stopped  \
  -v $(pwd)/zmachine/saves:/root/saves \
  -v $(pwd)/zmachine/zcode:/root/zcode \
  -p 3002:80 \
  spkane/zmachine-api:latest
docker container run-d \
  --name=hubot \
  --network=botnet \
  --restart unless-stopped  \
  -v $(pwd)/hubot/scripts:/home/hubot/scripts \
  -p 3001:8080 \
  -e RESPOND_TO_DM="true" \
  -e HUBOT_ALIAS=". " \
  -e LISTEN_ON_ALL_PUBLIC="true" \
  -e ROCKETCHAT_AUTH="password" \
  -e ROCKETCHAT_URL="rocketchat:3000" \
  -e ROCKETCHAT_ROOM="" \
  -e ROCKETCHAT_USER="hubot" \
  -e ROCKETCHAT_PASSWORD="bot-pw!" \
  -e BOT_NAME="bot" \
  -e EXTERNAL_SCRIPTS="hubot-help,hubot-diagnostics,hubot-zmachine" \
  -e HUBOT_ZMACHINE_SERVER="http://zmachine:80" \
  -e HUBOT_ZMACHINE_ROOMS="zmachine" \
  -e HUBOT_ZMACHINE_OT_PREFIX="ot" \
  rocketchat/hubot-rocketchat:latest
echo "Environment setup…"
exit 0

在这一点上,你可能已经可以相当轻松地理解这个脚本的大部分内容。正如你可能已经注意到的,这个脚本很难阅读,不太灵活,编辑起来会很麻烦,并且可能在几个地方出现意外错误。如果我们遵循Shell脚本的最佳实践,处理所有可能的错误,以保证其可重复性,那么它的长度将是现在的两到三倍。如果不经过大量工作提取公共功能来处理错误,每次像这样启动一个新项目,你也必须重新编写其中很多逻辑。这不是一个很好的方法来处理每次使用的过程。这就是好的工具的作用。使用Docker Compose可以实现相同的功能,同时使它更具可重复性、易读性、易理解性和易维护性。

与这个混乱的Shell脚本相比,Docker Compose通常通过一个单一的声明性YAML文件来配置每个项目,命名为docker-compose.yaml。这个配置文件非常容易阅读,并且在很大程度上具有可重复性,每个用户运行它时都会有相同的体验。下面是一个示例docker-compose.yaml文件,可以用来取代前面脆弱的Shell脚本:

version: '3'
services:
  mongo:
    build:
      context: ../mongodb/docker
    image: spkane/mongo:4.4
    restart: unless-stopped
    environment:
      MONGODB_REPLICA_SET_MODE: primary
      MONGODB_REPLICA_SET_NAME: rs0
      MONGODB_PORT_NUMBER: 27017
      MONGODB_INITIAL_PRIMARY_HOST: mongodb
      MONGODB_INITIAL_PRIMARY_PORT_NUMBER: 27017
      MONGODB_ADVERTISED_HOSTNAME: mongo
      MONGODB_ENABLE_JOURNAL: "true"
      ALLOW_EMPTY_PASSWORD: "yes"
    # Port 27017 already exposed by upstream
    # See the newer upstream Dockerfile:
    # https://github.com/bitnami/containers/blob/
    # f9fb3f8a6323fb768fd488c77d4f111b1330bd0e/bitnami/
    # mongodb/5.0/debian-11/Dockerfile#L52
    networks:
      - botnet
  rocketchat:
    image: rocketchat/rocket.chat:5.0.4
    restart: unless-stopped
    labels:
      traefik.enable: "true"
      traefik.http.routers.rocketchat.rule: Host(`127.0.0.1`)
      traefik.http.routers.rocketchat.tls: "false"
      traefik.http.routers.rocketchat.entrypoints: http
    volumes:
      - "../rocketchat/data/uploads:/app/uploads"
    environment:
      ROOT_URL: http://127.0.0.1:3000
      PORT: 3000
      MONGO_URL: "mongodb://mongo:27017/rocketchat?replicaSet=rs0"
      MONGO_OPLOG_URL: "mongodb://mongo:27017/local?replicaSet=rs0"
      DEPLOY_METHOD: docker
    depends_on:
      mongo:
        condition: service_healthy
    ports:
      - 3000:3000
    networks:
      - botnet
  zmachine:
    image: spkane/zmachine-api:latest
    restart: unless-stopped
    volumes:
      - "../zmachine/saves:/root/saves"
      - "../zmachine/zcode:/root/zcode"
    depends_on:
      - rocketchat
    expose:
      - "80"
    networks:
      - botnet
  hubot:
    image: rocketchat/hubot-rocketchat:latest
    restart: unless-stopped
    volumes:
      - "../hubot/scripts:/home/hubot/scripts"
    environment:
      RESPOND_TO_DM: "true"
      HUBOT_ALIAS: ". "
      LISTEN_ON_ALL_PUBLIC: "true"
      ROCKETCHAT_AUTH: "password"
      ROCKETCHAT_URL: "rocketchat:3000"
      ROCKETCHAT_ROOM: ""
      ROCKETCHAT_USER: "hubot"
      ROCKETCHAT_PASSWORD: "bot-pw!"
      BOT_NAME: "bot"
      EXTERNAL_SCRIPTS: "hubot-help,hubot-diagnostics,hubot-zmachine"
      HUBOT_ZMACHINE_SERVER: "http://zmachine:80"
      HUBOT_ZMACHINE_ROOMS: "zmachine"
      HUBOT_ZMACHINE_OT_PREFIX: "ot"
    depends_on:
      - zmachine
    ports:
      - 3001:8080
    networks:
      - botnet
networks:
  botnet:
    driver: bridge

docker-compose.yaml文件使得描述每个服务的所有重要要求以及它们之间的通信方式变得简单。我们还获得了很多免费的验证和逻辑检查,这些是我们甚至没有时间写入Shell脚本中的,并且在某些情况下可能会出错,无论我们有多么小心。

那么,在这个YAML文件中,我们告诉Compose做了什么呢?文件的第一行简单地告诉Docker Compose这个文件是为哪个版本的Compose配置语言设计的:

version: '3'

我们的文件其余部分分为两个部分:services和networks。

首先,让我们快速查看networks部分。在这个docker-compose.yaml文件中,我们定义了一个单独命名的Docker网络:

networks:
  botnet:
    driver: bridge

这是一个非常简单的配置,告诉Docker Compose创建一个名为botnet的单独网络,使用(默认的)bridge驱动程序,它将在Docker网络和主机的网络堆栈之间建立桥接连接。

services部分是配置中最重要的部分,它告诉Docker Compose要启动哪些应用程序。在这里,services部分定义了五个服务:mongo,mongo-init-replica,rocketchat,zmachine和hubot。然后,每个命名的服务包含一些子部分,告诉Docker如何构建、配置和启动该服务。

如果你看一下mongo服务,你会看到第一个子部分叫做build,其中包含一个context关键字。这告诉Docker Compose可以构建此镜像,并且构建所需的文件位于../../mongodb/docker目录中,该目录位于包含docker-compose.yaml文件的目录的两个层级以上。

build:
      context: ../../mongodb/docker

如果您查看mongodb/docker目录中的Dockerfile,您会看到以下内容:

FROM mongo:4.4

COPY docker-healthcheck /usr/local/bin/

# Useful Information:
# https://docs.docker.com/engine/reference/builder/#healthcheck
# https://docs.docker.com/compose/compose-file/#healthcheck
HEALTHCHECK CMD ["docker-healthcheck"]

请花点时间看一下HEALTHCHECK行。这告诉Docker应该运行什么命令来检查容器的健康状态。Docker不会根据此健康检查采取任何措施,但它会报告健康状况,以便其他组件可以利用此信息。如果您感兴趣,可以查看mongodb/docker目录中的docker-healthcheck脚本。

接下来的设置是image,它定义了您要应用于构建的镜像标签,或者要下载(如果您不是在构建镜像)然后运行的镜像标签。

image: spkane/mongo:4.4

通过restart选项,您可以告诉Docker何时重新启动容器。在大多数情况下,您希望Docker在您没有明确停止它们的情况下重新启动容器:

restart: unless-stopped

接下来,您会看到一个环境(environment)部分。这是您可以定义要传递到容器中的任何环境变量的地方:

environment:
      MONGODB_REPLICA_SET_MODE: primary
      MONGODB_REPLICA_SET_NAME: rs0
      MONGODB_PORT_NUMBER: 27017
      MONGODB_INITIAL_PRIMARY_HOST: mongodb
      MONGODB_INITIAL_PRIMARY_PORT_NUMBER: 27017
      MONGODB_ADVERTISED_HOSTNAME: mongo
      MONGODB_ENABLE_JOURNAL: "true"
      ALLOW_EMPTY_PASSWORD: "yes"

mongo服务的最后一个子部分networks告诉Docker Compose将此容器连接到哪个网络:

networks:
      - botnet

现在我们来看看rocketchat服务。这个服务没有build子部分,而是只定义了一个image标签,告诉Docker Compose不能构建这个镜像,而必须尝试拉取并启动具有定义标签的预先存在的Docker镜像。

在这个服务中,你会注意到的第一个新的子部分是volumes。

许多服务在开发过程中都有一些需要持久化的数据,尽管容器的本质是短暂的。为了实现这一点,最简单的方法是将本地目录挂载到容器中。volumes部分允许你列出所有你想要挂载到容器中的本地目录,并定义它们的目标位置。以下命令将将../rocketchat/data/uploads目录绑定挂载到容器内的/app/uploads目录:

volumes:
      - "../rocketchat/data/uploads:/app/uploads"

在rocketchat服务的环境部分,你会注意到MONGO_URL的值并没有使用IP地址或完全限定域名。这是因为所有这些服务都在同一个Docker网络上运行,Docker Compose会配置每个容器,使其能够通过其服务名称找到其他容器。这意味着我们可以轻松地配置像这样的URL,只需指向我们需要连接的容器的服务名称和内部端口。而且,如果我们重新排列一下,这些名称将继续指向我们堆栈中正确的容器。它们也很好,因为它们对读者来说非常明确地表示了该容器的依赖关系。

environment:
      …
      MONGO_URL: "mongodb://mongo:27017/rocketchat?replicaSet=rs0"

depends_on部分定义了一个在启动该容器之前必须运行的容器。默认情况下,docker-compose仅确保容器正在运行,而不是健康运行;然而,您可以利用Docker中的HEALTHCHECK功能以及Docker Compose中的condition语句,要求依赖服务在Docker Compose启动新服务之前处于健康状态。重要的是要记住,这仅影响启动过程。Docker会报告后续变得不健康的服务,但除非容器退出,否则不会采取任何措施来纠正这种情况,在这种情况下,如果配置了容器重新启动,Docker会重新启动容器。

depends_on:
      mongo:
        condition: service_healthy

ports部分允许您定义所有希望从容器映射到主机的端口:

ports:
      - 3000:3000

zmachine服务仅使用了一个新的子部分,称为expose。这个部分允许我们告诉Docker,我们希望将该端口暴露给Docker网络上的其他容器,而不是底层主机。这就是为什么您不需要提供主机端口来映射到这个端口的原因:

expose:
      - "80"

您可能会注意到,尽管我们在zmachine服务中暴露了一个端口,但我们在mongo服务中没有暴露端口。暴露mongo端口不会造成任何问题,但我们不需要这样做,因为它已经在上游mongo Dockerfile中暴露了。这有时可能有点不透明。在构建的镜像上使用docker image history命令可能会有所帮助。

在这里,我们使用了一个足够复杂的示例,让您了解到Docker Compose的一些功能,但这并不是详尽无遗的。在docker-compose.yaml文件中,您还可以配置许多其他内容,包括安全设置、资源配额等等。您可以在官方Docker Compose文档中找到有关Compose配置的详细信息。

启动服务

我们在 YAML 文件中配置了一组服务用于我们的应用程序。这告诉 Compose 我们要运行什么以及如何配置它。所以,让我们把它启动起来!为了运行我们的第一个 Docker Compose 命令,我们需要确保我们在与 docker-compose.yaml 文件相同的目录中:

$ cd rocketchat-hubot-demo/compose

一旦你在正确的目录中,你可以通过运行以下命令来确认配置是否正确:

$ docker compose config

如果一切正常,该命令将打印出你的配置文件。如果有问题,该命令将打印出带有详细信息的错误,如下所示:

services.mongo Additional property builder is not allowed

你可以使用build选项构建所需的任何容器。使用镜像的任何服务都将被跳过:

$ docker compose build

 => [internal] load build definition from Dockerfile                      0.0s
 => => transferring dockerfile: 32B                                       0.0s
 => [internal] load .dockerignore                                         0.0s
 => => transferring context: 2B                                           0.0s
 => [internal] load metadata for docker.io/bitnami/mongodb:4.4            1.2s
 => [auth] bitnami/mongodb:pull token for registry-1.docker.io            0.0s
 => [internal] load build context                                         0.0s
 => => transferring context: 40B                                          0.0s
 => [1/2] FROM docker.io/bitnami/mongodb:4.4@sha256:9162…ae209            0.0s
 => CACHED [2/2] COPY docker-healthcheck /usr/local/bin/                  0.0s
 => exporting to image                                                    0.0s
 => => exporting layers                                                   0.0s
 => => writing image sha256:a6ef…da808                                    0.0s
 => => naming to docker.io/spkane/mongo:4.4                               0.0s

你可以通过运行以下命令将 Web 服务在后台启动:

$ docker compose up -d

[+] Running 5/5
 ⠿ Network compose_botnet                  Created                        0.0s
 ⠿ Container compose-mongo-1               Healthy                       62.0s
 ⠿ Container compose-rocketchat-1          Started                       62.3s
 ⠿ Container compose-zmachine-1            Started                       62.5s
 ⠿ Container compose-hubot-1               Started                       62.6s

Docker Compose会为网络和容器名称添加项目名称的前缀。默认情况下,项目名称是包含docker-compose.yaml文件的目录名称。由于此命令在名为compose的目录中运行,你可以看到所有内容以compose作为项目名称开头。

一旦所有服务都启动,我们可以快速查看所有服务的日志(图8-1):

$ docker compose logs

image.png

在这里无法清楚地看到,但如果您跟着操作,注意所有日志都按服务进行了颜色编码,并且按照 Docker 接收日志行的时间进行交织。这样做可以更容易地跟踪发生的情况,即使有多个服务同时记录消息。

Rocketchat 可能需要一点时间来设置数据库并准备好接受连接。一旦 Rocketchat 日志打印出包含 "SERVER RUNNING" 的行,一切都应该准备就绪了:

$ docker compose logs rocketchat | grep "SERVER RUNNING"

compose-rocketchat-1  | |                SERVER RUNNING                |

目前为止,我们已经成功地启动了一个相当复杂的应用程序,其中包含一组容器。现在我们将查看这个简单的应用程序,以便您了解我们构建的内容,并更全面地了解 Compose 工具。虽然接下来的部分与 Docker 本身没有直接相关,但它旨在向您展示使用 Docker Compose 设置复杂且完全功能的 Web 服务是多么容易。

探索 Rocket.Chat

我们将很快深入了解我们设置背后的情况。但为了有效地做到这一点,我们现在应该花一点时间来探索我们构建的应用程序堆栈。Rocket.Chat 是我们使用 Docker Compose 启动的主要应用程序,它是一个开源的聊天客户端/服务器应用程序。为了了解它的工作原理,让我们打开一个网页浏览器,然后导航到 http://127.0.0.1:3000。 当你到达那里时,你会看到 Rocket.Chat 的管理员信息屏幕(图8-2)。

image.png

按照以下方式填写表单:

然后点击蓝色的“下一步”按钮。 然后您会看到组织信息屏幕(图8-3)。

image.png

这个表单的具体内容并不是关键,但您可以填写类似以下内容:

  • 组织名称:培训
  • 组织类型:社区
  • 组织行业:教育
  • 组织规模:1-10人
  • 国家:美国

然后点击蓝色的“下一步”按钮。 此时,您会看到注册服务器屏幕(图8-4)。

image.png

您可以简单地删除和取消选中所有内容,然后点击小蓝色的“继续作为独立服务器”链接。然后您会看到独立服务器配置屏幕(图8-5)。

image.png

点击蓝色的“确认”按钮。

恭喜您——您现在已经登录到一个完全功能的聊天客户端,但您还没有完成。Docker Compose 配置启动了一个 Hubot 聊天助手和神秘的 zmachine 实例,让我们来看看这些。

由于 Rocket.Chat 服务器是全新的,它还没有一个我们的机器人可以使用的用户。让我们来解决这个问题。 首先,点击左侧侧边栏顶部,您会看到一个紫色的带有字母 S 的框。在弹出菜单中点击“Administration”(图8-6)。

image.png

在管理面板中,点击“Users”(图8-7)。

image.png

在屏幕的右上角,点击“New”按钮,以显示“Add User”屏幕(图8-8)。

image.png

按照以下方式填写表单:

  • 姓名:hubot
  • 用户名:hubot
  • 电子邮件:hubot@example.com
  • 点击:已验证(蓝色)
  • 密码:bot-pw!
  • 角色:bot
  • 禁用:发送欢迎电子邮件(灰色)
  • 点击“保存”以创建用户。

为了确保机器人能够登录,我们还需要禁用默认启用的双因素认证。要做到这一点,请在浏览器左侧的管理侧边栏底部点击“Settings”(图8-9)。

image.png

设置屏幕显示出来了(图8-10)。

image.png

在新的文本搜索栏中,键入“totp”,然后点击“Accounts”下面的“Open”按钮。 您现在应该会看到一个很长的设置列表(图8-11)。

image.png

向下滚动至“Two Factor Authentication”部分,展开它,然后取消选择“Enable Two Factor Authentication”选项。 完成后,点击“保存更改”。 在管理面板左侧顶部,点击 X 关闭面板(图8-12)。

image.png

在左侧面板的“Channels”下,点击“general”(图8-13)。

image.png

最后,如果您在频道中还没有看到消息“Hubot has joined the channel”,请继续告诉 Docker Compose 重新启动 Hubot 容器。这将强制 Hubot 尝试重新登录到聊天服务器,现在已经有一个用户可以使用了:

$ docker compose restart hubot
Restarting unix_hubot_1 … done

如果一切按计划进行,您现在应该能够返回到您的网页浏览器,在聊天窗口中向 Hubot 发送命令了。

用于配置 Hubot 的环境变量将其别名定义为一个句点。因此,您现在可以尝试输入 . help 来测试机器人是否响应。如果一切正常,您应该会得到一个机器人理解并将作出响应的命令列表:

> . help
. adapter - Reply with the adapter
. echo <text> - Reply back with <text>
. help - Displays all of the help commands that this bot knows about.
. help <query> - Displays all help commands that match <query>.
. ping - Reply with pong
. time - Reply with current time
…

最后,尝试输入以下内容:

. ping

Hubot 应该会回复“PONG”。

如果您输入:

. time

然后,Hubot 将告诉您服务器上设置的时间。 因此,为了最后一次的娱乐,尝试在聊天窗口中输入 /create zmachine 来创建一个新的聊天频道。然后您应该能够在左侧的侧边栏中点击新的 zmachine 频道,并使用聊天命令 /invite @hubot 来邀请 Hubot 加入。

接下来,尝试在聊天窗口中输入以下命令,以玩一个基于聊天的著名游戏《巨大洞穴冒险》版本:

. z start adventure

more
look
go east
examine keys
get keys

. z save firstgame
. z stop
. z start adventure
. z restore firstgame

inventory

您现在已经看到了使用 Docker Compose 配置、启动和管理复杂的 Web 服务可以变得多么简单。这些服务需要多个组件来完成其工作。在接下来的部分,我们将探索 Docker Compose 包括的一些更多功能。

使用 Docker Compose 进行实践

现在您已经运行了完整的 Rocket.Chat 堆栈并了解了该应用程序在做什么,我们可以深入了解一下服务的运行情况。一些常见的 Docker 命令也可以作为 Compose 命令使用,但适用于特定的堆栈,而不是单个容器或主机上的所有容器。您可以运行 docker compose top 来查看您的容器的概览以及其中正在运行的进程:

$ docker compose top

compose-hubot-1
UID  PID   … CMD
1001 73342 … /usr/bin/qemu-x86_64 /bin/sh /bin/sh -c node -e "console.l…"
1001 73459 … /usr/bin/qemu-x86_64 /usr/local/bin/node node node_modules/…

compose-mongo-1
UID  PID   … CMD
1001 71243 … /usr/bin/qemu-x86_64 /opt/bitnami/mongodb/bin/mongod /opt/…

compose-rocketchat-1
UID   PID   … CMD
65533 71903 … /usr/bin/qemu-x86_64 /usr/local/bin/node node main.js

compose-zmachine-1
UID  PID   … CMD
root 71999 … /usr/bin/qemu-x86_64 /usr/local/bin/node node /root/src/server.js
root 75078 … /usr/bin/qemu-x86_64 /root/src/../frotz/dfrotz /root/src/…

与您通常使用 docker container exec 命令进入正在运行的 Linux 容器类似,您可以通过 Docker Compose 工具使用 docker compose exec 命令在容器内运行命令。因为 Docker Compose 是一个较新的工具,它在标准的 docker 命令上提供了一些方便的快捷方式。在 docker compose exec 的情况下,您无需传入 -i -t,并且可以使用 Docker Compose 服务的名称,而不必记住容器的 ID 或名称:

$ docker compose exec mongo bash

I have no name!@0078134f9370:/$ mongo
MongoDB shell version v4.4.15
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&…
Implicit session: session { "id" : UUID("daec9543-bb9c-4e8c-ba6b…") }
MongoDB server version: 4.4.15
…
rs0:PRIMARY> exit
bye
I have no name!@0078134f9370:/$ exit
exit

您还可以使用 Docker Compose 启动、停止,以及在大多数环境中暂停和取消暂停单个容器或所有容器,具体取决于您的需求:

$ docker compose stop zmachine
[+] Running 1/1
 ⠿ Container compose-zmachine-1  Stopped                                  0.3s
$ docker compose start zmachine
[+] Running 2/2
 ⠿ Container compose-mongo-1     Healthy                                  0.5s
 ⠿ Container compose-zmachine-1  Started                                  0.4s
$ docker compose pause
[+] Running 4/0
 ⠿ Container compose-mongo-1       Paused                                 0.0s
 ⠿ Container compose-zmachine-1    Paused                                 0.0s
 ⠿ Container compose-rocketchat-1  Paused                                 0.0s
 ⠿ Container compose-hubot-1       Paused                                 0.0s
$ docker compose unpause
[+] Running 4/0
 ⠿ Container compose-zmachine-1    Unpaused                               0.0s
 ⠿ Container compose-hubot-1       Unpaused                               0.0s
 ⠿ Container compose-rocketchat-1  Unpaused                               0.0s
 ⠿ Container compose-mongo-1       Unpaused                               0.0s

最后,当您想要将所有内容销毁并删除由 Docker Compose 创建的所有容器时,可以运行以下命令:

$ docker compose down
[+] Running 5/5
 ⠿ Container compose-hubot-1       Removed                               10.4s
 ⠿ Container compose-zmachine-1    Removed                                0.1s
 ⠿ Container compose-rocketchat-1  Removed                                0.6s
 ⠿ Container compose-mongo-1       Removed                                0.9s
 ⠿ Network compose_botnet          Removed                                0.1s

配置管理

Docker Compose 提供了一些重要的功能,可以帮助您大大提高 docker-compose.yaml 文件的灵活性。在本节中,我们将探讨如何避免将许多配置值硬编码到 docker-compose.yaml 文件中,同时仍然使它们在默认情况下易于使用。

默认值

如果我们查看 docker-compose.yaml 文件中 services:rocketchat:environment 部分,会看到类似以下内容:

environment:
      RESPOND_TO_DM: "true"
      HUBOT_ALIAS: ". "
      LISTEN_ON_ALL_PUBLIC: "true"
      ROCKETCHAT_AUTH: "password"
      ROCKETCHAT_URL: "rocketchat:3000"
      ROCKETCHAT_ROOM: ""
      ROCKETCHAT_USER: "hubot"
      ROCKETCHAT_PASSWORD: "bot-pw!"
      BOT_NAME: "bot"
      EXTERNAL_SCRIPTS: "hubot-help,hubot-diagnostics,hubot-zmachine"
      HUBOT_ZMACHINE_SERVER: "http://zmachine:80"
      HUBOT_ZMACHINE_ROOMS: "zmachine"
      HUBOT_ZMACHINE_OT_PREFIX: "ot"

现在,如果我们查看同一目录下的 docker-compose-defaults.yaml,我们会发现相同的部分如下所示:

RESPOND_TO_DM: ${HUBOT_RESPOND_TO_DM:-true}
      HUBOT_ALIAS: ${HUBOT_ALIAS:-. }
      LISTEN_ON_ALL_PUBLIC: ${HUBOT_LISTEN_ON_ALL_PUBLIC:-true}
      ROCKETCHAT_AUTH: ${HUBOT_ROCKETCHAT_AUTH:-password}
      ROCKETCHAT_URL: ${HUBOT_ROCKETCHAT_URL:-rocketchat:3000}
      ROCKETCHAT_ROOM: ${HUBOT_ROCKETCHAT_ROOM:-}
      ROCKETCHAT_USER: ${HUBOT_ROCKETCHAT_USER:-hubot}
      ROCKETCHAT_PASSWORD: ${HUBOT_ROCKETCHAT_PASSWORD:-bot-pw!}
      BOT_NAME: ${HUBOT_BOT_NAME:-bot}
      EXTERNAL_SCRIPTS: ${HUBOT_EXTERNAL_SCRIPTS:-hubot-help,
                          hubot-diagnostics,hubot-zmachine}
      HUBOT_ZMACHINE_SERVER: ${HUBOT_ZMACHINE_SERVER:-http://zmachine:80}
      HUBOT_ZMACHINE_ROOMS: ${HUBOT_ZMACHINE_ROOMS:-zmachine}
      HUBOT_ZMACHINE_OT_PREFIX: ${HUBOT_ZMACHINE_OT_PREFIX:-ot}

这是使用了一种叫做变量插值的技术,Docker Compose 直接从许多常见的类似于 bash 的 Unix shell 中借鉴而来。 在原始文件中,环境变量 ROCKETCHAT_PASSWORD 被硬编码为值 "bot-pw!":

ROCKETCHAT_PASSWORD: "bot-pw!"

通过使用这种新方法,我们表示如果在用户的环境中设置了 HUBOT_ROCKETCHAT_PASSWORD 变量,我们希望将 ROCKETCHAT_PASSWORD 设置为该变量的值;如果没有设置,那么 ROCKETCHAT_PASSWORD 应该被设置为默认值 "bot-pw!":

ROCKETCHAT_PASSWORD: ${HUBOT_ROCKETCHAT_PASSWORD:-bot-pw!}

这为我们提供了很大的灵活性,因为现在我们几乎可以将所有内容都配置化,同时为最常见的用例提供合理的默认值。我们可以通过使用新文件运行 docker compose up 来轻松测试这一点:

$ docker compose -f docker-compose-defaults.yaml up -d

[+] Running 5/5
 ⠿ Network compose_botnet          Created                                0.0s
 ⠿ Container compose-mongo-1       Healthy                               31.0s
 ⠿ Container compose-rocketchat-1  Started                               31.2s
 ⠿ Container compose-zmachine-1    Started                               31.5s
 ⠿ Container compose-hubot-1       Started                               31.8s

默认情况下,这将导致与之前启动的完全相同的堆栈。然而,现在我们可以通过在运行 docker compose 命令之前在终端中设置一个或多个环境变量来轻松进行更改:

$ docker compose -f docker-compose-defaults.yaml down
…

$ docker compose -f docker-compose-defaults.yaml config | \
    grep ROCKETCHAT_PASSWORD

      ROCKETCHAT_PASSWORD: bot-pw!

$ HUBOT_ROCKETCHAT_PASSWORD="my-unique-pw" docker compose \
    -f docker-compose-defaults.yaml config | \
    grep ROCKETCHAT_PASSWORD

      ROCKETCHAT_PASSWORD: my-unique-pw

这很不错,但是如果我们根本不想提供默认值,而是想要强制用户设置某些内容怎么办?我们也可以很容易地做到这一点。

强制性数值

要设置一个强制值,我们只需要稍微修改变量替换行。将默认密码传递似乎不是一个好主意,所以让我们继续使该值为必需。 在 docker-compose-defaults.yaml 文件中,ROCKETCHAT_PASSWORD 的定义如下:

ROCKETCHAT_PASSWORD: ${HUBOT_ROCKETCHAT_PASSWORD:-bot-pw!}

在更新的 docker-compose-env.yaml 文件中,我们可以看到它的定义如下:

ROCKETCHAT_PASSWORD:
        ${HUBOT_ROCKETCHAT_PASSWORD:?HUBOT_ROCKETCHAT_PASSWORD must be set!}

与包含默认值的情况不同,这种方法在环境中未设置为非空字符串时定义一个错误字符串。如果我们现在尝试启动这些服务,将会收到一个错误消息:

$ docker compose -f docker-compose-env.yaml up -d

invalid interpolation format for
  services.hubot.environment.ROCKETCHAT_PASSWORD.
You may need to escape any $ with another $.
required variable HUBOT_ROCKETCHAT_PASSWORD is missing a value:
  HUBOT_ROCKETCHAT_PASSWORD must be set!

输出为我们提供了一些关于可能出错的线索,但最后两行非常清楚,最后的消息是我们定义的确切错误消息,因此它可以根据实际情况设置为最合适的内容。 如果我们继续传递我们自己的密码,那么一切都会正常启动:

$ HUBOT_ROCKETCHAT_PASSWORD="a-b3tt3r-pw" docker compose \
    -f docker-compose-env.yaml up -d

[+] Running 5/5
 ⠿ Network compose_botnet          Created                      0.0s
 ⠿ Container compose-mongo-1       Healthy                     31.0s
 ⠿ Container compose-rocketchat-1  Started                     31.3s
 ⠿ Container compose-zmachine-1    Started                     31.5s
 ⠿ Container compose-hubot-1       Started                     31.8s

$ docker compose -f docker-compose-env.yaml down
…

dotenv 文件

传递单个环境变量并不是很困难,但是如果您需要传递许多自定义值,甚至一个真正的机密信息,那么在本地终端中设置它们并不理想。这就是 .env(dotenv)文件发挥作用的地方。

.env 文件是一种特殊的文件标准,旨在被需要特定于本地环境的附加配置信息的程序解析。 在上述用例中,我们必须设置一个密码来启动我们的Docker Compose环境。我们可以每次传递环境变量,但这至少有几个不理想的原因。如果我们能以一种相对安全的方式设置它,适用于单用户环境,并且能够使我们的生活变得更加轻松和减少出错的可能性,那将是很好的。

从本质上讲,.env 文件只是一个键值对的列表。由于该文件旨在唯一适用于本地环境,并且通常至少包含一个机密信息,我们应该首先确保永远不会意外地将这些文件提交到版本控制系统中。要在git中实现这一点,我们只需确保我们的 .gitignore 文件包括 .env,而在这种情况下,它已经包括了:

$ grep .env ../.gitignore
.env

假设我们在单用户系统上,现在我们可以在包含我们的 docker-compose.yaml 文件的同一目录中安全地创建一个 .env 文件。 对于这个示例,让我们继续,让我们的 .env 文件的内容看起来像这样:

HUBOT_ROCKETCHAT_PASSWORD=th2l@stPW!

我们可以在这个文件中添加许多更多的键值对,但为了保持简单,我们只关注这一个密码。如果您在创建这个文件后运行 git status,您应该注意到 git 完全忽略了这个新文件,这正是我们想要的效果:

$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

在前面的部分中,当我们运行 docker compose -f docker-compose-env.yaml up -d 而没有设置 HUBOT_ROCKETCHAT_PASSWORD 时,会出现错误,但是如果我们在创建了 .env 文件后再次尝试,一切应该能够正常工作:

$ docker compose -f docker-compose-env.yaml up -d

[+] Running 5/5
 ⠿ Network compose_botnet          Created                      0.0s
 ⠿ Container compose-mongo-1       Healthy                     31.1s
 ⠿ Container compose-rocketchat-1  Started                     31.3s
 ⠿ Container compose-zmachine-1    Started                     31.5s
 ⠿ Container compose-hubot-1       Started                     31.8s

让我们确认一下已经分配给 ROCKETCHAT_PASSWORD 的值是否与我们在 .env 文件中设置的值一致:

$ docker compose \
    -f docker-compose-env.yaml config | \
    grep ROCKETCHAT_PASSWORD

      ROCKETCHAT_PASSWORD: th2l@stPW!

我们可以看到,该值确实设置为我们在 .env 文件中定义的值。这是因为Docker Compose始终会读取与我们使用的docker-compose.yaml文件位于同一目录中的 .env 文件中定义的键值对。

在这里,了解生效的优先级非常重要。Docker Compose 所做的第一件事是读取在 docker-compose.yaml 文件中设置的所有默认值。然后它会读取 .env 文件,并覆盖文件中定义的任何默认值。最后,它会查看在本地环境中设置的任何环境变量,并使用这些变量覆盖先前定义的值。

这意味着文件中的默认值应该是最常见的设置,然后每个用户都可以在本地的 .env 文件中定义他们常见的更改,最后,当他们需要对特定用例进行不寻常的更改时,可以依赖本地环境变量。使用这些功能与Docker Compose一起有助于确保您可以构建一个非常可重复的流程,仍然具有足够的灵活性,以涵盖大多数常见的工作流程。

总结

现在,您应该对使用Docker Compose可以实现的各种任务有了很好的了解,以及如何利用这个工具来减少繁琐工作并增加开发环境的可重复性。 在下一章中,我们将探讨一些可用的工具,帮助您在数据中心和云中扩展Docker的规模。