开发Nodejs应用通常要使用多个中间件,开发人员要把代码跑起来就要在自己的机器上把中间件安装一遍,费时费力,如果同时开发多个项目就更麻烦了,经常要改来改去。本文以一个Nodejs+MongoDB项目为例,展示Docker的基本使用方法,同时提供了编写对Docker友好代码的方法。
项目说明
tms-api-gw是一个API网关项目,功能是将收到的http请求根据业务规则转发到对应的服务,每次收到的请求都要记录日志,并进行计数,结果保存到mongodb中。
希望通过Docker解决如下几个问题:
- 简化mongodb的部署,方便开发人员在本地运行应用。
- 对nodejs应用进行打包,实现整体发布,方便运维人员部署。
Docker
首先请按照官网说明安装Docker。
参考:https://docs.docker.com/get-started/#install-docker-desktop
使用Docker首先需要了解image
和container
的概念,简单说,image
是运行环境的模版,container
是根据模版创建的实例。image
用Dockerfile
进行定义,可以认为Dockerfile
是一个批处理命令,通过执行命令,在镜像上安装包、复制文件、设置参数。有了image
就可以通过run
命令生成容器,生成的时候可以指定运行时的参数。如果有多个相关联的容器,可以通过docker-compose
进行整体管理。docker-compose
根据编排文件docker-compose.yml
描述被管理的容器,通过docker-compose up
命令启动,docker-compose down
命令关闭,这样就不用对着每个容器单独执行命令。
参考:https://docs.docker.com/reference/
通常我们需要的image
都已经有基础版本,可以在hub.docker.com
上查找。每个镜像通常都有一堆版本,最重要的区别在于image
是在哪个linux的版本上建的,建议使用alpine
,因为这个版本最小。
下面我们结合项目需求具体跑一遍Docker。
MongoDB
查找基础镜像。
docker search mongo
将镜像拉到本地。
docker pull mongo
生成并运行容器。
docker run --rm --name tms-api-gw-mongo -p 27017:27017 -d mongo:latest
--rm
当容器退出时自动删除。
--name tms-api-gw-mongo
指定容器的名字,后面操作容器时用的到。
-p 27017:27017
将容器内部的27017端口映射到主机的27017 端口。
-d
是指定在后台运行。
进入容器查看数据。
docker exec -it tms-api-gw-mongo /bin/bash
在容器中用exit
命令退出容器。
这时在本地就有了个可用的MongoDB实例,数据保存在容器中,每次删除容器,数据就会清除,这样就总能用一个“干净”的MongoDB进行开发。
如果需要持久保留MongoDB中的数据,可以让容器将数据写到本地目录中,执行run
命令时指定参数。
docker run --rm --name tms-api-gw-mongo -p 27017:27017 -v $PWD/storage/mongodb:/data/db -d mongo:latest
-v $PWD/db:/data/db
将主机中当前目录下的db挂载到容器的/data/db,作为mongo数据存储目录。
为了管理容器需要用到几条命令:
docker ps -a #查看全部容器,不加-a参数只显示运行的。
docker stop container_name # 停止指定的容器
docker rm container_name # 删除指定的容器
Docker命令参考:https://docs.docker.com/engine/reference/commandline/cli/
Nodejs
先看看Nodejs官网的这篇文章:https://nodejs.org/zh-cn/docs/guides/nodejs-docker-webapp/,下面是以该文章为基础进行调整。
制作docker镜像。
在项目根目录新建Dockerfile
文件,文件内容如下,和Nodejs官网文章不一致的地方加了注释。
FROM node:alpine
# 安装cnpm
RUN npm install cnpm -g
WORKDIR /usr/src/app
COPY package*.json ./
# 只安装dependencies的包,不安装devDependencies的包;额外安装包。
RUN cnpm install --production \
&& cnpm install log4js
COPY . .
# 创建放配置文件的目录
RUN mkdir config
# 设置应用的环境变量
ENV TMS_API_GW_ENV='docker'
EXPOSE 3000
CMD [ "node", "./app.js" ]
COPY . .
是把本地当前目录下的内容复制到镜像的工作目录下/usr/src/app
,但是,node_modules
,config
这些内容不需要复制,因此要建立.dockerignore
文件,指定不需要复制的内容。
.*
node_modules
config
example
创建镜像,注意不要丢了最后面的点。
docker build -t tms-api-gw-node .
用docker images可以查看已有镜像。
运行容器
docker run --rm --name tms-api-gw-node -p 5678:3000 -v $PWD/config:/usr/src/app/config -d tms-api-gw-node
如果我们同时开发多个项目,经常会发生端口冲突的问题,通过-p
参数就可以在启动容器时指定端口了。
同一份代码需要在多个环境中运行,包括:开发,测试,生产等,我们通常是采用配置文件让代码和运行环境解耦。利用Docker,可以把代码和代码依赖的标准环境制作成镜像,在生成容器时再指定和环境相关的配置文件,这样Docker镜像的整体就变成了一个发布单元,可以极大简化运维工作。因此,在Dockerfile中我们创建了空的config
目录,通过参数-v $PWD/config:/usr/src/app/config
指定使用运行环境本地的配置文件。
前面介绍项目基本情况时提到需要在mongodb中存储api访问数据,当应用和mongodb都在容器中运行时就产生了一个问题:mongodb的地址是什么?
在本地开发环境我们通常写个localhost
,但是容器中的localhost
是容器并不是宿主机,应用无法访问到mongodb。为了解决这个问题,在项目中引入了环境变量,看代码config/gateway.sample.js
。
let host, port
if (process.env.TMS_API_GW_ENV === 'docker') {
host = 'docker.for.mac.host.internal'
port = 3000
} else {
host = 'localhost'
port = 5678
}
module.exports = {
...
}
前面Dockerfile
中指定了环境变量ENV TMS_API_GW_ENV='docker'
,代码中可以根据这个环境变量进行相应的设置,在容器中docker.for.mac.host.internal
代表了宿主机的地址,否则还是用localhost
,指定端口要和Dockerfile
中EXPOSE
的端口一致,这样不论是否在容器中Nodejs应用都可以访问到MongoDB。
如果容器是在后台运行,想查看Nodejs应用输出的日志,使用如下命令:
docker logs tms-api-gw-node
因为镜像是以alpine
为基础制作,进入容器的命令需要调整,将bash
改为sh
。
docker exec -it tms-api-gw-node /bin/sh
至此我们已经可以在容器中运行Nodejs应用。
参考:https://github.com/nodejs/docker-node/blob/master/README.md#how-to-use-this-image
参考:一篇关于Nodejs使用Docker的最佳实践,https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md
docker-compose
虽然已经可以用容器把Nodejs应用跑起来,但是还是不够方便,mongodb和nodejs容器要分别启停,命令还都挺长。能不能更简单呢?可以,用docker-compose。
参考:https://docs.docker.com/compose/compose-file/
Docker for Mac已经包含了Compose了,所以Mac用户不用单独安装Compose了。
创建docker-compose.yml
文件。
version: '3.7'
services:
app:
build: ./
image: tms-api-gw-node:latest
container_name: tms-api-gw-node
ports:
- '5678:3000'
volumes:
- ./config:/usr/src/app/config
depends_on:
- mongodb
mongodb:
image: mongo:latest
container_name: tms-api-gw-mongo
ports:
- '27017:27017'
logging:
driver: none
上面这个文件中指定的逻辑和前面通过run
命令分别运行容器是完全等效的。
启动容器
docker-compose up -d
关闭容器
docker-compose down
更新镜像
docker-compose build
反复更新镜像会导致产生无效的镜像,通过下面的命令删除这些无用镜像。
docker images
<none> <none> cb7a87c0359b 22 minutes ago 170MB
docker rmi $(docker images | grep "^<none>" | awk "{print $3}")
提示:实际在本机写代码时并不需要将Nodejs应用做成镜像再运行,因为这样每次修改代码都要重新build,既花费时间又产生许多无用镜像。这里演示Nodejs应用镜像主要是为了下一步进行发布做准备。
总结
虽然Docker整体比较复杂,但是作为开发人员只需要掌握基本概念和常用命令就可以把Docker跑起来,可以极大简化本地开发环境的管理工作,建议每个开发人员都尝试一下。
下一篇研究如何利用Docker进行Nodejs应用的部署。