镜像构建篇 - 我是如何实现 docker 镜像 2 分钟构建、部署

4,506 阅读8分钟

在前端开发过程当中,我们要解决的不止是页面的风格、浏览器的兼容、用户的体验,当然这些都是非常重要的一部分,但是有一部分,虽然平时我们不需要特别重视,但是它的存在,是对我们项目的及时修复处理一些及时性的问题,起到至关重要的作用。

看之前做好心里准备,也许这个东西有人会,但是之前我不会,会的别喷我,不会的可以问我

做这件事的目的

其实,这件事是一直之前我个人也没有注意到的一个问题,目光前期停留在怎么做页面,怎么写更好的效果,怎么样渲染的更快,怎么样用户体验最好,对项目部署这块一直没有上心研究过,因为没了解过这一块知识之前,我一直觉得这个时间它就是这么长,没办法优化的。

后来因为项目需要做本地版需求,镜像太大,前后端都需要做大程度的优化,这时候,我的目光才注视到了这一块的内容。

历程

之前的实现方式

在优化前的实现方式,其实很简单,就是在部署平台内,通过 git 拉取当前分支内容,然后根据 package.json 安装所有依赖,在进行 build 打包,然后在部署平台内进行镜像构建,构建完成后,通过平台发布镜像到容器。

优点: 省心,就点一下就完事

缺点: 镜像特大,发布慢,构建慢,一旦网络或者平台出现问题,镜像就有构建失败的风险,而且紧急修复时成本非常高

网上大部分实现方式

网上之前查询过一些方法,目前我查到的一些实现方式,就是通过 CI ,然后选择对应的分支去做 docker 镜像构建,最后发布到相应的容器当中。

优点: 镜像构建省心,可设置 cache ,减小镜像体积

缺点: 可控性差,github 项目使用 gitlab CI 时,频繁 push 还有一个 commit 更新延迟的情况,无法第一时间进行项目构建,不同的分支有不同的 npm 包需要去做 cache 处理,维护成本高

思考过程

本地模拟

在做优化之前,我是先去考虑的,这个可优化的范围在哪,是代码的体积吗?其实并不是,代码优化上几 M,也不现实,因为一共打包后代码也没有多大,这里不是我需要优化的重要的点,所以我准备先在本地按照平台构建镜像的方式,把镜像在本地做一遍构建;

这里需要一部分的 docker 方面的知识,但是不多,也不难,docker 的安装方式,我就不讲了,大家自行百度

通过本地执行 docker 命令:

docker build -t local-test:2150 PATH(自己当前项目目录)

命令行就是这个样子,要安装一堆乱七八糟,因为前面说过,镜像的构建是基于平台的,所以平台要安装很多的包,很多的内容,最后镜像才能正常的构建完成。

本地查看镜像信息

镜像正常构建完成后,我们可以在本地查到当前构建好的镜像:

docker images

然后我们找到当前构建好的本地镜像,后面就可以看见我们当前镜像的大小了:

现在我们就可以看出来,镜像特别大,1.65 G,很恐怖的一个大小,然后我们继续剖析,到底是什么占的这么大。

本地启 docker 容器

进入容器:

docker run -it local-test:2150 /bin/bash

查看当前镜像所有文件大小:

cd /
du -d1 -h

这样可以看到所有目录的大小,现在我们就可以知道,到底是什么这么占用镜像大小:

看一下 code:

一个 node_modules 完全没有用,可以干掉。

上面还有个 ./usr 文件夹,占了 1.2G ,也是类似的情况,这里就不占内容了。

其实镜像最主要的东西,就是 nginxbuild 后的 dist 文件夹,其他大部分的内容,都是没有用的,可以删除的,我们大概就知道问题出自哪里了。

如果可以发现问题的本质,那就不是问题,是阅历 - 热情的刘大爷

解决步骤

优化 dockerfile

先看一下我优化前的 dockerfile:

FROM debian:9

RUN echo \
  deb http://mirrors.aliyun.com/debian/ stretch main non-free contrib\
  deb-src http://mirrors.aliyun.com/debian/ stretch main non-free contrib\
  deb http://mirrors.aliyun.com/debian-security stretch/updates main\
  deb-src http://mirrors.aliyun.com/debian-security stretch/updates main\
  deb http://mirrors.aliyun.com/debian/ stretch-updates main non-free contrib\
  deb-src http://mirrors.aliyun.com/debian/ stretch-updates main non-free contrib\
  deb http://mirrors.aliyun.com/debian/ stretch-backports main non-free contrib\
  deb-src http://mirrors.aliyun.com/debian/ stretch-backports main non-free contrib\
  > /etc/apt/sources.list

