简介
现代的云原生应用由多个较小的服务组成,这些服务相互交互以形成一个有用的应用程序。我们将这称为微服务模式。
一个微服务应用可能包含以下七个独立的服务,它们共同协作以构建一个有用的应用程序:
- 网页前端
- 订单管理
- 目录管理
- 后端数据存储
- 日志记录
- 认证
- 授权
部署和管理诸如这些小型微服务的任务可能很复杂。这就是Compose的用武之地。
与使用脚本和冗长的Docker命令将微服务粘合在一起不同,Compose允许您在一个声明性配置文件中描述所有内容。您可以使用这个文件来部署和管理应用程序。
一旦应用程序部署完成,您可以使用一组简单的命令来管理其整个生命周期。您甚至可以将配置文件存储和管理在版本控制系统中。
这就是基础知识。让我们深入了解一下。
深入
我们将把"深入了解"部分分为以下几个部分:
- Compose背景
- 安装Compose
- Compose文件
- 使用Compose部署应用
- 使用Compose管理应用
Compose背景
当Docker刚刚出现时,一家名为Orchard的公司开发了一个名为Fig的工具,它使管理多容器应用变得非常简单。Fig是一个基于Python的工具,位于Docker之上,允许您在单个YAML文件中定义整个多容器微服务应用。您甚至可以使用Fig通过fig命令行工具部署和管理整个应用程序的生命周期。
在幕后,Fig会读取YAML文件并调用适当的Docker命令来部署和管理应用程序。
事实上,它非常优秀,以至于Docker公司收购了Orchard,并将Fig重新品牌为Docker Compose。命令行工具的名称从fig改为docker-compose,最近甚至将其与docker CLI合并,成为其自身的子命令。现在您可以在CLI上运行简单的docker compose命令。
此外,还有一个Compose规范,旨在创建一个定义多容器微服务应用的开放标准。该规范由社区主导,并与Docker实现分开。这有助于保持更好的治理和更清晰的界限。然而,我们应该期望Docker在Docker引擎中实现完整的规范。
这个规范本身是一个学习细节的好文档。
是时候看到Compose的实际效果了。
安装Compose
Compose现在随同Docker引擎一起提供,您不再需要将其作为单独的程序安装。
使用以下命令测试其是否工作正常。请确保使用docker compose命令,而不是docker-compose。
ruby
$ docker compose version
Docker Compose version v2.17.3
Compose文件
Compose使用YAML文件来定义微服务应用程序。
Compose YAML文件的默认名称是compose.yaml。不过,它还可以接受compose.yml,并且您可以使用-f标志来指定自定义的文件名。
以下示例展示了一个非常简单的Compose文件,它定义了一个具有两个微服务(web-fe和redis)的小型Flask应用。该应用是一个简单的Web服务器,用于计算访问网页的次数,并将该值存储在Redis缓存中。我们将称这个应用为multi-container,并将其用作本章其余部分的示例应用程序。
该文件位于本书GitHub仓库的multi-container文件夹中。
yaml
services:
web-fe:
build: .
command: python app.py
ports:
- target: 8080
published: 5001
networks:
- counter-net
volumes:
- type: volume
source: counter-vol
target: /app
redis:
image: "redis:alpine"
networks:
counter-net:
networks:
counter-net:
volumes:
counter-vol:
在我们深入了解之前,我们将快速浏览文件的基础内容。
首先要注意的是,该文件有3个顶层键:
- services
- networks
- volumes
还有其他的顶层键,比如secrets和configs,但我们不会深入研究它们。
顶层的services键是我们定义应用微服务的地方。这个示例定义了两个微服务:一个名为web-fe的Web前端,以及一个名为redis的内存缓存。Compose将把每个微服务部署到它自己的容器中。
顶层的networks键告诉Docker创建新的网络。默认情况下,现代版本的Compose会创建跨多个主机的覆盖网络(overlay networks)。然而,您可以使用driver属性来指定不同的网络类型。
以下的YAML可以用于您的Compose文件,以创建一个名为over-net的新覆盖网络,允许独立容器连接到它(attachable)。
yaml
networks:
over-net:
driver: overlay
attachable: true
顶层的volumes键是您告诉Docker创建新卷的地方。
我们具体的Compose文件
这个示例的Compose文件定义了两个服务,一个名为counter-net的网络,以及一个名为counter-vol的卷。它在这里再次显示出来:
yaml
services:
web-fe:
build: .
command: python app.py
ports:
- target: 8080
published: 5001
networks:
- counter-net
volumes:
- type: volume
source: counter-vol
target: /app
redis:
image: "redis:alpine"
networks:
counter-net:
networks:
counter-net:
volumes:
counter-vol:
大部分细节都在services部分,让我们仔细看看这部分。
services部分有两个二级键:
- web-fe
- redis
每个键都定义了一个微服务。重要的是要知道,Compose将每一个微服务部署为其自己的容器,并且将使用这些键的名称在容器名称中。在我们的示例中,我们定义了两个键:web-fe和redis。这意味着Compose将部署两个容器,一个将在其名称中包含web-fe,另一个将在其名称中包含redis。
在web-fe服务的定义内部,我们给Docker以下指令:
- build: . 这告诉Docker使用当前目录(.)中的Dockerfile构建一个新的镜像。新构建的镜像将在后面的步骤中用于为此服务创建容器。
- command: python app.py 这告诉Docker在此服务的每个容器中运行名为app.py的Python应用程序。app.py文件必须存在于镜像中,并且镜像必须安装了Python。Dockerfile会处理这两个要求。
- ports: 我们Compose文件中的示例告诉Docker将容器内的端口8080(目标)映射到主机上的端口5001(发布)。这意味着命中Docker主机上的端口5001的流量将被定向到容器上的端口8080。容器内的应用程序侦听端口8080。
- networks: 告诉Docker将服务的容器连接到哪个网络。该网络应该已经存在或者在顶层的networks键中定义。如果是覆盖网络,它需要具有attachable标志,以便可以将独立容器附加到它上面(Compose部署独立容器而不是Docker服务)。
- volumes: 告诉Docker将counter-vol卷(source:)挂载到容器内的/app(target:)。counter-vol卷需要已经存在或者在文件的顶层volumes键中定义。
总之,Compose将指示Docker为web-fe微服务部署一个独立的容器。它将基于与Compose文件在同一目录中的Dockerfile构建的镜像。此镜像将被启动为容器,并运行app.py作为其主要应用程序。它将连接到counter-net网络,在主机上的端口5001上暴露自身,并挂载一个卷到/app。
注意:实际上,在Compose文件中我们并不需要命令:python app.py选项,因为它已经在Dockerfile中定义过了。我们在这里展示它,以便您知道它的工作原理。您还可以使用Compose来覆盖在Dockerfile中设置的指令。
redis服务的定义更简单:
- image: redis:alpine 这告诉Docker基于redis:alpine镜像启动一个名为redis的独立容器。此镜像将从Docker Hub拉取。
- networks: redis容器还将连接到counter-net网络。
由于这两个服务将部署到同一个counter-net网络上,它们将能够通过名称相互解析。这一点很重要,因为应用程序配置为通过名称连接到Redis服务。
既然我们了解了Compose文件的工作原理,让我们部署它吧!
使用Compose部署应用程序
在本节中,我们将部署在上一节的Compose文件中定义的应用程序。为了做到这一点,您需要本地复制一份书籍的GitHub存储库,并从multi-container文件夹中运行所有命令。
如果您还没有这样做,请将Git仓库在本地克隆。您需要安装Git来完成这个操作。
bash
$ git clone https://github.com/nigelpoulton/ddd-book.git
Cloning into 'ddd-book'...
remote: Enumerating objects: 67, done.
remote: Counting objects: 100% (67/67), done.
remote: Compressing objects: 100% (47/47), done.
remote: Total 67 (delta 17), reused 63 (delta 16), pack-reused 0
Receiving objects: 100% (67/67), 173.61 KiB | 1.83 MiB/s, done.
Resolving deltas: 100% (17/17), done.
克隆仓库将创建一个名为ddd-book的新目录,其中包含书中使用的所有文件。
切换到ddd-book/multi-container目录,并将其用作本章其余部分的构建上下文。Compose还将使用目录的名称(multi-container)作为项目名称。稍后我们会看到这一点,但是Compose将在所有资源名称前加上multi-container_。
shell
$ cd ddd-book/multi-container/
$ ls -l
total 20
-rw-rw-r-- 1 ubuntu ubuntu 288 May 21 15:53 Dockerfile
-rw-rw-r-- 1 ubuntu ubuntu 332 May 21 15:53 README.md
drwxrwxr-x 4 ubuntu ubuntu 4096 May 21 15:53 app
-rw-rw-r-- 1 ubuntu ubuntu 355 May 21 15:53 compose.yaml
-rw-rw-r-- 1 ubuntu ubuntu 18 May 21 15:53 requirements.txt
让我们快速描述一下每个文件:
- compose.yaml 是Docker Compose文件,描述了应用程序以及Compose应该如何构建和部署它。
- app 是一个文件夹,包含应用程序代码和视图。
- Dockerfile 描述了如何为web-fe服务构建镜像。
- requirements.txt 列出了应用程序的依赖项。
请随意检查每个文件的内容。
app/app.py 文件是应用程序的核心,然而 compose.yaml 是将所有微服务粘合在一起的胶水。
让我们使用Compose来启动应用程序。您必须从刚刚从GitHub克隆下来的仓库的 multi-container 目录中运行以下所有命令。
markdown
$ docker compose up &
[+] Running 7/7
- redis 6 layers [||||||] 0B/0B Pulled 5.2s
- 08409d417260 Already exists 0.0s
- 35afda5186ef Pull complete 0.5s
- ebab1fe9c8cc Pull complete 1.5s
- e438114652e6 Pull complete 3.1s
- 80fd0bfc19ad Pull complete 3.1s
- ca04d454c47d Pull complete 1.1s
[+] Building 10.3s (9/9) FINISHED
<Snip>
[+] Running 4/4
- Network multi-container_counter-net Created 0.1s
- Volume "multi-container_counter-vol" Created 0.0s
- Container multi-container-redis-1 Started 0.6s
- Container multi-container-web-fe-1 Started 0.5s
应用程序启动需要几秒钟时间,输出可能会很冗长。在部署完成后,您可能还需要按回车键。
我们稍后会逐步解释发生了什么,但首先让我们谈谈 docker compose 命令。
docker compose up 是启动Compose应用程序的最常见方法。它会构建或拉取所有所需的镜像,创建所有所需的网络和卷,以及启动所有所需的容器。
我们没有指定Compose文件的名称或位置,因为它叫做 compose.yaml 并位于本地目录中。然而,如果它有不同的名称或位置,我们将使用 -f 标志。以下示例将从名为 prod-equus-bass.yml 的Compose文件部署应用程序。
r
$ docker compose -f prod-equus-bass.yml up &
通常,您会使用 --detach 标志将应用程序后台启动,如下所示。但是,我们将其在前台启动,并使用 & 来还原终端窗口。这会强制Compose将所有消息输出到终端窗口,我们稍后会使用这些消息。
现在应用程序已经构建并正在运行,我们可以使用普通的docker命令来查看Compose创建的镜像、容器、网络和卷 --- 请记住,Compose在幕后构建并使用普通的Docker构建。
ruby
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
multi-container-web-fe latest ade6252c30cc 7 minutes ago 76.3MB
redis alpine b64252cb5430 9 days ago 30.7MB
multi-container-web-fe:latest 镜像是由 compose.yaml 文件中的 build: . 指令创建的。这个指令让Docker使用同一目录中的Dockerfile来构建一个新的镜像。它包含了web-fe微服务,并且是从 python:alpine 镜像构建而来的。请查看Dockerfile的内容获取更多信息:
sql
FROM python:alpine << Base image
COPY . /app << Copy app into image
WORKDIR /app << Set working directory
RUN pip install -r requirements.txt << Install requirements
ENTRYPOINT ["python", "app.py"] << Set the default app
我在每行末尾添加了注释来帮助解释。
请注意,Compose将新构建的镜像命名为项目名称(multi-container)和Compose文件中指定的资源名称(web-fe)的组合。项目名称是Compose文件所在目录的名称。由Compose创建的所有资源都将遵循这种命名约定。
redis:alpine 镜像是通过Compose文件的 .Services.redis 部分中的 image: "redis:alpine" 指令从Docker Hub拉取的。
以下的容器列表显示了两个正在运行的容器。它们遵循相同的命名约定,每个容器都有一个表示实例编号的数字后缀 --- 这是因为Compose允许进行扩展和缩减。
bash
$ docker ps
ID COMMAND STATUS PORTS NAMES
61.. "python app/app.py" Up 5 mins 0.0.0.0:5001->8080/tcp.. multi-container-web-fe-1
80.. "docker-entrypoint.." Up 5 mins 6379/tcp multi-container-redis-1
multi-container-web-fe-1 容器正在运行应用程序的Web前端。它正在运行app.py代码,并在Docker主机上的所有接口上映射到端口5001。我们将在后面的步骤中连接到它。
以下的网络和卷列表显示了 multi-container_counter-net 网络和 multi-container_counter-vol 卷。
bash
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
46100cae7441 multi-container_counter-net bridge local
<Snip>
$ docker volume ls
DRIVER VOLUME NAME
local multi-container_counter-vol
<Snip>
成功部署应用程序后,您可以将网络浏览器指向Docker主机的端口5001,欣赏应用程序的全部魅力。
图片显示我连接到了IP地址为192.168.64.28,端口为5001的Docker主机。如果您使用的是Docker Desktop或其他本地环境,您可能可以在localhost:5001上连接。
点击浏览器的刷新按钮将导致网页上的计数器递增。查看应用程序(app/app.py)以了解计数器数据如何存储在Redis后端。
由于我们在前台启动了应用程序,因此我们可以在终端窗口中看到每次刷新都会记录HTTP 200响应代码。这表示成功的请求,您将会在每次加载网页时看到一个响应。
arduino
multi-container-web-fe-1 | 192.168.64.1 - - [21/May/2023 15:38:40] "GET / HTTP/1.1" 200 -
multi-container-web-fe-1 | 192.168.64.1 - - [21/May/2023 15:38:41] "GET / HTTP/1.1" 200 -
恭喜您!您已成功使用Docker Compose部署了一个多容器应用程序!
使用Compose管理应用程序
在本节中,您将了解如何停止、重启、删除和获取Compose应用程序的状态。您还将看到如何使用卷来直接注入应用程序的更新。
由于应用程序已经在运行,让我们看看如何将其关闭。为此,将 up 子命令替换为 down。
markdown
$ docker compose down
[+] Running 3/3
- Container multi-container-web-fe-1 Removed 0.3s
- Container multi-container-redis-1 Removed 0.2s
- Network multi-container_counter-net Removed 0.3
由于应用程序是在前台启动的,因此我们将在终端中获得详细的输出。这可以让您深入了解事物的工作原理,我建议您分析输出。在本书中,我们不会在这里详细说明,因为输出可能在不同版本的Compose之间发生变化。
很明显,两个容器(微服务)和网络已被删除。
然而,默认情况下不会删除卷。这是因为卷旨在成为长期持久的数据存储,其生命周期与应用程序生命周期完全解耦。运行 docker volume ls 将证明卷仍然存在于系统中。如果您向其中写入了任何数据,数据仍将存在。将 --volumes 标志添加到 docker compose down 命令将删除所有关联的卷。
作为 docker-compose up 操作的一部分构建或拉取的任何镜像也将保留在系统中。这意味着将来部署该应用程序将更快。将 --rmi all 标志添加到 docker compose down 命令将删除启动应用程序时构建或拉取的所有镜像。
让我们看一下其他一些 docker compose 子命令。
使用以下命令将应用程序再次启动,但这次是在后台运行。
arduino
$ docker compose up --detach
<Snip>
看到这次应用程序启动得更快了吗?counter-vol卷已经存在,并且所有镜像已经存在于Docker主机上。
使用 docker compose ps
命令显示应用程序的当前状态。
matlab
$ docker compose ps
NAME COMMAND SERVICE STATUS PORTS
multi-container-redis-1 "docker-entrypoint.." redis Up 28 sec 6379/tcp
multi-container-web-fe-1 "python app/app.py" web-fe Up 28 sec 0.0.0.0:5001->8080
您可以看到两个容器,它们正在运行的命令,它们的当前状态以及它们正在侦听的网络端口。
使用 docker compose top 列出在每个服务(容器)内部运行的进程。
objectivec
$ docker compose top
multi-container-redis-1
UID PID PPID ... CMD
lxd 22312 22292 redis-server *:6379
multi-container-web-fe-1
UID PID PPID ... CMD
root 22346 22326 0 python app/app.py python app.py
root 22415 22346 0 /usr/local/bin/python app/app.py python app.py
返回的PID号码是从Docker主机(而不是从容器内部)看到的PID号码。
使用 docker compose stop 命令停止应用程序,而不删除其资源。然后使用 docker compose ps 命令显示应用程序的状态。
markdown
$ docker compose stop
[+] Running 2/2
- Container multi-container-redis-1 Stopped 0.4s
- Container multi-container-web-fe-1 Stopped 0.5
$ docker compose ps
NAME COMMAND SERVICE STATUS PORTS
以前的Compose版本通常会列出停止状态的容器。验证Compose的这两个微服务的容器仍然存在于系统中,并且处于停止状态。
ruby
$ docker ps -a
CONTAINER ID COMMAND STATUS NAMES
f1442d484ccd "python app/app.py" Exited (0)... multi-container-web-fe-1
541efbd7185d "docker-entrypoint" Exited (0)... multi-container-redis-1
您可以使用 docker compose rm 删除已停止的Compose应用程序。这将删除容器和网络,但不会删除卷或镜像。也不会删除项目构建上下文目录中的应用程序源代码(app.py、Dockerfile、requirements.txt 和 compose.yaml)。
在应用程序处于停止状态时,使用 docker compose restart 命令重新启动它。
markdown
$ docker compose restart
[+] Running 2/2
- Container multi-container-redis-1 Started 0.4s
- Container multi-container-web-fe-1 Started 0.5s
验证应用程序已经重新启动。
bash
$ docker compose ls
NAME STATUS CONFIG FILES
multi-container running(2) /home/ubuntu/ddd-book/multi-container/compose.yaml
运行以下命令以一次性停止和删除应用程序。它还将删除用于启动应用程序的任何卷和镜像。
bash
$ docker-compose down --volumes --rmi all
Stopping multi-container-web-fe-1 ... done
Stopping multi-container-redis-1 ... done
Removing multi-container-web-fe-1 ... done
Removing multi-container-redis-1 ... done
Removing network multi-container_counter-net
Removing volume multi-container_counter-vol
Removing image multi-container_web-fe
Removing image redis:alpine
使用卷插入数据
让我们最后一次部署应用程序,看看卷的工作原理。
arduino
$ docker compose up --detach
<Snip>
如果您查看Compose文件,您会看到它定义了一个名为 counter-vol 的卷,并将其挂载到 web-fe 容器中的 /app 目录。
yaml
volumes:
counter-vol:
services:
web-fe:
volumes:
- type: volume
source: counter-vol
target: /app
第一次部署应用程序时,Compose检查是否已经存在一个名为 counter-vol 的卷。由于不存在,Compose就创建了它。您可以使用 docker volume ls 命令查看它,并可以使用 docker volume inspect multi-container_counter-vol 命令获取更详细的信息。
bash
$ docker volume ls
RIVER VOLUME NAME
local multi-container_counter-vol
$ docker volume inspect multi-container_counter-vol
[
{
"CreatedAt": "2023-05-21T19:49:25+01:00",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "multi-container",
"com.docker.compose.version": "2.17.3",
"com.docker.compose.volume": "counter-vol"
},
"Mountpoint": "/var/lib/docker/volumes/multi-container_counter-vol/_data",
"Name": "multi-container_counter-vol",
"Options": null,
"Scope": "local"
}
]
值得知道的是,Compose在部署服务之前会构建网络和卷。这是有道理的,因为网络和卷是较低级的基础设施对象,由服务(容器)消耗。
如果我们再次查看 web-fe 的服务定义,我们会看到它将 counter-app 卷挂载到容器的 /app 目录中。我们还可以从 Dockerfile 中看到 /app 是应用程序安装和执行的位置。这意味着应用程序代码正在从Docker卷中运行。见图9.2。
这意味着我们可以在容器外部更改卷中的文件,并立即在应用程序中反映出这些更改。让我们看看这是如何工作的。
接下来的步骤将引导您完成以下过程:
- 更新项目构建上下文中的 app/templates/index.html 内容。
- 将更新后的 index.html 复制到容器的卷(位于Docker主机的文件系统上)。
- 刷新网页并查看更新内容。
注意:如果您在Mac或Windows上使用Docker Desktop,则这种方法无法工作。这是因为Docker Desktop在这些平台上在轻量级虚拟机中运行Docker,所有卷都存在于虚拟机中。
使用您喜欢的文本编辑器编辑 index.html。确保从 multi-container 目录运行该命令。
shell
$ vim app/templates/index.html
将第16行的文本更改为以下内容,然后保存您的更改。
css
<h2>Sunderland til I die</h2>
现在您已经更新了应用程序,需要将其复制到Docker主机上的卷中。每个Docker卷存在于Docker主机文件系统的特定位置。使用以下docker inspect命令查找卷在Docker主机上的暴露位置。
perl
$ docker inspect multi-container_counter-vol | grep Mount
"Mountpoint": "/var/lib/docker/volumes/multi-container_counter-vol/_data",
将更新后的 index.html 文件复制到前面命令返回的目录下的适当子目录中(请记住,这在Docker Desktop上不起作用)。一旦您这样做,更新后的文件将出现在容器中。
您可能需要在命令前加上 sudo,而且应该在一行上运行它,不要使用 \ 将其拆分为多行。在本书中,它被拆分为多行只是为了避免换行。
shell
$ cp ./counter-app/app.py \
var/lib/docker/volumes/multi-container_counter-vol/_data/app/templates/index.html
更新后的应用程序文件现在已经在容器中。连接到应用程序以查看您的更改。您可以通过将网络浏览器指向Docker主机的IP地址,端口为5001来实现这一点。
图9.3显示了更新后的应用程序。
在生产环境中,您不会像这样进行更新操作,但这演示了卷的工作原理。
恭喜您!您已经使用Docker Compose部署和管理了一个多容器微服务应用程序。
在回顾我们学到的命令之前,重要的是要理解这只是一个非常简单的示例。Docker Compose能够部署和管理更复杂的应用程序。
使用Compose部署应用程序 - 命令
-
docker compose up 是部署Compose应用程序的命令。它会创建应用程序所需的所有镜像、容器、网络和卷。它期望Compose文件的名称为 compose.yaml,但您可以使用 -f 标志指定自定义文件名。通常使用 --detach 标志在后台启动应用程序。
-
docker compose stop 命令会停止Compose应用程序中的所有容器,但不会从系统中删除它们。可以使用 docker compose restart 轻松重新启动它们。
-
docker compose rm 命令会删除已停止的Compose应用程序。它会删除容器和网络,但默认情况下不会删除卷和镜像。
-
docker compose restart 命令会重新启动使用 docker compose stop 停止的Compose应用程序。如果您在应用程序停止时对其进行更改,这些更改不会出现在重新启动的应用程序中。您需要重新部署应用程序以获取更改。
-
docker compose ps 命令会列出Compose应用程序中的每个容器。它显示当前状态、每个容器内部运行的命令以及网络端口。
-
docker compose down 命令会停止并删除正在运行的Compose应用程序。它会删除容器和网络,但不会删除卷和镜像。