在过去的几年中,Docker和Node.js都变得非常流行。对于开发人员来说利用这些新技术来改善自己的开发体验很有必要,而且在此过程中还可以学习新技术。遵循"Coding到老,学习到老,折腾到老"的宗旨,本文我们将介绍将如何结合Node.js与Docker来改善开发人员体验,包括使用docker build和利用Docker Compose来实现无缝的本地前端开发环境。
概述
本文中,我们以Express.js为一个示例展开,需要实现了解一丁点Node.js和npm的基础知识。还要了解Express.js框架的基础知识。
对Docker也要有一定的概念和要会基础操作(不会也没关系,很容易)。
最后本文全程使用Linux(Mac) shell终端命令行。
创建Express.js项目
为了要生成示范应用程序,需要使用Express应用程序生成器。需要运行以下npx命令行:
- npx express-generator --view=pug --git <app-name>
Express生成器将生成Express应用。--view=pub选项表示使用pug视图引擎。--git表示用来给项目添加一个git .gitignore文件。
生成效果如下:
测试Express应用
要测试该应用程序,需要运行npm install安装所有必需的npm模块。然后,运行以下命令以启动应用程序:
- DEBUG=nodejs-docker-express:* npm start
如果没有异常,应该会到一条类似的消息。
- nodejs-docker-express:server Listening on port 3000
上面的命令非常简单:它运行一个环境变量DEBUG=nodejs-docker-express,用来表示服务器进行详细的调试。
对Windows系统,使用的参数要修改为:
- set DEBUG=nodejs-docker-express:* & npm start
现在打开浏览器,在地址栏并输入localhost:3000并访问:
这样示例的Express.js应用就已经在运行OK了。是不是非常简单?有此基本的"Hello,World!"为基础,我们进一步深入。
Docker多阶段构建
容器化应用程序有很多好处:首先,无论运行平台是什么,其行为都相同。借助Docker容器,应用程序可以轻松部署到各个公有容器云(比如AWS Fargate,Google Cloud Run),自建的K8S集群中,甚至本地docker上。
容器化,基础是Dockerfile。Dockerfile是构建Docker镜像的基础。用Dockerfile编译生成的镜像运行时,就称之为容器。
如图示,整个过程非常简单:从Dockerfile构建Docker镜像。运行镜像,得到运行时容器。
Dockerfile
Dockerfile有一些类似命令行的语句:
- FROM node:14-alpine as base
- WORKDIR /src
- COPY package*.json /
- EXPOSE 3000
- FROM base as production
- ENV NODE_ENV=production
- RUN npm ci
- COPY . /
- CMD ["node", "bin/www"]
- FROM base as dev
- ENV NODE_ENV=development
- RUN npm install -g nodemon && npm install
- COPY . /
- CMD ["nodemon", "bin/www"]
通过Docker镜像的分层继承,创建了一个精简的production镜像和一个功能更丰富,以开发为重点的dev镜像。
在Dockerfile中,使用了多阶段构建,整个过程分为三个阶段:base,production和dev。production和dev依赖于base,base为node:14-alpine的基础镜像,该基础镜像需要从DockerHub获取,这是一个官方Alpine基础OS的Node.js官方镜像,主镜像为345MB,Node.js镜像大概不到40M。
- WORKDIR /src
- COPY package*.json /
- EXPOSE 3000
WORKDIR语句设置了Docker运行的工作目录,其后的命令都在该工作目录运行。COPY语句,复制package*.json(package.json和package-lock.json)容器中。
EXPOSE语句,设置Node.js Express Web服务器的监听端口。上述步骤对于开发和生产阶段都是通用的。
现在我们来看看生产目标阶段是如何构建的。
production
在生产阶段,继续从基础阶段开始的工作,FROM语句指示Docker从base开始。ENV语句设置Docker将环境变量NODE_ENV为production。
- FROM base as production
- ENV NODE_ENV=production
- RUN npm ci
- COPY . /
- CMD ["node", "bin/www"]
变量ENV设置为production可以使性能提高三倍,并且提供一些其他优化,比如缓存视图。npm install命令只会安装主要依赖项,忽略开发依赖项。这些设置非常适合生产环境。
接着使用RUN语句运行npm ci而非npm install。npm ci适用于持续集成和部署。和npm install相比,会绕过某些面向用户的功能。当然,npm ci需要一个package-lock.json文件才能工作。
之后,还是使用COPY语句将代码复制到工作目录。
最后使用CMD语句,运行Node应用服务器和/srcbin/www
dev
我们利用了多阶段构建,并在开发阶段添加开发所需的组件:
- FROM base as dev
- ENV NODE_ENV=development
- RUN npm install -g nodemon && npm install
- COPY . /
- CMD ["nodemon", "bin/www"]
大体上和生产极端类似,差异为NODE_ENV环境变量设置为development。
接着,用RUN语句安装nodemon。每当文件更改时,nodemon都会重新启动服务器,从而开发体验更加流畅。同时执行npm install,该命令会递归安装dev依赖项。例如,如果要使用Jest测试应用程序,那将是开发依赖项之一。
请注意,这两个命令通过&&放在一起,创建更少的Docker层,于构建缓存非常有用。这是撰写Dockerfile时候常用的一个技巧。
和生产阶段相同,将代码复制到容器。但是,用nodemon取代了Node服务器,这样在每次文件/src更改时会重新启动它。
.dockerignore
和git的.gitignore一样,docker也使用.dockerignore来忽略不想放入Docker镜像的文件。通过忽略无关的文件更改,它有助于使Docker镜像保持身材,而且能使构建缓存更高效。本示例中.dockerignore
- .git
- node_modules
非常简单,告诉Docker不COPY.git文件夹和node_modules从主机复制到Docker容器。
使用Docker Compose
到目前为止,我们创建一个使用运行Node.js Express应用程序Docker所需的大部分功能。为了更便捷,我们还建议用Docker Compose,这样可以更轻松地使用单个或多个容器运行应用程序。这样也无需要记住很长的命令来构建或运行容器。只需通过:
- docker-compose build
- docker-compose up
但是docker-compose使用yml的配置文件和dockerfile略有不同:
上述,我们指定Docker Compose的版本,在本例中为3.8,对应Docker引擎19.0.3支持的最新版本。这样可以支持多阶段Docker构建。
接着,指定正在使用的服务。在本教程中,只有一个名为web的服务,具有context为当前目录的构建以及一个重要的构建参数target设置为dev。这告诉Docker在dev阶段构建Docker映像。
之后,通过volumes制定 Docker卷。它指示Docker从Docker容器上的./和主机本地/src目录复制和同步更改。当我们在主机中更改文件时,这将很有用,并且文件也将立即反映到容器中。
command语句运行npm run start:dev,start:dev执行内容定义在package.json,内容为:
- "start:dev": "nodemon ./bin/www"
表示使用nodemon启动Web服务器。在开发环境中,可以在每次保存文件时重新启动服务器。
接下来,用ports语句设置docker端口映射主机的3000端口与容器3000端口。在构建容器时,公开了端口3000, Web服务器就会在3000上运行。
最后,设置了两个环境变量。首先,将其NODE_ENV设置为development,因为这样可以看到详细的Debug信息,也没有任何视图缓存。然后,将debug设置为*,让Web服务器打印出所有内容的详细调试消息。
测试应用程序
前面,设置了弄好了基础构建配置文件,接着构建Docker镜像。使用BuildKit优化Docker构建。启用BuildKit可以更快地构建Docker镜像,运行以下命令:
- COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose build
该命令告诉Compose在BuildKit上构建Docker镜像。它应该在一段时间内运行并构建Docker镜像,如下所示:
Docker镜像大约在14秒内构建完成,使用BuildKit可以更快。运行该镜像:
docker-compose up
然后浏览器访问localhost:3000:
这样我们,自配置的应用程序在Docker上已经完美运行。我们来改改源文件,看看效果。
我们修改下源码将" Welcome to Express"更改为" Welcome to Express with Docker"来测试。在源文件目录/src下,找到routes/index.jsline文件,修改语句为:
- res.render('index', { title: 'Express with Docker' });
保存文件,然后可以看到Web服务器已经重新启动,表示Docker卷和nodemon可以都可以正常工作。
F5刷新浏览器,内容已经修改:
总结
本文中我们利用Docker和Docker Compose构建了一个简单的Nodejs的开发和运行环境。Node.js和docker的配合很好。通过使用docker-compose,开发体验更加流畅。当然这这是一个很简单的开始,对于更复杂的应用(比如需要访问数据库)才是Docker Compose的用武之地,他可以同时启动和管理多个容器,比如给开发环境增加Mongo或MySQL添加为应用程序的数据源,只需很轻松地增加一个docker-compose配置的服务语句就可以搞定整个环境。