序言
很多时候我们都有将已有代码迁到容器镜像中的想法,此时,你需要的是一个可重复创建 Docker 镜像的机制,确保创建出来的 Docker 镜像中的代码与你代码库中最新的代码一致。
Dockfile 通过提供一种构建 Docker 镜像的陈述式一致性方法,解决了这类需求。
此外,你有时也想将整个应用程序容器化,这意味着你会集中部署和管理的多个异构容器。
与 Dockerfile 类似,Docker Compose 也是通过这种方法,提供一种定义整个技术栈的方式,包括网络和存储需求。这种方式不仅让构建容器化的应用程序变得更简单,也让应用程序的管理和扩容变得更方便。
本文将使用一个基于 Node.js 和 MongoDB 的 Web 应用程序作为案例,介绍如何使用 Dockerfile 来构建 Docker 镜像。
我们会创建一个自定义的网络,用来让容器间进行通信;同时,我们也会使用 Docker Compose 来启动和扩展容器话的应用。
前提准备
在正式开始之前,你需要:
- 一个 Ubuntu 16.04 的云服务,包括一个非 root 账号和一个防火墙(可以参考 )
- 最新版的 Docker 社区版
第一步:使用 Dockerfile 构建一个镜像
首先,切换到你的根目录,然后通过 Git 命令从 GitHub 的官方 repository 克隆本教程的示例 Web 应用程序。
1 2 3 |
$ cd ~ $ git clone https://github.com/janakiramm/todo-app.git |
以上命令会将示例 Web 应用程序拷贝到一个名叫 todo-app 的新目录下。
切换到 todo-app 目录下,使用 ls 命令查看该目录的内容:
1 2 3 |
$ cd todo-app $ ls |
此目录下包含两个子目录和两个文件:
- app : 存放示例应用程序源代码的目录
- compose :Docker Compose 配置文件所在的目录
- Dockerfile :包含构建 Docker 镜像说明的文件
- README.md :一句话描述示例应用程序的文件
运行 cat Dockerfile 得到以下内容:
~/todo-app/Dockerfile | |
---|---|
FROM node:slim LABEL maintainer = “jani@janakiram.com” RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY ./app/ ./ RUN npm install CMD [“node”, “app.js”] |
可以看一下这个文件内容的细节:
- FROM 指的是你要基于哪个基础镜像来构建自己的镜像。本例是基于基础镜像 node:slim 进行构建的,它是一个公开的 Node.js 镜像,仅包含了运行 node 所需要最小的 package。
- LABEL 是一个键值对,一般用来增加描述信息。本例中,它包含的是维护者的邮件信息。
- RUN 用来在容器中执行命令。它一般包含类似创建目录和通过基础的 Linux 命令完成容器初始化的任务。
- WORKDIR 定义了所有命令执行的目录。它通常是代码所在的目录。
- COPY 将主机中的文件拷贝到容器镜像中。在本例中,我们会将整个 app 拷贝到镜像中。
- 第二个 RUN 命令通过执行 npm install 来安装 package.json 中定义的应用程序的相关依赖。
- CMD 运行保证容器持续运行的进程。本例中,我们将使用 app.js 参数执行 node 命令。
现在该从 Dockerfile 中构建镜像了。
通过 -t 给镜像打上 registry 用户名、镜像名称和一个可选标签:
1 2 |
$ docker build -t sammy/todo-web . |
输出结果确定该镜像已经构建成功,且打上了合适的标记:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Sending build context to Docker daemon 8.238MB Step 1/7 : FROM node:slim ---> 286b1e0e7d3f Step 2/7 : LABEL maintainer = "jani@janakiram.com" ---> Using cache ---> ab0e049cf6f8 Step 3/7 : RUN mkdir -p /usr/src/app ---> Using cache ---> 897176832f4d Step 4/7 : WORKDIR /usr/src/app ---> Using cache ---> 3670f0147bed Step 5/7 : COPY ./app/ ./ ---> Using cache ---> e28c7c1be1a0 Step 6/7 : RUN npm install ---> Using cache ---> 7ce5b1d0aa65 Step 7/7 : CMD node app.js ---> Using cache ---> 2cef2238de24 Successfully built 2cef2238de24 Successfully tagged sammy/todo-web:latest |
我们可以通过运行 docker images 验证该镜像已经创建:
1 2 |
$ docker images |
同时,我们也可以看到该镜像的大小以及已创建多久:
1 2 |
Output from docker images |
1 2 3 |
REPOSITORY TAG IMAGE ID CREATED SIZE sammy/todo-web latest 81f5f605d1ca 9 minutes ago 236MB |
由于该示例应用程序也需要一个 MangoDB 容器,我们可以通过以下命令获得:
1 2 |
$ docker pull mongo:latest |
输出结果说明了正在拉取的是哪个镜像,以及下载进度:
1 2 3 4 |
latest: Pulling from library/mongo Digest: sha256:18b239b996e0d10f4ce2b0f64db6f410c17ad337e2cecb6210a3dcf2f732ed82 Status: Downloaded newer image for mongo:latest |
现在,我们已经将运行该应用程序需要的一切准备完毕。我们可以创建一个自定义的网络用于容器之间的通信。
第二步:创建一个网络来关联容器
如果我们通过 docker run 命令分别启动 Web 应用程序和数据库的容器,他们彼此无法找到对方。
想要知道原因,可以看看 Web 应用程序的数据库配置文件内容:
1 2 |
$ cat app/db.js |
在导入 Mongoose(这是一个在 Node.js 异步环境下对 MongoDB 进行便捷操作的对象模型工具)和定义一个新的数据库 schema 之后,Web 应用程序试图连接到一个主机名为 DB 的数据库,但它此时并不存在。
~/todo-app/app/db.js | |
---|---|
var mongoose = require( ‘mongoose’ ); var Schema = mongoose.Schema; var Todo = new Schema({ mongoose.model( ‘Todo’, Todo ); mongoose.connect( ‘mongodb://db/express-todo’ ); |
为了确保属于同一个应用的容器能找到彼此,我们需要在同一个网络中启动它们。
除了在安装时默认创建的 Default 网络外,Docker 也提供了创建自定义网络的能力。
你可以通过以下命令检查当前可用的网络:
1 2 |
$ docker network ls |
Docker 创建的每个网络都基于一个驱动。在以下输出结果中,我们可以看到名叫 bridge 的网络是基于 bridge 驱动的。而 local 的 scope 意味着该网络仅在该主机上可用:
1 2 3 4 5 |
NETWORK ID NAME DRIVER SCOPE 5029df19d0cf bridge bridge local 367330960d5c host host local f280c1593b89 none null local |
现在我们可以为应用程序创建一个自定义网络,叫 todo_net, 然后基于此网络启动容器:
1 2 |
$ docker network create todo_net |
输出结果显示该网络已经被创建出来:
1 2 |
C09f199809ccb9928dd9a93408612bb99ae08bb5a65833fefd6db2181bfe17ac |
现在列举出当前可用的网络:
1 2 |
$ docker network ls |
在此我们可以看到 todo_net 已经可用:
1 2 3 4 5 6 |
NETWORK ID NAME DRIVER SCOPE 5029df19d0cf bridge bridge local 367330960d5c host host local f280c1593b89 none null local bc992f0b2be6 todo_net bridge local |
现在使用 docker run 命令,我们可以通过 –network 将网络指向此网络。现在我们将 Web 和数据库容器都启动,并赋予特定的主机名。这样可以确保容器找到彼此。
首先,启动 MongoDB 数据库容器:
1 2 3 4 5 6 |
$ docker run -d \ $ --name=db \ $ --hostname=db \ $ --network=todo_net \ $ mongo |
以上命令的意思如下:
- -d 指的是以后台运行的模式运行容器
- –name 和 –hostname 将用户定义的名字赋给容器。 –hostname 也会在 Docker 管理的 DNS 服务中添加一个入口,这样能通过主机名来解析容器
- –network 指示 Docker 引擎在一个自定义的网络上启动容器,而不是默认的 bridge 网络上
当我们看到 docker run 命令返回的一个长长的字符串时,这意味着容器已经成功启动。但是这并不能保证容器真的在运行中:
1 2 |
aa56250f2421c5112cf8e383b68faefea91cd4b6da846cbc56cf3a0f04ff4295 |
可以通过 docker logs 命令验证 DB 容器是否已经起来并在运行中:
1 2 |
docker logs db |
以上命令会将容器的 log 打印到 stdout 中。log 的最后一行意味着 MongoDB 已经准备好,并在等在链接:
1 2 3 4 |
2017-12-10T02:55:08.284+0000 I CONTROL [initandlisten] MongoDB starting : pid=1 port=27017 dbpath=/data/db 64-bit host=db . . . . 2017-12-10T02:55:08.366+0000 I NETWORK [initandlisten] waiting for connections on port 27017 |
现在我们要启动 Web 容器了并进行验证了。这次我们使用 –publish=3000:3000,它意味着将主机的 3000 端口发布给容器的 3000 端口:
1 2 3 4 5 6 7 |
$ docker run -d \ $ --name=web \ $ --publish=3000:3000 \ $ --hostname=web \ $ --network=todo_net \ $ sammy/todo-web |
和以前一样,你将得到一个长长的字符串。
下面来验证该容器已经起来了并在运行中:
1 2 |
$ docker logs web |
输出结果确定 Express — 本文使用的应用程序基于的 Node.js 框架 — 正在监听 3000 端口。
1 2 |
Express server listening on port 3000 |
验证 Web 容器可以通过 ping 命令与 DB 容器进行通话。我们可以通过执行 docker exec 命令,并使用关联到伪 TTY (-t)的非交互模式( -i ):
1 2 |
$ docker exec -it web ping db |
该命令会产生标准的 ping 输出结果,这样我们就能知道两个容器之间是否可以互相通信:
1 2 3 4 5 |
PING db (172.18.0.2): 56 data bytes 64 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.210 ms 64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.095 ms ... |
注意:如果出现 exec: “ping”: executable file not found in $PATH. 错误,可以通过如下命令去 Web 容器中安装一下 ping 命令依赖的 package:
1 2 3 4 5 |
$ docker exec web -t $ sudo apt-get update $ sudo apt-get install iputils-ping $ exit |
可以通过 CTRL+C 来停止 ping 命令。
最后,使用浏览器打开 http://your_server_ip:3000 来访问实例 Web 应用程序。你将看到一个标记了 Container Todo Example 的标签和一个接受 todo 任务作为输入的文本框。
为了避免命名重复,你现在可以停止容器,并通过 docker rm 和 docker network remove 命令清理相关资源:
1 2 3 4 |
$ docker rm -f db $ docker rm -f web $ docker network remove todo_net |
此时,我们已经将 Web 应用程序容器化了——包含 2个单独的容器。
下一步,我们将探索一个更健壮的方法。
第三步:部署一个多容器的应用程序
尽管我们已经可以启动关联的容器,但这种处理多容器应用程序的方式并不优雅。我们需要一种更好的方式来声明所有相关容器,并将他们作为一个逻辑单元进行管理。
Docker Compose 是一个开发者可以用来处理多容器应用程序的框架。与 Dockerfile 一样,它也是使用声明式的机制来定义整个栈。现在,我们将把 Node.js 和 MongoDB 应用转化为基于 Docker Compose 的应用程序。
首先安装 Docker Compose:
1 2 |
$ sudo apt-get install -y docker-compose |
然后检查以下位于示例 Web 应用程序的 compose 目录下的 docker-compose.yaml 文件:
1 2 |
$ cat compose/docker-compose.yaml |
docker-compose.yaml 文件将所有东西整合到一起。它在 DB: 块中定义了 MongoDB 容器,在 web: 块中定义了 Node.js Web 容器,并在 networks: 块中定义了自定义网络。
注意:通过 build: ../. 标识,我们将 Compose 指向了位于app 目录下的 Dockerfile。这意味着 Compose 会在启动 Web 容器之前先构建镜像。
~/todo-app/compose/docker-compose.yaml | |
---|---|
version: ‘2’ services: db: image: mongo:latest container_name: db networks: – todonet web: build: ../. networks: – todonet ports: – “3000” networks: todonet: driver: bridge |
现在切换到 compose 目录下,通过 docker-compose up 命令启动应用程序。
在 docker run 命令中, -d 意味着以后台运行模式启动容器:
1 2 3 |
$ cd compose $ docker-compose up -d |
输出结果显示 Docker Compose 创建了一个名叫 compose_todotest 的网络,并在这个网络上启动这两个容器:
1 2 3 4 |
Creating network "compose_todonet" with driver "bridge" Creating db Creating compose_web_1 |
注意:我们并没有提供显式的主机端口映射。在这种情况下,Docker Compose 会随机分配一个端口给 Web 应用程序。我们可以通过以下命令找到这个端口:
1 2 |
$ docker ps |
可以看到 Web 应用程序在主机上的端口为 32782:
1 2 3 4 |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 6700761c0a1e compose_web "node app.js" 2 minutes ago Up 2 minutes 0.0.0.0:32782->3000/tcp compose_web_1 ad7656ef5db7 mongo:latest "docker-entrypoint..." 2 minutes ago Up 2 minutes 27017/tcp db |
我们可以通过打开浏览器访问 http://your_server_ip:32782 进行验证。然后我们会看到在第二步结尾的那个 Web 应用程序。
使用 Docker Compose 完成多容器应用程序的启动和运行之后,我们可以进一步看看如何管理和扩展应用程序。
第四步:管理和扩展应用程序
Docker Compose 让无状态 Web 应用程序的扩展变得非常容易。我们可以通过一个命令启动 10 个 Web 容器:
1 2 |
$ docker-compose scale web=10 |
我们可以从输出结果实时看到正在被创建和启动的实例:
1 2 3 4 5 6 7 8 9 10 |
Creating and starting compose_web_2 ... done Creating and starting compose_web_3 ... done Creating and starting compose_web_4 ... done Creating and starting compose_web_5 ... done Creating and starting compose_web_6 ... done Creating and starting compose_web_7 ... done Creating and starting compose_web_8 ... done Creating and starting compose_web_9 ... done Creating and starting compose_web_10 ... done |
可以通过运行 docker ps 命令验证 Web 应用程序已经扩展到 10 个实例了:
1 2 |
$ docker ps |
注意:Docker 会给每个 Web 容器分配一个随机端口。你可以使用任何一个端口来访问该应用程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES cec405db568d compose_web "node app.js" About a minute ago Up About a minute 0.0.0.0:32788->3000/tcp compose_web_9 56adb12640bb compose_web "node app.js" About a minute ago Up About a minute 0.0.0.0:32791->3000/tcp compose_web_10 4a1005d1356a compose_web "node app.js" About a minute ago Up About a minute 0.0.0.0:32790->3000/tcp compose_web_7 869077de9cb1 compose_web "node app.js" About a minute ago Up About a minute 0.0.0.0:32785->3000/tcp compose_web_8 eef86c56d16f compose_web "node app.js" About a minute ago Up About a minute 0.0.0.0:32783->3000/tcp compose_web_4 26dbce7f6dab compose_web "node app.js" About a minute ago Up About a minute 0.0.0.0:32786->3000/tcp compose_web_5 0b3abd8eee84 compose_web "node app.js" About a minute ago Up About a minute 0.0.0.0:32784->3000/tcp compose_web_3 8f867f60d11d compose_web "node app.js" About a minute ago Up About a minute 0.0.0.0:32789->3000/tcp compose_web_6 36b817c6110b compose_web "node app.js" About a minute ago Up About a minute 0.0.0.0:32787->3000/tcp compose_web_2 6700761c0a1e compose_web "node app.js" 7 minutes ago Up 7 minutes 0.0.0.0:32782->3000/tcp compose_web_1 ad7656ef5db7 mongo:latest "docker-entrypoint..." 7 minutes ago Up 7 minutes 27017/tcp db |
你也可以使用相同灵命缩减 Web 容器:
1 2 |
$ docker-compose scale web=2 |
你可以实时看到正在被移除的实例:
1 2 3 4 5 6 7 8 9 |
Stopping and removing compose_web_3 ... done Stopping and removing compose_web_4 ... done Stopping and removing compose_web_5 ... done Stopping and removing compose_web_6 ... done Stopping and removing compose_web_7 ... done Stopping and removing compose_web_8 ... done Stopping and removing compose_web_9 ... done Stopping and removing compose_web_10 ... done |
最后,再次检查这些实例:
1 2 |
$ docker ps |
输出结果确认只有 2 个实例被留下来了:
1 2 3 4 5 |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 36b817c6110b compose_web "node app.js" 3 minutes ago Up 3 minutes 0.0.0.0:32787->3000/tcp compose_web_2 6700761c0a1e compose_web "node app.js" 9 minutes ago Up 9 minutes 0.0.0.0:32782->3000/tcp compose_web_1 ad7656ef5db7 mongo:latest "docker-entrypoint..." 9 minutes ago Up 9 minutes 27017/tcp db |
现在你可以停止该应用程序,与之前一样,你可以清理资源,避免命名冲突:
1 2 3 4 |
$ docker-compose stop $ docker-compose rm -f $ docker network remove compose_todonet |
结论
本文主要介绍了 Dockerfile 和 Docker Compose。
本文开头介绍 Dockerfile 是一个构建镜像的声明式机制,然后我们介绍了 Docker 网络的基础知识。最后,我们演示了使用 Docker Compose 扩展和管理多容器应用程序。
本文作者:马小婷