在 Windows 上部署基于 Docker 的 Web 服务器环境进阶 - Dockerfile 和 Docker Compose

概述

在上一篇《在 Windows 上部署基于 Docker 的 Web 服务器环境 - 零基础入门》中,已经对如何安装、使用 Docker 进行了基本的介绍,而在本进阶篇中,将通过一个实际的例子,介绍两个工具 - Dockerfile 和 Docker Compose,将环境的部署、迁移更进一步简化。读者按照教程一步一步走,就能够将一个 NodeJS 服务在 Docker 中运行起来,本文依然非常入门浅显,更多资料还请参考官方文档。

首先需要说明的,是为什么需要这两个工具。

##Dockerfile

如果用虚拟机镜像的方式,将自己的服务器环境移交给别人,需要告诉别人自己的镜像地址,让别人自行下载,但是这样有两个缺点:

  1. 镜像经过多次修改,可能非常大,下载会很慢;
  2. 别人如果需要使用自己的镜像,可能还得再增加一点原来镜像中缺少的个性化配置(例如不同开发机可能绑定了不同的域名,或者有不同的 Client ID、Secret 之类的),而原来的镜像中可能已经写死了。

为了解决这两个问题,Docker 还提供了将镜像打包的过程脚本化的办法,就是 Dockerfile,这样,别人只要拿到一份体积很小的 Dockerfile,稍加需要自定义配置的编辑,即可在本地编译出一份属于自己的镜像。

Docker Compose

现代的 Web 开发越来越复杂,可能依赖着好几个别的服务,如果手工给每个镜像配置目录映射、端口映射将是非常复杂的工作,为了解决这个问题,提出了 Docker Compose 的概念。

一个 Compose 即是一个应用与服务的集合,通过一个文件配置好之后,一行命令即可启动应用和所有依赖服务,在这里可以理解为 Compose 也是一个独立的服务集合容器,在该容器中,应用对服务可以无限制访问,但这些服务对外又是被隔离的,只有自己应用所指定的端口是被开放给 Host,可供开发者访问。

场景

为了简化说明,让读者您更佳好地理解其中的理念和用法,我想通过一个很简单,但很实际的例子来描述如何通过这两个工具,来进行服务器部署。

一个 NodeJS 应用,使用 ES6 语法编写,在 Node 0.12.10 上运行,因为 Node 0.12.10 没有 ES6 支持**(最新版本的 Node 已经有 ES6 语法支持了)**,因此需要在 node 的服务镜像中加入 Babel 6 以提供 ES6 脚本运行支持(Dockerfile 自定义镜像),然后随着业务发展,会增加 MySQL 支持,因此需要该应用加入 MySQL 服务,在启动应用时,MySQL 也需要一起启动(Docker Compose 应用服务集合)。

Dockerfile 自定义镜像

把以下的代码,都保存到同一个目录下即可。

应用代码

server.js - 应用

/* 加载模块以创建 HTTP 服务 */
const http = require('http');

/* 定义服务器将监听的地址 */
const host = '0.0.0.0';        // 因为需要将 Docker 服务对外,因此需要放开 IP 限制。
const port = 8000;

/* 配置 HTTP 模块来响应请求 */
const server = http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello Docker\n');
});

/* 监听主机和端口 */
server.listen(port, host);

/* 在终端上输出启动信息 */
console.log(`Server running at http://${host}:${port}/`);

/* 处理 Ctrl + C 键,当按下时退出 */
process.on('SIGINT', () => {
  console.log("Exiting...");
  process.exit();
});

package.json - 应用配置文件

{
  "name": "docker-example",
  "version": "0.0.1",
  "author": "[email protected]",
  "dependencies": {
    "babel-core": "^6.5.2",
    "babel-preset-es2015": "^6.5.0",
    "mysql": "^2.10.2"
  }
}

.babelrc - Babael 配置文件

{
  "presets": [
    "es2015"
  ]
}

安装应用运行镜像 node 0.12.10

因为 node 最新版本已经到 5.7.0 了,我们需要通过 :0.12.10 tag 来指定 node 版本。

$ docker pull node:0.12.10

与上篇提过的操作系统镜像不同,该镜像不提供 Shell,直接 run 后无法进入 Linux 环境的,只能通过 run 来执行一些该镜像里包含的命令,例如可以看看 node 的版本:

