一、应用环境
操作系统:
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中配置的编译部署流程完成自动化编译部署
三、容器部署的整体思路与架构
- 访问jenkins.***.domain域名,通过nginx容器代理,指向jenkins容器
- 通过jenkins构建“服务端镜像”,实现“服务端容器”的自动化编译部署
- 通过jenkins构建“前端镜像”,实现“前端容器”的自动化编译打包并共享数据卷给nginx
- 访问antd.***.domain域名,通过nginx容器代理,指向antd容器共享的前端静态文件
- 访问antd.***.domain/api路由,通过nginx容器代理,指向api服务器容器
- 实现api服务器容器与Mysql、Redis容器间的数据访问(处于安全考虑,数据容器不对外开放端口,无法通过域名直接访问)
- 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 软件安装