Docker Compose实例之nginx反向代理GitLab

在上一篇文章(Docker快速搭建GitLab私有仓库)中探索了如何用docker实现最简单的GitLab服务。但是现实场景中往往会遇到复杂的情况和需求,光用docker指令可能就比较繁琐了。

举个例子???? 如下图所示,在一个服务器上要部署一个GitLab,N个其它服务(那N个服务或许还要与GitLab进行隔离),如果用docker 指令一个一个的run起来,管理起来可就麻烦了。 网络示意图

未分类

当然docker作为流行工具,不会让我们那么累,我们借助Docker Compose的文件来描述这个多容器的配置,并通过一条命令就能启动这些容器。

Docker Compose 的配置文件 docker-compose.yml 如下:

version: '3.6'
services:
  gitlab: # gitlab服务名
    container_name: gitlab-site # gitlab 容器名
    image: gitlab/gitlab-ce:latest
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'https://gitlab.example.com'  # git域名
        unicorn['worker_timeout'] = 60
        unicorn['worker_processes'] = 2
        nginx['enable'] = true
        nginx['client_max_body_size'] = '250m'
        nginx['redirect_http_to_https'] = true
        nginx['ssl_certificate'] = "/etc/ssl/gitlab.example.com.crt" # 加密证书文件
        nginx['ssl_certificate_key'] = "/etc/ssl/gitlab.example.com.key"
        nginx['ssl_ciphers'] = "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256"
        nginx['ssl_prefer_server_ciphers'] = "on"
        nginx['ssl_protocols'] = "TLSv1.1 TLSv1.2"
        nginx['ssl_session_cache'] = "builtin:1000  shared:SSL:10m"
        nginx['listen_addresses'] = ["0.0.0.0"]
        nginx['http2_enabled'] = true
    volumes:
      - "/srv/gitlab/config:/etc/gitlab"         # 此处使用的是绝对路径
      - "/srv/gitlab/logs:/var/log/gitlab"
      - "/srv/gitlab/data:/var/opt/gitlab"
      - "/srv/ssl:/etc/ssl"                         # 加密证书文件路径映射
    networks:
      - git-network  # 使用 git-network 网络,与 other-network 相隔离
other-app: # 用 nginx 模拟的其它服务
    container_name: other-app-nginx
    image: nginx:stable-alpine
    volumes:
      - "./nginx-conf-site:/etc/nginx/conf.d:ro"     # 此处使用的是相对路径
      - "./contents:/usr/share/nginx/html/sites:ro" # 相对的是 docker-compose.yml 的位置
    networks:
      - other-network   # 使用 other-network 网络,与 git-network 相隔离
  nginx-reverse:  # nginx 反向代理服务
    container_name: nginx-reverse
    depends_on:    # 反向代理依赖的服务名
      - gitlab  
      - other-app
    image: nginx:stable-alpine
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - "./nginx-conf-reverse:/etc/nginx/conf.d:ro"  # 此处使用的是相对路径
      - "/srv/ssl:/etc/ssl:ro"   # 此处使用的是绝对炉具
    networks:  # nginx反向代理使用的网络
      - git-network     # gitlab使用的网络
      - other-network  # 其它app使用的网络
networks:  # 声明网络
  git-network:  
  other-network:
nginx反向代理的配置文件1: git-reverse.conf ,放在与docker-compose.yml 所在目录相对的 nginx-conf-reverse 目录下,作用是将对 https://gitlab.example.com 的访问进行转发

server{
    listen      443 ssl http2;    # 监听 443 端口
    listen [::]:443 ssl http2;
    server_name gitlab.example.com;

    ssl_certificate        /etc/ssl/gitlab.example.com.crt;
    ssl_certificate_key    /etc/ssl/gitlab.example.com.key;
    ssl_protocols TLSv1.2 TLSv1.1 TLSv1;

    location / {
      proxy_pass https://gitlab-site;   # 转发给名为 "gitlab-site" 的 容器
    }
}

nginx反向代理的配置文件2: other-reverse.conf ,放在与docker-compose.yml 所在目录相对的 nginx-conf-reverse 目录下, 作用是将对 http://other.example.com 的访问进行转发

