手把手教你使用docker和webhook实现前端自动化部署

656 阅读10分钟

技术栈:

  • docker
  • node
  • pm2
  • shell
  • webhook

docker

docker是什么:docker可以灵活的创建/销毁/管理多个容器 (container) 在容器中可以搭建任何你想要的环境,安装docker后可以自由创建任意多的容器

安装

点击安装docker 下载完成后重启电脑,打开控制台输入docker -v查看是否安装成功

重要概念

docker的三个概念

  1. 镜像(image)
  2. 容器(container)
  3. 仓库(repository)

镜像就是容器模板,一个镜像可以创建多个容器,好比js中类和示例的关系

获取镜像有两种方式

  1. 创建Dockerfile文件
  2. 使用dockerHub或者其他来源的镜像

Dockerfile

Dockerfile文件就是一个docker镜像的配置文件,定义如何生成镜像

创建文件

创建文件index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <h2>hello docker</h2>
    </body>
</html>

创建Dockerfile文件

FROM nginx
COPY index /usr/share/nginx/html/index.html
EXPOSE 80
  • FROM nginx:基于官方nginx镜像
  • COPY index /usr/share/nginx/html/index.html:当前文件夹下的index.html替换容器的/usr/share/nginx/html/index.html
  • EXPOSE 80:容器对外暴露80端口(容器内的端口,真实端口需要在创建容器的时候定义)

创建镜像(image)

当前目录下打开控制台运行以下命令构建镜像:

docker build . -t test-image:latest
  • build:创建镜像
  • .:使用当前目录下的Dockerfile文件
  • -t:使用tag标记当前镜像版本
  • test-image:指定镜像名
  • :latest:指定tag版本

查看镜像

docker images

REPOSITORY有一个名字为test-image说明镜像已经创建成功

创建容器(container)

docker run -d -p 80:80 --name test-container test-image:latest
  • run:创建并运行docker容器
  • -d:后台运行容器
  • 80:80:将当前服务器的80端口(冒号前)映射到容器的80端口(冒号后)
  • --name:指定容器名字
  • test-image:latest:使用的镜像名字。

浏览器打开localhost会显示index.html的内容

查看容器

docker ps -a
  • ps:查看docker容器(正在运行中的)
  • -a:查看所有容器,包括未在运行状态的容器

dockerHub

github是储存代码的仓库,dockerHub是储存镜像的仓库。开发者可以将Dockerfile生成的镜像上传到dockerHub来存储自定义镜像,也可以直接使用官方提供的镜像。

docker pull nginx
docker run -d -p 81:80 --name nginx-container nginx

由于上一步本地80端口已经被占用了,所以这次使用81端口来映射容器的80端口,浏览器访问localhost:81应该就可以看到Welcome tonginx了。

为什么要用docker

  • 环境统一 docker解决了一个世纪难题:在我电脑上明明是好的呀 开发者可以将开发环境用docker镜像上传到docker仓库,在生产环境中拉取并运行相同镜像,保持环境一致。 注册dockerHub账号并登录
docker login    # 本地docker登录dockerHub账号
docker build . -t 你的账号/docker-image-test:0.1    # 构建用于上传到dockerHub的镜像(需要加上账号前缀)
docker push 你的账号/docker-image-test:0.1  # 提交名为docker-image-test的镜像。
# 打开dockerHub查看有没有上传成功,成功继续往下执行

下面测试拉取镜像

docker images -a    # 查看本地镜像并复制刚刚构建的镜像的id
docker rmi 复制的id # 删除本地刚才构建的镜像
docker images -a    # 查看并确认已经删除成功
docker pull 你的账号/docker-image-test:0.1  # 拉取账号下的docker-test-image镜像
docker images -a    # 确认拉取成功
  • 便于回滚 在创建镜像是可以使用tag标记当前版本,如果某个版本的环境有问题,可以快速回滚到之前的版本。
  • 环境隔离 使用cocker可以使服务器更干净,构建用到的环境可以都放在容器中。
  • 高效/节省资源 相对于真实服务器和虚拟机,容器不包含操作系统。容器的创建和销毁都十分高效。

其他常用命令

  • 停止容器:docker stop 容器id
  • 启动容器:docker start 容器id
  • 删除容器:docker rm 容器id(必须先停止)
  • 查看容器日志:docker logs 容器名字
  • 进入容器:docker exec(进入容器) -it(分配一个伪终端,即使没有附加也保持STDIN打开) 容器名字 /bin/sh
  • 容器与主机间数据拷贝:docker cp 容器名字:容器路径 本地路径

前端自动化部署

