Docker03 Docker部署初步实践
一次迁移到Docker的小尝试。
问题描述
服务A是一个静态博客网站,由Nginx提供HTTP服务(80
端口),代码仓库为GitA。当向GitA中提交新的文件时,会触发Gitlab的Webhook的Push Events,向另一个端口8888
提交一个POST请求。
服务B利用NodeJS,监听了8016
端口,当收到Webhook触发的POST请求后,会进行一系列的动作,拉取GitA中代码,清空文件夹,利用Hexo进行编译,将编译好的文件提供给服务A使用。
现在我的工作就是要将在传统服务器上的这两个服务迁移到Docker上来。由于这个博客的访问量很小,不用考虑太多优化的问题,所以只能算的上是Dokcer部署的“初步实践”。
如果保持原来的代码不做任何修改,也就是需要同时使用Nginx提供静态服务+NodeJS提供监听编译服务,有下面几种方案:
- 方案一:构建两个镜像,手动控制端口暴露
- 方案二:在同一个容器中,通过NPM命令同时启动两个服务
- 方案三:构建两个容器,通过
docker compose
控制端口
如果对现有的代码进行修改,完全使用NodeJS提供静态服务和监听编译服务,那么就有方案四:完全使用NodeJS进程。
相应的代码在我的代码仓库里。
准备工作
准备.dockerignore
文件
.dockerignore
文件和.gitrignore
文件,也就是制作镜像时排除在外的文件
1 | node_modules |
Win7下docker镜像的ip地址
我的工作电脑是Win7系统,使用的是Docker官方提供的ToolBox的工具,工具使用没有问题,但是遇到了一个小坑,开发完了镜像,通过localhost
访问指定端口,无论如何也连接不上。
后来发现是IP的问题,其实Docker一启动时就告诉了我暴露出来的地址了,奈何我自己眼瞎:
通过docker-machine env
这个命令也可以查看分配的IP,其中export DOCKER_HOST
就是docker镜像分配的IP
1 | $ docker-machine env |
所以,应该通过192.168.99.100
加上对应的端口号去访问镜像
准备好之后开始逐个方案进行介绍。
方案一:构建两个镜像,手动控制端口暴露
(1)Nginx容器
在Dockerfile中暴露出80
端口,映射为本机IP的(http://192.168.99.100
)的80
端口。
同时在global.conf
设定转发规则,将访问api
地址的请求转发到本机IP的8016
端口。
最后,在nginx.conf
中设定一些基本的nginx配置项,关键点是daemon off
,将Nginx服务设定为前台方式运行,这是因为在Docker中服务要以前台方式启动。
Docker容器默认把容器内部第一个进程,也就是pid=1
的进程作为Docker容器是否正在运行的依据。如果此程序挂了,那么Docker便会直接退出。当Nginx在后台运行时,Nginx并不是pid
为1
的程序,而是正在执行的bash
。bash
执行完了nginx后便结束了,容器也就退出了。
同理,forever start app.js
后,bash
的pid
为1,此时bash
执行后退出,容器也就退出了。正确的使用方式应该是forever app.js
,保证进程处于前台。
Dockerfile:
1 | FROM nginx:latest |
global.conf:
1 | server { |
nginx.conf:
1 | user www-data; |
然后开始操作:
1 | # 构建镜像 |
运行成功后,可以对当前容器进行操作:
1 | # 进入某个容器的控制台 |
(2)NodeJS容器
在Dockerfile中,暴露出8016
端口
Dockerfile文件:
1 | FROM node:latest |
在packjage.json
文中定义NPM脚本:
1 | { |
然后同样开始构建并运行镜像:
1 | # 构建镜像 |
如果需要去在docker中使用node,那么就没必要去安装pm2等工具了,直接使用node
命令运行脚本,如果你怕你的容器会挂掉,可以加上restart
等相关参数比如docker run .... --restart=always
(3)总结
这种方法比较简单,通过分别构建了两个镜像来实现Nginx和NodeJS服务的共存。
优点是比较简单,并且遵循了一个容器一个进程的最佳实践,充分解耦
缺点是端口的转发实际上是在宿主外层实现的,并且需要手动控制端口暴露,手动向global.conf
中传入宿主的IP地址,实现两个服务的连接。如果服务比较复杂的时候,手动控制的难度和可维护性的难度也会大大提高。
方案二:在同一个容器中,通过NPM命令同时启动两个服务
这个Dockerfile里,我们首先继承自官方的Node镜像,然后通过RUN apt-get update
和RUN apt-get -y -q install nginx
安装Nginx,其他的内容没有什么特殊
Dockerfile:
1 | FROM node:latest |
关键点是在package.json
的scripts
中,我们通过&
来连接两个命令,就可以做到同时启动Nginx和Node
1 | { |
这是因为:
如果是并行执行(即同时的平行执行),可以使用
&
符号。如果是继发执行(即只有前一个任务成功,才执行下一个任务),可以使用&&
符号。 — 《阮一峰 - npm scripts 使用指南》
然后执行容器的构建和运行命令:
1 | docker image build -t web01 . |
也可以通过使用pm2进行守护启动多个进程,我还没有尝试。
总结
这种方案在一个容器中同时启动了Nginx和NodeJS进程,端口的转发实在容器内实现,不再需要传入宿主IP,同时代码不需要有任何改动,还是一种比较经济的实现方法。
方案三:构建两个容器,通过Docker Compose控制端口
Docker Compose是用来管理多容器的Docker应用,Win7下的Toolbox已经自带了Docker Compose,所以直接使用即可。
Docker Compose的使用需要编写docker-compose.yml
:
1 | version: "3" |
上面这个文件定义了两个容器服务。首先明确使用的compose file
版本是3,然后在services
中定义服务。第一个是Ngxin服务。
可以用build
指定对应镜像的Dockerfile的地址。Compose会利用它自动构建这个镜像,然后使用这个镜像启动服务。
这样生成的容器名称是[yml所在文件夹名称]_[服务名]_1
,我把docker-compose.yml
放在了compo
文件夹下,那么上面自动构建的两个容器的名字分别就是compo_nginx_1
和compo_nodejs_1
。
如果镜像已经构建好,可以直接使用,那么就无需使用build
字段,改用image
字段即可,内容是对应的image
的名称。
如果同时指定build
和image
:
1 | build: ./dir |
那么就会在./dir
目录下生成名称为webapp
,标签为tag
的镜像。ports
字段指定容器暴露出的端口。
links
在两个容器之间建立连接,实际上相当于在容器内部改写host文件,nodejs:nodeServer
意味着在Nginx内部使用nodeServer
这个host
时就会指向NodeJS这个container所在的IP地址。这样在Nginx的配置文件中,配置转发规则时就可以这样配置了:
1 | location /api { |
如果直接改写容器的Host文件是不会生效的,而且容器的IP不是固定的,所以采用这种方式就避免了直接在两个容器之间手动关联IP。
docker-compose.yml
文件编写完成后,就可以启动两个容器了。
1 | docker-compose up --build -d |
--build
会强制在运行容器前重新构建镜像,-d
让容器在后台运行。启动之后:
1 | Starting compo_nodejs_1 ... done |
启动成功,访问80
端口会指向index.html
文件,访问/api
会转发到Nodejs监听的8016
端口。
总结
相比直接构建两个容器手动控制端口,使用Compose更加的合理、清晰。
但是官方文档对于links
字段有警告,它属于一个遗留功能,未来将被移除,所以建议使用自定义网络(user-defined networks)来代替links
实现两个容器之间的通信。
但是自定义网络不支持容器间共享环境变量(而links
支持)。如果需要共享环境变量,需要使用volumes
。
方案四:完全使用NodeJS进程
Nginx提供的静态文件服务完全可以有NodeJS实现。我这里面使用了Koa监听端口,koa-static
中间件实现静态服务,同时通过request
实现了请求的转发
对app.js
改造如下:
1 | const path = require('path'); |
Dockerfile基本上没有变化:
1 | FROM node:latest |
然后执行命令,创建并运行容器:
1 | docker image build -t koa01 . |
服务正常开启,访问http://192.168.99.100/
就可以获得静态页面,访问http://192.168.99.100/api
,Koa会将请求转发到容器的8016
端口,执行相应的代码。
总结
这种方法只用了NodeJS,抛弃了Nginx,对于前端来说实现更容易一点,但是也就没有办法享受Nginx易配置、实现负载均衡等功能了,并且需要对代码进行改造。
最终采取的也是这种方案,只不过在此基础上进行了简化,编译的触发不再通过不同端口实现,都是在80
端口上实现,更加简单。
参考
- Nginx and Node.js with Docker
- Using Docker with nginx and NodeJS
- 简书 - Docker多容器部署实践(nginx+node.js)
- Robby - 容器化 Node.js express(Mac)
- Sean’s Notes - Dockerfile指令详解
- medium - Docker Compose 讓 Nginx 與 Nodejs web server共舞
- segmentfault -为什么在docker中服务要以前台方式启动?
- stackoverflow - Error starting node with forever in docker container