S5.6:深入Docker Compose

声明:所有的实验示例大部分来自《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'

可以看到我在最上方定义了三个扩展标签labelsloggingnetworks,并使用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项目

你可能感兴趣的:(S5.6:深入Docker Compose)