docker + github webhook 实现自动化部署前端项目

2,083 阅读4分钟

我又来了,之前写过一篇# 利用gitHub的webhook实现前端项目自动化部署---个人摸索的一次学习总结,是利用服务器监听webhook的push触发本地打包,然后部署到nginx静态目录,至于nginx配置需要预先配置好。 不过,最近接触了docker,是真香!于是有了这一期docker版本。

Docker

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器或Windows 机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。----百度百科。

个人理解其实docker就是更轻量,更便捷,更易于移植的虚拟机,我们的应用服务运行在他集成好一套虚拟环境,主要分为两个概念:

镜像(image):类似操作系统镜像,docker创建虚拟环境需要基于一个docker镜像,镜像定义了系统需要什么初始的内容。

容器(container):就是虚拟环境,基于image创建, 像一个虚拟机,实际上是一个进程,所以更轻量。

更多docker知识不再赘述,可自行了解。

准备

一个github前端项目,并在云服务器上拉取代码,一台云服务器-----服务器安装好git、node、docker,因为是使用github的wehooks, 而且我使用node运行服务监听wehooks,git、node必不可少, git、node安装过程不是本文重点,仅列出云服务器 docker 安装方法:

# 1: 安装必要的一些系统工具

sudo yum install -y yum-utils

# 2: 添加软件源信息,使用阿里云镜像

sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

# 3: 安装 docker-ce

sudo yum install docker-ce docker-ce-cli containerd.io

# 4: 开启 docker服务

sudo systemctl start docker

为前端项目添加docker配置

在项目根目录添加Dockerfile文件,定义要生成的镜像:

# build stage 第一个阶段
# 基于node:12-alpine(这个node镜像较小,足够用) 命名build-stage 
FROM node:12-alpine as build-stage 
# 工作目录 app
WORKDIR /app
# 复制package声明文件到工作目录,安装依赖
COPY package*.json ./
RUN npm install
# 复制其他文件到工作目录
COPY . .
# 项目打包
RUN npm run build

# deploy stage 第二个阶段
# 基于nginx:stable-alpine(你也可以指定nginx版本,不过生成的镜像可能大一些)
FROM nginx:stable-alpine 
# 从build-stage阶段 复制 /app/build/ 文件夹的内容到nignx静态目录 /usr/share/nginx/html
COPY --from=build-stage /app/build/ /usr/share/nginx/html
# docker内环境暴露出80端口
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

添加 .dockerignore 文件:

node_modules

我的是前端单页项目,还需要接口转发,所以在项目里加了nginx.conf文件,利用deploy.sh引用,看个人需要。

配置 webhook

为git项目配置webhook, 项目变动会带参数请求配置的接口: image.png Secret用来校验请求合法性

在服务器上创建服务监听webhook

创建一个node服务, index.js

const Koa = require('koa');
const Router = require('koa-router');
const body = require('koa-body'); // 用来解析post参数
const crypto = require('crypto')
const secret = '你在webhook中配置的secret';

const app = new Koa();
const router = new Router();

function run_cmd(cmd, args, callback) {
  var spawn = require('child_process').spawn;
  var child = spawn(cmd, args);
  var resp = '';
  var endTip = `${args[1]}部署完成, runing on port ${args[2]}`;
  child.stdout.on('data', function(buffer) { resp += buffer.toString(); });
  child.stdout.on('end', function() { callback (resp + ' ' + endTip) });
}

function sign (data) {
  return `sha1=${crypto.createHmac('sha1', secret).update(data).digest('hex')}`
}

app.use(body());

// 一个捕捉错误的中间件
app.use(async (ctx, next) => {
  try{
    await next();
  }catch(err){
    console.log(err);
    ctx.throw(500)
  }
})
// post
router.post('/dockerDeploy', async ctx => {
  const port = ctx.query.port;
  const header = ctx.request.header;
  // 参数就是payload
  const body = ctx.request.body;
  // 事件
  const event = header['x-github-event'];
  // 签名
  const sig = header['x-hub-signature'];
  // 本地生成签名
  const localsig =  sign(JSON.stringify(body));
  // 分支
  const ref = body.ref;
  // 项目名称
  const name = body.repository.name;
  
  console.log(ref, event, port)
  // 验证签名存在
  if(!sig) {
    ctx.body = 'no signature!'
    return;
  }
  // 验证签名合法
  if(sig !== localsig) {
    ctx.body = 'signature is wrong!'
    return;
  }
  ctx.body = 'ok';
  // 仅在 push 主分支时处理
  if (ref === 'refs/heads/main' && event === 'push') {
  // 运行
    run_cmd('sh', ['./deploy.sh', name, port], function(text){ console.log(text) });
  }
})


app.use(router.routes()).use(router.allowedMethods());

app.listen(7776, () => { 
  console.log('listening on 7776 success!') 
})

deploy.sh

#!/bin/bash

# 项目名称
WEB_NAME="$1"
# 端口号
PORT="$2"
# 服务器上的项目代码库路径
PROJECT_PATH="/root/project/${WEB_NAME}/"


echo '部署开始.....'

cd $PROJECT_PATH

git pull
echo '更新代码'

# 创建映射挂载的目录
mkdir -p /usr/dokcer_nginx_data/$WEB_NAME/{conf.d,log}

# 删除原配置文件
rm -rf /usr/docker_nginx_data/$WEB_NAME/conf.d/*

# 移动nginx.conf并重命名
cp nginx.conf /usr/docker_nginx_data/$WEB_NAME/conf.d/default.conf



# 创建镜像
docker build . -t ${WEB_NAME}-image:latest
echo '创建镜像'

# 我们每次生成镜像是都未指定标签,从而重名导致有空悬镜像,删除一下
docker rmi $(docker images -f "dangling=true" -q)
echo '删除空悬镜像dangling image'

# 查找docker容器,停止并销毁他
docker ps -a -f "name=^${WEB_NAME}-container" --format="{{.Names}}" | xargs -r docker stop | xargs -r docker rm
echo '销毁旧容器'

docker run -d -p ${PORT}:80 -v /usr/docker_nginx_data/${WEB_NAME}/conf.d:/etc/nginx/conf.d -v /usr/docker_nginx_data/log:/var/log/nginx --name ${WEB_NAME}-container ${WEB_NAME}-image:latest
echo '以新镜像创建的容器运行并挂载本地的nginx配置文件, 80端口映射到所主机PORT端口'

tips: docker run 的 -v是 宿主机目录:docker环境目录, 挂载的是目录!!

package.json:

{
  "name": "webhook",
  "version": "1.0.0",
  "description": "",
  "main": "deploy.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "koa": "^2.13.1",
    "koa-router": "^7.4.0",
    "koa-body": "^4.1.0"
  }
}

最后用pm2 start index.js --watch 启动服务

这样我们建立了一个服务监听github webhook的推送, 触发脚本执行更新代码、对应目录创建、复制配置文件、基于dockerfile创建docker镜像,创建docker容器,运行。项目打包部署在docker的虚拟环境中,映射在我们宿主机的对应端口,并且我们利用-v 将docker容器中的nginx配置引用目录改为了宿主机的自定义目录,从此前端只管愉快的的写代码->提交代码,告别手工打包上传部署项目的时代!