一文学会用 Docker 和 Docker Compose 部署 Node.js 微服务

后端业务逻辑一般比较复杂,全堆在一个 http 服务里不太现实,所以基本都会用微服务架构来开发了。

比如这样:

7c112c4f1b61ecdca2f7afb4fb0aaa2a.png

把不同模块的业务逻辑拆分到不同微服务里,然后它们和主服务通过 tcp 通信,最终由主服务返回 http 响应。

比如我用 nest 开发的一个微服务项目(具体开发过程见上篇文章):

两个微服务分别监听了 8888 端口和 9999 端口:

6259f8ede9a7c5696bfe37571986496e.png

用 yarn start 把它们跑起来:

5e6339c9911347e5f726d1d040daefc1.png

主服务里注册了这两个微服务:

857b60d7360c82804ef95f5074ce09a1.png

它监听了 3000 端口,同样用 yarn start 把它跑起来:

6d4e5c151690b0c7bdccac04e2e3db73.png

计算的微服务里有一个求和的逻辑:

c2e9021ea2ca453e1ce75f6776f4a54e.png

日志的微服务里有一个打印日志的逻辑:

9378a572fc1df2c9e9a983645cc677e9.png

主服务里接受参数,然后把它传给两个微服务:

932284678f9022eb68b77ffddffe1476.png

跑起来效果是这样的:

e7e5ddb8325ef94b74e146ac91ce02d4.png

返回的是求和的结果,并且日志微服务做了打印:

20cce9f5f8eaa04d1cd51750268db593.png

这说明微服务和 http 服务开发成功了。

那问题来了,开发完以后怎么部署到线上呢?

其实部署过程说起来也简单,就是执行 npm run build,然后把产物 dist 目录放到服务器上。

比如这个:

5eaaafc7a03400d67819c28d36562ce7.png

然后用 node 跑起来:

4cb01374ab852bd3f0a1ae90d8aef38c.png

但一般我们不会直接这么搞,而是会使用 docker 来做,因为这样手动搞的话每个服务都要这样来一次,太麻烦了,而且容易出错。

docker 可以通过 Dockerfile 把构建和运行流程封装起来,还可以把运行环境也封装到镜像里,这样每次部署只需要重新构建镜像,然后服务器把镜像拉下来跑就行。

比如 main 服务的 Dockerfile 我们会这么写:

FROM node:alpine as development

WORKDIR /usr/app

COPY package.json ./

RUN npm install

COPY . .

RUN npm run build

FROM node:alpine as production

WORKDIR /usr/app

COPY package.json ./

RUN npm install --only=production

COPY . .

COPY --from=development /usr/app/dist ./dist

CMD ["node", "dist/main.js"]

一行行来看:

FROM node:alpine as development

这一行是继承 node 基础镜像的意思,as 后面是给它起个名字。

WORKDIR /usr/app

把容器内的当前目录设置为 /user/app

COPY package.json ./

把宿主机的 package.json 复制到容器当前目录,也就是 /user/app 下。

RUN npm install

package.json 复制过去了,自然就可以在容器内安装依赖了。

COPY . .

然后再把其余的内容都复制过去。

这里可以加个 .dockerignore 文件来排除 node_modules 的复制

07195ff4011a47f67ded16930fb1c06a.png
RUN npm run build

复制完之后执行 npm run build,就在容器内生成了 dist 目录。

FROM node:alpine as production

为啥用重新创建了个镜像呢?

这是因为 build 完之后我们就只需要 dist 目录了,其余的源码啥的都不需要,自然可以在一个新容器里,然后把上个容器的 dist 目录复制过去。

WORKDIR /usr/app

COPY package.json ./

RUN npm install --only=production

COPY . .

然后同样是设置当前目录,复制 package.json,执行 npm install,然后复制其它文件。

这里 npm install 加个 --only=production 可以只安装 dependecies 下的包。

怎么复制呢?还记得我们 as 后面指定了一个名字么,就通过那个来指定从上个容器复制:

COPY --from=development /usr/app/dist ./dist

其实这种叫做分阶段构建,不然你要写两个 Dockerfile 才行。

最后指定容器运行起来的时候执行的命令,也就是 node dist/main.js 把这个服务跑起来:

CMD ["node", "dist/main.js"]

有了这个 Dockerfile 就可以通过 docker build 命令生成 docker 镜像了:

docker build -t main-app .

这行命令的意思就是从 . 目录下的 Dockerfile 来构建一个 docker 镜像,名字是 main-app。

它会一层层构建,我们刚好 14 行命令:

0632050d2571cc49652a7cc38026ecf4.png

构建完可以看到这个镜像的 hash。

当然,在 docker desktop 里也可以看到:

f0640846d2c42023f1d6e528211990b4.png

这里用到的 docker desktop 从官网下载就行:

68e6c4145cae46ba9dd85940eb886ed9.png

它除了会安装 docker 桌面端以外,也会同时安装 docker 和 docker-compose。

然后把它跑起来:

docker run -p 3000:3000 main-app
f71eea1e486a68d4a86b3b411ce759f1.png

-p 是端口映射的意思,也就是把宿主机的 3000 端口映射到容器的 3000 端口,这样宿主机就可以访问 3000 端口的 http 服务了。

0c2ad231ac1ad98024130c1d1c14fcf2.png

确实可以访问,只不过报了 500,因为两个微服务还没起嘛。

然后我们写下 log 微服务的 DockerFile:

FROM node:alpine As development

WORKDIR /usr/app

COPY package.json ./

RUN npm install

COPY . .

RUN npm run build

