本文讲的是在Docker中创建应用,【编者的话】下面内容是在基于Docker,用node.js开发和部署网络应用过程中获得的经验和教训。

本例中,将从头开始开发一个基于Docker的socket.io聊天例子,一直到可以实用,因此希望可以从这些教训中学到什么,例如:

  • 使用Docker开始一个节点应用
  • 不要做“root”敢死队成员
  • 用binds使得test-edit-reload流程更短
  • 在容器内管理node_modules使得重建更快(有一个窍门)
  • 使用npm shrinkwrap确保可重复使用
  • 开发和生产团队共享同一个Dockerfile

本文默认读者已经对Docker和node.js有一定熟悉程度。如果需要了解Docker,可以参见这篇介绍文章。

开始

我们会从头开始,最终的代码可以从github获得, 其中从头到尾都有每一步的标签。这里是第一步的代码,如果感兴趣可以访问。

如果没有Docker,就需要在节点上安装系统以及其他依赖包,并且运行npm init来创建新包,这种方法并非不好,但是如果我们使用Docker的话,可以学习更多东西,(当然,使用Docker最重要的原因在于不需要在节点上安装所有东西)。我们会以生成“bootstrapping container”作为开始,然后为应用设置npm包。

需要生成两个文件,Dockerfile和docker-compose.yml,后续我们会加入更多内容。bootstrapping Dockerfile内容如下:

FROM node:4.3.2RUN useradd --user-group --create-home --shell /bin/false app &&\
npm install --global npm@3.7.5
ENV HOME=/home/app
USER app
WORKDIR $HOME/chat

这个文件很短,但是都很重要:

  1. 第一行,从最新long term support(LTS)官方发行版docker image开始。我倾向于指定一个特定版本,而不是一个浮动的标签,例如node:argon 或者 node:latest,因此如果别人在一台不同设备生成此image时,会得到相同版本,而不是因为版本升级带来版本变化。
  2. 创建一个普通用户,起名叫app,在容器中运行此app。如果不这样,容器内进程将会以root运行,违反了安全最佳实践。需要Docker指南为了简单跳过这一步,虽然我们会做一些额外的工作,但是却很重要。
  3. 安装最新版本npm,最近npm变化很大,特别是npm shrinkwrap,以后shrinkwrap将会变化很大。同时,最好在Dockerfile中指定版本,以避免升级带来的麻烦。
  4. 最后,我们在一个RUN内部嵌入了两条命令,减少了生成映像的层数。本例中,并不很明显,但是不使用太多层是一个好习惯,可以减少占用磁盘空间,节省下载时间;另外下载,解压缩,创建,安装以及清理时都可以一步完成,而不需要每一步都保存每层的增量部分。

下一步生成bootstrapping compose文件: docker-compose.yml:

chat:
build: .
command: echo 'ready'
volumes:
- .:/home/app/chat

文件定义了一个简单服务,从Dockerfile创建。到这一步只是echo “ready”然后就退出。volume这一行:/home/app/chat,告诉docker挂载应用目录,将主机上的 . 目录挂载到容器内部/home/app/chat目录,因此任何主机上变化会自动在容器内部出现,反之亦然。对于保证开发test-edit-reload周期尽量短非常重要。然而当npm安装依赖包时候会出现一些问题,我们后面会谈到。

现在,我们可以继续了。运行docker-compose时,docker会生成如Dockerfile中指定的映像,用此映像启动一个容器,运行echo命令,意味着所有配置工作一切正常。

$ docker-compose up Building chat Step 1 : FROM node:4.3.2 ---> 3538b8c69182 ... lots of build output ... Successfully built 1aaca0ac5d19 Creating dockerchatdemo_chat_1 Attaching to dockerchatdemo_chat_1 chat_1 | ready dockerchatdemo_chat_1 exited with code 0

启动此映像生成一个容器,在其中运行交互式shell,运行初始包文件:

$ docker-compose run --rm chat /bin/bashapp@e93024da77fb:~/chat$ npm init --yes... writes package.json ...app@e93024da77fb:~/chat$ npm shrinkwrap... writes npm-shrinkwrap.json ...app@e93024da77fb:~/chat$ exit

