Docker我的前端项目

忘了从哪年起,Docker就开始流行起来,根据一个兄弟的描述来说,Docker刚开始起源于一家快要倒闭的公司,因为快要倒闭就将自己的项目开源出来,结果受到了广大开发者的关注,开始风靡起来。介绍一个东西怎么用之前,我们先弄清楚两点,这个东西是个什么东西,这个东西对我们有什么用。

Docker是个什么东西

Docker有两个基础的概念,一个是容器,一个是镜像,我们打个比方,容器就是盒子,镜像就是模具或图纸,应用就是零件,这里泛指的应用不应该理解成一个完整的应用,比如一台服务器,可能可以分成数据库,应用服务器,nginx应用等等。盒子是用来装零件的,也可以用来装更小的盒子。镜像可以用来生产零件,并将零件安装到盒子里面变成一个可以用的道具。而道具就是我们最后完整的应用。Docker就是我们从组装应用到发布应用的一整套方案。

Docker对我们有什么用

Docker为我们一整套的构建和发布应用方案,它将一个应用的发布分为两步,一步是构建,一步是运行,那么对于一个构建出来的应用,它可以有多个运行方案,我们可以在运行的时候设置端口,环境等等,我们可以利用这个特性来很方便的实现,多环境,多端口的部署,或者简单的实现node的负载均衡,多服务对于node来讲是比较重要的,虽然有类似于pm2之类的模块已经实现了。其次,Docker在构建应用的时候,根据描述重新构建应用的环境,和宿主机的环境无关,所以只要宿主机上安装了docker,就不需要在乎宿主机的运行环境,对于安装了docker的机器而言,就是构建一次,处处运行。

Docker的运作

环境的描述 ---- 构建 ------> 镜像 ------ 运行 -----> 容器

解释一下上面的图,首先我们会对我们需要的环境进行描述,Docker提供了一个Dockerfile来描述我们需要的环境,Dockerfile大致是这样的:

FROM node:6.10.2
WORKDIR /usr/src/app
ADD package.json /usr/src/app
RUN cnpm install
ADD . /usr/src/app
RUN mkdir -p logs
CMD ["pm2-docker", "start", "pm2-cyd-docker.json"]

具体的语法就不介绍了,在官网可以查到,可以简单的看出,我们要做的就是把应用需要的环境搭建好。当我们去构建这个镜像的时候,我们可以发现每当我们执行一条语句之后,docker都会为我们生成一个sh1值,这个值是用来标志执行到当前生成的镜像,docker的镜像是基于栈的,这很符合我们搭建环境的习惯,比如我们在需要一个可以运行node服务器的服务的时候,首先我们会需要一个简单的操作系统,可能是centos或者ubuntu,之后我们需要curl之类的基础命令行函数来获取到node的包,之后我们可能才能安装node环境,然后我们可以安装一些基于node的模块。docker做的是根据我们的这些构建需求,一步一步去构建,每一次构建的时候都会为当前的版本打上一个标志,并缓存这一步构建的镜像。当我们对项目进行了更改,再次去构建这个镜像的时候,docker会根据每条命令当前的镜像和构建的命令来考虑需不需要再次去执行这次构建,比如我们的镜像总是基于ubuntu镜像,但是每次的第一步都会去执行安装node环境的命令,docker就会跳过这一步,直接才能够缓存中获取到上一次执行这个命令产生的镜像。从这里我们看出,我们docker的构建是可以从一个现成的镜像开始构建的,就如同例子里面FROM node:6.10.2,是从一个现成的镜像开始构建的。所以我们可以制作一些基础的镜像来防止一些重复的构建,加快构建速度。

