跳到文章

Speed Up Your Deployment Using Docker Compose

Docker 的出现,优秀的隔离性让我们可以将任何应用部署到任何服务器上面,不再需要关注服务器的环境配置,简化了单个应用的部署流程。但在更多的情况下,应用往往会依赖于其他服务,比如前端网页依赖于后端服务,后端服务依赖于数据库。我们可以依次部署每个服务,但每次重复的手工操作让人厌烦。我们需要一个工具,只要描述好整个应用之间的服务依赖关系,将这个描述文件交给这个工具,一条命令便能启动整个系统。这种类型的工具有很多,这篇博客里聊的是Docker Compose

本文假设你有基本的 Docker 使用基础,你将会学习到以下几点:

  • Docker Compose 是什么?它适用于哪些场景?
  • 安装和使用 Docker Compose
  • 注入环境变量来配置你的程序
  • 挂载磁盘来持久化你的容器数据

Overview

Docker Compose 用于定义和运行多容器应用,它使用 YAML 文件来描述服务之间的依赖关系,一条简单的docker compose up命令便能启动你的整个应用。 Compose 多用于单主机部署,比如在本地电脑上搭建你的开发环境,CI/CD 服务器上搭建集成测试环境等简单场景。当然 Docker 官方说 Compose 也可以用于生产环境,只要你用 Swarm 就好了。但目前的情况是,Kubernetes 大行其道,Swarm 日渐式微,就连 Docker 官方也都迫不得已拥抱了 Kubernetes。现在生产环境多用 Kubernetes 来部署管理多容器应用,如果你对 Kubernetes 感兴趣,可以参考我之前的入门文章Kubernetes101

我个人不是任何技术的信徒,每种技术都有它存在的道理。Docker Compose 简单方便,不需要花费精力部署调试 Kubernetes,适用于开发、测试以及个人的小应用场景;Kubernetes 成熟、自由、社区资源丰富,适用生产环境。建议大家可以亲自动手实践一下这两项技术,说实话,也花不了一晚上的时间。亲身体验之后,才能给技术恰如其分地归类,知道它们适用的场景,需要的时候顺手就用好了。

Setup

Windows 和 Mac 下的 Docker 客户端自带 Docker Compose, Ubuntu 下直接apt install docker-compose即可

详情参考Install Docker Compose

Compose Up

以一个简单的 express app 为例

## docker-compose.yaml
version: "2"
services:
  mysql:
    image: "mysql"
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: "123"
  app:
    image: "ccccly/express-app:docker-compose"
    ports:
      - "3000:3000"
    environment:
      MYSQL_HOST: "mysql"
      MYSQL_USER: "root"
      MYSQL_PASSWORD: "123"

可以看出,app 使用的 Docker 镜像是 ccccly/express-app:docker-compose,在 3000 端口上对外提供服务,app 依赖于一个 mysql 服务,mysql 的用户名是 root,密码是 123。被依赖的 mysql 服务使用的镜像是 mysql,在 3306 端口上对外提供服务。

这时候,使用

## 默认使用当前目录下面的 docker-compose.yaml 文件,如果你的 compose 文件不是这样子命名的,你需要额外加上 compose 的文件名
$ docker-compose up

就能启动这个由 express-app 和 mysql 组成的应用了。

app 的代码是这样子的:

app.get('/', (req, res) => {
  pool.getConnection((err, connection) => {
    if (err) return;
    connection.query('SELECT 1 + 1 AS solution', (err, results, fields) => {
      connection.release();
      if (err) {
        console.log(err);
      }
      const solution = results[0].solution;
      res.json({ solution });
    });
  });
});

app.listen(3000);

有 ‘/’ 的请求来时,执行 ‘SELECT 1 + 1 AS solution’ 的 sql 语句,返回给客户端

$ curl localhost:3000
{"solution": 2}

Environment Variable Injection

在配置 mysql 的连接时,由于本机开发环境和线上环境是不一样的,如果 mysql 连接直接写死在代码中,那么每次部署,我们都需要修改代码来连接相应的数据库,这是很不干净的做法。为了确保本机和线上的代码是一致的,我们需要使用环境变量(Environment Variable)将配置 mysql 连接信息。

环境变量其实就是程序运行的环境中一些参数的值,比如NUMBER_OF_PROCESSORS=8这个环境变量就告诉操作系统运行中的所有软件,“这台电脑的处理器有 8 个,请你们按需使用”;**PATH=C:\Users\GeekC\go\bin;**告诉操作系统说这个文件夹下面有一些可执行文件,其它地方找不到的话,你可以来这边找找(go)。很多小伙伴喜欢使用 nvm 来管理自己的 node 版本,其实就是通过修改环境变量或者软链接的方式来更换可执行文件(node)的寻找地址。

回到正题,关于在代码中通过环境变量注入 mysql 的连接信息。我们首先在代码中这样配置 mysql 连接:

