在Docker中运行Node.js的Web应用

在Docker环境下搭建了nodejs的Web应用运行环境:

  • node.js
  • MongoDB
  • Redis
  • winston和morgan,日志

以下介绍一下搭建环境的步骤和注意事项。

准备工作

需要安装Docker,我的环境是Ubuntu Serer 14.04虚拟机。

如果直接用:

1
apt-get install docker.io

无法获得比较新的Docker版本。

我参照这里:Docker 1.2 on Ubuntu 14.04.1,安装了Docker 1.2版本。

即,使用Docker官方的第三方Ubuntu源。

加入Docker的GPG Key

1
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9

加入Docker的源:

1
sudo sh -c "echo deb https://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list"

更新包列表:

1
sudo apt-get update

安装Docker

1
sudo apt-get install lxc-docker

重启系统:

1
sudo reboot

如果执行下面命令并看到类似的结果就说明安装成功了:

1
2
$ docker version
Client version: 1.2.0

最简单的通过Docker执行nodejs

执行一个简单的nodejs命令:

1
node --version

不是使用本地的nodejs,而是使用Docker,只需执行:

1
2
$ sudo docker run -it --rm node node --version
v0.10.33

对于第一次运行上面命令,会出现类似:

1
2
3
4
5
6
7
8
Unable to find image 'node' locally
Pulling repository node
63d7e1e1d897: Pulling dependent layers
511136ea3c5a: Download complete
36fd425d7d8a: Download complete
aaabd2b41e22: Download complete
f99c114b8ec1: Downloading [==>
...

Docker本地并没有node的镜像(image),需要到官网(https://hub.docker.com)上查询这个名字的镜像,并下载到本地。

这个过程可能比较漫长,在我这里需要30分钟左右。

总之,下载完镜像(700多MB)后,镜像会启动一个容器(container)。可以把镜像看做Java的类(class),容器看做对象(object)。

这个容器包含一个最小的可运行的轻量级的虚拟机,当然还有nodejs。

说下命令的参数:

1
docker run -it --rm node node --version

其中--it

  • i,容器的标准输入保持打开
  • t,Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入

--rm,运行结束后删除容器。

再后面就是我们要执行的命令。

将Web Application跑起来

首先,要准备一个简单的Web Application。我这里写好了一个简单的:https://github.com/MarshalW/ProtoWebApp/tree/m1

拿到项目文件后,先用宿主的node安装:

1
$ sudo npm install

然后跑起来测试一下,看是否能在浏览器上访问。

下面,是用Docker里的nodejs跑这个Web Application了(在项目的根目录下):

1
sudo docker run --rm -it -p 3000:3000 --name ProtoWebApp -v "$(pwd)":/webapp -w /webapp  node npm start

在这里:

  • -v:分割的路径,前者表示宿主的路径(在这里也就是expressjs项目的主目录),后者表示映射到Docker容器的路径。
  • -w,表示将-v映射的/webapp目录设置为work directory,也就是运行node命令的目录。这个设置将覆盖Dockfiie中的设置:/Data

如果需要让Docker容器跑在后台,可以加上-d

1
sudo docker run --rm -itd -p 3000:3000 --name ProtoWebApp -v "$(pwd)":/webapp -w /webapp  node npm start

另外,如想了解这个镜像都包含哪些内容,可以看这里:Dockerfile/nodejs

日志的处理

运维中需要记录几种日志:

  • HTTP请求日志,为了以后分析访问量等数据时使用
  • 应用日志,可能有错误或者其他调试信息,便于发现错误,排错

HTTP请求日志

很多情况下未必用到这个,因为在nodejs的Web Appp前,可能还有Nginx,用后者做端口代理。

目前的Expressjs,是4.x版本,使用的HTTP日志,是morgan

可以在app.js中看到:

1
var logger = require('morgan');

默认的日志是对接到标准输出上的。

我们希望在生产环境(production)下和开发环境(development)情况下不一样:

  • 生产环境(production):HTTP日志记录到文件
  • 开发环境(development):打印到标准输出

这需要做两件事:

  1. 通过docker命令设置为production
  2. app.jsproduction情况下记录日志到文件中

docker run命令中加入production变量设置:

1
sudo docker run --rm -it -p 3000:3000 --name ProtoWebApp -v "$(pwd)":/webapp -w /webapp -e NODE_ENV=production  node npm start

即,-e NODE_ENV=production

设置保存日志到文件。找到app.js的这行:

1
app.use(logger('dev'));

改为:

1
2
3
4
5
6
7
8
9
if (app.get('env') === 'development') {
app.use(logger('dev'));
}

if (app.get('env') === 'production') {
var fs = require('fs')
var accessLogStream = fs.createWriteStream(__dirname + '/access.log', {flags: 'a'})
app.use(logger('combined', {stream: accessLogStream}))
}

这样,当development模式打印到标准输出,production模式下输出到项目根目录下的access.log文件中。

源代码见这里:https://github.com/MarshalW/ProtoWebApp/tree/m2

应用日志

这个日志是必须要有的,可帮助开发者发现和诊断问题。

使用的是winston

需要将winston加入到package.json中:

1
"winston":""

然后引入库:

1
var winston = require('winston');

再设置文件路径(我这里是app.log):

1
2
3
4
5
6
if (app.get('env') === 'production') {
var accessLogStream = fs.createWriteStream(__dirname + '/access.log', {flags: 'a'});
app.use(logger('combined', {stream: accessLogStream}));

winston.add(winston.transports.File, { filename: 'app.log' });
}

Docker不需要设置什么,就可以在项目的根目录下看到app.log文件了,如果运行没有问题的话。

连接Redis

和nodejs镜像类似,可以通过如下命令将Redis跑起来:

1
$ sudo docker run -d --name redis -p 6379:6379 redis

当Docker本地没有redis镜像的时候,会自动先下载该镜像的最新版本。redis镜像内容见:Dockerfile/redis

然后,我们可以启动Web App:

1
2
3
4
5
6
7
8
9
10
11
12
$ sudo docker run --rm -it -p 3000:3000 --name ProtoWebApp --link redis:redis -v "$(pwd)":/webapp -w /webapp -e NODE_ENV=production  node npm start

> ProtoWebApp@0.0.0 start /webapp
> node ./bin/www

info: Hello again distributed logs

Reply: OK
Reply: 0
Reply: 0
2 replies:
0: hashtest 1
1: hashtest 2

比上面启动nodejs的方式,多了:--link redis:redis,冒号前的redis表示镜像名称,后面的redis表示这里使用的别名。

另外,创建client的代码有点不同:

1
2
var redis = require("redis"),
client = redis.createClient(6379, "redis");

其中"redis"是redis容器的别名。

或者讲究点也可以这样:

1
2
3
var redisHost  = process.env.REDIS_PORT_6379_TCP_ADDR;
var redis = require("redis"),
client = redis.createClient(6379, redisHost);

源代码见这里:https://github.com/MarshalW/ProtoWebApp/tree/m4

连接MongoDB

执行命令,启动mongoDB:

1
sudo docker run -d -p 27017:27017 -v "$(pwd)"/db:/data/db --name mongodb dockerfile/mongodb

数据库文件保存在当前目录下的db目录下,如果不存在目录的话会自动创建。

package.json中加入:

1
"mongoose":""

app.js代码中加入:

1
2
3
4
5
6
7
8
9
10
11
//测试mongoDB
var mongoose = require('mongoose');
mongoose.connect('mongodb://mongodb/test');

var Cat = mongoose.model('Cat', { name: String });

var kitty = new Cat({ name: 'Zildjian' });
kitty.save(function (err) {
if (err) console.log(err);
console.log('meow');
});

执行docker命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ sudo docker run --rm -it -p 3000:3000 --name ProtoWebApp --link redis:redis --link mongodb:mongodb  -v "$(pwd)":/webapp -w /webapp -e NODE_ENV=production  node npm start

> ProtoWebApp@0.0.0 start /webapp
> node ./bin/www

info: Hello again distributed logs

js-bson: Failed to load c++ bson extension, using pure JS version
Reply: OK
Reply: 0
Reply: 0
2 replies:
0: hashtest 1
1: hashtest 2
meow

源代码见这里:https://github.com/MarshalW/ProtoWebApp/tree/m5

以上。