RUN apt-get update && apt-get -y upgrade
RUN apt-get install -y git-core curl build-essential
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
RUN apt-get install -y nodejs
RUN npm config set registry https://registry.npm.taobao.org

RUN mkdir /code
ADD . /code
WORKDIR /code

RUN npm install
RUN npm run build:test

RUN cp -fr /code/dist/ /usr/share/nginx/html/
COPY nginx/prod.conf /etc/nginx/nginx.conf

EXPOSE 80

STOPSIGNAL SIGTERM

CMD ["nginx", "-g", "daemon off;"]

细看一下内容,其实我们要关心的东西并不多,我们要做的就是把一个 build 好的包,放到 /usr/share/nginx/html/ , 然后把 nginx 配置放到 /etc/nginx/nginx.conf 当中,然后配置端口,启动 nginx

精简 dockerfile

把上面的东西删掉后,发现还需要 build ,这个时候,我想了个弯道超车的办法:本地 build

FROM debian:9

ADD ./dist/ /usr/share/nginx/html/dist/
COPY nginx/prod.conf /etc/nginx/nginx.conf

EXPOSE 80

STOPSIGNAL SIGTERM

CMD ["nginx", "-g", "daemon off;"]

这样是不是很干净,都不用重新再在本地构建一遍镜像,我们也知道那些没有用的东西不会存在在现在构建的镜像当中,因为我们就没有做别的事情,就是两个 copy 动作,然后启动,完事。

自动构建

镜像优化完后,最大的问题我们就解决了,接下来要解决的事情,就是我们不可能没完没了的老是在本地 build ,比如比较忙的时候同时操作多个项目,或者需要频繁的在不同分支开发不同的功能,自己一遍一遍的 build,会被自己搞死。

优化的目的我们也是为了提高工作效率,减少重复性工作成本,这样如果手动的,还不如那样,因为手动 build 总有失误的时候,常在河边走,哪有不湿鞋嘛。

在这里呢,我是基于 git hooks 做的一个自动化构建的实现,git hooks 有一个钩子是 pre-push,就是在代码推送前做的一些行为。

在开发中,一般情况下,每次代码的推送,就意味着我可能要部署一版到服务器上,不论是测试还是正式。

我们每次发布的镜像, 镜像名是可以相同的,但是需要通过 tag 去做区分,我目前是通过时间字符串去做的 tag

// .huskyrc.js
module.exports = {
  hooks: {
    'pre-push': `\
      npm run build && \
      docker build -t images:tag PATH(当前项目目录) && \
      // 这里发送到平台上,用于镜像的发布
    `,
  },
}

环境区分

在项目当中,不可能只有一个分支,也不可能只有一个服务器,最起码我们要区分一个测试,一个正式,两个服务器。

如果平时我们的工作,代码提交是按照 git flow 的方式来的,那么这个问题就很好解决了,获取当前分支:

// 获取当前分支名称
const branch = execSync('git rev-parse --abbrev-ref HEAD')
  .toString()
  .replace(/\s+/, '')

通过分支去区分我们要执行的 build 命令,images 镜像名称,dockerfile 文件:

var branchAuthority = {
  'dev': {
    docker: 'Dockerfile.test',
    image: 'local-test',
    build: 'npm run build:test',
  },
}

修改 .huskyrc.js:

module.exports = {
  hooks: {
    'pre-push': `\
      ${build} && \
      docker build -f ${docker} -t ${image:tag} ${path}
    `,
  },
}

整体的流程大概就是这个样子了,剩下的就是一些细节上的优化,比如如果规划 branchAuthority 结构,镜像构建完成、发布完成后是否需要删除本地镜像等优化型行为了。

总结

效果对比

优化前: 镜像大小 1.65G,构建及发布时间 600s 左右;

优化后: 镜像大小 350M,构建及发布时间 120s 左右

优化后的步骤

  • 安装 git hooksdockerfile
  • 做环境、镜像、docker 区分
  • 本地构建完,本地启个容器,先测试一下流程有没有问题,没有问题在嵌入到 pre-push
  • 最后就可以跑起来

结尾

其实整体梳理下来,没有什么技术型的难点,而且也很简单,就是一个优化的过程,自己没事干瞎琢磨出来的一个解决办法,也许大家还有更好的办法去解决项目构建部署的问题,有的话可以互相交流一下。

都将近一年没写文章了,这一年发生了很多事情,在加上工作上的变动,磨磨唧唧就一年过去了,也不知道这个排版和文字大家有没有看得懂,有没有不清楚的地方,有的话大家就指出来,接下来会正常更新文章。