在没有使用自动化部署之前,想要更新网站内容需要

  • git push提交代码
  • 本地运行npm run build生成构建物
  • 将构建产物通过ftp等形式上传到服务器

实现自动化部署之后

  • git push提交代码
  • 服务器自动更新镜像
  • 镜像中自动运行npm run build生成构建物
  • 服务器自动创建容器

可以发现实现自动化部署后,开发者只需要将代码提交到仓库,其他的事情都可以通过服务器上的的脚本自动化完成。

服务器

首先你得有一台服务器,以下内容以 centos7 系统为例。

安装环境

  • docker 本地有了docker环境,服务器也需要安装docker
curl -sSL https://get.daocloud.io/docker | sh   # 安装docker
systemctl start docker                          # 启动docker
  • git 自动化部署需要涉及到拉取最新代码,所以这里安装一下git环境
yum install git
  • node
curl -sL https://rpm.nodesource.com/setup_10.x | bash -  # 设置nodesource
yum -y install nodejs                                   # 安装node
node -v
  • pm2 可以后台运行脚本
npm i pm2 -g

创建项目

创建本地项目

npx create-react-app docker-test

打开gitee(github也可,这里我用gitee),创建项目 打开本地项目进入控制台

git remote add origin gitee项目地址
git push -u origin master   (可能会弹出登录,输入gitee账号密码即可)

当控制台显示Branch 'master' set up to track remote branch 'master' from 'origin'.时,刷新浏览器可以看到远程仓库已经创建完毕。

webhook

打开远程仓库,点击管理——webhooks——添加webhook

  • URL:填写服务器地址,需要加http或https协议,端口号暂定3000
  • WebHook 密码/签名密钥:不需要填写
  • 选择事件:选择默认的Push,当仓库发生push事件时,gitee会自动向我们填写的url发送POST请求。
  • 激活:开启webhook

处理项目更新请求

当服务器接受到项目更新后发送的post请求后,需要创建/更新镜像来实现自动化部署。

创建Dockerfile

打开本地项目 新建一个Dockerfile文件,用于构建镜像

# build stage
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY . .
RUN npm i
RUN npm run build

# production stage
FROM nginx:stable-alpine as production-state
COPY --from=build-stage /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

build stage构建阶段

  • FROM node:lts-alpine as build-stage:使用node镜像,版本为lts-alpine(比latest版本小巧,更适合作为docker镜像使用)
  • WORKDIR /app:将工作区设置为/app,和其它文件系统隔离
  • COPY . . :拷贝文件到容器的/app目录
  • RUN npm i:打开控制台执行npm i
  • RUN npm run build:打开控制台执行npm run build

production stage部署阶段

  • FROM nginx:stable-alpine as production-stage:使用stable-alpine版本的nginx镜像,并将该阶段命名为production-stage
  • COPY --from=build-stage /app/build /usr/share/nginx/html:--from=build-stage可以从build-stage阶段读取文件,所以当前命令是从build-stage阶段的/app/build拷贝到本阶段/usr/share/nginx/html下
  • EXPOSE 80:容器对外暴露80端口
  • CMD ["nginx", "-g", "daemon off;"]:CMD对应的命令结束后容器就会被销毁,容器创建后运行nginx -g daemon off命令,可以使nginx一直在前台运行

本地项目里打开控制台

# 复制当前文件夹下的Dockerfile到远程服务器,我的登录账号为root,服务器地址47.103.74.196,目标目录为/root
scp ./Dockerfile root@47.103.74.196:/root
# Are you sure you want to continue connecting (yes/no)?    yes
# root@47.103.74.196's password:    输入连接密码(看不到)

创建.dockerignore

类似于.gitignore,.dockerignore可以在创建镜像复制文件时忽略某些文件。

在本地项目中创建.dockerignore文件

# .dockerignore
node_modules

复制.dockerignore到服务器

scp ./.dockerignore root@47.103.74.196:/root

创建http服务器

