目录
使用nginx+pm2+github-webhook+docker实现项目自动部署
注:docker也能实现pm2的守护进程功能(持续启动项目),所以使用了docker就不需要使用pm2了
但是需要注意的是使用node启动的webhook服务器不能使用docker,因为在webhook内部的sh脚本执行时需要到服务器的前后端项目文件中去执行,如果对webhook使用了docker,那么webhook所在的docker内不会存在前后端项目文件,所以webhook项目只能自己手动去服务器拉取启动,所以webhook项目使用pm2来管理
补充:
github-action+docker实现自动部署和github-webhook+docker实现自动部署原理一样,但是实现步骤有区别:
github-action+docker实现自动部署的详细流程可以看我这篇文章:github-action+docker实现项目自动部署
(1)action是在github中的项目创建workflow工作流(yml脚本文件),yml脚本文件主要执行:
1、对仓库项目进行依赖下载及打包
2、根据项目创建的dockerfile文件生成docker镜像并将镜像push提交到腾讯云镜像仓库中
3、ssh登录腾讯云服务器pull提交的docker镜像并启动
上面的步骤执行完后push提交仓库代码会自动触发yml脚本文件完成自动部署
需要执行的包括创建yml脚本文件、dockerfile文件以及在github配置secret
(2)webhook是在github创建钩子,然后自己配置webhook服务器用于接收提交仓库代码的行为,然后在webhook服务器项目中新建.sh脚本文件用于触发webhook后对应在服务器中执行操作,包括下载项目依赖、打包项目,然后通过项目的dockerfile文件生成docker镜像并启动
webhook执行流程为push仓库代码后触发webhook钩子绑定的webhook服务器地址,执行自己定义的webhook服务器代码,然后根据代码会执行项目对应的sh脚本文件,会在服务器中下载项目依赖、打包项目、根据dockerfile创建docker镜像并启动
总结:action和webhook的原理一样,但是action更简便,不需要自己定义webhook服务器,而且还有一个不同在于action将项目下载依赖、打包项目、生成docker镜像放在了服务器外(github中)执行,而webhook把执行步骤都放在了服务器内执行
一、项目手动部署
在开始介绍自动部署之前,先看一下手动部署怎样实现的,以便于与自动部署进行对比:
1、本地项目上传到github仓库
2、xshell连接服务器,通过yum安装运行项目需要的环境(git、nvm)
# 安装git
yum install git -y
# 安装nvm
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
# 执行nvm命令
source /root/.bashrc
3、生成服务器对于github的ssh公钥并将公钥配置在github中,方便服务器拉取github代码
# 服务器生成ssh
ssh-keygen -t rsa -b 4096 -C "[email protected]"
# 查看生成的ssh公钥
cat /root/.ssh/id_rsa.pub
4、通过nvm安装node(npm),切换npm淘宝镜像源
# nvm安装node的最新稳定版本
nvm install stable
# 切换npm淘宝镜像源,使用cnpm
npm install -g cnpm --registry=https://registry.npm.taobao.org
5、通过yum安装yum-utils等yum工具、切换yum阿里源
# 安装yum工具
yum install -y yum-utils device-mapper-persistent-data lvm2
# 切换yum源
yum-config-manager \
--add-repo \
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
6、服务器新建/usr/projects目录存放项目代码,使用git clone将github的前后端项目代码克隆到该目录
mkdir /usr/projects
# 通过ssh拉取前端项目
git clone [email protected]:zhufengnodejs/vue-front.git
# 通过ssh拉取后端项目
git clone [email protected]:zhufengnodejs/vue-back.git
7、启动项目:切换到后端项目目录->cnpm i 下载依赖->npm run start启动项目(注意服务器开启端口号安全组)、启动前端项目步骤一样
cd /usr/projects/vue-back
# 下载后端项目依赖
cnpm i
# 启动项目
npm run start
# curl命令可以查看url显示的结果
curl http://119.3.102.56:3000/api/users
此时手动部署的项目在服务器断开后就不能访问了,而且本地修改代码后需要手动上传代码然后服务器拉取代码再安装依赖重新启动等,很麻烦
二、项目自动部署
介绍了手动部署项目的方式,再来看看怎样实现自动部署:
1、通过yum安装docker,服务器新建/etc/docker目录修改docker配置文件配置docker阿里云镜像源加速
(docker相当于一个镜像仓库,里面存放着很多镜像比如nginx、centos、包括自己配置的镜像空间存放单独的项目等,配置docker加速可以让docker下载这些镜像的速度加快)
(可以将前端和后端项目分别存放在单独的docker镜像空间中,一个docker镜像空间运行一个项目,下载的项目依赖的版本比如vue、node等互不冲突)
# 安装docker
yum install -y docker-ce docker-ce-cli containerd.io
# 配置docker阿里云镜像源加速
mkdir -p /etc/docker
# 执行命令向文件写入镜像源
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://fwvjnv59.mirror.aliyuncs.com"]
}
EOF
# 重载所有修改过的配置文件
systemctl daemon-reload
systemctl restart docker
2、在github项目的settings中的webhooks配置webhook服务器(前端项目和后端项目都需要配置)
(表示该项目提交到github上就会触发配置的webhook,会发请求到webhook配置的服务器上进行逻辑处理比如执行脚本等)
(注意配置的密码在github发请求时会在请求头以签名的形式传递过来,vue-webhook会验证该签名是否正确)
3、本地新建vue-webhook项目使用node开启该服务器用于接收github发来的请求(webhook.js)、
项目上传到仓库、
服务器clone代码到/usr/projects/vue-webhook、
服务器启动该项目
4、服务器通过npm下载pm2管理vue-webhook项目持续开启
(可以通过pm2 logs查看项目启动日志,通过pm2 flush清空日志)
(使用docker之后就可以不使用pm2管理node项目了,可以通过docker logs -f 容器名查看启动日志,容器启动失败就使用docker logs查看启动日志排错)
cnpm i pm2 -g
配置本地vue-webhook项目package.json文件的启动方式为pm2启动
{
"scripts": {
//pm2启动项目,--watch出现错误自动重启项目
"start": "pm2 start ./webhook.js --watch --name='vue-webhook'",
//停止项目
"stop": "pm2 stop vue-webhook"
},
}
上传更新的vue-webhook代码到仓库
服务器拉取vue-webhook仓库代码
5、配置自动构建后端项目:本地vue-webhook新增后端项目vue-back的自动构建脚本vue-back.sh(名字随便取)
(服务器执行该自动构建脚本可以自动拉取仓库代码并将该项目创建单独的docker空间存放并启动)
# vue-webhook/vue-back.sh
#后端项目自动构建配置
#!/bin/bash
WORK_PATH='/usr/projects/vue-back'
cd $WORK_PATH
# echo命令--语句打印输出在服务器
echo "清理代码"
git reset --hard origin/master
git clean -f
echo "拉取最新代码"
git pull origin master
echo "删除旧容器"
docker stop cha-node-container
docker rm cha-node-container
#删除旧镜像之前必须要先删除依赖镜像的容器
#删除旧镜像是为了旧镜像一直存在占用内存,后续可以思考不删除旧镜像,而是将新镜像修改版本号,旧镜像以老版本号继续存在
echo "删除旧镜像"
docker rmi cha-node:1.0.0
echo "开始构建新镜像"
# 执行到这里会执行后端项目配置的Dockerfile文件中的代码配置创建docker(注意要指定项目的版本号vue-back:1.0.0)
docker build -t vue-back:1.0.0 .
echo "启动新容器"
# 3000:3000代表将服务器主机的3000端口连接到docker内部项目启动的3000端口(访问服务器3000端口就会访问docker内部启动项目的3000端口)
docker container run -p 3000:3000 -d --name vue-back-container vue-back:1.0.0
其中自动创建一个docker镜像存放后端项目需要在后端项目vue-back中新建Dockerfile文件配置该项目的docker镜像空间(包括项目版本、docker内下载项目依赖包、项目启动端口号等)
# vue-back/Dockerfile
#配置后端项目的docker镜像空间(基于node镜像创建docker容器)
FROM node
LABEL name="vue-back"
#目前该项目版本号
LABEL version="1.0.0"
#将服务器下该项目下所有文件拷贝到docker的/app文件夹下
COPY . /app
WORKDIR /app
#docker中配置npm镜像源
RUN npm install -g cnpm --registry=https://registry.npm.taobao.org
#docker中安装项目依赖
RUN cnpm install
#项目端口号(docker对外暴露端口号)
EXPOSE 3000
#在docker中启动项目
CMD npm start
在vue-back后端项目中新建.dockerignore忽略文件,忽略不需要打包到项目的docker镜像空间中的文件
# vue-back/.dockerignore
.git
node_modules
package-lock.json
# 不把docker配置文件打包到docker中,配置文件只在创建docker时有用
Dockerfile
.dockerignore
6、配置自动构建前端项目:和上面构建后端项目步骤一样,但是配置代码不一样在vue-webhook项目新建vue-front.sh脚本,脚本配置不同点在于需要执行npm run build打包项目、
# vue-webhook/vue-front.sh
#前端项目自动构建配置
#!/bin/bash
WORK_PATH='/usr/projects/vue-front'
cd $WORK_PATH
echo "清理代码"
git reset --hard origin/master
git clean -f
echo "拉取最新代码"
git pull origin master
echo "下载项目依赖"
cnpm install
echo "打包最新代码"
npm run build
echo "删除旧容器"
docker stop cha-vue-container
docker rm cha-vue-container
echo "删除旧镜像"
docker rmi cha-vue:1.0.0
echo "开始构建新镜像"
docker build -t vue-front:1.0.0 .
echo "启动新容器"
docker container run -p 80:80 -d --name vue-front-container vue-front:1.0.0
再新建vue-front.conf配置前端项目的nginx(监听端口静态资源请求返回静态资源目录、动态资源请求反向代理到后端)(注意:这里的nginx不是服务器安装的nginx,而是前端项目docker空间中安装的nginx)、
# vue-front/vue-front.conf
# 前端项目的nginx配置
server{
listen 80;
server_name 119.3.102.56;
# 静态资源请求
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# 动态资源请求使用反向代理
location /api {
proxy_pass http://119.3.102.56:3000;
}
}
在vue-front项目新建Dockerfile文件配置前端项目的docker镜像(配置自动将打包后的前端项目dist目录放在nginx静态资源目录下,将前端vue-front.conf配置文件放在nginx配置目录下)、
# vue-front/Dockerfile
# nginx服务器
FROM nginx
LABEL name="vue-front"
LABEL version="1.0.0"
# 将打包后的dist文件夹下的文件拷贝到docker中的nginx静态资源文件夹目录html下
COPY ./dist/ /usr/share/nginx/html/
# 将前端nginx配置文件拷贝到docker中的nginx子配置文件夹目录下
COPY ./vue-front.conf /etc/nginx/conf.d/
# 前端项目docker对外暴露端口号设置为80
EXPOSE 80
再新建.dockerignore配置忽略文件
# vue-front/.dockerignore
.git
node_modules
package-lock.json
Dockerfile
.dockerignore
7、在本地vue-webhook项目中的webhook.js文件中处理接收到github传来的触发仓库项目上传操作的请求(即配置了webhook的项目有上传操作)
(处理请求包括验证github请求的请求头中的签名是否正确、验证是否为push操作、如果满足条件开启进程执行sh命令,即sh执行对应有上传仓库操作的项目的脚本(.sh),执行该脚本后会将该项目自动拉取仓库最新代码并刷新该项目的docker并下载依赖后启动该项目)
// vue-webhook/webhook.js
const http = require('http')
let crypto = require('crypto');
//开启进程
var spawn = require('child_process').spawn;
let sendMail = require('./sendMail');
//在github上配置webhooks时输入的secret
const SECRET = '123456';
//配置数据加密方法
function sign(data) {
return 'sha1=' + crypto.createHmac('sha1', SECRET).update(data).digest('hex')
}
const server = http.createServer((req, res) => {
console.log('------------分割线开始-------------');
console.log('方法:', req.method, '路径:', req.url);
if (req.method === 'POST' && req.url == '/webhook') {
console.log('请求成功');
let buffers = [];
//接收github传递的数据(请求体)
req.on('data', function (data) {
console.log('接收github请求体数据');
buffers.push(data);
});
req.on('end', function () {
console.log('接收github请求体数据完成');
let body = Buffer.concat(buffers);
//获取github传递过来的请求头信息
let sig = req.headers['x-hub-signature'];
let event = req.headers['x-github-event'];
let id = req.headers['x-github-delivery'];
//判断github传递的签名是否符合要求
if (sig !== sign(body)) {
console.log('签名验证失败-签名不正确');
console.log('github签名:', sig, '自己的签名:', sign(body))
return res.end('Not Allowed');
}
console.log('签名验证成功-签名正确');
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ "ok": true }));
//判断是否是push项目到仓库
if (event === 'push') {
console.log('push代码到仓库操作执行');
let payload = JSON.parse(body);
//开启进程处理执行对应项目的脚本(第一个参数是命令,第二个参数是文件名)
console.log(`开始执行脚本文件${payload.repository.name}.sh`);
let child = spawn('sh', [`./${payload.repository.name}.sh`]);
let buffers = [];
//获取执行脚本的进程传递回来的信息(项目构建部署信息)
child.stdout.on('data', function (buffer) { buffers.push(buffer) });
child.stdout.on('end', function () {
let logs = Buffer.concat(buffers).toString();
console.log('获取到执行脚本传递回来的信息');
//将部署信息整合发邮件通知
sendMail(`
<h1>部署日期: ${new Date()}</h1>
<h2>部署人: ${payload.pusher.name}</h2>
<h2>部署邮箱: ${payload.pusher.email}</h2>
<h2>提交信息: ${payload.head_commit && payload.head_commit['message']}</h2>
<h2>布署日志: ${logs.replace("\r\n", '<br/>')}</h2>
`);
});
}
})
} else {
res.end('Not Found!');
}
})
server.listen(4000, () => {
console.log('webhook on 4000....');
})
vue-webhook项目中新建sendEmail.js文件配置发邮件的代码,然后配置webhook.js代码在自动化构建成功后调用sendEmail.js的函数发邮件告知部署信息
// vue-webhook/sendEmail.js
const nodemailer = require('nodemailer');
let transporter = nodemailer.createTransport({
//host: 'smtp.ethereal.email',
service: 'qq', // 使用了内置传输发送邮件 查看支持列表:https://nodemailer.com/smtp/well-known/
port: 465, // SMTP 端口
secureConnection: true, // 使用了 SSL
auth: {
user: '[email protected]',
// 这里密码不是qq密码,是你设置的smtp授权码
pass: 'jjthfwzqjsimejfj',
}
});
function sendMail(message) {
console.log('开始发邮件');
let mailOptions = {
from: '"1776875119" <[email protected]>', // 发送地址
to: '[email protected]', // 接收者
subject: '部署通知', // 主题
html: message // 内容主体
};
// send mail with defined transport object
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
return console.log(error);
}
console.log('Message sent: %s', info.messageId);
console.log('------------分割线结束-------------');
});
}
module.exports = sendMail;
上传vue-webhook项目到仓库、
服务器拉取vue-webhook项目并启动该项目(即启动该项目接收github请求并执行对应项目的脚本实现对应项目的自动构建及部署)
cd /usr/projects/vue-webhook
git pull origin master
cnpm i
# 启动项目(pm2)
npm start
# 查看pm2启动该项目打印的日志
pm2 logs
自动构建部署流程
以前端代码举例,本地修改了前端代码后将代码上传到github仓库,由于该前端项目的仓库配置了webhooks,github监听到上传操作就会发请求到配置webhooks时填写的url(即vue-webhook项目启动的node服务),服务器的vue-webhook接收到github请求就会验证github请求及签名等(验证安全),验证通过就会执行对应前端项目的脚本文件(在服务器执行sh 前端脚本.sh命令),脚本文件会下将仓库代码pull下来,在脚本文件执行到创建docker时就会执行前端项目的Dockerfile文件将项目添加到docker并下载所需环境及依赖后启动前端项目,项目部署成功后会将部署信息以邮件的形式通知我
上面的流程执行完后的效果就是本地修改代码并上传到仓库后,收到邮件通知则代表项目自动构建及部署完成,可以直接访问url查看上传到仓库代码的最新效果
docker概念补充
1、服务器安装docker后,可以创建多个docker容器,每个docker容器相当于一个小的操作系统,每个docker容器中可以安装不同的镜像(nginx、node等),通过这些镜像组成一个项目运行环境,每个docker容器内部自成一个独立的环境,互不影响,比如可以通过nginx镜像为前端项目创建一个docker容器,通过node镜像为后端创建一个docker容器,然后分别在各自的docker容器中启动项目,将启动的docker的端口号对应到服务器主机的端口号,这样外界就可以正常访问了
2、docker这样启动多个容器,不同容器内部有不同的环境,可以解决不同环境(比如node版本不同等)的项目运行在单一环境的服务器出现错误的情况,可以将不同的项目分别创建对应的docker容器,在容器中配置对应的环境,然后在服务器中启动
3、在仓库拉下项目代码后通过Dockerfile文件构建项目的docker容器(在docker内部配置该项目所需环境并启动),就可以不用担心项目运行的环境和本地/服务器的环境不同而报错的问题
4、docker可以通过docker ps
查看启动的容器,然后可以通过docker exec -it 容器名 /bin/bash
进入容器内部,通过exit
退出进入的容器,进入到容器内部会发现容器内部相当于一个小的linux系统,该有的文件夹都有(/etc、/usr等),nginx一样的是下载到etc/nginx里面,nginx静态资源目录也是在/usr/share/nginx/html里面,就是说每一个docker容器内部都是一个完整的linux系统,可以像在linux系统下载或操作文件一样在容器内部进行操作