声明:所有的实验示例大部分来自《learn-docker-in-a-month-of-lunches》的作者Elton Stoneman,但运行结果并不都是照搬,大部分实验结果可能与原书不同
一、前言
之前在【S5:应用部署(Docker Compose)】中简单介绍了如何使用优秀的单机Docker应用部署工具——Docker Compose。但是经过一定的使用之后,我想你也发现了Docker Compose上一些比较麻烦的问题,比如一个Docker Compose部署文件只能启动一个应用,而且当这个应用非常庞大的时候,Docker Compose部署文件中难免出现许多重复的现象,这些重复会隐含一些不必要的潜在风险,毕竟复制粘贴重复的东西难免会出现疏忽,一旦错误发生,你或许得将花费上百倍的时间才能定位到错误。再者,Docker Compose文件的内容有上百行甚至上千行也太折磨人了,我们这时就急切的想将Docker Compose文件拆成多个文件。除此之外,像是启动命令太长、环境配置太长等问题也是急需解决的。
这次,我们将更深入了解一些Docker Compose的使用技巧,来解决上述这些问题。
二、使用同一个Docker Compose部署文件创建多个应用
首先,让我们先探究一下为什么之前的命令对于一个Docker Compose文件只能够部署一个应用。
- 如果你有Elton Stoneman的项目(附在文章底部),那么请进入
diamol/ch10/exercises/todo-list
目录中,如果没有请创建一个目录名为todo-list
,然后创建一个文件docker-compose.yml
,填入以下内容:
version: "3.7"
services:
todo-web:
image: diamol/ch06-todo-list
ports:
- 80
environment:
- Database:Provider=Sqlite
networks:
- app-net
networks:
app-net:
external:
name: nat
是的,你可能会发现在
ports
一项中,似乎只定义了一个80,没有定义映射的端口。这样写其实并没有问题,因为Docker Compose会帮你随机选取一个端口进行映射,至于如何查看这个随机的端口你可以使用docker container port todo-list_todo-web_1 80
这个命令。
- 连续部署两次这个应用
$ docker-compose up -d
Creating todo-list_todo-web_1 ... done
$ docker-compose up -d
todo-list_todo-web_1 is up-to-date
可以看到第二个命令的结果是
todo-list_todo-web_1 is up-to-date
,这说明Docker Compose认为这个容器已经启动了,所以没必要再次启动。除此外关于todo-list_todo-web_1
这个容器名才是值得注意的地方,也就是说是因为Docker Compose认为第二次启动的容器也叫todo-list_todo-web_1
,所以才判断它已经启动了,所以没必要再启动,那么如果我们有办法修改这个容器名,让Docker Compose认为这是一个不同的容器,那么它不就能启动了吗?要做到这点,我们首先得了解一下这个容器名的命名规则,你会发现todo-list_todo-web_1
中的todo-list
其实是你部署文件所在的目录名,todo-web
是你部署文件中定义的服务的名字,后面的数字则代表这个服务的实例号。其实,更准确的说,todo-list
这个前缀其实是一个名叫project
的属性,如果你在部署时没有指定该属性,则默认为部署文件所在的目录名。知道了这些,让我们来试试修改该属性然后再部署一次看看效果吧
- 命令行中指定
-p
修改project
属性,然后部署
$ docker-compose -p todo-list-test up -d
Creating todo-list-test_todo-web_1 ... done
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d45347885468 diamol/ch06-todo-list "dotnet ToDoList.dll" 46 seconds ago Up 44 seconds 0.0.0.0:32786->80/tcp todo-list-test_todo-web_1
691b25dec19a diamol/ch06-todo-list "dotnet ToDoList.dll" 14 minutes ago Up 14 minutes 0.0.0.0:32785->80/tcp todo-list_todo-web_1
可以看到第二个容器名为
todo-list-test_todo-web_1
,而它确实被启动了!
注意:该部署文件中指定了网络相关的配置,所以这两个容器运行在同一个网络环境中,但是如果你在部署文件中不指定网络相关的配置的话,那么它将会创建默认的网络环境,该网络的名字也与project
属性相关,所以你完全没有必要担心两个不同project
的应用是否会运行在同一个网络环境中,除非像本例一样特别指定。
三、解决Docker Compose部署文件中重复的配置
- 先来看一段比较长的代码
networks:
app-net:
name: image-gallery-prod
services:
accesslog:
image: diamol/ch09-access-log
labels:
app-name: image-gallery
logging:
options:
max-file: '10'
max-size: 100m
networks:
app-net: {}
grafana:
depends_on:
- prometheus
image: diamol/ch09-grafana
labels:
app-name: image-gallery
logging:
options:
max-file: '10'
max-size: 100m
networks:
app-net: {}
ports:
- published: 3000
target: 3000
image-gallery:
image: diamol/ch09-image-gallery
labels:
app-name: image-gallery
public: web
logging:
options:
max-file: '10'
max-size: 100m
networks:
app-net: {}
ports:
- published: 80
target: 80
iotd:
image: diamol/ch09-image-of-the-day
labels:
app-name: image-gallery
public: api
logging:
options:
max-file: '10'
max-size: 100m
networks:
app-net: {}
ports:
- published: 8080
target: 80
prometheus:
environment:
DOCKER_HOST: ''
image: diamol/ch09-prometheus
labels:
app-name: image-gallery
logging:
options:
max-file: '10'
max-size: 100m
networks:
app-net: {}
ports:
- published: 9090
target: 9090
version: '3.7'
可以看到,其中logging这一块重复最多,这还是只有这么几十行代码的情况下,就已经看的让人眼花了,如果几百上千行的,不说看得累,万一有个变动,都要改死。所以如果我们能像定义全局变量似的定义这么一个东西用于代替这些重复的值,那么问题不就解决了,而恰好yaml文件本身就有这么个功能,让我们先看看修改后的部署文件吧,至于格式稍后再细说
- 修改后的部署文件
x-labels: &logging
logging:
options:
max-size: '100m'
max-file: '10'
x-labels: &labels
app-name: image-gallery
x-labels: &networks
app-net: {}
networks:
app-net:
name: image-gallery-prod
services:
accesslog:
image: diamol/ch09-access-log
labels:
<<: *labels
<<: *logging
networks:
<<: *networks
grafana:
depends_on:
- prometheus
image: diamol/ch09-grafana
labels:
<<: *labels
<<: *logging
networks:
<<: *networks
ports:
- published: 3000
target: 3000
image-gallery:
image: diamol/ch09-image-gallery
labels:
<<: *labels
public: web
<<: *logging
networks:
<<: *networks
ports:
- published: 80
target: 80
iotd:
image: diamol/ch09-image-of-the-day
labels:
<<: *labels
public: api
<<: *logging
networks:
<<: *networks
ports:
- published: 8080
target: 80
prometheus:
environment:
DOCKER_HOST: ''
image: diamol/ch09-prometheus
labels:
<<: *labels
<<: *logging
networks:
<<: *networks
ports:
- published: 9090
target: 9090
version: '3.7'
可以看到我在最上方定义了三个扩展标签
labels
、logging
、networks
,并使用yaml合并符号<<:
来将这些标签所代表的内容合并到指定位置中。至于具体格式,就不细说了,看一下就懂的。
注意:该特性只能作用于当前文件内,对于别的文件是没有效力的,尤其是接下来要说的内容——多个部署文件进行合并部署。
四、将庞大的Docker Compose文件拆分
为什么要拆分?拆分的情况其实有很多,比如单纯的是内容太过庞大了,一个文件维护起来很麻烦,或者是为了更好的分门别类的管理,比如专管网络的、专管服务的、专管配置的等等,或者说更实际一点,就是为了形成多个不同环境的部署文件,比如测试环境下和正式环境下的数据库相关配置是不同的,那就单独把数据库相关配置拆分出来形成两个文件,一个正式环境的,一个测试环境的,而部署的时候只要指定需要的文件就可以轻松的实现测试环境与正式环境的转换。
拆分其实非常容易,只要将需要拆分的部分的结构完整的复制到另一个文件之中就可以了。下面让我们观察两个被拆分后的文件
- docker-compose.yml
version: "3.7"
services:
numbers-api:
image: diamol/ch08-numbers-api:v3
networks:
- app-net
numbers-web:
image: diamol/ch08-numbers-web:v3
environment:
- RngApi__Url=http://numbers-api/rng
networks:
- app-net
- docker-compose-dev.yml
version: "3.7"
services:
numbers-api:
ports:
- "8087:80"
healthcheck:
disable: true
numbers-web:
entrypoint:
- dotnet
- Numbers.Web.dll
ports:
- "8088:80"
networks:
app-net:
name: numbers-dev
观察后你应该可以理解什么叫“把拆分部分的完整结构复制到另外一个文件中就可以了”,就是如果你要拆分services标签下的numbers-api的内容,那么连带numbers-api的父级标签services也要一同被复制过去。只有格式一样,Docker Compose才能正确将它们合并。
- 使用
config
命令尝试查看合并后的内容
$ docker-compose -f docker-compose.yml -f docker-compose-dev.yml config
networks:
app-net:
name: numbers-dev
services:
numbers-api:
healthcheck:
test:
- NONE
image: diamol/ch08-numbers-api:v3
networks:
app-net: {}
ports:
- published: 8087
target: 80
numbers-web:
entrypoint:
- dotnet
- Numbers.Web.dll
environment:
RngApi__Url: http://numbers-api/rng
image: diamol/ch08-numbers-web:v3
networks:
app-net: {}
ports:
- published: 8088
target: 80
version: '3.7'
docker-compose config
命令会尝试合并你列出的文件,并校验是否符合规范是否能够被运行,如果可以则会顺利输出内容,否则将会报错。额外还需要注意的是,如果两个文件中定义了相同的内容,以在命令行中定义的顺序的后位的内容为优先,以这个例子来说就是以docker-compose-dev.yml的内容为优先。
- 运行
$ docker-compose -f docker-compose.yml -f docker-compose-dev.yml up -d
Creating network "numbers-dev" with the default driver
Creating numbers_numbers-api_1 ... done
Creating numbers_numbers-web_1 ... done
五、另一个环境变量配置标签env_file
之前我们用的环境变量配置的标签environment
只适用于配置少量环境变量的情况,如果要配置大量的环境变量,而且想要将这些环境变量配置在多个应用间分享,那只靠environment
是做不到的,env_file
可以让你配置一系列环境配置文件,Docker Compose会去读取这些配置文件,并将其作为应用的环境变量。
- 让我们先来看看其定义的格式:
services:
todo-web:
ports:
- 8089:80
environment:
- Database:Provider=Sqlite
env_file:
- ./config/logging.debug.env
- 接下来看看配置文件的格式
Logging__LogLevel__Default=Debug
Logging__LogLevel__System=Information
Logging__LogLevel__Microsoft=Information
键值之间使用=号进行分割,每个配置独占一行即可
六、配置.env
文件用以减轻命令行的长度
如果拆分的文件过多,那么部署时输入的命令长度又会成为一个新的问题,为了解决这个问题,Docker Compose可以运行你在命令执行的那个目录下配置一个名为.nev
的文件,该文件中不仅可以设置环境变量,还可以设置要进行合并的部署文件的名字、工程名字(project)等等,简单的示例如下:
# container configuration - ports to publish:
TODO_WEB_PORT=8877
TODO_DB_PORT=5432
# compose configuration - files and project name:
COMPOSE_PATH_SEPARATOR=;
COMPOSE_FILE=docker-compose.yml;docker-compose-test.yml
COMPOSE_PROJECT_NAME=todo_ch10
使用了该文件之后,执行docker-compose up -d
的效果等同于执行docker-compose -f docker-compose.yml -f docker-compose-test.yml -p todo_ch10 --env-file .env up -d
,不信,你可以使用第二个复杂的命令部署应用,然后使用docker-compose down
来尝试停止应用,你会发现结果是成功的。
注意:使用.env
文件的最大效用,只是用来设置一个默认的部署的命令而已,当然如果你不需要环境切换,那么.nev
的作用就是帮助你少敲一些命令罢了。
参考文档:
[1] learn-docker-in-a-month-of-lunches
[2] 官方文档
[3] Compose file version 3 reference
附:
[1] Elton Stoneman的github项目