或许你创建webhook后已经发现了,远程仓库发送的测试请求超时了,所以需要创建一个http服务来处理webhook发送的post请求 (需要在安全组打开此端口,阿里云栗子:ecs.console.aliyun.com/#/securityG…) 本地项目创建文件index.js

const http = require('http')

http.createServer(async (req, res) => {
    if (req.method === 'POST' && req.url === '/') {
        // ......
    }
    res.end('success')
}).listen(3000, () => {
    console.log('server is ready')
})

拉取仓库代码

仓库触发push事件,服务器收到post请求之后需要拉取最新代码 修改index.js

const http = require('http')
const path = require('path')
const fs = require('fs')
const { execSync } = require('child_process')

const repository = 'docker-test'

const deleteFolderRecursive = path => {
    if (fs.existsSync(path)) {  // 路径存在
        fs.readdirSync(path).forEach(v => { // 读取该路径下的文件
            const curPath = path + '/' + v
            if (fs.statSync(curPath).isDirectory()) {
                deleteFolderRecursive(curPath)  // 如果是一个文件夹,则递归删除
            } else {
                fs.unlinkSync(curPath)  // 直接删除文件
            }
        })
        fs.rmdirSync(path)
    }
}

http.createServer(async (req, res) => {
    let projectDir = ''
    if (req.method === 'POST' && req.url === '/') {
        console.log('收到请求', req.url)
        projectDir = path.resolve(`./${repository}`)    // 定义项目目录
        deleteFolderRecursive(projectDir)       // 递归删除原来克隆的项目
        execSync(`git clone https://gitee.com/zhangfangbiao/${repository}.git`, {   // 开始克隆
            stdio: 'inherit'
        })
    }
    res.end(projectDir)
}).listen(3000, () => {
    console.log('server is ready')
})

创建镜像和容器

上面步骤已经可以成功拉取代码了,现在需要做的就是使用Dockerfile创建镜像,更新容器。 修改index.js

const http = require('http')
const path = require('path')
const fs = require('fs')
const { execSync } = require('child_process')

const repository = 'docker-test'

const deleteFolderRecursive = path => {
    if (fs.existsSync(path)) {  // 路径存在
        fs.readdirSync(path).forEach(v => { // 读取该路径下的文件
            const curPath = path + '/' + v
            if (fs.statSync(curPath).isDirectory()) {
                deleteFolderRecursive(curPath)  // 如果是一个文件夹,则递归删除
            } else {
                fs.unlinkSync(curPath)  // 直接删除文件
            }
        })
        fs.rmdirSync(path)
    }
}

http.createServer(async (req, res) => {
    let projectDir = ''
    if (req.method === 'POST' && req.url === '/') {
        console.log('Received request', req.url)
        projectDir = path.resolve(`./${repository}`)    // 定义项目目录
        deleteFolderRecursive(projectDir)       // 递归删除原来克隆的项目
        execSync(`git clone https://gitee.com/zhangfangbiao/${repository}.git`, {   // 开始克隆
            stdio: 'inherit'
        })
        // 复制Dockerfile到项目目录
        fs.copyFileSync(path.resolve('./Dockerfile'), path.resolve(projectDir, './Dockerfile'))
        // 复制.dockerignore到项目目录
        fs.copyFileSync(path.resolve('./.dockerignore'), path.resolve(projectDir, './.dockerignore'))
        // 创建镜像
        execSync(`docker build . -t ${repository}-image:latest`, {
            stdio: 'inherit',
            cwd: projectDir,
        })
        // 销毁容器(查找名字为repository-container开头的容器并停止,然后删除)
        execSync(`docker ps -a -f "name=^${repository}-container" --format="{{.Names}}" | xargs -r docker stop | xargs -r docker rm`, {
            stdio: 'inherit',
        })
        // 创建新的容器(使用repository-image:latest镜像构建容器并把容器命名为repository-container,本地8888端口映射容器的80端口)
        execSync(`docker run -d -p 8888:80 --name ${repository}-container  ${repository}-image:latest`, {
            stdio: 'inherit'
        })
        console.log('deploy success')
    }
    res.end(projectDir)
}).listen(3000, () => {
    console.log('server is ready')
})

服务器开启服务

scp ./index.js root@47.103.74.196:/root

然后再服务器运行

# 使用pm2后台运行index.js脚本文件,并命名为webhook
pm2 start /root/index.js --name webhook
pm2 logs webhook
# 查看日志

触发webhook

试一下能否正常运行,提交代码然后push,打开pm2日志,可以看到Received request已经可以正常打印,下面开始克隆代码构建镜像 当打印deploy success后,浏览器打开47.103.74.196:8888就可以看到项目正在运行了。 整个过程

  • 提交代码并push
  • gitee触发POST请求到服务器的3000端口
  • 服务器收到请求
  • 定义项目目录,删除原来的项目,并克隆最新代码
  • 复制Dockerfile和.dockerignore到项目目录
  • 创建镜像
    • 使用node镜像
    • 设置工作目录为/app
    • 复制代码到工作目录
    • 安装依赖
    • 打包
    • 使用nginx镜像
    • 复制构建产物,从/app/build到容器的/usr/share/nginx/html文件夹下
    • 容器暴露80端口
    • 使nginx在容器中保持前台运行
  • 更新容器,把容器的80端口映射到本地的8888端口

项目地址:gitee.com/zhangfangbi…