解释完了构建,我们现在得到了镜像,那么接下来解释一下运行。一个构建好的镜像,就是一个待运行且安装好所有环境的应用,可以非常方便的将应用运行起来。所以运行不是一个需要很多解释的地方,但是运行的设置,可以稍作解释,来帮助我们更好的理解docker的优势。首先,docker的在运行应用的时候可以映射docker容器内外的端口,这让我们在运行的时候可以随意制定端口,当然了,这也没有什么了不起的,毕竟几乎现在的服务器应用在启动的时候都可以通过命令设置端口,不过docker现在不用管内部具体是什么类型的应用,也不用管这个应用具体设置端口的方式,就可以设置端口的出口了。docker可以设置当前的环境变量,这和我们直接启动应用一样,就不多说了。docker提供了可共享的资源的映射,docker在运行的时候可以指定一些资源对于宿主机的映射,这让我们可以在运行时,将一些缓存文件或者日志文件在宿主机上读写,可以加快服务启动的速度,也可以灵活的修改处理应用产生或者使用的文件。

除此之外docker还提供了一系列容器内应用设置的修改,启动,停止,重启, 日志的管理

到此为止,我们造出了一个盒子,而线上的应用往往是由很多的应用综合而成的,所以我们需要启动一堆盒子,关于这一堆盒子的怎么一起运行起来,docker提供了docker-compose,看一下官方的例子:

services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code
redis:
image: "redis:alpine"

这里描述了两个应用,一个应用是服务器应用,一个是redis应用,通过这个文件的描述,docker会构建两个应用的镜像,将两个镜像通过描述的配置启动起来。这种整合的配置相比于每个小应用各自启动各自配置而言,更加利于多应用的配置, 配置显而易见,可读性好。

接下来稍微讲解一下最近项目的经验,以及遇到的问题:

我们的第一个目标是docker化一个前端项目,这个项目是基于Node.js服务的,webpack打包了若干个零零散散的小项目。

传统的node.js的docker策略一般是这样的:

FROM node:6.10.2
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
ADD package.json .
RUN npm install
ADD . .
RUN npm build
RUN npm upload:assets
CMD ["pm2", "start", ".....json"]

在这个项目里应用,会发现,npm install的速度非常慢,因为要安装的npm包往往非常多,其次就是上传图片资源,因为如果我们不缓存已经上传过的图片的话,我们每次都会上传所有的图片,那么时间又会大幅度增加。再加上本来就比较长的打包时间,用这个方案构建应用,我们花了15~30分钟。这实在是太长了,所以我们需要优化这个方案来适应一个前端项目,总结了一下我们的问题主要是缓存问题,因为没有任何的缓存,所以我们需要每次都下载npm包,每次都要上传所有的图片资源,所以我们需要缓存住一些文件。所以思考过之后,我觉得应该把下载依赖和打包的事情放在运行的时候,所以Dockerfile就变成了这么简单

FROM node:6.10.2
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
ADD . .
CMD ["sh", "start.sh"]

然后将其余的部分放到了start.sh的脚本中,在运行的时候添加了node_modules和上传图片缓存的宿主机映射,经过这次的改进,在构建的时候几乎是秒构建,在运行的时候,几乎只存在一个打包的时间和少量上传资源的时间,加起来也就不到5分钟。

完成了这个项目之后,又来了一个新的挑战,现在的目标项目也是一个node 项目,前端上会分成若干个项目,和上面那个不痛,这些前端项目结构都差不多,并且都可以独立打包,而且这个项目之后会有越来越多的这这样的前端项目往里塞,如果使用和上面那个项目相同的构建和运行方式,之后的打包时间会不停增长,可能会产生不可预期的问题。所以我们需要改变策略。

针对这个项目,我们把项目拆分开来,node服务器只会docker服务器逻辑方面的应用,而每个前端的子项目中,每个子项目也能Docker成一个应用,那么一个前端子项目,只能编译出一些js、css文件和一些图片,怎么变成一个应用呢。对此,参照webpack-dev-server的思想,制作了一个只提供转发功能的服务器来承载并转发页面的请求,将请求转发到对应的node服务器docker应用。这样的修改让应用的逻辑更加清晰,独立了各个应用,但是也让服务变得更加零散,难以管理,之后可能会配合docker-compose和nginx整合成一个大型应用来处理这样的问题,待问题解决之后,再来补充。