《精通Docker第三版》完整目录:
第一章 Docker概览
第二章 创建容器镜像
第三章 存储和发布镜像
第四章 管理容器
第五章 Docker Compose
第六章 Windows容器
第七章 Docker Machine
第八章 Docker Swarm
第九章 Docker和Kubernetes
第十章 在公有云上运行Docker
第十一章 Portainer – 一个Docker的GUI
第十二章 Docker安全
第十三章 Docker工作流
第十四章 Docker进阶
本章中我们会来看另一个核心Docker工具,称为Docker Compose,还有一个当前在开发中的Docker App。我们会将本章分解成如下小节:
- Docker Compose的介绍
- 第一个Docker Compose应用
- Docker Compose YAML文件
- Docker Compose命令
- Docker App
技术准备
和此前章节一样,我们将继续使用本地Docker安装。同样,本章中的截图都取自我个人偏好的操作系统,macOS。
不论你是在哪个平台上安装的Docker,我们所运行的Docker命令可以在三种操作系统中运行。但是,有极少数的支持命令,可能只能在macOS或基于Linux的操作系统上运行。
本章中所使用的完整代码请参见:https://github.com/alanhou/mastering-docker。
访问如下视频来查看代码实时操作:
Docker Compose简介
在第一章 Docker概览中,我们讨论了设计Docker所解决的一些问题。并讲解了如何处理这些挑战,如在一个容器中以两个隔离的进程同时运行两个应用,也即在相同主机上可以运行相同软件栈的两个完全不同的版本,如PHP 5.6和PHP 7,这个我们在第二章 创建容器镜像中已经实现了。
在第四章 管理容器的最后,我们启动了一个由多个容器组成的应用,而不是在单容器中运行所需的软件栈。我们所启动的示例应用,Moby计数器,是由Node.js编写并使用Redis作为后台键值对的存储,在该例中存储的是屏幕中Docker logo的位置。
这表示我们应启动两个容器,一个给应用一个给Redis。虽然因为应用本身非常基础这么做非常的简单,手动启动各容器存在很多的劣势。
例如,如果我们想要同事部署相同的应用,需要将如下命令发送给他们:
1 2 3 4 5 |
$ docker image pull redis:alpine $ docker image pull russmckendrick/moby-counter $ docker network create moby-counter $ docker container run -d --name redis --network moby-counter redis:alpine $ docker container run -d --name moby-counter --network moby-counter -p 8080:80 russmckendrick/moby-counter |
好吧,我可以省略掉前两条命令,因为在运行时会拉取未拉取的镜像,但在应用变得越来越复杂的时候,我就要发送不断增长的命令、指令集。
我还要说清楚他们应该按照相应的顺序执行这些命令。此外,我要包含解决可能出现的潜在问题的细节,这时我们会发现自己处于“运维躺枪”的境地,而这正是我们所竭力避免的。
虽然Docker的责任在于创建镜像并使用这些镜像来启动容器,人们把它看作一种阻止我们找寻自我的技术。借助于Docker, 人们不再需要担心所启动的应用的环境不一致性,因为这些都打包到镜像中了。
基于这一原因,在2014年7月,Docker购买了一家英国初创公司Orchard Laboratories,它提供两种基于容器的产品。
其中第一款产品为基于Docker主机托管平台:可将其看作Docker Machine的混合产品,本章稍后我们会来学习它,以及Docker自身。通过一个单独的命令orchard,你可以启动一个主机、然后通过一个新启动的主机代理你的Docker命令。例如,可以使用如下命令:
1 2 |
$ orchard hosts create $ orchard docker run -p 6379:6379 -d orchardup/redis |
这些命令将在Orchard平台上启动一个Docker主机以及一个Redis容器。
第二款产品是一个名为Fig的开源项目。Fig让我们可以使用YAML来定义多容器应用的结构。然后它会读取YAML文件并按照定义自动启动这些容器。这样的优势在于因其是一个YAML文件,对于开发人员而言,在他们的代码库中将fig.yml文件和Dockerfile一起传递简单直接。
就这两款产品,Docker购买Orchard Laboratories意在Fig。此后一段时间,Orchard服务停止,并用于2015的2月,Fig变为了Docker Compose。
它作为 Mac和Windows上 Docker 安装的一部分,以及第一章 Docker概览中Linux上Docker的安装中我们安装了Docker Compose。我们暂不深入讨论它能做什么,先使用Docker Compose来启动上一章中手动启动的两个容器应用。
第一个Docker Compose应用
已经讨论过Docker Compose使用YAML文件,通常命名为dockercompose.yml,来定义多容器应用是什么样的。我们在第四章 管理容器中启动中的双容器应用Docker Compose的形式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
version: "3" services: redis: image: redis:alpine volumes: - redis_data:/data restart: always mobycounter: depends_on: - redis image: russmckendrick/moby-counter ports: - "8080:80" restart: always volumes: redis_data: |
即便不逐行进行分析,以上的代码应该已经很直白了。要启动应用,我们只需进入包含docker-compose.yml文件的目录并运行如下命令:
1 |
$ docker-compose up |
在如下的Terminal输出中可以看到,在启动时发生了许多事:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ docker-compose up Creating network "mobycounter_default" with the default driver Creating volume "mobycounter_redis_data" with default driver Creating mobycounter_redis_1 ... done Creating mobycounter_mobycounter_1 ... done Attaching to mobycounter_redis_1, mobycounter_mobycounter_1 redis_1 | 1:C 14 Apr 2019 02:45:02.106 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo redis_1 | 1:C 14 Apr 2019 02:45:02.107 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=1, just started redis_1 | 1:C 14 Apr 2019 02:45:02.107 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf redis_1 | 1:M 14 Apr 2019 02:45:02.111 * Running mode=standalone, port=6379. redis_1 | 1:M 14 Apr 2019 02:45:02.111 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. redis_1 | 1:M 14 Apr 2019 02:45:02.111 # Server initialized redis_1 | 1:M 14 Apr 2019 02:45:02.111 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. redis_1 | 1:M 14 Apr 2019 02:45:02.111 * Ready to accept connections mobycounter_1 | using redis server mobycounter_1 | ------------------------------------------- mobycounter_1 | have host: redis mobycounter_1 | have port: 6379 mobycounter_1 | server listening on port: 80 mobycounter_1 | Connection made to the Redis server |
可以看到,通过前面的几行,Docker Compose执行了如下操作:
- 如我们在docker-compose.yml文件的最后定义的那样,它使用默认驱动器创建了一个名为mobycounter_redis_data的数据卷。
- 使用默认网络驱动创建了一个名为mobycounter_default的网络,我们并没有要求Docker Compose这么做。这个问题一会儿即见分晓。
- 它启动了两个容器,一个名为mobycounter_redis_1,另一个名为mobycounter_mobycounter_1。
你可能还看到这个多容器应用的Docker Compose命名空间以mobycounter作为前缀。它取自我们存放mobycounter文件的目录名。
一旦启动,Docker Compose与mobycounter_redis_1和mobycounter_mobycounter_1进行连接并将输出内容流向Terminal会话中。在Terminal屏幕中,你可以看到redis_1和mobycounter_1开始和彼此进行了交互。
使用docker-compose up来运行Docker Compose时,它会在前台运行。按下Ctrl + C会停止容器并将访问返回到Terminal会话中。
Docker Compose YAML文件
在更深入使用Docker Compose之前,我们先来深入分析docker-compose.yml文件,因其是Docker Compose的核心。
ℹ️YAML是一个递归的首字母缩写(YAML Ain’t Markup Language)。很多应用都使用它作配置同时让定义的结构化数据格式更易于人类阅读。本例中你看到的缩进非常之重要,因为它定义了数据的结构。
Moby计数器应用
我们用于启动多容器应用的docker-compose.yml文件分成三个部分。
第一部分仅用于指定我们用于定义Docker Compose 语言的版本,本例中,我们使用Docker和Docker Compose最近的一个版本,使用了版本号3:
1 |
version: "3" |
下一部分中定义了容器,该部分为services。格式如下:
1 2 3 4 5 |
services: --> container name: ----> container options --> container name: ----> container options |
我们的示例中定义了两个容器。我将两者分开了以易于阅读:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
services: redis: image: redis:alpine volumes: - redis_data:/data restart: always mobycounter: depends_on: - redis image: russmckendrick/moby-counter ports: - "8080:80" restart: always |
定义服务的语法和使用docker container run命令启动容器时非常相近。我说相近是因为虽然在阅读定义时很容易理解 ,只有在仔细查看时才会了解到Docker Compose和 docker container run命令之间有很多不同。
例如,在运行docker container run命令时没有针对如下的标记:
- image;这告知Docker Compose下载和使用哪个镜像。在运行ocker container run 时并没有这个选项,因为在命令行中你只能运行一个容器。在前面章节中已经看到,镜像总是在命令行最后进行定义而无需传递一个标记。
- volume:这和–volume标记是一样的,但是它能接收多个数据卷。它只会使用在Docker Compose YAML文件中声明的数据卷,一会儿就会了解到更多。
- depends_on:这在docker container run命令中永远不会生效,因为该命令仅针对单个容器。在Docker Compose中,depends_on用于帮助构建一些逻辑来组织各容器启动的顺序。例如,仅在成功启动容器 A 之后才启动容器 B。
- ports:这基本上是–publish标记,可接收一系列端口。
唯一在运行 docker container run时存在的等价标记是:
- restart:这与–restart相同并接收相同的输入。
我们的Docker Compose YAML文件最后一部分是声明我们的数据卷:
1 2 |
volumes: redis_data: |
示例投票应用
已经提到过Moby计数器应用的 Docker Compose文件是一个非常简单的示例。我们来看一个更为复杂的Docker Compose并了解如何引入构建容器以及多个网络。
在本书的 Git 仓库中,你可以在Chapter05目录中找到一个名为example-voting-app的文件夹。这是从官方的Docker示例仓库中复制的投票应用。
打开docker-compose.yml文件可以看到该应用由5个容器、2个网络和一个数据卷组成。现在先忽略其它文件,我们将在后续的章节中进行学习。先来看看docker-compose.yml中发生了什么:
1 2 3 |
version: "3" services: |
可以看到,开头处通过简单地定义版本,然后列出一系列services。第一个容器名为vote:它是一个Python应用,允许用户提交投票。从下面的定义可以看出,这里并没有下载一个镜像,而是使用build代替 image 命令从头开始构建一个镜像:
1 2 3 4 5 6 7 8 9 10 |
vote: build: ./vote command: python app.py volumes: - ./vote:/app ports: - "5000:80" networks: - front-tier - back-tier |
这里的build指令告诉Docker Compose使用Dockerfile构建一个容器,该文件可在./vote下找到。Dockerfile文件本身对于Python应用的表述非常直接。
一旦启动了容器,接着我们会将宿主机的./vote文件挂载到容器中,这通过传递想要挂载的文件夹路径以及想要挂载的容器来实现的。
我们告诉容器在启动时运行python app.py。并将宿主机上的5000与容器中的80端口进行了映射,最后我将附加了两个网络到容器,一个名为front-tier,另一个名为back-tier。
front-tier包含将端口映射到宿主机的容器,back-tier预留用于无需暴露端口以及私有隔离网络运行的容器。
接下来,我们有另一个与front-tier网络相连接的容器。这个容器显示了投票的结果。result容器包含一个Node.js应用,它与一会儿会说到的PostgreSQL数据库相连接,因投票实时反馈到投票容器中它显示实时的结果。和vote容器一样,该镜像使用./result文件夹下的Dockerfile在本地进行构建。
1 2 3 4 5 6 7 8 9 10 11 |
result: build: ./result command: nodemon server.js volumes: - ./result:/app ports: - "5001:80" - "5858:5858" networks: - front-tier - back-tier |
我们暴露的端口是5001,通过它我们可以连接并查看结果。接下来也是最后一个应用容器名为worker。
1 2 3 4 5 6 7 |
worker: build: context: ./worker depends_on: - "redis" networks: - back-tier |
worker容器运行一个.NET应用,它的唯一任务是连接Redis并通过将其传送给运行在名为 db 的容器上的PostgreSQL数据库来注册每个投票。该容器同样是使用Dockerfile来进行构建,但这次不是传递Dockerfile所存放文件夹路径,我们使用了context。它设置了docker构建的工作路径并允许我们定义额外的选项如标签和修改Dockerfile的名称。
因为这个容器仅仅是连接redis和db容器,它也就无需暴露任何端口来被直接连接,它也不需要与运行在front-tier网络中的任一容器进行通讯,也就是说我们只需将其添加到back-tier网络中即可。
那么,现在我们就有了投票应用,从终端用户注册投票并将它们发送到redis容器,在那里投票由worker容器进行处理。service中的 redis 容器定义如下:
1 2 3 4 5 6 |
redis: image: redis:alpine container_name: redis ports: ["6379"] networks: - back-tier |
该容器使用官方的Redis镜像并且不从Dockerfile进行构建,我们要确保6379端口可用,但仅在back-tier网络中。我们还指定了容器的名称,通过container_name将其设置为redis。这是为了避免考虑由Docker Compose生成默认的名称,如果你还记得的话,Docker Compose会使用文件夹名来在它们自己的命名空间中启动容器。
紧接着的也是最一个容器是PostgreSQL容器,我们前面已提到其名称为db:
1 2 3 4 5 6 7 |
db: image: postgres:9.4 container_name: db volumes: - "db-data:/var/lib/postgresql/data" networks: - back-tier |
可以看到,它与redis容器极其相似,使用了官方镜像,但是你也会注意到并没有暴露任何端口,因为这是官方镜像的一个默认选项。我们还指定了容器的名称。
因其是存储投票的地方,我们将创建并挂载一个数据卷来作为PostgreSQL数据库的持久化存储:
1 2 |
volumes: db-data: |
最后是我们一直有提到的两个网络:
1 2 3 |
networks: front-tier: back-tier: |
运行docker-compose up会输出很多启动时正在发生的反馈,第一次启动应用大约会花费5分钟。如果你按照本文操作并启动了自己的应用,接下来的就是启动时的缩减版本。
小贴士:你可能会得到npm ERR! request to https://registry.npmjs.org/nodemon failed, reason: Hostname/IP doesn’t match certificate’s altnames的错误。这时,使用有权限用户运行echo “104.16.16.35 registry.npmjs.org” >> /etc/hosts命令来写入/etc/hosts。
首先会创建网络并准备好容器要使用的数据卷:
1 2 3 |
Creating network "example-voting-app_front-tier" with the default driver Creating network "example-voting-app_back-tier" with the default driver Creating volume "example-voting-app_db-data" with default driver |
然后会构建投票容器镜像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
Building vote Step 1/7 : FROM python:2.7-alpine 2.7-alpine: Pulling from library/python bdf0201b3a05: Already exists 782863018686: Pull complete 56c01527c728: Pull complete 5785fcaddca9: Pull complete Digest: sha256:4bb294c07976a311784c23e0a5becd83f7a862eb8fa296f5713c7cf3d109473d Status: Downloaded newer image for python:2.7-alpine ---> 6260494ffce3 Step 2/7 : WORKDIR /app ---> Running in 8e48feed592e Removing intermediate container 8e48feed592e ---> 37d82a0b29a0 Step 3/7 : ADD requirements.txt /app/requirements.txt ---> ec8d6a22ce40 Step 4/7 : RUN pip install -r requirements.txt ---> Running in 54413564fd34 [大量的 Python的构建输出...] Removing intermediate container 54413564fd34 ---> 1fb7742f5b62 Step 5/7 : ADD . /app ---> 2f6620271472 Step 6/7 : EXPOSE 80 ---> Running in 3152d7925696 Removing intermediate container 3152d7925696 ---> 215a5d063362 Step 7/7 : CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--log-file", "-", "--access-logfile", "-", "--workers", "4", "--keep-alive", "0"] ---> Running in 3dded8dbfdf8 Removing intermediate container 3dded8dbfdf8 ---> bf08b0163572 Successfully built bf08b0163572 Successfully tagged example-voting-app_vote:latest WARNING: Image for service vote was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`. |
一旦这一投票镜像已构建,就开始创建worker镜像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
Building worker Step 1/5 : FROM microsoft/dotnet:2.0.0-sdk 2.0.0-sdk: Pulling from microsoft/dotnet 3e17c6eae66c: Pull complete 74d44b20f851: Pull complete a156217f3fa4: Pull complete 4a1ed13b6faa: Pull complete 18842ff6b0bf: Pull complete e857bd06f538: Pull complete b800e4c6f9e9: Pull complete Digest: sha256:f4ea9cdf980bb9512523a3fb88e30f2b83cce4b0cddd2972bc36685461081e2f Status: Downloaded newer image for microsoft/dotnet:2.0.0-sdk ---> fde8197d13f4 Step 2/5 : WORKDIR /code ---> Running in 71fa0be353c1 Removing intermediate container 71fa0be353c1 ---> 4db41647f059 Step 3/5 : ADD src/Worker /code/src/Worker ---> 6ea2fe8d6a6a Step 4/5 : RUN dotnet restore -v minimal src/Worker && dotnet publish -c Release -o "./" "src/Worker/" ---> Running in 9c3e2cc63b59 Restoring packages for /code/src/Worker/Worker.csproj... [这里有许多.net 构建的输出...] Restore completed in 23.05 sec for /code/src/Worker/Worker.csproj. Microsoft (R) Build Engine version 15.3.409.57025 for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Worker -> /code/src/Worker/bin/Release/netcoreapp2.0/Worker.dll Worker -> /code/src/Worker/ Removing intermediate container 9c3e2cc63b59 ---> 30668bb87c14 Step 5/5 : CMD dotnet src/Worker/Worker.dll ---> Running in b4d127d945b3 Removing intermediate container b4d127d945b3 ---> 16b79c907058 Successfully built 16b79c907058 Successfully tagged example-voting-app_worker:latest WARNING: Image for service worker was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`. |
然后拉取了 redis 镜像:
1 2 3 4 5 6 7 8 |
Pulling redis (redis:alpine)... alpine: Pulling from library/redis bdf0201b3a05: Already exists 542e0c4f2f18: Pull complete cbf113c39f65: Pull complete 09158274ea6c: Pull complete ffc2a2e9a3a6: Pull complete bcdc222d2d8e: Pull complete |
以下为db容器拉取了PostgreSQL镜像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Pulling db (postgres:9.4)... 9.4: Pulling from library/postgres 27833a3ba0a5: Pull complete ed00742830a6: Pull complete dc611c2aceba: Pull complete a61becab5279: Pull complete 8dcff41e7aea: Pull complete 820bf1bbf0d7: Pull complete 050804429905: Pull complete 782c81275334: Pull complete 3c8274aa0e5a: Pull complete 4738a5d221b9: Pull complete c0420b43adff: Pull complete b3c37902a889: Pull complete 0df57a4018b9: Pull complete e75d47c9b7af: Pull complete |
接下来是重头戏了,开始构建结果镜像了。 Node.js输出较繁琐,你看在屏幕上看到很多Dockerfile中npm区域执行的输出,事实上,会有超过250行的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
Building result Step 1/11 : FROM node:8.9-alpine 8.9-alpine: Pulling from library/node 605ce1bd3f31: Pull complete 79b85b1676b5: Pull complete 20865485d0c2: Pull complete Digest: sha256:6bb963d58da845cf66a22bc5a48bb8c686f91d30240f0798feb0d61a2832fc46 Status: Downloaded newer image for node:8.9-alpine ---> 406f227b21f5 Step 2/11 : RUN mkdir -p /app ---> Running in 457324c5e0ff Removing intermediate container 457324c5e0ff ---> ff8bdbadb2d1 Step 3/11 : WORKDIR /app ---> Running in cdfb6f884119 Removing intermediate container cdfb6f884119 ---> 2a9b5e8d7fe3 Step 4/11 : RUN npm install -g nodemon ---> Running in 27d34d662867 [nodejs 相关的输出...] Removing intermediate container 27d34d662867 ---> 3bfd25ff51c1 Step 5/11 : RUN npm config set registry https://registry.npmjs.org ---> Running in d2377c42ee62 Removing intermediate container d2377c42ee62 ---> 1bc75fce5e0b Step 6/11 : COPY package.json /app/package.json ---> 77d0cb6f8fab Step 7/11 : RUN npm install && npm ls && npm cache clean --force && mv /app/node_modules /node_modules ---> Running in 6746fdcb136f [nodejs 相关的输出...] Removing intermediate container 6746fdcb136f ---> 6b75fa8c6b23 Step 8/11 : COPY . /app ---> 07f003e707d6 Step 9/11 : ENV PORT 80 ---> Running in 66ef9ab709eb Removing intermediate container 66ef9ab709eb ---> d37884534b43 Step 10/11 : EXPOSE 80 ---> Running in 873e506a36c9 Removing intermediate container 873e506a36c9 ---> bf546b85c200 Step 11/11 : CMD ["node", "server.js"] ---> Running in cbb4f6901160 Removing intermediate container cbb4f6901160 ---> 5f5dc7ec7d5f Successfully built 5f5dc7ec7d5f Successfully tagged example-voting-app_result:latest WARNING: Image for service result was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`. |
该应用的result部分可通过http://localhost:5001进行访问。默认是没有投票的,被分成了50/50:
译者注:如使用官方的源码以上页面会因为 AngularJS的导入而出现空白的显示,需修改result/views/index.html中对应的内容
应用的投票部分可通过http://localhost:5000进行访问:
点击 CATS或DOGS都会登记投票,你应该可以在Terminal中看到如下的Docker Compose日志输出:
上面有一些错误,因为Redis的表结构仅在投票应用登记了第一个投票后才会创建, 一旦投了一票后,就会创建Redis表结构,worker会接收该投票并通过写入db容器来对其进行处理。投票完成后,result容器会实时进行更新:
在后续章节学习Docker Swarm 栈和Kubenetes集群时我们会再来看Docker Compose的YAML文件。这里我们先回到Docker Compose并来了解一些我们可以运行的命令。
Docker Compose命令
本章已过半,我们只用到了一个Docker Compose命令docker-compose up。如果你照着操作,这时运行docker container ls -a命令,会在终端中看到类似如下的信息:
1 2 3 4 5 6 7 |
$ docker container ls -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 53628d623ad1 example-voting-app_worker "/bin/sh -c 'dotnet …" 22 seconds ago Exited (137) 8 seconds ago example-voting-app_worker_1 6fd2bed2d8cd postgres:9.4 "docker-entrypoint.s…" 23 seconds ago Exited (137) 8 seconds ago db 851eb6b9219c example-voting-app_vote "python app.py" 23 seconds ago Exited (137) 8 seconds ago example-voting-app_vote_1 7222e1ea9b70 example-voting-app_result "nodemon server.js" 23 seconds ago Exited (137) 8 seconds ago example-voting-app_result_1 197ee1c0c9cf redis:alpine "docker-entrypoint.s…" 23 seconds ago Exited (137) 8 seconds ago redis |
可以看到,这里有很多状态为EXITED的容器。这是因为在我们使用Ctrl + C返回到Terminal时,Docker Compose的容器就被停止了。
选择一个Docker Compose应用并进入包含docker-compose.yml文件的对应目录,我们会一起来使用更多的Docker Compose命令。这里我将使用Example Vote应用。
up和 ps
第一个命令是docker-compose up,但这次我们会添加一个标记。在你所选择的应用文件夹中,运行如下命令:
1 |
$ docker-compose up -d |
这会再次以非连接的方式启动之前的应用:
1 2 3 4 5 6 |
AlansMac:example-voting-app alan$ docker-compose up -d Starting db ... done Starting example-voting-app_result_1 ... done Starting redis ... done Starting example-voting-app_vote_1 ... done Starting example-voting-app_worker_1 ... done |
在返回到Terminal之后,你可以使用如下命令查看运行中的容器:
1 |
$ docker-compose ps |
可以在如下终端输出中可以看到,所有容器的状态都是Up
1 2 3 4 5 6 7 8 9 |
$ docker-compose ps Name Command State Ports ------------------------------------------------------------------------------------------------------------------ db docker-entrypoint.sh postgres Up 5432/tcp example-voting-app_result_1 nodemon server.js Up 0.0.0.0:5858->5858/tcp, 0.0.0.0:5001->80/tcp example-voting-app_vote_1 python app.py Up 0.0.0.0:5000->80/tcp example-voting-app_worker_1 /bin/sh -c dotnet src/Work ... Up redis docker-entrypoint.sh redis ... Up 0.0.0.0:32773->6379/tcp |
在运行这些命令时,Docker Compose只知道在docker-compose.yml文件的service区中定义的容器,所有其它不属于服务栈的容器都会被忽略。
config
运行如下命令会验证我们的docker-compose.yml文件:
1 |
$ docker-compose config |
如果不存在问题,会在屏幕上打印出Docker Compose YAML文件中已渲染的一份拷贝,这是Docker Compose解释这一证件的方式。如果你不想要看到这一错误,只是是要检查错误,可以运行如下命令:
1 |
$ docker-compose config -q |
这是–quiet的简写。我们所使用的示例文件中并不存在任何错误,如果有其它错误,则会像如下内容这样显示:
1 |
ERROR: yaml.parser.ParserError: while parsing a block mapping in "./docker-compose.yml", line 1, column 1 expected <block end>, but found '<block mapping start>' in "./docker-compose.yml", line 27, column 3 |
pull, build和 create
接下来的两条命令可以帮助我们来对Docker Compose应用的启动来做准备。下面这条命令会读取我们的Docker Compose YAML文件并拉取它所读取的镜像:
1 |
$ docker-compose pull |
如下命令会对在我们的文件中所找到的指令执行build操作:
1 |
$ docker-compose build |
这些命令对于你初次定义基于Docker Compose的应用而又不通过启动应用来进行测试时非常有用。docker-compose build命令可用于在原始构建镜像的Dockerfile有更新时触发新的构建。
pull和build命令仅仅是生成/拉取应用所需的镜像,它们本身不会去配置容器。需要使用如下命令来进行配置:
1 |
$ docker-compose create |
该命令会创建但不启动容器。它与docker container create命令操作的方式相同,会在启动容器前处于exited的状态。对create命令可以传递几个用得到的标记:
- –force-recreate: 它会重新创建容器,哪怕是在配置文件未发生修改而无需重建
- –no-recreate: 在容器已存在时不进行重建,该标记不能与前一个标记同时使用
- –no-build: 它不会重建镜像,哪怕是需要构建的镜像并不存在
- –build: 它会在创建容器之前构建镜像
start, stop, restart, pause,和unpause
以下命令和docker container对应的命令的功能相同,唯一的区别是它们作用于所有的容器的修改。
1 2 3 4 5 |
$ docker-compose start $ docker-compose stop $ docker-compose restart $ docker-compose pause $ docker-compose unpause |
也可以通过传递名称来定位单个服务,例如,对db服务进行暂停和取消暂停,我们可以运行如下命令:
1 2 |
$ docker-compose pause db $ docker-compose unpause db |
top, logs和events
接下来的三个命令全部用于显示运行中容器及Docker Compose中所发生状况的反馈信息。
下面一条命令,我对应的docker container命令一样,显示由Docker Compose所启动的每个容器运行的进程信息:
1 |
$ docker-compose top |
可以从如下的Terminal输出中看到,每个容器都被分割为独立的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
$ docker-compose top db PID USER TIME COMMAND ------------------------------------------------------------------------ 15080 999 0:00 postgres 15768 999 0:00 postgres: checkpointer process 15769 999 0:00 postgres: writer process 15770 999 0:00 postgres: wal writer process 15771 999 0:00 postgres: autovacuum launcher process 15772 999 0:00 postgres: stats collector process 15899 999 0:00 postgres: postgres postgres 172.20.0.6(37686) idle 15907 999 0:00 postgres: postgres postgres 172.20.0.3(58836) idle example-voting-app_result_1 PID USER TIME COMMAND ----------------------------------------------------------- 15189 root 0:00 node /usr/local/bin/nodemon server.js 15852 root 0:00 /usr/local/bin/node server.js example-voting-app_vote_1 PID USER TIME COMMAND ------------------------------------------------------- 15243 root 0:00 python app.py 15877 root 0:00 /usr/local/bin/python /app/app.py example-voting-app_worker_1 PID USER TIME COMMAND ------------------------------------------------------------- 15686 root 0:00 /bin/sh -c dotnet src/Worker/Worker.dll 15878 root 0:00 dotnet src/Worker/Worker.dll redis PID USER TIME COMMAND ---------------------------------- 15066 rpc 0:00 redis-server |
如果你仅想要看单个服务的信息,只需在运行该命令时传递服务的名称即可:
1 |
$ docker-compose top db |
下一条命令将每一个运行中的容器的日志输出到屏幕中:
1 |
$ docker-compose logs |
和docker container命令类似,你可以传递-f或–follow等标记来保持日志持续输出直至按下Ctrl + C为止。同样你可以通过将名称放到命令之后来仅打印单个服务的日志:
1 2 3 4 5 6 7 8 |
db | LOG: autovacuum launcher shutting down db | LOG: database system was interrupted; last known up at 2019-04-17 02:54:04 UTC db | LOG: database system was not properly shut down; automatic recovery in progress db | LOG: record with zero length at 0/16EA480 db | LOG: redo is not required db | LOG: MultiXact member wraparound protections are now enabled db | LOG: database system is ready to accept connections db | LOG: autovacuum launcher started |
events命令也是和docker container对应命令类似,它输出事件,比如由其它我们所讨论的命令实时触发的事件。例如,运行如下命令:
1 |
$ docker-compose events |
新打开一个窗口运行docker-compose pause命令即可获得如下输出:
1 2 3 4 5 6 |
$ docker-compose events 2019-04-17 14:09:53.633717 container pause 7222e1ea9b70a2a36eecebd9064e5473328944d794d978d621784415bad55853 (image=example-voting-app_result, name=example-voting-app_result_1) 2019-04-17 14:09:53.652416 container pause 53628d623ad1d69fa455eb143ad5c2f61f6e37b72853f5558fb1c3ab2c60bbf8 (image=example-voting-app_worker, name=example-voting-app_worker_1) 2019-04-17 14:09:53.656111 container pause 851eb6b9219c040d6e4cbef3d8b5918d6f06a90c9de0d19ed37cc101871d32a0 (image=example-voting-app_vote, name=example-voting-app_vote_1) 2019-04-17 14:09:53.676774 container pause 197ee1c0c9cf21f673584cd79fd20008939bf3532f5bcf5dc960f5b21b2d23d2 (image=redis:alpine, name=redis) 2019-04-17 14:09:53.678229 container pause 6fd2bed2d8cdfec0e6f760826eed329d17d1a66396f68161bcde348bbd771309 (image=postgres:9.4, name=db) |
这两条命令与docker container对应的命令运行相似。运行如下命令:
1 |
$ docker-compose exec worker ping -c 3 db |
译者注:记得先执行 docker-compose unpause
这会在运行中的worker容器中会启动一个新的进程来对db容器执行3次ping操作,如下所示:
1 2 3 4 5 6 7 8 |
$ docker-compose exec worker ping -c 3 db PING db (172.20.0.5): 56 data bytes 64 bytes from 172.20.0.5: icmp_seq=0 ttl=64 time=0.118 ms 64 bytes from 172.20.0.5: icmp_seq=1 ttl=64 time=0.270 ms 64 bytes from 172.20.0.5: icmp_seq=2 ttl=64 time=0.222 ms --- db ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max/stddev = 0.118/0.203/0.270/0.063 ms |
run命令对于在应用内一次性地运行容器化命令时非常有用。例如,如果你要使用composer等包管理器来更新存储在数据卷中的项目的依赖,可以运行如下命令:
1 |
$ docker-compose run --volume data_volume:/app composer install |
这会以install命令运行composer容器并将data_volume挂载到容器中的/app。
scale
scale命令会接收向其传递的服务来按所定义的数量来对服务进行扩充操作,比如,要添加更多的worker容器,只需要运行如下命令:
1 |
$ docker-compose scale worker=3 |
但这会给出如下的警告:
1 |
WARNING: The scale command is deprecated. Use the up command with the --scale flag instead. |
现在我们应当使用的命令如下:
1 |
$ docker-compose up -d --scale worker=3 |
虽然scale命令来当前版本的Docker Compose中仍然可用,会在该软件的未来版本中进行删除。
你会注意到我选择对worker容器进行扩充。这是有原因的,如果尝试运行如下命令就会明白了:
1 |
$ docker-compose up -d --scale vote=3 |
你会注意到虽然Docker Compose创建了两个额外的容器,它们未能启动并报出如下错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ docker-compose up -d --scale vote=3 db is up-to-date example-voting-app_result_1 is up-to-date redis is up-to-date WARNING: The "vote" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash. Starting example-voting-app_vote_1 ... Starting example-voting-app_vote_1 ... done Creating example-voting-app_vote_2 ... error Creating example-voting-app_vote_3 ... error ERROR: for example-voting-app_vote_2 Cannot start service vote: driver failed programming external connectivity on endpoint example-voting-app_vote_2 (a6fc51bef042e4190fc63ef99fb431c2ac25fe8eeab23f14a60d7076ecf59bcb): Bind for 0.0.0.0:5000 failed: port is already allocated ERROR: for example-voting-app_vote_3 Cannot start service vote: driver failed programming external connectivity on endpoint example-voting-app_vote_3 (e6439cb250f108a55d6e160bd1babbf07a88bb53028184d02ac24b8a09d1dae0): Bind for 0.0.0.0:5000 failed: port is already allocated ERROR: for vote Cannot start service vote: driver failed programming external connectivity on endpoint example-voting-app_vote_2 (a6fc51bef042e4190fc63ef99fb431c2ac25fe8eeab23f14a60d7076ecf59bcb): Bind for 0.0.0.0:5000 failed: port is already allocated ERROR: Encountered errors while bringing up the project. |
这是因为我们不能将三个独立的容器映射到相同的端口上。有规避的方式,我们将在后面的章节中了解更多细节。
kill, rm和down
我们最后来看的这三条Docker Compose命令用于删除/终止Docker Compose应用。第一个命令通过停止运行中容器进程来停止运行中的容器。即 kill 命令:
1 |
$ docker-compose kill |
运行这一命令时要非常小心,因为它不会像在运行docker-compose stop时那样等待容器正常的停止,也就是说使用docker-compose kill命令有可能会导致数据损失。
接着是rm命令,这会删除状态为exited的所有容器:
1 |
$ docker-compose rm |
最后一个命令是down命令。你可能已经猜到了,它的效果与运行docker-compose up的效果相反:
1 |
$ docker-compose down |
这会删除由docker-compose up所创建的容器和网络。如果想要删除所有,可以通过运行如下命令:
1 |
$ docker-compose down --rmi all --volumes |
这会删除所有你在运行docker-compose up命令时产生的容器、网络和镜像(包含拉取和构建的),包括Docker Compose 应用之外还在用的镜像。但如果镜像在使用中,会触发一个错误且不会被删除:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ docker-compose down --rmi all --volumes Removing example-voting-app_worker_1 ... done Removing example-voting-app_vote_1 ... done Removing db ... done Removing redis ... done Removing example-voting-app_result_1 ... done Removing network example-voting-app_front-tier Removing network example-voting-app_back-tier Removing volume example-voting-app_db-data Removing image example-voting-app_vote Removing image example-voting-app_result Removing image redis:alpine ERROR: Failed to remove image for service redis: 409 Client Error: Conflict ("conflict: unable to remove repository reference "redis:alpine" (must force) - container 98d5b4b8c23e is using its referenced image c8eda26fcdab") Removing image example-voting-app_worker Removing image postgres:9.4 |
在以上的输出中可以看到,还有另一个容器Moby计数器应用在使用redis镜像,因此没有被删除。但是其它Example Vote应用所使用的镜像都被删除了,包含docker-compose up所构建的镜像和从Docker Hub上所下载的镜像。
Docker App
在开始这一节之前,我应该发布一条警告:
我们将要讨论的功能还处于实验阶段。它还处于开发的早期阶段,仅用作未来功能的一个预览。
基于这一原因,我只会讲在macOS版本的安装。但在安装前,我们来讨论下Docker App究竟是什么。
虽然Docker Compose文件在向其他人分享时非常有用,你可能注意到本章至此缺失了一个非常重要的元素,那就是不能像Docker镜像那样发布Docker Compose文件。
Docker认识到了这一问题,并在开发一个新功能,称为Docker App,旨在解决这一问题。
Docker App是一个自包含的二进制,有助于你创建可通过Docker Hub或Docker企业仓库分享的应用包。
小贴士:我看建议查看GitHub项目的Releases页面(你可以在扩展阅读部分找到这一链接)来确保你使用的是最新版本。如果版本晚于0.4.1,你需要在下面的命令中替换掉版本号。
要在macOS上安装Docker App,你可以运行如下命令来先设定要下载的版本号:
1 |
$ VERSION=v0.4.1 |
译者注:当前翻译时 Docker App 的最新版本为 v0.6.0,已不再支持 save, ls 等命令,因此采用原书相同版本进行的操作
现在你已经有了正确的版本,可以开始使用如下命令拼接并进行下载了:
1 2 3 |
$ curl -SL https://github.com/docker/app/releases/download/$VERSION/docker-app-darwin.tar.gz | tar xJ -C /usr/local/bin/ $ mv /usr/local/bin/docker-app-darwin /usr/local/bin/docker-app $ chmod +x /usr/local/bin/docker-app |
完成以上操作,你就可以运行如下命令在屏幕上打印该二进制文件的基本信息了:
1 |
$ docker-app version |
没有跟着操作的读者可以在下面看到以上命令的完整输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ VERSION=v0.4.1 $ curl -SL https://github.com/docker/app/releases/download/$VERSION/docker-app-darwin.tar.gz | tar xJ -C /usr/local/bin/ % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 615 0 615 0 0 790 0 --:--:-- --:--:-- --:--:-- 801 100 11.0M 100 11.0M 0 0 757k 0 0:00:14 0:00:14 --:--:-- 684k $ mv /usr/local/bin/docker-app-darwin /usr/local/bin/docker-app $ chmod +x /usr/local/bin/docker-app $ docker-app version Version: v0.4.1 Git commit: 48c0769c Built: Wed Aug 22 12:01:46 2018 OS/Arch: darwin/amd64 Experimental: off Renderers: none |
我们使用的docker-compose.yml文件要做些微调。版本号由3升级为3.6 。不这样做会导致如下错误:
1 |
Error: unsupported Compose file version: 3 |
我们要运行的也是产生以上报错的命令如下:
1 |
$ docker-app init --single-file mobycounter |
该命令接收我们的docker-compose.yml文件并将其嵌入.dockerapp文件。一开始该文件中会有一些注释详细描述进入到下一步之前你需要做的修改。我们在仓库里放了一个未修改版本的文件,在Chapter5/mobycounter-app 文件夹下,名为mobycounter.dockerapp.original。
mobycounter.dockerapp文件编辑后的版本如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
version: latest name: mobycounter description: An example Docker App file which packages up the Moby Counter application namespace: masteringdocker3 maintainers: - name: alan email: --- version: "3.6" services: redis: image: redis:alpine volumes: - redis_data:/data restart: always mobycounter: depends_on: - redis image: russmckendrick/moby-counter ports: - "${port}:80" restart: always volumes: redis_data: --- { "port":"8080" } |
可以看到,它分成了3个部分,第一部分包含应用的元数据,如下所示:
- version:这是将要发布到Docker Hub上的应用版本号
- name:在Docker Hub上所显示的应用名
- description:应用的简短描述
- namespace:这是你的Docker Hub用户名或者你有授权的组织名
- maintainer:一组应用的维护人员
第二部分包含我们的Docker Compose文件。你可能注意到有几处用变量进行了替换。在本例中,我们用${port}替换了端口8080。变量port的默认值在最后一个部分中进行了定义。
完成了.dockerapp文件之后,你就可以运行如下命令来将这一Docker App保存为镜像:
1 |
$ docker-app save |
我以运行如下命令来查看本机中活跃的Docker App:
1 |
$ docker-app ls |
因Docker App基本上是封装在标准Docker镜像中的一些元数据,你也可以通过运行命令来进行查看:
1 |
$ docker image ls |
如果你没有照着操作的话,可以查看终端中的输出结果如下:
1 2 3 4 5 6 7 8 |
$ docker-app save Saved application as image: masteringdocker3/mobycounter.dockerapp:latest $ docker-app ls REPOSITORY TAG IMAGE ID CREATED SIZE masteringdocker3/mobycounter.dockerapp latest 8d11cd8c8e57 14 seconds ago 499B $ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE masteringdocker3/mobycounter.dockerapp latest 8d11cd8c8e57 24 seconds ago 499B |
运行如下命令会显示该Docker App的概要信息,与通过docker image inspect来查看构建镜像的细节方法相似:
1 |
$ docker-app inspect masteringdocker3/mobycounter.dockerapp:latest |
可以从如下的终端输出中看到,运行 docker-app inspect比docker image inspect命令输出的信息更为友好:
1 2 3 4 5 6 7 8 9 |
$ docker-app inspect masteringdocker3/mobycounter.dockerapp:latest mobycounter latest Maintained by: alan An example Docker App file which packages up the Moby Counter application Setting Default ------- ------- port 8080 |
我们已完成了应用,现在需要将其推送到Docker Hub上。要实现推送,只需要运行如下命令:
1 2 3 4 |
$ docker-app push The push refers to repository [docker.io/masteringdocker3/mobycounter.dockerapp] 14dc8e8ac4dc: Pushed latest: digest: sha256:f055104453493f044c9826cd2a6734084bb93dd55d472110a102ccca23b4e6fe size: 524 |
这表示我们的应用已经在Docker Hub上进行了发布:
那么, 如何获取该Docker App呢?首先我们需要删除本地镜像。运行如下命令来实现删除:
1 |
$ docker image rm masteringdocker3/mobycounter.dockerapp:latest |
删除后进入另一个目录:
1 |
$ cd ~/ |
现在,让我们下载这一Docker App,对端口做出修改,并进行启动:
1 |
$ docker-app render masteringdocker3/mobycounter:latest --set port="9090" | docker-compose -f - up |
同样,如果有读者没有照着操作,可在以下终端输出查看前述命令运行效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ docker-app render masteringdocker3/mobycounter:latest --set port="9090" | docker-compose -f - up Creating network "alan_default" with the default driver Creating volume "alan_redis_data" with default driver Creating alan_redis_1 ... done Creating alan_mobycounter_1 ... done Attaching to alan_redis_1, alan_mobycounter_1 redis_1 | 1:C 17 Apr 2019 15:41:15.122 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo redis_1 | 1:C 17 Apr 2019 15:41:15.122 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=1, just started redis_1 | 1:C 17 Apr 2019 15:41:15.122 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf redis_1 | 1:M 17 Apr 2019 15:41:15.124 * Running mode=standalone, port=6379. redis_1 | 1:M 17 Apr 2019 15:41:15.124 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. redis_1 | 1:M 17 Apr 2019 15:41:15.124 # Server initialized redis_1 | 1:M 17 Apr 2019 15:41:15.124 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. redis_1 | 1:M 17 Apr 2019 15:41:15.124 * Ready to accept connections mobycounter_1 | using redis server mobycounter_1 | ------------------------------------------- mobycounter_1 | have host: redis mobycounter_1 | have port: 6379 mobycounter_1 | server listening on port: 80 mobycounter_1 | Connection made to the Redis server |
可以看到,我们甚至无需手动下载这个Docker App镜像,就启动并运行了该应用。访问http://localhost:9090/应该会显示邀请你点击来添加logo的页面。
对于这种常规的前面运行的Docker Compose应用,按下Ctrl + C就可以返回terminal。
你可以运行如下命令来与应用进行交互或终止应用:
1 2 |
$ docker-app render masteringdocker3/mobycounter:latest --set port="9090" | docker-compose -f - ps $ docker-app render masteringdocker3/mobycounter:latest --set port="9090" | docker-compose -f - down --rmi all --volumes |
Docker App中还有更多的功能。但我们现在还不适合讲解更多的细节。在第八章 Docker Swarm和第九章 Docker和Kubernetes中我们会再讲到Docker App。
正如在这一部分前面所提到的,这一功能还处于开发的早期阶段,我们这里讨论的命令和功能在未来都有可能发生变化 。但虽然还处于早期阶段,我希望读者能看到Docker App的优势以及它是如何基于Docker Compose的坚实基础来进行构建的。
总结
希望读者能喜欢这一章有关Docker Compose的内容,也希望大家能像我一样,看到它在进化成为非常有用的第三方工具并成为Docker核心体验的一个重要组成部分。
Docker Compose引入了一些有关运行和管理我们的容器的关键概念。我们会在第八章 Docker Swarm和第九章 Docker和Kubernetes中更进一步的了解这些概念。
下一章中,我们将暂时告别基于Linux平台的容器,短暂地学习Windows容器。
课后问题
- Docker Compose文件使用哪种开源格式?
- 在我们初始的Moby计数器Docker Compose文件中,哪个标记和Docker命令行对应的标记功能相同?
- 是非题:通过Docker Compose文件你只能使用来自Docker Hub的镜像吗?
- 默认情况下,Docker Compose如何决定要使用的命名空间?
- 为docker-compose up添加哪一个标记来在后台启动容器?
- 对Docker Compose文件执行语法检查最好的方式是什么?
- 说明Docker App如何运行的基本原理。
扩展阅读
有关果园实验室(Orchard Laboratories)的更多信息,如下:
- 果园实验室官网: https://www.orchardup.com/
- 果园实验室加入Docker:https://blog.docker.com/2014/07/welcoming-the-orchard-and-fig-team
有关Docker App项目的更多信息,参见:
- GitHub仓库: http://github.com/docker/app/
- 发布页面 – https://github.com/docker/app/releases
最后,有关我们未讲解到的一些主题的扩展链接:
- YAML项目主面: http://www.yaml.org/
- Docker示例仓库::https://github.com/dockersamples/