$ docker run -it node:0.12.10 node --version
v0.12.10

同样的,我们可以直接通过 run 安装 Babel6,但是这样安装的仅可供自己的 Container 使用,当然,也可以将安装后的 Babel6 的 Container push 到自己的镜像里,但是小的服务可以这样做,复杂的运行环境可没那么简单,Babel 体积很小,自己从基础的 node 镜像上安装也很简单,因此需要 Dockerfile。

安装应用依赖

这里需要通过 node 镜像中的 npm 命令,为 Node 应用安装上依赖的包。

需要说明的是,Dockerfile 只适合公用的部分,例如可公用的类库;私有的或者应用的部分不能集成到镜像中,比如 package.json 中的 dependencies 都是安装到应用里的本地模块,那些模块如果通过 npm install -g 安装的的话,启动应用时会找不到,所以这里为了应用能运行起来,依然有安装依赖这一步。(使用 npm link 那是另外一个话题了)

依赖的包已经定义在 package.json 中了,而 --no-bin-links 参数是给 Windows 系统使用的,因为文件系统限制,Linux 的 ln 命令不能再 NTFS 文件系统上建立软链接,如果不加的话,会报 Protocol Error 错误。

这里还为 run 增加了一个 -w 参数,意思是切换到该工作目录下,再执行 npm 命令。

$ docker run -v $(pwd):/code -w /code -it node npm install --no-bin-links

此处有八阿哥

node-0.12.10 带的 npm 2.14.9 貌似有 bug,在执行这一步时可能会因为文件名问题出现 EPERM 错误,此时 pull 一下最新版本的 node 镜像,并且重新执行上面命令即可安装成功。

打包自定义镜像

打包镜像需要首先编写 Dockerfile,它定义了新的镜像将从哪儿来,将往哪儿去。

Dockerfile

# 下载镜像
FROM node:0.12.10

# 维护人信息
MAINTAINER XQ Kuang <[email protected]>

# 安装 Babel,这里其实可以执行多次 RUN,以行分隔
RUN npm install -g babel-cli

# 增加一个默认的 .babelrc 配置文件
ADD .babelrc /root/

# 暴露端口以供映射
EXPOSE 8000

然后使用 build 命令进行打包,其中 -t 参数是指定一个 Repository ,这样方便启动 Container,暂定为 xuqingkuang/docker-example,因为该名称可以推送到 Docker Hub,最好起 Docker Hub 上的名字,最后一个参数是 Dockerfile 的存放路径。

$ docker build -t xuqingkuang/docker-example .

docker_build

测试镜像

经过一段时间脚本的执行,新的镜像已经产生了,可以通过 images 检查一下,然后用 babel-node 来执行之前的服务器脚本 server.js,如果可以正常运行,那我们的镜像就打包成功了。

$ docker run -p 80:8000 -v $(pwd):/www -it xuqingkuang/docker-example babel-node /www/server.js

3_docker_run

Docker Compose 连接服务容器

通过 Dockerfile 定制的镜像,我们已经有了个 NodeJS 的应用容器了,但是有一天随着时间发展,该应用容器有了扩展的需求,新的需求是连接 MySQL,并且通过 MySQL 将当前日期打印出来,于是乎代码被进一步扩充。

应用代码

server-mysql.js

/*  加载模块以创建 HTTP 服务和 MySQL 连接 */
const http  = require('http');
const mysql = require('mysql');

/* 定义服务器将监听的地址,以及 MySQL 连接的参数 */
const host = '0.0.0.0';        // 因为需要将 Docker 服务对外,因此需要放开 IP 限制。
const port = 8000;
const mysqlOptions = {
  host     : 'mysql',          // 这里和 docker-compose.yaml 中定义的 mysql 配置一致
  user     : 'nodejsapp',
  password : 'ILoveDocker'
}

/* 创建 MySQL 连接 */
const mysqlConnection = mysql.createConnection(mysqlOptions);
mysqlConnection.connect();

/* 配置 HTTP 模块来响应请求 */
const server = http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  // 查询数据库,取当前时间,并且返回给浏览器
  mysqlConnection.query('SELECT NOW() AS now;', (err, rows, fields) => {
    res.write(`Now is ${rows[0].now}\n`);
    res.end('Hello Docker\n');
  });
});

