Docker越来越流行。可轻松灵活地进行环境隔离、扩容、运维管理。对于业务开发人员来说,随着持续集成的发展,对代码质量和快速迭代的要求越来越高。对于前端来说,也更容易在CI环境中集成开发、测试和部署。例如可以为流水线设置Lint/Test/Security/Audit/Deploy/Artifact等任务,更好的控制项目质量。现在无论是前端、后端还是运维,都在强调devops的概念。接下来我会写一系列关于devops在前端应用的文章。下面我将介绍如何使用Docker部署前端应用。千里之行,始于足下。一步开始的意思就是让它先跑起来。先让它跑起来先简单介绍一个典型的前端应用部署流程npminstall,安装依赖npmrun构建、编译、打包、生成静态资源和服务静态资源,比如nginx,介绍完部署流程,简单写一个DockerfileFROMnode:10-alpine#代表生产环境ENVPROJECT_ENVproduction#很多包会根据这个环境变量表现不同#另外webpack中打包也会根据这个环境变量进行优化,但是打包的时候create-react-app会打包写入环境变量ENVNODE_ENVproductionWORKDIR/codeADD./codeRUNnpminstall&&npmrunbuild&&npminstall-ghttp-serverEXPOSE80CMDhttp-server./public-p80现在前端服务已经运行,可以完成其他阶段的部署了。一般情况下,下面就变成了运维的工作,但是扩展自己的知识边界总是对的。其他阶段描述如下使用nginx或traefik作为反向代理。我在我的内部集群中使用traefik,详情请参阅traefikeasyentry使用kubernetes或dockercompose进行容器编排。我在我的内部集群中使用compose,有关详细信息,请参阅dockercompose。使用gitlabci、droneci或githubactions做CI/CD自动部署。这个时候镜像有两个问题,导致每次部署的时间很长,不利于产品的快速交付。没有快速交付,就没有敏捷开发(Agile)。构建镜像的时间过长。图片体积太大,连1G+都用了很久。图像缓存我们注意到,与项目的源文件相比,package.json相对稳定。如果没有新的安装包下载,再次构建镜像时不需要重新构建依赖。然后你可以在npminstall上节省一半的时间。对于ADD,如果要添加的文件内容的校验和没有变化,就可以使用缓存。把package.json/package-lock.json和源文件分开写,写镜像是个不错的选择。目前如果没有新的安装包更新,可以节省一半的时间FROMnode:10-alpineENVPROJECT_ENVproductionENVNODE_ENVproduction#http-server不用改也可以使用缓存RUNnpminstall-ghttp-serverWORKDIR/code#第一次添加这两个文件并充分利用缓存ADD包。jsonpackage-lock.json/codeRUNnpminstall--productionADD./codeRUNnpmrunbuildEXPOSE80CMDhttp-server./public-p80有更多关于使用缓存的细节,需要特别注意。和RUNgitclone一样,如果不更新命令字符串,就会使用缓存,当命令是非幂等时,这会导致问题。关于缓存和可能出现的问题,可以参考我的文章Dockerfile在CI环境下的最佳实践优化FROMnode:10-alpineENVPROJECT_ENVproductionENVNODE_ENVproduction#http-server不用改也可以使用缓存RUNnpminstall-ghttp-serverWORKDIR/code#第一次添加这两个文件,充分利用缓存ADDpackage.jsonpackage-lock.json/codeRUNnpmciADD./codeRUNnpmrunbuildEXPOSE80CMDhttp-server./public-p80主要在CI环境做了一点改动:使用npmci代替npmi,经过实验,npmci可以减少近一半取决于安装时间。$npminstalladded1154packagesin60s$npmciadded1154packagesin35s另外,当package.json和package-lock.json的版本不匹配时,npmci会报异常,提前检测不安全信息,早发现问题,早解决问题。多阶段构建受益于缓存,现在图像构建时间要快得多。但是此时镜像的大小还是偏大,也会导致部署时间变长。原因如下。考虑每个CI/CD部署的过程。在构建服务器(Runer)上构建镜像,并将镜像推送到镜像仓库服务器。在生产服务器上拉取镜像。启动容器是显而易见的。图片尺寸过大,会在前两步上传和下载。造成传输效率低下,增加每次部署的时延。即使构建服务器和生产服务器在同一个节点下,也不存在延迟问题(基本不可能)。减小图像大小还可以节省磁盘空间。镜像体积过大完全是因为node_modules这个臭名昭著的volume:node_modules的size,不过最终我们只需要构建生成的静态资源即可。对于源文件和node_modules下的文件,体积太大,没有必要,造成浪费。这时候可以利用Docker的多阶段构建来提取编译后的文件,即打包生成的静态资源,对Dockerfile进行改进FROMnode:10-alpineasbuilderENVPROJECT_ENVproductionENVNODE_ENVproduction#http-server也可以使用缓存WORKDIR/codeADDpackage。jsonpackage-lock.json/codeRUNnpmciADD./codeRUNnpmrunbuild#ChooseasmallerbaseimageFROMnginx:10-alpineCOPY--from=builder/code/public/usr/share/nginx/html此时,镜像大小从1G??+变为5000万+。如果此时的部署只是为了方便在测试环境或者多分支环境下测试,那大功告成,问题完美解决。使用对象存储服务(OSS)分析50M+的镜像量。nginx:10-alpine的镜像大小为16M,其余40M为静态资源。生产环境中的静态资源往往维护在独立的域名上,使用CDN加速。如果你上传静态资源到文件存储服务,也就是OSS,使用CDN加速OSS,就不需要进入镜像了。在生产环境中,对静态资源的CDN也有强烈的需求。这时候图片大小会控制在20M以下。虽然大大缩小了镜像的体积,但是会增加复杂度,增加镜像构建时间(比如上传到OSS)。测试环境或者分支环境不需要使用OSS。关于静态资源,可以分为两部分:/build,这类文件在项目中通过require/import引用,会被webpack打包哈希,通过publicPath修改资源地址。此类文件无需进入mirror/static即可上传到oss并永久缓存。此类文件直接引用项目中的根路径,直接进入镜像。如果上传到OSS,通过脚本命令npmrunuploadOss将静态资源上传到OSS可能会增加复杂度(批量修改publicPath)。更新后的Dockerfile如下FROMnode:10-alpineasbuilderENVPROJECT_ENVproductionENVNODE_ENVproduction#http-server也可以使用缓存不改更小的基础镜像FROMnginx:10-alpineCOPY--from=buildercode/public/index.htmlcode/public/favicon.ico/usr/share/nginx/html/COPY--from=buildercode/public/static/usr/share/nginx/html/static总结本文结束后,前面构建镜像需要注意以下几点结尾。在镜像中使用基于alpine的镜像来减小镜像的大小。镜像中需要锁定node的版本号,alpine的版本号尽量锁定,比如node:10.19-alpine3.11。(我在示例代码中没有这么详细指出)选择合适的环境变量NODE_ENV和PROJECT_ENV,比如在测试环境中构建npmci而不是npmi,避免版本问题,提高依赖安装包速度。单独添加json,充分利用镜像缓存,采用多阶段构建,减小镜像体积。如有需要,请将静态资源上传至CDN