docker单宿主机多容器实现自动化编译部署

一、应用环境

操作系统:
CentOS 7.4
应用软件:
Docker 19.03.4、 Certbot 0.40.1、nginx 1.17.5、Jenkins 2.202、Mysql Server 8.0.18、Redis server 5.0.6
api服务器:
ThinkJS 3.0 (基于 Koa 2.x)
前端应用:
Ant Design Pro V1
域名解析:(指向当前主机)
jenkins.***.domain、antd.***.domain、api.***.domain

二、自动化编译部署的需求分析:

前后端开发上传代码至git服务器,通过设置的webhooks调起jenkins中配置的编译部署流程完成自动化编译部署

三、容器部署的整体思路与架构

各容器组织架构.png
  1. 访问jenkins.***.domain域名,通过nginx容器代理,指向jenkins容器
  2. 通过jenkins构建“服务端镜像”,实现“服务端容器”的自动化编译部署
  3. 通过jenkins构建“前端镜像”,实现“前端容器”的自动化编译打包并共享数据卷给nginx
  4. 访问antd.***.domain域名,通过nginx容器代理,指向antd容器共享的前端静态文件
  5. 访问antd.***.domain/api路由,通过nginx容器代理,指向api服务器容器
  6. 实现api服务器容器与Mysql、Redis容器间的数据访问(处于安全考虑,数据容器不对外开放端口,无法通过域名直接访问)
  7. Certbot生成泛域名证书支持https访问

四、容器部署需解决的问题

1、容器间通信:

1)容器每次重启分配给容器的内部ip都会改变,所以无法通过访问容器ip的形式进行容器间通信;
2)一个容器如何与不同网络间的容器通信

解决方案:
4.1.1 创建两个桥接网络net0 - 网络名称natnet、net1 - 网络名称intranet
4.1.2 natnet网络用于nginx容器与jenkins、api服务器容器通信(jenkins、api服务器容器需设置该网络下的网络别名)
4.1.3 intranet网络用于api服务器容器与Mysql、Redis容器通信(Mysql、Redis容器需设置该网络下的网络别名)

2、数据(文件)共享:

容器间是相互独立的,前端容器打包生成的文件如何共享给nginx容器使用

解决方案:
4.2.1 通过挂载数据卷的形式,将宿主机下的数据共享目录分别挂载到多个容器下用于共享数据

3、自定义镜像中依赖库的重复安装:

镜像的创建基于一个已有的基础镜像,每次重新构建镜像时都必须重新下载依赖,如何减少依赖的重复下载

解决方案:
4.3.1 判断目标镜像是否构建,未构建则基于基础镜像构建新的镜像,已构建则基于已构建的镜像更新镜像

五、具体实现步骤

ps:docker安装,镜像获取及使用参考底部链接此处不再赘述

1、网络设置
#1、创建转发网络,供nginx代理转发
docker network create natnet
#2、创建内部网络,供服务访问数据库
docker network create intranet
2、容器设置
#创建nginx容器,加入natnet网络,映射主机80、433端口,
#挂载nginx配置文件路径,日志路径,网站路径、证书路径,并在后台运行
docker run --name nginx \
  --network natnet \
  -p 80:80 -p 443:443 \
  -v /var/nginx/conf.d:/etc/nginx/conf.d \
  -v /var/nginx/logs:/var/log/nginx \
  -v /var/website:/var/website \
  -v /etc/letsencrypt:/etc/letsencrypt \
  -d nginx

#创建mysql容器,加入intranet网络并设置别名,挂载文件路径,并在后台运行
docker run --name mysql \
  --network intranet --network-alias mysql \
  -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=MyPassW0rd.. \
  -d mysql

#创建redis容器,加入intranet网络并设置别名,挂载文件路径,并在后台运行
docker run --name redis \
  --network intranet --network-alias redis \
  -v redis-data:/data \
  -d redis

