升级 前端docker自动化部署

2,578

前言

前段时间学习了docker相关的内容,docker可以实现数据隔离、跨平台等,加快项目后续部署。

在了解docker部署流程后,学习了CI/CD的概念,通过对gitlabdocker的 简单配置即可实现持续集成,大幅提高生产效率。

本人基于gitlabdocker配置前端部署大致流程:

服务器监听远端git分支是否出现新提交 --> 服务器拉取对应分支代码 --> 基于node镜像实现依赖下载和源码编译 --> 基于nginx镜像及编译文件build前端镜像 --> 基于docker-compose.yml运行相应容器

联系之前实现的前端自动化部署工具 (详情请见:从零开始 Node实现前端自动化部署), 准备对其升级并支持docker部署。

构思

考虑兼容传统部署方式基础上,支持本地编译后代码的上传部署和源码上传、远端编译部署两种方式。

考虑部分简单项目可能无需使用docker-compose进行容器编排,直接使用docker run 指令即可。

因此考虑支持以下方式进行项目部署:

部署方式legacydockerdocker-compose
本地打包编译dist
源码远端打包编译

由此考虑用户使用流程如下:

选择项目-->选择部署方式-->选择上传源码or编译后代码-->远端自动部署

分为以下两种实现思路:

  1. 源码上传至云端,云端借助Dockerfile基于node镜像实现依赖下载、打包编译,基于nginx镜像及编译文件build前端镜像,最后运行容器。
  2. 项目本地编译后,打包上传至云端,直接基于nginx镜像及编译文件build前端镜像,最后运行容器。

升级前的准备

为更好理解该项目实现,可提前准备以下内容:

  1. 熟悉docker部署流程及Dockerfiledocker-compose.yml语法规则
  2. 成功安装docker、docker-compose等基础环境的远端服务器
  3. node基础知识

升级

逻辑梳理 模块划分

首先对原项目进行逻辑梳理与模块划分,明确各模块的功能,最终文件目录如图:

文件压缩 升级支持过滤列表

由于此次需要实现对源码的打包上传,需要实现对node_modeles相关文件的过滤。考虑根据配置文件中exclude字段配置过滤列表实现打包过滤功能。

在进行压缩前,读取目标文件目录targetDir的子文件名称,排除存在过滤列表excludeFiles中的文件,返回过滤后的文件名数组。

在压缩过程中,旧版本使用archive.directory()实现对整个文件目录的压缩,现改为基于文件数组依次进行打包,使用fs.statSync(filePath).isDirectory()判断子文件是否为文件夹,使用archive.file()archive.directory()进行对应打包处理。

compress.js 源码:

const fs = require('fs')
const archiver = require('archiver')
const join = require('path').join

function compress (targetDir, localFile, excludeFiles, homeDirName = 'web/') {
  return new Promise((resolve, reject)=>{
    // filter exclude files
    const filterDir = filterExcludeFiles(targetDir, excludeFiles)
    console.log('正在压缩文件...')
    let output = fs.createWriteStream(localFile) // create file stream write
    const archive = archiver('zip', {
      zlib: { level: 9 } // set compress level
    })
    output.on('close', () => {
      console.log('压缩完成!共计 ' + (archive.pointer() / 1024 /1024).toFixed(3) + 'MB')
      resolve('Compression complete')
    }).on('error', (err) => {
      console.error('压缩失败', err)
      reject('Compression failed')
    })
    archive.on('error', (err) => {
      throw err
    })
    archive.pipe(output) // save file by pipe
    // append file and dir
    filterDir.forEach(file => {
      const filePath = join(targetDir, file)
      const stat = fs.statSync(filePath)
      if (stat.isDirectory()) {
        archive.directory(filePath, homeDirName + file)
      } else {
        archive.file(filePath, { name: file, prefix: homeDirName })
      }
    })
    archive.finalize() // make sure file stream write completely
  })
}

// filter exclude files
function filterExcludeFiles (targetDir, excludeFiles = []) {
  return fs.readdirSync(targetDir).filter(file => {
    return (!excludeFiles.includes(file))
  })
}

module.exports = compress

升级终端颜色

基于colors实现终端命令的颜色区分显示,使用如下:

const colors = require('colors')

colors.setTheme({
  silly: 'rainbow',
  input: 'grey',
  verbose: 'cyan',
  prompt: 'grey',
  data: 'grey',
  help: 'cyan',
  debug: 'blue',
  info: 'blue',
  error: 'red',
  warn: 'yellow',
  success: 'green'
})

console.log('粗体文字'.bold)
console.log('出现错误'.error)

tip: 部署方式等选择依然基于inquirer实现。

新增Dockerfile docker-compose.yml

这里以支持源码编译的Dockerfile为例,介绍前端镜像构建过程:

  1. 引用node:lts-alpine3.12(基础版node镜像,文件较小)
  1. 切换为阿里源(若依赖下载较慢,可切换为阿里源)
  2. 指定工作目录/tmp/cache(容器内)
  3. 添加当前同级目录中package.json(package-lock.json存在时请添加)
  4. 执行依赖安装
  5. 拷贝当前目录文件至工作目录
  6. 执行编译指令
  7. 引用socialengine/nginx-spa:latest(包含SPA相关配置的nginx镜像,监听地址为/app/index.html)
  8. 拷贝 node镜像 /tmp/cache/dist 至 nginx镜像 /app目录下(编译后文件夹可自定义)