const pool = mysql.createPool({
  host: process.env.MYSQL_HOST || 'localhost',
  user: process.env.MYSQL_USER || 'root',
  password: process.env.MYSQL_PASSWORD || '123'
});

意思就是,如果环境变量 MYSQL_HOST 有值,那么就 host 就使用这个值,否则使用 localhost。MYSQL_USER 和 MYSQL_PASSWORD 同理。

在本机开发的时候,由于本机没有配置环境变量,则默认使用 localhost。

在线上部署的时候,通过在 Compose 文件定义环境变量:

environment:
  MYSQL_HOST: "mysql"
  MYSQL_USER: "root"
  MYSQL_PASSWORD: "123"

这时候,host 就变成了 “mysql”,它是一个服务名,对应 Compose 中的 mysql 服务。在 Compose 文件中,服务与服务之间的连接使用服务名来寻址的方式。docker-compose 在启动的时候,会自动把服务名和服务对应的 IP 地址写入到每个容器里的 HOST(负责 DNS 解析) 文件中。

通过注入环境变量,确保了代码和环境配置的松藕合,更利于自动化部署。

Volume Mount

一旦容器被删除,容器里数据也就丢失了。显然,我们不希望看到 mysql 中存储的数据就这么随容器的消亡而消失,我们需要持久化 mysql 中存储的数据。在 Docker 中,通过使用挂载磁盘的方式,将容器中需要存储的数据保存在挂载的磁盘中。

只需要在 mysql 中加一条配置,就可以将 mysql 中的数据存储到主机的 ./data 目录中

services:
  mysql:
    volumes:
      - ./data:/var/lib/mysql

另一方面,容器也可以通过挂载磁盘来使用外部的配置文件,比如 nginx

services:
  nginx:
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

最后面的 :ro 的意思是 read only,因为配置文件不允许在程序运行的时候被改动

Conclusion

在单主机上部署多容器应用时,Docker Compose 是个不错的选择。它胜在简单方便,在没有也不需要 Kubernetes 的时候,可以尝试一下 Docker Compose,可以显著提升我们产品的交付部署效率。

最后,提供一份前两天写的一个 Compose 文件,供大家参考。

version: '2'                                    
services:                                     
  rabbitmq: // mqtt broker                          
    image: "cyrilix/rabbitmq-mqtt" // Docker 镜像     
    container_name: "rabbitmq" // 启动后的容器的名字
    restart: "always"  // 失败后自动重启                   
    hostname: "rabbitmq-hostname" // rabbitmq 需要知道主机的名字                        
    ports:                                      
      - "1883:1883" // 声明开放的端口                             
    environment:                                
      RABBITMQ_DEFAULT_USER: "rabbitmq-user" // 用户名         
      RABBITMQ_DEFAULT_PASS: "rabbitmq-password" // 密码   
  influxdb:                                     
    image: "influxdb" // 时序数据库                       
    container_name: "influxdb"                  
    restart: "always"                           
    ports:                                      
      - "8086:8086"                             
    volumes:                                    
      - ~/docker-data/influxdb:/var/lib/influxdb // 时序数据库的数据持久化在主机的 ~/docker-data/influxdb 目录
    environment:                                
      INFLUXDB_DB: "influxdb-database"                   
      INFLUXDB_HTTP_AUTH_ENABLED: "true"        
      INFLUXDB_ADMIN_USER: "influx-admin-user"            
      INFLUXDB_ADMIN_PASSWORD: "influxdb-admin-password"   
      INFLUXDB_USER: "influxdb-user" // 数据库访问的用户名
      INFLUXDB_USER_PASSWORD: "influxdb-user-password" // 数据库访问的密码     
  forwarder:                                    
    image: "mqtt-influxdb-forward" // 自己写的消息转发服务,将 mqtt 消息经过处理之后,转发到 influxdb
    container_name: "mqtt-influxdb-forward"     
    restart: "always"                           
    depends_on: // 声明该服务依赖  rabbitmq 和 influxdb 服务                                 
      - rabbitmq                                
      - influxdb                                
    environment:                                
      MQTT_ENDPOINT: "mqtt://rabbitmq"          
      MQTT_USERNAME: "rabbitmq-user"                 
      MQTT_PASSWORD: "rabbitmq-password"              
      MQTT_TOPIC: "test/topic"                    
      INFLUX_HOST: "influxdb"                   
      INFLUX_DATABASE: "influxdb-database"               
      INFLUX_USERNAME: "influxdb-user"               
      INFLUX_PASSWORD: "influxdb-user-password"            
      INFLUX_WRITE_THRESHOLD: 10                
  grafana: // 数据可视化框架,从 influxdb 取数据,使用图表的方式展示出来
    image: "grafana/grafana"                    
    container_name: "grafana"                   
    restart: "always"                           
    ports:                                      
      - "3000:3000"                             
    environment:                                
      GF_SECURITY_ADMIN_USER: "grafana-user"        
      GF_SECURITY_ADMIN_PASSWORD: "grafana-passowrd"