server{
    listen      80;   # 监听 80 端口
    server_name other.example.com;  # 其它服务的域名
    location / {
      proxy_pass http://other-app-nginx;  # 转发到"其它"服务
    }
}
本例中的“其它服务”由一个nginx静态网站模拟,其配置文件other-site.conf放在与docker-compose.yml 所在目录相对的 nginx-conf-site 目录下:

server {
    listen 80;
    server_name other.example.com;
    location / {
        root   /usr/share/nginx/html/sites/other-site;
        index  index.html;
    }
}

当然,上述配置中用 nginx 模拟的“其它服务” 可以是任意的网络服务,你不需要的话,把这部分删掉也没问题。

最后,执行一个指令

sudo docker-compose up -d

GitLab服务、其它服务、Nginx反向代理 就会依次启动起来。

Docker Compose 编排 DevOps 工具

Docker nginx 反向代理 设置 介绍了通过 nginx 反向代理关联容器。此为真实的使用场景。通过 Gitea 作为代码管理工具;Kanboard 作为任务管理;Jenkins 作为 CI 工具。这样的组合比较适合小型团队使用,相比起 GitLab 这种巨无霸来说,部署简单,使用简单。

准备

安装 Docker

$ curl -fsSL get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh

<output truncated>

If you would like to use Docker as a non-root user, you should now consider
adding your user to the "docker" group with something like:

sudo usermod -aG docker your-user

Remember to log out and back in for this to take effect!

WARNING: Adding a user to the "docker" group grants the ability to run
        containers which can be used to obtain root privileges on the
        docker host.
        Refer to https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface
        for more information.

安装 Docker Compose

$ sudo curl -L https://github.com/docker/compose/releases/download/1.19.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ docker-compose --version

注:Docker 以及 Docker Compose 的安装,官方文档讲得非常清晰,在此不再赘述。

docker-compose.yml 文件

 version: "3.5"

services:
  mysql:
    image: mysql:latest
    container_name: mysql
    ports:
      - "3306:3306"
    networks:
      - devops
    environment:
      - MYSQL_ROOT_PASSWORD=/run/secrets/db_root_password
    volumes:
      - type: bind
        source: ./mysql/conf.d
        target: /etc/mysql/conf.d
      - type: bind
        source: ./mysql/data
        target: /var/lib/mysql
      # - ./mysql/conf.d:/etc/mysql/conf.d
      # - ./mysql/data:/var/lib/mysql
    secrets:
      - db_root_password
    restart: always

  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    ports:
      - "10080:3000"
      - "10022:22"
    networks:
      - devops
    environment:
      - VIRTUAL_HOST=git.vking.io
      - VIRTUAL_PORT=3000
      - GITEA_CUSTOM=/etc/gitea
    depends_on: 
      - mysql
    volumes:
      - type: bind
        source: ./gitea
        target: /data
      - type: bind
        source: ./gitea/custom
        target: /etc/gitea
      # - ./gitea:/data
      # - ./gitea/custom:/etc/gitea
    restart: always

  task:
    image: kanboard/kanboard:latest
    container_name: kanboard
    ports:
      - "8888:80"
    networks:
      - devops
    environment:
      - VIRTUAL_HOST=task.vking.io
      - VIRTUAL_PORT=80
    volumes:
      - type: bind
        source: ./kanboard/data
        target: /var/www/app/data
      - type: bind
        source: ./kanboard/plugins
        target: /var/www/app/plugins
      # - ./kanboard/data:/var/www/app/data
      # - ./kanboard/plugins:/var/www/app/plugins
    restart: always

  jenkins:
    image: jenkins/jenkins:lts
    container_name: jenkins
    ports:
      - "8081:8080"
      - "50000:5000"
    networks:
      - devops
    environment:
      - VIRTUAL_HOST=jenkins.vking.io
      - VIRTUAL_PORT=8080
    volumes:
      - type: bind
        source: ./jenkins/data
        target: /var/jenkins_home
      # - ./jenkins/data:/var/jenkins_home
    restart: always

  nginx:
      image: jwilder/nginx-proxy:alpine
      container_name: nginx
      ports:
        - "80:80"
      depends_on: 
        - gitea
        - task
        - jenkins
      networks:
        - devops
      volumes:
        - type: bind
          source: /var/run/docker.sock
          target: /tmp/docker.sock
        # - /var/run/docker.sock:/tmp/docker.sock
      restart: always