Dockerfile

FROM node:lts-alpine3.12 as build

# 若依赖下载较慢,可切换为阿里源
RUN npm config set registry https://registry.npm.taobao.org

WORKDIR /tmp/cache

ADD package.json .
# 存在package-lock.json时启用
ADD package-lock.json .
RUN npm install

ADD . .
# 编译指令可自定义
RUN npm run build

FROM socialengine/nginx-spa:latest as nginx
# 编译后文件夹可自定义
COPY --from=build /tmp/cache/dist /app

docker-compose.yml较为简单,指定容器名、镜像名、重启方式、映射端口(宿主机端口:容器端口)等信息。

ps: 远端安装最新版本docker-compose,这里使用3.8的语法,低版本可使用2.x语法

docker-compose.yml

version: "3.8"

services: 
  web:
    # 容器名、镜像名请保持与配置文件一致
    container_name: spa_web
    restart: always
    image: spa/web:dev
    ports: 
      - 8900:80

docker部署流程

在主程序中,根据用户选择进行部署判断,主要分为以下流程

  1. docker、docker-compose安装检查
  1. 上传Dockerfiledocker-compose.yml
  2. 构建docker镜像
  3. 启动容器前检查是否存在同名容器,存在则停止并删除同名容器
  4. 根据配置使用docker run docker-compose.yml启动容器
  5. 展示当前运行中容器状态
  6. 提示完成部署

tips:

  • 若服务器网速较慢,可提前安装nodenginx的docker镜像,以便加快后续部署速度
    • docker pull node:lts-alpine3.12
    • docker pull socialengine/nginx-spa:latest

spp.js docker部分代码:

// docker流程
// docker 部署流程 docker env check --> upload Dockerfile --> build image
const dockerFilePath = deployDir + releaseDir
await runCommand(ssh, `docker -v`, '/')
await uploadFile(ssh, getAbsolutePath(BUILD__MODE === 'dist' ? docker_file : docker_file__build), dockerFilePath + '/Dockerfile') // upload Dockerfile
console.log('5- 开始构建docker镜像...请耐心等待'.bold)
await runCommand(ssh, `docker build -t ${ image } .`, dockerFilePath)
console.log('6- 准备启动docker容器...请耐心等待')
if (DEPLOY__MODE === 'docker') {
  if ((await runCommand(ssh, `docker ps -f name=${ container_name }`)).indexOf('\n') !== -1) {
    console.log('存在同名容器,正在删除同名容器...')
    await runCommand(ssh, `docker stop ${ container_name }`, '')
    await runCommand(ssh, `docker rm ${ container_name }`, '')
  }
  await runCommand(ssh, `docker run --name ${container_name} -p ${ports} -d ${image}`, dockerFilePath)
} else {
  // docker-compose 部署流程 upload docker-compose --> run docker-compose --> show container
  await runCommand(ssh, `docker-compose -v`, '/')
  await uploadFile(ssh, getAbsolutePath(docker_compose), dockerFilePath + '/docker-compose.yml') // upload docker-compose
  if ((await runCommand(ssh, `docker ps -f name=${ container_name }`)).indexOf('\n') !== -1) {
    console.log('存在同名容器,正在删除同名容器...')
    await runCommand(ssh, `docker stop ${ container_name }`, '')
    await runCommand(ssh, `docker rm ${ container_name }`, '')
  }
  await runCommand(ssh, 'docker-compose up -d', dockerFilePath)
}
// 显示当前运行中容器
console.log('7- 当前运行中的容器...'.bold)
await runCommand(ssh, 'docker ps', dockerFilePath)
console.log(`恭喜!${ name }部署成功`.success)

至此完成该项目的主要升级,更多细节请参考my-auto-deploy

使用

这里选取两种情况进行展示:

  • docker + source build
  • docker-compose + dist

这里以react在线壁纸为例,服务器已开放8800、8900端口。

  1. 拉取代码至本地,安装依赖,执行本地构建,目录如图:

  1. 该项目构建后产生build文件夹且不存在package-lock.json文件,因此修改配置文件Dockerfile,如图

配置文件

Dockerfile

  1. 运行部署程序,选择相应信息开始部署,直至部署完成,如图

选择部署

部署成功(容器信息和预期结果一致)

  1. 访问对应地址,验证部署成功,如图

  1. 修改 docker-compose.yml 端口为8900后,再次进行部署,如图

docker-compose.yml

选择部署

  1. 由于存在同名容器,部署过程中会停止并删除同名容器,之后使用最新的镜像启动容器,如图

  1. 访问对应地址,验证部署成功,如图

最后

🎉该项目已开源至 github 欢迎下载使用 后续会完善更多功能 🎉 源码及项目说明

Tip: 喜欢的话别忘记 star 哦😘,有疑问🧐欢迎提出 prissues ,积极交流。

其他文章:

从零开始 Node实现前端自动化部署

从零开始 React Hook实现在线壁纸网站