在 Windows 上部署基于 Docker 的 Web 服务器环境进阶 - Dockerfile 和 Docker Compose
概述
在上一篇《在 Windows 上部署基于 Docker 的 Web 服务器环境 - 零基础入门》中,已经对如何安装、使用 Docker 进行了基本的介绍,而在本进阶篇中,将通过一个实际的例子,介绍两个工具 - Dockerfile 和 Docker Compose,将环境的部署、迁移更进一步简化。读者按照教程一步一步走,就能够将一个 NodeJS 服务在 Docker 中运行起来,本文依然非常入门浅显,更多资料还请参考官方文档。
首先需要说明的,是为什么需要这两个工具。
##Dockerfile
如果用虚拟机镜像的方式,将自己的服务器环境移交给别人,需要告诉别人自己的镜像地址,让别人自行下载,但是这样有两个缺点:
- 镜像经过多次修改,可能非常大,下载会很慢;
- 别人如果需要使用自己的镜像,可能还得再增加一点原来镜像中缺少的个性化配置(例如不同开发机可能绑定了不同的域名,或者有不同的 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 .
测试镜像
经过一段时间脚本的执行,新的镜像已经产生了,可以通过 images
检查一下,然后用 babel-node 来执行之前的服务器脚本 server.js
,如果可以正常运行,那我们的镜像就打包成功了。
$ docker run -p 80:8000 -v $(pwd):/www -it xuqingkuang/docker-example babel-node /www/server.js
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
测试 Dock Compose
还是连接 Docker Host 的端口,应用本身除了增加了 mysql 读取当前时间,没有任何变化。
我们可以看到,现在的时间,已经被加入到网页了。
停止 Docker Compose
按下 Ctrl + C 键即可停止 Docker Compose,可以看出这里与启动时相反,是先停止应用,再停止服务的。
这里有个小技巧,按下一次 Ctrl + C 是“优雅地”等待所有服务完成再退出,再按下一次就“强制地“关闭 Container 了。
Ctrl + C
结尾
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 使用。
版权所有丨转载请注明出处:https://kxq.io/archives/在windows上部署基于docker的web服务器环境进阶-dockerfile和dockercompose