secrets:
  db_root_password:
    file: ./mysql/my_secret.txt

networks:
  devops:
    name: devops-network

注: 通过 volumes bind 方式挂载的外部文件/目录,如果不存在的话,不会自动创建。

使用

  • MySQL 的管理员密码,通过 mysql/my_my_secret.txt 设置,构建容器的时候会自动加载并设置。
  • 不同 services 管理的域名,通过环境变量设置 VIRTUAL_HOST=域名;VIRTUAL_PORT=端口
  • 创建镜像并执行 docker-compose up -d
  • 删除容器及 volumn 数据 docker-compose down -v

后记

因为通过反向代理隐藏了暴露端口的细节,如果没有外部注册的域名的话,还需要通过 Dnsmasq 进行内部域名解析。

CentOS7 安装 Docker 和 Docker-compose

注意需要使用 root 账户或者可以使用 sudo 的账户

1、安装 Docker

# 安装依赖
sudo yum install -y yum-utils 
  device-mapper-persistent-data 
  lvm2

# 添加docker下载仓库
sudo yum-config-manager 
    --add-repo 
    https://download.docker.com/linux/centos/docker-ce.repo

# 安装docker-ce
sudo yum install docker-ce

# 启动docker-ce
sudo systemctl start docker

# 验证
sudo docker --version

sudo docker run hello-world

2、安装 docker-compose

sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

sudo chmod +x /usr/local/bin/docker-compose

docker-compose --version

关于docker-Compose基本使用

简介

Compose 是一个用户定义和运行多个容器的 Docker 应用程序。在 Compose 中你可以使用 YAML 文件来配置你的应用服务。然后,只需要一个简单的命令,就可以创建并启动你配置的所有服务。
使用 Compose 基本会有如下三步流程:

  • 在 Dockfile 中定义你的应用环境,使其可以在任何地方复制。
  • 在 docker-compose.yml 中定义组成应用程序的服务,以便它们可以在隔离的环境中一起运行。
  • 最后,运行dcoker-compose up,Compose 将启动并运行整个应用程序。

安装docker-Compose

目前有两种主流安装方式,笔者使用了第一种方式。

第一种

下载最新的docker-compose文件

curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose

下载完成后需要对/usr/local/bin/docker-compose目录进行赋权

chmod +x /usr/local/bin/docker-compose

测试结果

docker-compose --version

输出

docker-compose version 1.16.1, build 6d1ac21

第二种

通过pip方式安装

pip install docker-compose

前提是需要你的服务器已经装了pip组件

卸载

第一种

rm /usr/local/bin/docker-compose

第二种

pip uninstall docker-compose

使用

我们这里以kafka为例

version: '2'

services:
  zoo1:
    # 依赖于wurstmeister/zookeeper镜像,本地无则自动下载
    image: wurstmeister/zookeeper
    restart: unless-stopped
    hostname: zoo1
    # 映射端口
    ports:
      - "2181:2181"
    # 容器名称
    container_name: zookeeper
  kafka1:
    # 依赖于wurstmeister/kafka镜像
    image: wurstmeister/kafka
    # 映射端口
    ports:
      - "9092:9092"
    # 目录挂载 【容器目录:宿主机目录】
    volumes:
      - /var/log/kafka/logs:/var/docker/kafka/logs
    # 配置环境变量
    environment:
      KAFKA_ADVERTISED_HOST_NAME: localhost
      KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181"
      KAFKA_BROKER_ID: 1
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_CREATE_TOPICS: "stream-in:1:1,stream-out:1:1"
    # 解决服务启动顺序问题,例如下面容器会先确定zoo1和redis两个服务,最后才启动kafka1服务
    depends_on:
      - zoo1
      - redis(实际无该容器)
    # 容器名称
    container_name: kafka

执行docker-compose

首先将docker-compos.yml上传至服务器,然后进入目录执行:

docker-compose up -d

则开始后台构建服务

如果想单独启动一个服务,你可以:

docker-compose up -d 指定服务名称

例子:docker-compose up -d zoo1

服务排编案例

