在Docker中建立Node.js应用的经验与技巧之二

上页

  现在展示安装Docker中Node.js的依赖包,我们还是通过Dockerfile文件来安装依赖包,这样,当我们运行docker-compose up时,一切都会准备好。

首先,我们需要在Dockerfile中运行npm install,但是之前我们需要找到package.json 和 npm-shrinkwrap.json,改变如下:

diff --git a/Dockerfile b/Dockerfile
index c2afee0..9cfe17c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,5 +5,9 @@ RUN useradd --user-group --create-home --shell /bin/false app &&\

ENV HOME=/home/app

+COPY package.json npm-shrinkwrap.json $HOME/chat/
+RUN chown -R app:app $HOME/*
+
USER app
WORKDIR $HOME/chat
+RUN npm install

虽然只是很小的改变,但是有下面重要几点:

  1. 这里我们原本能复制COPY主机上整个应用目录到$HOME/chat, 不只是像现在只复制包文件,后面我们会看到在docker构建是节省时间,只复制我们需要的,再运行npm install复制剩余的,这能利用docker build的层缓存。

  2. 容器中文件COPY命令是由root掌管权限,因此我们普通用户app是无法读写这些文件,这样我们在复制以后使用chown改变它们所有者到app。

  3. 最后一行我们运行 npm install. T这将以app用户运行并安装依赖在容器的 $HOME/chat/node_modules目录下.

最后一点可能引起麻烦,因为我们绑定容器的$HOME/chat与我们主机上应用目录捆绑在一起了,很不幸,node_modules目录在主机不存在,这样绑定有效地隐藏了我们安装的node模块。

 

node_modules卷

我们采取用卷 (use a volume )来对node_modules绑定,在配置文件增加一行如下:

diff --git a/docker-compose.yml b/docker-compose.yml
index 9e0b012..9ac21d6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,3 +3,4 @@ chat:
  command: echo 'ready'
  volumes:
    - .:/home/app/chat
+ - /home/app/chat/node_modules

注意点如下:

1. 在构建时,npm install会安装依赖包到image中的$HOME/chat/node_modules,如下图是image中目录文件结构:

~/chat$ tree # in image
.
├── node_modules
│   ├── abbrev
...
│   └── xmlhttprequest
├── npm-shrinkwrap.json
└── package.json

2.当我们稍后从这个image启动一个容器时,因为这个配置将主机应用目录绑定到容器的$HOME/chat,下面是我们主机的目录文件:

~/chat$ tree # 在容器中没有node_modules卷
.
├── Dockerfile
├── docker-compose.yml
├── node_modules
├── npm-shrinkwrap.json
└── package.json

在容器或image中node_modules目录被bind隐藏了,我们只看到一个空node_modules目录

3.现在我们还没有完成,Docker下一次会创建一个卷,这个卷会在image中包含$HOME/chat/node_modules 一份复制,并且在容器中安装它,这次,反过来,在主机上反而是隐藏了node_modules目录:

~/chat$ tree # 容器中有了node_modules 卷
.
├── Dockerfile
├── docker-compose.yml
├── node_modules
│   ├── abbrev
...
│   └── xmlhttprequest
├── npm-shrinkwrap.json
└── package.json

现在达到我们目地了,我们主机上源文件被绑定到容器中,允许快速改变,依赖包也可以在容器中适用,这样我们能使用它们运行应用了。

包安装和Shrinkwrap

   让我们重新构建image,并且准备安装包:

$ docker-compose build
... builds and runs npm install (with no packages yet)...

聊天应用需要express 4.10.2版本,这样我们安装npm install,使用--save来保存依赖到package.json配置文件中,按如下相应更新npm-shrinkwrap.json:

$ docker-compose run --rm chat /bin/bash
app@9d800b7e3f6f:~/chat$ npm install --save express@4.10.2
app@9d800b7e3f6f:~/chat$ exit

使用npm的 shrinkwrap特性的理由是:当你给你package.json中依赖包升级版本以后,你并不能升级它们的依赖包,这样你就不能保证所有依赖路径上包的版本正确性,因此建议采取shrinkwrap。

最后,当我们一次性运行docker-compose run,下次运行docker构建时,docker会探测到package.json和shrinkwrap变化,那时得再运行npm install,我们需要将这些包安装到image中:

$ docker-compose build
... lots of npm install output
$ docker-compose run --rm chat /bin/bash
app@912d123f3cea:~/chat$ ls node_modules/
accepts cookie-signature depd ...
...
app@912d123f3cea:~/chat$ exit

最终的配置见:code on github.

运行应用

  最后准备安装应用,复制剩余的源文件,有index.js和index.html,我们会安装socket.io包,使用npm instal -save

在我们的Dockerfile中,我们能告诉docker什么命令可以运行启动一个容器:node index.js,然后我们从docker组合文件中移除虚假的命令,最后,告诉docker 暴露容器的3000端口,这样能够浏览器访问:

diff --git a/Dockerfile b/Dockerfile
index 9cfe17c..e2abdfc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,3 +11,5 @@ RUN chown -R app:app $HOME/*
  USER app
  WORKDIR $HOME/chat
  RUN npm install
+
+CMD ["node", "index.js"]
diff --git a/docker-compose.yml b/docker-compose.yml
index 9ac21d6..e7bd11e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,7 @@
  chat:
   build: .
-    command: echo 'ready'
+   ports:
+    - '3000:3000'
  volumes:
    - .:/home/app/chat
    - /home/app/chat/node_modules

最后构建命令:

$ docker-compose build
... 许多构建信息输出
$ docker-compose up
Recreating dockerchatdemo_chat_1
Attaching to dockerchatdemo_chat_1
chat_1 | listening on *:3000

最后表示在3000端口监听了,通过浏览器可以打开http://localhost:3000

Node.js专题