#创建jenkins容器,加入natnet网络并设置别名,挂载文件路径,并在后台运行
docker run --name jenkins \
  -u root \
  --network natnet --network-alias jenkins \
  -v jenkins-data:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v $(which docker):/usr/bin/docker \
  -v "$HOME":/home \
  -d jenkins/jenkins
3、Nginx解析

创建nginx容器时已将配置目录挂载至宿主机/var/nginx/conf.d目录下(在该目录下添加如下配置文件)

forbidden.conf(显示的定义一个 default server 禁止ip以及未绑定域名的访问)

# 显示的定义一个 default server 禁止ip以及未绑定域名的访问
server {
  listen 80 default_server;
  server_name _;
  return 403; # 403 forbidden
}
server {
  listen 443 default_server;
  server_name _;
  return 403; # 403 forbidden
}

ssl_certificate.conf(泛域名证书路径)证书的申请请自行百度

# 证书路径
ssl_certificate /etc/letsencrypt/live/***.domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/***.domain/privkey.pem;

jenkins.conf(jenkins服务配置)

server {
  listen 80;
  listen [::]:80;
  server_name jenkins.***.domain;

  location / {
    # 重定向到https
    rewrite ^/(.*)$ https://${server_name}$1 permanent;
  }
}

server {
  listen 443 ssl http2;
  server_name jenkins.***.domain;

  # 证书的公私钥
  include conf.d/ssl_certificate.conf;

  location / {
    proxy_pass http://jenkins:8080; #此处的jenkins为运行jenkins容器时配置的网络别名
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   Host              $host;
    proxy_set_header   X-Real-IP         $remote_addr;
  }
}

ps:至此,重载nginx配置即可访问jenkins服务

proj_name.conf(项目服务配置)

server {
  listen 80;
  listen [::]:80;
  server_name ***.domain www.***.domain proj_name.***.domain;

  location / {
    # 重定向到https
    rewrite ^/(.*)$ https://${server_name}$1 permanent;
  }
}

server {
  listen 443 ssl http2;
  server_name ***.domain www.***.domain proj_name.***.domain;
  # gzip config
  gzip on;
  gzip_min_length 1k;
  gzip_comp_level 9;
  gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
  gzip_vary on;
  gzip_disable "MSIE [1-6]\.";

  root /var/website/proj_name; #此路径为前端容器共享数据卷目录

  # 证书的公私钥
  include conf.d/ssl_certificate.conf;

  location / {
    # 用于配合 browserHistory使用
    try_files $uri $uri/ /index.html;
  }
  location /api {
    proxy_pass http://proj_name.api:8360/api; #此处为api容器网络别名,端口及模块路由
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   Host              $http_host;
    proxy_set_header   X-Real-IP         $remote_addr;
  }
}
4、自动化编译部署

4.1 构建proj_name/api镜像并运行

在api工程根目录创建Dockerfile并根据自身项目修改具体配置
示例内容如下(工程基于ThinkJS 3.0)

ARG  BASE_IMAGE=node
FROM ${BASE_IMAGE}

WORKDIR /proj/server
COPY package.json ./package.json
RUN npm i --production --registry=https://registry.npm.taobao.org

COPY src ./src
COPY view ./view
#COPY www ./www
COPY production.js ./production.js

ENV DOCKER=true
EXPOSE 8360
CMD [ "node", "./production.js" ]

将如下内容复制到jenkins相应项目配置 -> 构建中,(或在api工程根目录创建build.sh并复制如下内容,而后在jenkins相应项目配置 -> 构建中运行该脚本)
示例内容如下(工程基于ThinkJS 3.0)
ps:注意修改镜像名称、容器名称,及运行容器时的配置

#!/bin/bash

#构建的镜像名称
IMAGE='proj_name/api'
#运行的容器名称
CONTAINER='proj_name.api'

#构建镜像并启动容器
function build_run {
  #使用根目录下的Dockerfile构建镜像,默认使用node作为基镜像
  docker build -t $IMAGE \
    --build-arg BASE_IMAGE=${1:-"node"} .

  #停止并移除旧容器
  remove_container

  #创建容器,加入指定网络,并在后台运行
  docker run --name $CONTAINER \
    --network intranet \
    -d $IMAGE
  #连接其他网络并设置别名
  docker network connect --alias $CONTAINER natnet $CONTAINER
}

#移除旧容器
function remove_container {
  #判断容器是否已存在
  cID=`docker ps -aqf 'name='$CONTAINER`
  if [ -z "$cID" ]; then
    #容器不存在
    echo '未找到该容器,将创建新的容器并启动'
    return 1
  fi

  #判断容器是否运行
  cID=`docker ps -qf 'name='$CONTAINER`
  if [ -n "$cID" ]; then
    #停止容器
    echo '该容器已运行,将关闭该容器'
    docker stop $CONTAINER
  fi
  #移除容器
  echo '该容器已停止运行,将移除该容器'
  docker rm $CONTAINER
}

#判断镜像是否已存在
imgID=`docker images -q $IMAGE`
if [ -z "$imgID" ]; then
  #镜像不存在,构建镜像并运行容器
  echo '未找到该镜像,开始构建新的镜像。。。。'
  build_run
else
  #镜像已存在,更新镜像并运行容器
  echo '该镜像已存在,开始更新镜像。。。。'
  build_run $IMAGE
fi

4.2 构建proj_name.web镜像并运行

在web工程根目录创建Dockerfile并根据自身项目修改具体配置
示例内容如下(工程基于Ant Design Pro)

ARG  BASE_IMAGE=node
FROM ${BASE_IMAGE}

WORKDIR /usr/src/app/
COPY package.json ./
RUN npm install --registry=https://registry.npm.taobao.org

COPY ./ ./

CMD ["npm", "run", "build"]

将如下内容复制到jenkins相应项目配置 -> 构建中,(或在api工程根目录创建build.sh并复制如下内容,而后在jenkins相应项目配置 -> 构建中运行该脚本)
示例内容如下(工程基于Ant Design Pro)
ps:注意修改镜像名称、容器名称,及容器共享数据卷的挂载目录(供nginx容器读取)

#!/bin/bash

#构建的镜像名称
IMAGE='proj_name/web'
#运行的容器名称
CONTAINER='proj_name.web'

#构建镜像并启动容器
function build_run {
  #使用根目录下的Dockerfile构建镜像,默认使用node作为基镜像
  docker build -t $IMAGE \
    --build-arg BASE_IMAGE=${1:-"node"} .

  #停止并移除旧容器
  remove_container

  #创建容器,挂载编译后的文件路径,并在后台运行
  docker run --name $CONTAINER \
    -v /var/website/proj_name:/usr/src/app/dist \
    -d $IMAGE
}

#移除旧容器
function remove_container {
  #判断容器是否已存在
  cID=`docker ps -aqf 'name='$CONTAINER`
  if [ -z "$cID" ]; then
    #容器不存在
    echo '未找到该容器,将创建新的容器并启动'
    return 1
  fi

  #判断容器是否运行
  cID=`docker ps -qf 'name='$CONTAINER`
  if [ -n "$cID" ]; then
    #停止容器
    echo '该容器已运行,将关闭该容器'
    docker stop $CONTAINER
  fi
  #移除容器
  echo '该容器已停止运行,将移除该容器'
  docker rm $CONTAINER
}

#判断镜像是否已存在
imgID=`docker images -q $IMAGE`
if [ -z "$imgID" ]; then
  #镜像不存在,构建镜像并运行容器
  echo '未找到该镜像,开始构建新的镜像。。。。'
  build_run
else
  #镜像已存在,更新镜像并运行容器
  echo '该镜像已存在,开始更新镜像。。。。'
  build_run $IMAGE
fi
5、配置jenkins与git服务端的Webhooks

请自行百度,不再赘述!

六、结束:

ps:最后可将上诉步骤自行整合成 docker-compose.yml

参考:
Docker 软件安装