version: "3"
  services:
    # 指定服务名称
    #服务注册与发现中心
    simonEureka:
      image: simon/eureka-server:2.0.1-SNAPSHOT
      hostname: simonEureka
      ports:
        - "8100:8100"
    #配置中心    
    simonConfig:
      image: simon/config-server:2.0.1-SNAPSHOT
      hostname: simonConfig
      ports:
        - "8101:8101"
      depends_on:
        - simonEureka
      # always – 不管退出状态码是什么始终重启容器。当指定always时,docker daemon将无限次数地重启容器。容器也会在daemon启动时尝试重启,不管容器当时的状态如何。
      # no – 容器退出时不要自动重启。这个是默认值。
      # on-failure[:max-retries] – 只在容器以非0状态码退出时重启。可选的,可以退出docker daemon尝试重启容器的次数。
      # unless-stopped - 不管退出状态码是什么始终重启容器,不过当daemon启动时,如果容器之前已经为停止状态,不要尝试启动它。
      restart: always
    #路由网关  
    apigateway:
      image: simon/apigateway:2.0.1-SNAPSHOT
      ports:
        - "8102:8102"
      depends_on:
        - simonEureka
        - simonConfig
      restart: always
    #监控平台  
    admin:
      image: simon/admin:2.0.1-SNAPSHOT
      ports:
        - "8103:8103"
      depends_on:
        - simonEureka
        - simonConfig
      restart: always

这个时候我们服务器simon目录的文件应该如下:

apigateway:2.0.1-SNAPSHOT.jar
admin:2.0.1-SNAPSHOT.jar
config-server:2.0.1-SNAPSHOT.jar
eureka-server:2.0.1-SNAPSHOT.jar
docker-compose.yml

注意点

如果我们的yml文件不是docker-compose.yml时我们在进行服务排编是需要指定yml文件名称。

docker-compose -f docker-kafka.yml up -d

当我们遇到服务启动需要先后顺序时,我们可以对docker-compose.yml根据服务的先后顺序进行拆分。

常用的docker-compose命令

未分类

Docker Compose 之进阶篇