在主机上可以看到如下结构,可以commit此版本内容:

$ tree
.
├── Dockerfile
├── docker-compose.yml
├── npm-shrinkwrap.json
└── package.json

可以访问Github上的代码 。

安装依赖包​

下一步是安装应用的依赖包。我们希望通过Dockerfile在容器内部安装,因此当第一次运行docker-compose up时,应用就是可用状态。

为实现此功能,需要在Dockerfile中运行npm install,在此之前,需要得到package.json和npm-shrinkwrap.json文件,他们会被读入映像内。改变如下:

diff --git a/Dockerfile b/Dockerfileindex 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/*
++RUN chown -R app:app $HOME/*+​ USER appWORKDIR $HOME/chat+RUN npm install

同样,很小的改变,但要点如下:

  1. 不仅是打包文件,还可以COPY主机上所有应用目录到$HOME/chat,后面我们将会看到此时只拷贝必要文件将会在docker build时节省时间,其它可以在运行完npm install之后copy,这其实是更好利用docker build的分层cache功能。
  2. 通过COPY命令进入容器内的文件在容器内属主是root,也就是说普通用户app不能读写他们,因此使用chown改变文件属性(当然如果可以在USER app之后做copy操作,而使得文件属主是app用户是最佳方案,但是现在还不是时候。)
  3. 最后,在运行npm instsall之前多加了一步,使得以用户app权限运行,安装所有依赖包到容器的$HOME/chat/node_modules目录下。(另外,可以添加npm cache clean移除安装时下载的tar文件;然而在重建image时并没有帮助,只是增加空间而已。)

最有一点,当开发使用此映像时会引起一些麻烦,因为绑定了在容器内部$HOME/chat到主机的应用目录。不幸的是,node_modules目录在主机上并不存在,这一绑定实际上“隐藏”了我们安装的node modules。

node_modules Volume 窍门

有几个方法可以解决此问题,但是最优雅的解决办法应该是使用一个内置包含node_modules的卷。为了实现此目的,需要在docker compose文件末尾加一行如下:

diff --git a/docker-compose.yml b/docker-compose.ymlindex 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}}} 尽管一点点变化,但是牵涉和后台很大变化:

build过程中,npm install安装依赖包(下一节中添加)到映像内的$HOME/chat/node_modules 目录下,我们在影像中用蓝色标识出来: {{{~/chat$ tree # in image . ├── node_modules │   ├── abbrev ... │   └── xmlhttprequest ├── npm-shrinkwrap.json └── package.json
当使用compose文件从映像启动容器时,docker首先将从主机将应用目录绑定到容器内的$HOME/chat目录下,用红色标识如下:

~/chat$ tree # in container without node_modules volume
.
├── Dockerfile
├── docker-compose.yml
├── node_modules
├── npm-shrinkwrap.json
└── package.json

但是映像内的node_modules被绑定隐藏了;容器内部,我们只能看到主机上空的node_modules目录。

然而,通过上面的改变,Docker接下来会创建一个卷,包含$HOME/chat/node_modules在影像中,并被挂载到容器中,再次覆盖了主机上绑定的node_modules.

~/chat$ tree # in container with node_modules volume
.
├── Dockerfile
├── docker-compose.yml
├── node_modules │   ├── abbrev ... │   └── xmlhttprequest├── npm-shrinkwrap.json └── package.json​

我们所期望的都实现了:主机上的源文件都被绑定到容器,使得更快改变内容;容器内运行应用的依赖包也可用。(备注:这些卷中依赖包到底存放在哪里呢?简短说,存放在主机上由docker管理的独立目录中,参见docker文档中关于volume部分。)

打包安装和Shrinkwrap

重新生成映像,生成安装包。

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

chat 应用需要特定4.10.2版本,因此需要npm install后用--save选项将依赖包保存到package.json,更新npm-shrinkwrap.json。

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

注意,一般并不需要声明确切版本,只运行npm install --save express就可以使用最新版本,因为package.json和shrinkwrap中持有下次build运行时的依赖包名称。

使用npm shrinkwrap的原因如下:尽管可以在package.json中更新直接依赖包的版本,但是并不能更新一些松散依赖包的版本,这就意味着未来生成映像时,(如果不使用shrinkwrap)并不能保证拉下来的是同一版本的依赖包,使得应用出错。这种问题经常出现,因此提倡使用shrinkwrap,如果熟悉ruby的dependency manager,npm-shrinkwrap.json功能跟Gemfile.locl是类似的。

最后,并不耗费任何多余的东西,因为容器就是在最后docker-compose run才运行,安装的模块消失了。但是下次运行docker build时,docker会检测出package.json和shrinkwrap发生变化,必须重新运行npm install,这非常关键。所需要的包将会被安装到影像中:

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

可以访问Github上的代码。

运行应用

最后终于可以安装有应用了,生成index.js 和 index.html,如前所示,运行npm install --save安装socket.io包:

在Dockerfile中,可以告诉docker当使用映像启动容器时运行什么命令,例如node index.js,从docker compose文件中移除占位命令(dummy command),docker会从Dockerfile中运行这条命令。最后,告诉docker compose在容器中暴露3000端口给主机,以从浏览器中访问:

diff --git a/Dockerfile b/Dockerfileindex 9cfe17c..e2abdfc 100644--- a/Dockerfile​+++ b/Dockerfile​@@ -11,3 +11,5 @@ RUN chown -R app:app $HOME/*​ USER app WORKDIR $HOME/chatRUN npm install+
+CMD ["node", "index.js"]diff --git a/docker-compose.yml b/docker-compose.ymlindex 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
最后需要重新build一次,就可以运行docker-compose了

$ docker-compose build
... lots of build output$ docker-compose up
Recreating dockerchatdemo_chat_1
Attaching to dockerchatdemo_chat_1
chat_1 | listening on *:3000

然后,(如果运行在Mac上,需要做一些端口转发工作将端口3000数据从boot2docker虚机转发到主机)可以通过浏览器访问: http://localhost:3000.

可以访问Github上的代码。

​DevProd环境下的Docker

现在,开发环境应用运行在docker compose下,很酷的事情,下面看看其他可行的步骤:

如果想将应用部署到生产环境,很明显需要将应用源码生成到映像中,实现此功能,需要在执行完npm install后将应用目录拷贝到容器中,这样只有当package.json或者npm-shrinkwrap.json发生改变,docker将只重新运行npm install,而改变源文件并不会重新运行。注意,我们还需要解决以root权限拷贝文件的问题:

diff --git a/Dockerfile b/Dockerfile
index e2abdfc..68d0ad2 100644--- a/Dockerfile+++ b/Dockerfile@@ -12,4 +12,9 @@ USER app WORKDIR $HOME/chat
RUN npm install+USER root
+COPY . $HOME/chat
+RUN chown -R app:app $HOME/*
+USER app
+ CMD ["node", "index.js"]

现在我们可以独立运行此容器,不需要从主机挂载任何卷,docker compose可以从多个compose文件中编辑避免代码重复,但是因为应用很简单,我们可以加入第二个compose文件,docker-compose.prod.yml,将应用运行在生产环境。​

chat:
build: .
environment:
NODE_ENV: production
ports:
- '3000:3000'

运行应用在生产模式:

$ docker-compose -f docker-compose.prod.yml up
Recreating dockerchatdemo_chat_1
Attaching to dockerchatdemo_chat_1
chat_1 | listening on *:3000

同样可以指定开发容器,例如,应用运行在nodemon状态,当源文件发生改变时,容器自动重装(reload)(注意,如果运行在Mac上,可能不会工作的很好,因为virtualbox共享目录和inotify集成的不太好)。容器内运行npm install --save-dev nodemon,重新生成(rebuilding),可以覆盖默认生产命令:node index.js,在容器中可以更好调整开发配置:

diff --git a/docker-compose.yml b/docker-compose.yml
index e7bd11e..d031130 100644--- a/docker-compose.yml+++ b/docker-compose.yml@@ -1,5 +1,8 @@ chat:
build: .+  command: node_modules/.bin/nodemon index.js
+  environment:
+    NODE_ENV: development   ports:- '3000:3000'
volumes:

注意,必须给nodemon全路径,因为nodemon作为npm依赖包被安装;可以配置npm脚本运行nodemon,但是我运行时有一些问题。容器运行npm脚本会花大约10秒钟shutdown(默认超时),因为npm并不从docker转发TERM信号给实际进程。所以最好直接运行命令行(这个问题应该在npm3.8.1+,因此很快就可以使用npm脚本了)。

$ docker-compose up
Removing dockerchatdemo_chat_1
Recreating 3aec328ebc_dockerchatdemo_chat_1
Attaching to dockerchatdemo_chat_1
chat_1 | [nodemon] 1.9.1
chat_1 | [nodemon] to restart at any time, enter `rs`
chat_1 | [nodemon] watching: *.*
chat_1 | [nodemon] starting `node index.js`
chat_1 | listening on *:3000

指定docker compose文件可以使同一个Dockerfile和映像用在不同环境中,尽管不是最节省空间的,因为我们可能在生产环境中安装开发依赖包,但是我想这是dev-prod开发模式中性价比最好的方法。就如哲人所说“‘test as you fly, fly as you test.’

$ docker-compose run --rm chat /bin/bash -c 'npm test'npm info it worked if it ends with ok
npm info using npm@3.7.5
npm info using node@v4.3.2
npm info lifecycle chat@1.0.0~pretest: chat@1.0.0
npm info lifecycle chat@1.0.0~test: chat@1.0.0> chat@1.0.0 test /home/app/chat> echo "Error: no test specified" && exit 1Error: no test specified
npm info lifecycle chat@1.0.0~test: Failed to exec test script
npm ERR! Test failed.  See above for more details.

(提示:运行npm 时带--silent参数可以不输出多余部分)

可以访问Github上的代码。

结论

  • 本文中我们使一个应用运行在基于Docker的开发和生产系统中,真酷!
  • 我们跳过了主机安装的一些步骤这样直接进入节点环境配置,希望不会造成困难,因为这只需要做一次。
  • npm将依赖包安装到挂载卷子目录下使得我们的方案实现相对麻烦一些,(其它解决方案,例如ruby的bundler,将依赖包安装到其它路径下)但是我们可以通过内置卷的技巧解决这个问题。
  • 这只是一个很简单的应用,后续会有很多基于此应用的讨论,其思路将涵盖:
    • 架构项目与多服务基础之上,例如一个API,一个worker和一个静态前端。一个大repo看起来比管理每个服务各自的repo更容易,但是却引入了其它的复杂性。
    • 使用 npm link减少服务间共享代码包
    • 使用docker代替其它生产环境下日志管理和进程监控* 状态和配置管理,包括数据库迁移

如果到这里,也许可以在twitter上follow我,或者在overleaf上寻找一份工作。​

原文链接:Lessons from Building a Node App in Docker(翻译:杨峰)

原文发布时间为:2016-05-04 
本文作者:hokingyang
本文来自云栖社区合作伙伴DockerOne,了解相关信息可以关注DockerOne。
原文标题:在Docker中创建应用

在Docker中创建应用相关推荐

  1. Docker中创建MySQL容器,将宿主机目录直接挂载到目录

    Docker中创建MySQL容器,将宿主机目录直接挂载到目录 1.在Docker中下载MySQL镜像 docker pull mysql:5.7.25 2.创建目录/tmp/mysql/data和/t ...

  2. 在Docker中创建CentOS容器

    在Docker中创建CentOS容器 前提 镜像准备 运行并保存容器 再次运行容器 前提 前提是机器上安装了docker,并运行了docker服务.本人为图方便(没钱买服务器,懒得装虚拟机),使用的操 ...

  3. Docker中创建nginx容器出现docker: Error response from daemon: driver failed programming exter...解决

    使用nginx.conf配置文件创建nginx容器时出现: 解决: 根据出现的错误查找相应端口进程 netstat -apn | grep 80 找到后杀死进程 kill -9 954 重新启动Doc ...

  4. docker中创建RabbitMQ并在管理端界面打开

    windows 下安装docker: https://blog.csdn.net/weixin_42338555/article/details/81979504 1.拉取rabbitmq镜像 doc ...

  5. docker中创建Jmeter及在外部使用JMeter-Server控制

    Jmeter分布式测试环境中有两个角色:Master和Slaves Master节点:向参与的Slaves节点发送测试脚本,并聚合Agent节点的执行结果,部署一台 Slaves节点:接收并执行Mas ...

  6. docker中创建MySQL及在外部使用Navicat连接

    1.拉取镜像 $docker pull mysql 2.创建并启动一个mysql容器 docker run --name mysql -e MYSQL_ROOT_PASSWORD=123456 -p ...

  7. docker中创建redis及在外部使用rdm连接

    1.下载镜像 docker pull redis 2.启动redis docker run -d -p 6379:6379 --name myredis redis 3.连接Redis 打开rdm,填 ...

  8. (二)Docker中以redis.conf配置文件启动Redis

    一.准备工作 1.创建两个目录:/redis和/redis/redis01/data 我的是redis分布式集群,有多个redis,目录结构如下: 2.复制redis.conf到/redis目录下:去 ...

  9. docker中容器与容器之间通讯

    概述 Docker 中存在多个容器时,容器与容器之间经常需要进行通讯,例如nacos访问mysql,redis集群中各个节点之间的通讯. 解决方案 Docker 中容器与容器之间进行通讯的解决方案一般 ...

  10. 在docker中编译tor 源码

    在docker中编译tor 源码 前言 一.docker中创建自定义镜像及容器 1. 创建镜像的目录并拉取Ubuntu16.04镜像: 2.书写Dockerfile,并build构建镜像 二.编译to ...

最新文章

  1. 三维重建【三】-------------------(三维重建资料收集)
  2. linux 打印函数宏,linux内核中的嵌入式汇编宏函数
  3. 基于stm32f107 stm32cube 和 LWIP 协议实现 udp 组播通信
  4. ELK之ElasticSearch快速入门
  5. 链接静态库的顺序问题
  6. SAP之成本中心类型与功能范围
  7. 详解 Java NIO
  8. pytorch的4种边界Padding方法--ZeroPad2d、ConstantPad2d、ReflectionPad2d、ReplicationPad2d
  9. 【python】socket编程常量错误问题-1 'AF_INET'
  10. HttpClient 4.3学习笔记
  11. Linux 开源 ssh 工具,【原创开源】jssh linux scp ssh 免密登录工具
  12. 【转载】微信公众平台发展趋势猜想
  13. 喜羊羊与灰太狼java_喜羊羊与灰太狼之懒洋洋风波
  14. JAVA毕业设计家用电器销售网站计算机源码+lw文档+系统+调试部署+数据库
  15. HTML之设置背景、边框、边距和补白
  16. C99/Cpp 使用printf 时format大全
  17. python3批量查询域名权重、标题
  18. python编程 | pdf转excel的python方法
  19. python爬虫_抓取瓦片图片信息并将其拼接_以mapbar为例(适用交通工程类专业)
  20. 配置IPSG防止主机私自更改IP地址上网(动态绑定)

热门文章

  1. 模拟双色球系统判断中奖情况
  2. 学术文献也有身份证?
  3. NIVIDIA 硬解码学习1
  4. CPC认证的常规测试项目
  5. Hibernate 的检索策略
  6. 现在气传导耳机什么牌子最好?性价比超高的气传导耳机推荐
  7. 深入了解示波器(三):示波器的带宽
  8. 浅谈选择示波器时的“5倍法则”
  9. autojs遍历当前页面所有控件_移动端控件(一)-弹窗(Alert/Dialog)
  10. TensorFlow2.0 学习笔记(五):循环神经网络(RNN)