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 软件安装

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343