FROM node:alpine as production

WORKDIR /usr/app

COPY package.json ./

RUN npm install --only=production

COPY . .

COPY --from=development /usr/app/dist ./dist

CMD ["node", "dist/main.js"]

一毛一样,就不解释了。

然后执行

docker build -t ms-log .

同样,是用当前目录的 Dockerfile 构建一个名字为 ms-log 的镜像:

e1c38e17d1f3938e6e1f8cb02d74bd95.png

然后用

docker run -p 9999:9999 ms-log

把这个容器跑起来,映射容器内的 9999 端口到宿主机的 9999 端口。

849127b8fc3e08c653671c1777a10a95.png

还有一个计算微服务,我们同样这么搞,就不展开了。

这时候你就可以在 docker desktop 里面看到这三个 image(镜像):

34ba0e673709f932d19f744769e1b55a.png

还有它们仨跑起来的 container(容器):

a618899bbbe74fe6117fcfdf0f82f4ca.png

这时候你浏览器访问一下 locahost:3000

30d25a8d7a85281d0b69500be67e4c13.png

你就会发现返回了计算的结果,日志微服务容器内也打印了日志:

b5b3c380537804955eddc29df80aef98.png

在 docker desktop 里看更方便一些:

9e157627bcfed260790b30d53b5ca63f.gif

至此,我们 docker 部署 node 微服务就成功了!

其实有一点比较重要的我前面没说,这里提一下:

两个微服务要起服务的时候要指定 0.0.0.0 这个 ip:

76e8031e8d73aefdb7781f85d3f374d1.png

这涉及到 0.0.0.0 和 127.0.0.1 的区别:

127.0.0.1 和 localhost 一样,都是只本机地址。

0.0.0.0 不是一个 ip 地址,它指代的是本地所有网卡的 ip。

这里如果用默认的 localhost,那服务只在容器内生效,要指定 0.0.0.0 才行。

再就是主服务里访问这两个微服务的时候要用宿主机 ip 地址访问:

a82b531c491edfde92c9b0d872daeb9a.png

同样是因为访问的是宿主机的 ip 的那个服务。

有的同学可能会问,跑三个就执行三次 docker build 和 docker run,也太麻烦了吧,要是我有 10 个微服务,之间还有先后顺序的要求呢?

没错,这样确实比较麻烦,所以有了 docker compose。

这个也是 docker 自带的工具。

我们在根目录写这样一个 docker-compose.yml 的文件:

services:
  main-app:
    build:
      context: ./main-app
      dockerfile: ./Dockerfile
    depends_on:
      - ms-calc
      - ms-log
      - rabbitmq
    ports:
      - '3000:3000'
  ms-calc:
    build:
      context: ./micro-service-calc
      dockerfile: ./Dockerfile
    ports:
      - '8888:8888'
  ms-log:
    build:
      context: ./micro-service-log
      dockerfile: ./Dockerfile
    ports:
      - '9999:9999'
  rabbitmq:
    image: rabbitmq
    ports:
      - '5672:5672'

这个还是比较容易看懂的。

分别指定了 main-app、ms-calc、ms-log、rabbitmq 的 dockerfile 的地址以及端口映射。

而且通过 depends 指定了先后顺序。

这样只要跑一次 docker-compose up 就可以把它们全部跑起来。

(rabbitmq 那个只是用来测试的,其实没用到)

特别要注意 context 的配置,这个是指定路径的基础目录的,比如这个 package.json:

72d833db94cf61012129a24de880a59d.png

加上 context: ./micro-service-log 那就是 ./micro-service-log/package.json。

不加找不到路径。

我们把那 3 个容器停掉:

4eda54b97c06aeabc4b02629b9326917.png

执行

docker-compose up

跑起来是这样的:491be4725845f03fc7245d4d8c37d191.png

上面都是 rabbitmq 这个容器的日志。

我们访问下 localhost:3000

26b085ece05ca7e02ef675e7c491f262.png

可以看到 main-app 和 ms-log 的日志:

c274ddcc719d7164d08100bb69f6afbf.png

这是因为 docker-compose 把终端合并了,加了个前缀来区分。

在 docker desktop 也可以看到新跑起来的 4 个容器:

226721895c0cc89a869c0e5389360d48.png

这样 3 个node服务就都跑起来了。感受到 docker compose 的好处了么?

它可以批量创建一批容器,并且指定顺序、参数之类的,也就是容器编排。

总结

我们分别用 docker 和 docker compose 实现了 Node.js 的微服务部署。

dockerfile 里指定宿主机文件到容器内的复制,npm install 以及把 node 服务跑起来的逻辑。

可以使用分阶段构建功能来优化,也就是 from 的时候通过 as 指定一个名字,然后之后再一个 from 重新创建镜像,这时可以从上个镜像里复制文件。

之后执行 docker build 根据 Dockerfile 构建镜像,通过 docker run -p 宿主机端口:容器内端口 把镜像跑起来。

可以通过 docker desktop 来管理,更方便一些。

这里涉及到的 ip 要指定 0.0.0.0 或者具体的宿主机 ip 才行,要注意一下。

但这样三个服务就要跑 3 次 docker 镜像,比较麻烦。

可以用 docker-compose 来做容器编排,指定容器的 dockerfile、启动顺序等等。

这就是用 Docker 或者 Docker Compose 部署 node 微服务的方式,你学会了么?

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

64f2f82dfdf567699a449b74a5eb7cc1.png

猜你喜欢

转载自blog.csdn.net/qiwoo_weekly/article/details/129604936
今日推荐