/* 监听主机和端口 */
server.listen(port, host);

/* 在终端上输出启动信息 */
console.log(`Server running at http://${host}:${port}/`);

/* 处理 Ctrl + C 键,当按下时退出 */
process.on('SIGINT', () => {
  console.log('Closing MySQL Connection');
  mysqlConnection.close();
  console.log("Exiting...");
  process.exit();
});

编写 docker-compose.yaml

Docker compose 的配置文件是个 YAML,其实就是个简单的没有花括号的 Object 结构。

需要特别说明的说明的是 links 段,里面的内容必须和下面的服务名称保持一致,对于服务 Container 而言,可以理解为一个独立的虚拟机,所有端口都开放,应用通过 links 连接它相当于连接一台独立的虚拟机,所以没有端口映射一说。

这里只 link 了一个服务,现实开发中,还可以将更多的服务连接起来。

docker-compose.yaml

version: "2"                                   # 版本
services:                                      # 服务
  web:                                         # NodeJS 应用
    image: xuqingkuang/docker-example          # 源自镜像
    ports:                                     # 端口映射
     - "80:8000"
    volumes:                                   # 目录映射
     - .:/code
    links:                                     # 关联服务映射 - 重点
     - mysql
    command: babel-node /code/server-mysql.js  # 启动后执行命令
  mysql:                                       # 服务名称
    image: mysql                               # 服务镜像
    environment:                               # 服务环境变量
     - MYSQL_ALLOW_EMPTY_PASSWORD=yes
     - MYSQL_USER=nodejsapp
     - MYSQL_PASSWORD=ILoveDocker

启动 Docker Compose

全部准备完成之后, 就可以启动 Docker Compose 了,可以看到它先启动了服务,然后才启动了应用。

同时这里可以看到 Compose 给应用和每个服务都设置了一个单独的名字,事实上服务连接就是依靠这些独一无二的名字连接起来的。

$ docker-compose up

docker_compose_up

测试 Dock Compose

还是连接 Docker Host 的端口,应用本身除了增加了 mysql 读取当前时间,没有任何变化。

我们可以看到,现在的时间,已经被加入到网页了。

docker_compose_test

停止 Docker Compose

按下 Ctrl + C 键即可停止 Docker Compose,可以看出这里与启动时相反,是先停止应用,再停止服务的。

这里有个小技巧,按下一次 Ctrl + C 是“优雅地”等待所有服务完成再退出,再按下一次就“强制地“关闭 Container 了。

Ctrl + C

docker_compose_stop

结尾

Docker 的基本使用两篇就到此结束了,它的应用服务隔离、多版本共存其实非常适合在开发环境的服务器架设中使用,我希望这样的技术能在集团内更广泛地被使用。

本文之所以能完成,要感谢以下几位:

  • 感谢 @大知 指导我使用了 Docker Compose,让我理解了它的理念,如果没有他,那我只能产出上个入门篇了;
  • 还要感谢 @劲叔 和 @零一 率先在组内率先试用 Docker 部署服务,准备开发环境,希望你们能用得开心;
  • 最后要感谢 @澄苍 和整个网站工程部门,也只有这样灵活而且敏感的团队,让我可以在工作之余,去尝试这样的新技术。

欢迎讨论,有任何问题,也可以钉钉我 @释我。

One More 八阿哥

在使用 Docker Toolbox 的过程中,无意中在 @劲叔 那里发现了一个八阿哥,他的代码放在 D 盘上,但在 Docker 中使用 run -v 参数无法映射目录,Docker Compose 中也无法通过 volumes 映射目录。

经过仔细排查,才发现是因为 VirtualBox 中默认只是共享了用户的目录,而其它目录并未共享,导致在 Docker Quickstart Terminal 中映射的目录,在 Docker Host 中根本无法访问。

所以建议大家,请将自己的项目代码放到 %HOME% 自己的用户目录里(OS X 里是 $HOME),这样方便 Docker 使用。

virtualbox_share_folders

版权所有丨转载请注明出处:https://kxq.io/archives/在windows上部署基于docker的web服务器环境进阶-dockerfile和dockercompose