笔者在前文(https://www.cnblogs.com/sparkdev/p/9753793.html)和(https://www.cnblogs.com/sparkdev/p/9787915.html)两篇文章中分别介绍了 docker compose 的基本概念以及实现原理。本文我们将继续探索 docker compose,并通过 demo 介绍一些主要的用法。
说明:本文的演示环境为 ubuntu 16.04。

应用多个 compose 配置文件

docker-compose 命令默认使用的配置文件是当前目录中的 docker-compose.yml 文件,当然我们可以通过 -f 选项指定一个其它名称的配置文件,比如:

$ docker-compose -f docker-compose-dev.yml up

更酷的是我们可以添加多个 -f 选项,docker-compose 会自动合并它们,当然也会根据先后顺序把一些重复的配置项覆盖掉。 下面我们来演示一个常见的使用场景,先创建一个名称为 docker-compose-base.yml 的配置文件,其内容如下:

version: '3'
services:
  web:
    build: .
  redis:
    image: "redis:latest"

然后再创建名称为 docker-compose-dev.yml 的配置文件:

version: '3'
services:
  web:
    ports:
     - "5000:5000"

下面的命令会同时应用这两个配置文件:

$ docker-compose -f docker-compose-base.yml -f docker-compose-dev.yml config

config 命令不会执行真正的操作,而是显示 docker-compose 程序解析到的配置文件内容:

未分类

很显然,我们指定的两个配置文件的内容被合并了。接下来我们再来看看配置文件覆盖的情况。新创建一个名为 docker-compose-prod.yml 的配置文件,编辑其内容如下:

version: '3'
services:
  web:
    ports:
     - "80:5000"
  redis:
    image: "redis:alpine"

然后执行下面的命令:

$ docker-compose -f docker-compose-base.yml -f docker-compose-prod.yml config

未分类

这次 docker-compose-prod.yml 文件中的 image 设置覆盖了 docker-compose-base.yml 文件中的设置,并且映射的端口也改成了 80:5000。
就像 demo 中演示的那样,我们可以通过多次指定 -f 选项的方式配置不同的环境,并且共用一份基础的配置文件。

其实 docker-compse 还默认还支持一种合并、覆盖配置文件的写法,就是使用约定的文件名称 docker-compose.yml 和 docker-compose.override.yml。下面我们把 docker-compose-base.yml 文件改名为 docker-compose.yml,把 docker-compose-prod.yml 文件改名为 docker-compose.override.yml,并直接执行不带 -f 选项的命令:

$ docker-compose config

结果和前面是一样的,docker-compose 自动合并了配置文件 docker-compose.yml 和 docker-compose.override.yml。这种方式虽然省去了指定 -f 选项的麻烦但其缺点也是很明显的,就是无法指定更多不同的应用场景。

使用 network

Docker 提供的 network 功能能够对容器进行网络上的隔离,下面的 demo 中我们创建三个 service 和两个虚拟网络(注意,该 demo 主要是演示 network 的用法,所以笔者并没有配置 proxy service 中的 nginx):

version: '3'
services:
  proxy:
    image: nginx
    ports:
      - "80:80"
    networks:
      - frantnet
  webapp:
    build: .
    networks:
      - frantnet
      - endnet
  redis:
    image: redis
    networks:
      - endnet
networks:
  frantnet:
  endnet:

其中的 proxy 和 webapp 连接到网络 frantnet 上,webapp 和 redis 连接在了 endnet 上(请使用《Docker Compose 简介》一文中介绍的 web 应用和 Dockerfile 来创建 webapp service)。请使用下面的命令来启动应用:

$ docker-compose -p testnet -f docker-compose-net.yml up -d

未分类

从上图我们可以看到该命令一共创建了两个 network 和 三个容器。然后我们检查一下这三个容器的网络连接状态。先从 testnet_webapp_1 中 ping 另外的两个容器:

未分类

因为 webapp 服务同时连接到了 frantnet 和 endnet 两个网络中,所以它可以同时连接这两个网络中的其它容器(proxy 和 redis)。接下来再看看容器 proxy 和 redis 是否可以直接连通,我们从容器 testnet_redis_1 中 ping proxy(注意,执行这个操作前需要在容器 testnet_redis_1 中通过 apt-get update && apt-get install iputils-ping 命令安装 ping 命令):

未分类

无法从容器 testnet_redis_1 中 ping 通 proxy 容器,这也就说明我们通过不同的虚拟网络实现了容器网络之间的隔离,从而在最大程度上去保护后端网络的安全。

按顺序启动容器

默认情况下 compose 启动容器的顺序是不确定的,但是有些场景下我们希望能够控制容器的启动顺序,比如应该让运行数据库的程序先启动。我们可以通过 depends_on 来解决有依赖关系的容器的启动顺序问题,看下面的 demo:

version: '3'
services:
  proxy:
    image: nginx
    ports:
      - "80:80"
    depends_on:
      - webapp
      - redis
  webapp:
    build: .
    depends_on:
      - redis
  redis:
    image: redis

启动应用:

未分类

无论我们执行多少次这样的启动操作,这三个容器的启动顺序都是不变的。如果不应用 depends_on,每次执行 up 命令容器的启动顺序可能都是不一样的。
需要注意的是 depends_on 只是解决了控制容器启动顺序的问题,如果一个容器的启动时间非常长,后面的容器并不会等待它完成启动。如果要解决这类问题(等待容器完成启动并开始提供服务),需要使用 wait-for-it 等工具。

配置数据卷(volume)

数据卷是处理容器中的持久化数据的主要方式,在 compose 中我们可以通过两种方式来指定数据卷:

  • 使用命名的数据卷
  • 直接指定主机上的路径来创建数据卷

下面的 demo 演示了这两种数据卷的配置方式:

version: "3.2"
services:
  web:
    image: nginx:alpine
    volumes:
      - type: volume
        source: mydata
        target: /data
      - type: bind
        source: ./nginx/logs
        target: /var/log/nginx
  jenkins:
    image: jenkins/jenkins:lts
    volumes:
      - jenkins_home:/var/jenkins_home
      - mydata:/data
volumes:
  mydata:
  jenkins_home:

在这个例子中我们一共创建了三个数据卷,分别是两个命名的数据卷 jenkins_home 和 mydata:

未分类

其中的 jenkins_home 数据卷是给 jenkins 保存数据的。如果要在多个容器之间共享数据卷,就必须在顶级的 volumes 节点中定义这个数据卷,比如 mydata 数据卷,它被 web 和 jenkins service 共享了。比如我们在 web service 中的 mydata 数据卷中创建一个名为 hello 的文件,该文件会同时出现在 jenkins service 中:

未分类

我们还创建了一个 bind 类型的 volume 在当前目录下的 nginx/logs 目录下保存 nginx 的日志:

未分类

配置日志驱动

我们还可以通过 logging 节点为 service 指定日志驱动及其相关的选项:

version: '3'
services:
  web:
    build: .
    ports:
     - "5000:5000"
    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"
  redis:
    image: "redis:latest"

上面的代码指定日志驱动为 json-file,存储日志的最大文件 size 为 200k,最多存储 10 这样大的文件。

在 compose file 文件中应用模板

从版本 3.4 开始,可以在 compose file 文件中使用 extension fields,其实我们可以简单的把它理解为可以重用的代码模板。模板的定义必须以 x- 开头,然后以 & 开头的字符串为模板命名,之后就可以以 * 加上模板的名称引用模板:

version: '3.4'
x-logging:
  &default-logging
  driver: json-file
  options:
    max-size: "200k"
    max-file: "10"

services:
  web:
    build: .
    ports:
     - "5000:5000"
    logging: *default-logging
  redis:
    image: "redis:latest"
    logging: *default-logging

运行下面的命令看看模板替换的情况:

$ docker-compose -p template -f docker-compose-template.yml config

未分类

上图显示所有对模板的引用都被替换成了模板的内容。

总结

Docker compose 是一件强有力的效率工具,本文只是介绍了一些常见的用法。如果你还想掌握更多内容,请参考 compose file 的官方文档。

Docker Compose 原理

Docker 的优势非常明显,尤其是对于开发者来说,它提供了一种全新的软件发布机制。也就是说使用 docker 镜像作为软件产品的载体,使用 docker 容器提供独立的软件运行上下文环境,使用 docker hub 等提供镜像的集中管理,这其中最重要的是使用 Dockerfile 定义容器的内部行为和关键属性来支持软件运行。但是实际的生产环境往往需要定义数量庞大的 docker 容器,并且容器之间具有错综复杂的联系。手动的记录和配置这些复杂的容器关系,不仅效率低下而且容易出错。所以,我们迫切需要一种像 Dockerfile 定义 docker 容器一样能够定义容器集群的编排和部署工具。于是,Docker Compose 出现了(其实应该说 Fig 出现了,docker 收购了 Fig 并改名为 compose)。
Dockerfile 重现一个容器,compose 则用来重现容器的集群。
说明:本文的演示环境为 ubuntu 16.04。

编排和部署

编排(orchestration)

编排指根据被部署的对象之间的耦合关系,以及被部署对象对环境的依赖,制定部署流程中各个动作的执行顺序,部署过程所需要的依赖文件和被部署文件的存储位置和获取方式,以及如何验证部署成功。这些信息都会在编排工具中以指定的格式(比如配置文件或特定的代码)来要求运维人员定义并保存起来,从而保证这个流程能够随时在全新的环境中可靠有序地重现出来。

部署(deployment)

部署是指按照编排所指定的内容和流程,在目标机器上执行环境初始化,存放指定的依赖文件,运行指定的部署动作,最终按照编排中的规则来确认部署成功。

所以说,编排是一个指挥家,他的大脑里存储了整个乐曲此起彼伏的演奏流程,对于每一个小节每一段音乐的演奏方式都了然于胸。而部署就是整个乐队,他们严格按照指挥家的意图用乐器来完成乐谱的执行。最终,两者通过协作就能把每一位演奏者独立的演奏通过组合、重叠、衔接来形成高品位的交响乐。这也是 docker compose 要完成的使命。

Compose 原理

笔者在前文《Docker Compose 简介》 (https://www.cnblogs.com/sparkdev/p/9753793.html)中演示了官方的示例,本文不再赘述,接下来我们去探索 compose 的工作原理。先来了解两个 compose 中常常提及的概念:

project

通过 docker compose 管理的一个项目被抽象称为一个 project,它是由一组关联的应用容器组成的一个完整的业务单元。简单点说就是一个 docker-compose.yml 文件定义一个 project。
我们可以在执行 docker-compose 命令时通过 -p 选项指定 project 的名称,如果不指定,则默认是 docker-compose.yml 文件所在的目录名称。

service

运行一个应用的容器,实际上可以是一个或多个运行相同镜像的容器。可以通过 docker-compose up 命令的 –scale 选项指定某个 service 运行的容器个数,比如:

$ docker-compose up -d --scale redis=2

未分类

了解了上面的基本概念之后,让我们一起看看 compose 的一次调用流程:

未分类

右上角的 docker-compose 定义了一组 service 来组成一个 project,通过 docker-compose.yml 中 service 的定义与 container 建立关系(service 与容器的对应关系),最后使用 container 来完成对 docker-py(Python 版的 docker client) 的调用,向 docker daemon 发起 http 请求。注意,这里的 project, service 和 container 对应的都是 docker-compose 实现中的数据结构。下面让我们结合上图来介绍 docker-compose 工作的大致流程。

首先,用户执行的 docker-compose up 命令调用了命令行中的启动方法,功能非常简单。一个 docker-compose.yml 文件定义了一个 project,docker-compose up 提供的命令行参数则作为这个 project 的启动参数交由 project 模块处理。

然后,如果当前宿主机已经存在与该应用对应的容器,docker-compose 则进行行为逻辑判断。如果用户指定可以重新启动已有服务,docker-compose 就会执行 service 模块的容器重启方法,否则就直接启动已有容器。这两种操作的区别在于前者会停止旧的容器,创建并启动新的容器,并把旧容器移除掉。在这个过程中创建容器的各项自定义参数都是从 docker-compose up 命令和 docker-compose.yml 中传入的。

接下来,启动容器的方法也很简洁,这个方法中完成了一个 docker 容器启动所需的主要参数的封装,并在 container 模块执行启动。

最后,contaier 模块会调用 docker-py 客户端来执行向 docker daemon 发起创建容器的 POST 请求。

由此可见 docker-compose 工作的整体流程非常清晰、简洁!

重新启动 services

前面我们提到当前宿主机已经存在与该应用对应的容器,docker-compose 会进行判断并决定是否重新启动已有服务。下面我们就通过 demo 来演示几个常见的场景(我们依然使用前文中提到的官方 demo)。

强制 recreate

Recreate 就是删除现有的容器并且重新创建新的容器,为 docker-compose up 命令指定 –force-recreate 选项可以强制 recreate 容器:

未分类

创建个别容器

如果应用中的个别 service 对应的容器被删除了,docker-compose up 命令会新建相关的容器:

未分类

启动个别容器

与上面类似,如果应用中的个别 service 对应的容器被停止(stop)了,docker-compose up 命令会重新启动相关的容器:

未分类

总结

Docker-compose 总体上给人的感觉是并不复杂。本文只是从概览的角度梳理了一遍 docker-compose 的整体执行流程,主要目的是理解它的工作原理。至于相关的使用技巧等细节,笔者会在接下来的文章中进行介绍。

docker-compose传参问题

一、docker-compose传参问题

1、Dockerfile中CMD和ENTRYPOINT命令详解

CMD 和 ENTRYPOINT 指令都是用来指定容器启动时运行的命令。
单从功能上来看,这两个命令几乎是重复的。单独使用其中的一个就可以实现绝大多数的用例。
但是还是有些许区别,我们来一起看看。

1.1 exec 模式和 shell 模式

CMD 指令

CMD 指令的目的是:为容器提供默认的执行命令。
CMD 指令有三种使用方式,其中的一种是为 ENTRYPOINT 提供默认的参数:

CMD ["param1","param2"]

另外两种使用方式分别是 exec 模式和 shell 模式:

exec 模式

CMD ["executable","param1","param2"] // 这是 exec 模式的写法,注
意需要使用双引号。

exec 模式的特点是不会通过 shell 执行相关的命令,所以像 $HOME 这样的环境变量是取不到的:

FROM ubuntu
CMD [ "echo", "$HOME" ]

把上面的代码保存到 test1 目录的 Dockerfile 中,然后进入 test1 目录构建镜像并启动一个容器:

$ docker build --no-cache -t test1 .
$ docker run --rm test1

未分类

一般的镜像都会提供容器启动时的默认命令,但是有些场景中用户并不想执行默认的命令。用户可以通过命令行参数的方式覆盖 CMD 指令提供的默认命令。比如通过下面命令创建的镜像:

FROM ubuntu
CMD [ "top" ]

把上面的代码保存到 test1 目录的 Dockerfile 中,然后进入 test1 目录构建镜像并启动一个容器:

$ docker build -t test1 .
$ docker run -idt --name testcon test1

然后查看容器中的进程 ID:

$ docker exec testcon ps aux

在启动容器时我们通过命令行指定参数 ps aux 覆盖默认的 top 命令:

未分类

从上图可以看到,命令行上指定的 ps aux 命令覆盖了 Dockerfile 中的 CMD [ “top” ]。实际上,命令行上的命令同样会覆盖 shell 模式的 CMD 指令。

shell 模式

CMD command param1 param2 // 这是 shell 模式的写法。

注意命令行参数可以覆盖 CMD 指令的设置,但是只能是重写,却不能给 CMD 中的命令通过命令行传递参数。

使用 shell 模式时,docker 会以 /bin/sh -c “task command” 的方式执行任务命令。也就是说容器中的 1 号进程不是任务进程而是 bash 进程,看下面的例子:

FROM ubuntu
CMD top

把上面的代码保存到 test2 目录的 Dockerfile 中,然后进入 test2 目录构建镜像并启动一个容器:

$ docker build -t test2 .
$ docker run -itd --name testcon2 test2

然后查看容器中的进程 ID:

$ docker exec testcon2 ps aux

未分类

1.2 ENTRYPOINT 指令

ENTRYPOINT 指令的目的也是为容器指定默认执行的任务。
ENTRYPOINT 指令有两种使用方式,就是我们前面介绍的 exec 模式和 shell 模式:

exec 模式和 shell 模式的基本用法和 CMD 指令是一样的,下面我们介绍一些比较特殊的用法。

exec 模式

ENTRYPOINT ["executable", "param1", "param2"] // 这是 exec 模式的写法,注意需要使用双引号。

指定 ENTRYPOINT 指令为 exec 模式时,命令行上指定的参数会作为参数添加到 ENTRYPOINT 指定命令的参数列表中。用下面的代码构建镜像 test1:

FROM ubuntu
ENTRYPOINT [ "top", "-b" ]

运行下面的命令:

$ docker run --rm test1 -c

未分类

我们在命令行上添加的参数被追加到了 top 命令的参数列表中。
由 CMD 指令指定默认的可选参数:

FROM ubuntu
ENTRYPOINT [ "top", "-b" ]
CMD [ "-c" ]

使用这段代码构建镜像 test2 并不带命令行参数启动容器:

$ docker run --rm test2

这时容器中运行的命令为:top -b -c。

如果我们指定命令行参数:

$ docker run --rm test2 -n 1

-n 1 会覆盖 通过 CMD [ “-c” ] 指定的参数,容器执行的命令为:top -b -n 1

未分类

shell 模式

ENTRYPOINT command param1 param2 // 这是 shell 模式的写法。

2、总结

同时使用 CMD 和 ENTRYPOINT 的情况
对于 CMD 和 ENTRYPOINT 的设计而言,多数情况下它们应该是单独使用的。当然,有一个例外是 CMD 为 ENTRYPOINT 提供默认的可选参数。
我们大概可以总结出下面几条规律:

  • 如果 ENTRYPOINT 使用了 shell 模式,CMD 指令会被忽略。
  • 如果 ENTRYPOINT 使用了 exec 模式,CMD 指定的内容被追加为 ENTRYPOINT 指定命令的参数。
  • 如果 ENTRYPOINT 使用了 exec 模式,CMD 也应该使用 exec 模式。

真实的情况要远比这三条规律复杂,好在 docker 给出了官方的解释,如
下图所示:

未分类

当我们无法理解容器中运行命令的行为时,说不定通过这个表格可以解开疑惑!

对于 Dockerfile 来说,CMD 和 ENTRYPOINT 是非常重要的指令。它们不是在构建镜像的过程中执行,而是在启动容器时执行,所以主要用来指定容器默认执行的命令。但是提供两个功能类似的指令,必然会给用户带来理解上的困惑和使用中的混淆。希望本文能够帮助大家理解二者的区别与联系,并更好的使用二者。