准备工作

  • 安装1.13或者更高版本的docker。
  • 已阅读第一部分中的概念。
  • 简单测试您的环境已经完全准备好:
docker run hello-world

引言

现在是时候使用docker的方式来构建应用了。我们从这个应用层次结构的底部开始,本文将描述容器相关的内容。关于如何成为一个具有生产能力的服务将在第三部分中详述。最后在第五部分中将描述顶层的服务堆栈,包含所有服务的交互问题。
- 堆栈(Stack)
- 服务(Services)
- 容器(Container 当前层级)

新的开发环境

在过去,如果想编写一个Python应用,你的首要任务就是在你的机器上安装Python运行环境。而且还需要该环境能够符合你的应用运行所需的扩展库,并且还要与你的生产环境进行匹配。

如果使用docker, 你只需获取一个可以移植的python运行环境镜像,不需要额外安装。然后你就可以构建一个包含程序代码运行的基本python环境以及运行时的依赖项一起的python镜像。

这个可移植镜像通过一个叫做Dockerfile的文件定义。

使用Dockerfile定义一个容器

Dockerfile中定义了容器内部环境中发生的事情,比如在环境内对网络接口和磁盘驱动器的虚拟化。由于该环境与系统的其他部分时相互隔离的,所以你需要将内部端口隐射到外部环境中来,并指定那些内容需要复制到环境中去。另外,在执行操作后,你可以认定该Dockerfile的程序构建操作在任何地方运行都是相同的。

Dockerfile

创建一个空的目录,并将目录定位到新的目录中(使用cd命令),然后创建一个名为Dockerfile的文件,将下面的内容拷贝到该文件中并保存。注意文件中的每一个注释。

# 使用python官方运行环境作为父镜像
FROM python:2.7-slim

# 将/app设置成工作目录
WORKDIR /app

# 将当前目录中的内容拷贝到容器的/app目录中去
COPY . /app

# 安装requirements.txt指定的库
RUN pip install --trusted-host pypi.python.org -r requirements.txt

# 对外开放容器的80端口
EXPOSE 80

# 定义环境变量
ENV NAME World

# 当容器启动后运行app.py文件
CMD ["python", "app.py"]

下面将进行将创建Dockerfile中使用到的外部文件,如app.pyrequirements.txt

应用本身

Dockerfile文件所在目录中创建名为app.pyrequirements.txt的两个文件。这样就完成了我们的应用创建,这个过程看起来非常简单。当构建上述Dockerfile时,由于使用了的COPY命令,所以app.pyrequirements.txt将会出现该环境中,又因为使用EXPOSE命令,所以外部网络可以通过HTTP的方式访问app.py

requirements.txt

Flask
Redis

app.py

from flask import Flask
from redis import Redis, RedisError
import os
import socket

# 连接Redis
redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2)

app = Flask(__name__)

@app.route("/")
def hello():
    try:
        visits = redis.incr("counter")
    except RedisError:
        visits = "<i>cannot connect to Redis, counter disabled</i>"

    html = "<h3>Hello {name}!</h3>" \
           "<b>Hostname:</b> {hostname}<br/>" \
           "<b>Visits:</b> {visits}"
    return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname(), visits=visits)

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=80)

我们可以看出pip install -r requirements.txt使用来下载python依赖库FlaskRedis的,然后应用输出了环境变量NAME,同时也输出了socket.gethostname()的结果。最后因为Redis未运行(我们仅仅安装了python的库文件,而不是Redis本身),我们可以预料这里将会运行失败并抛出异常信息。

注意:在容器内部访问容器ID时就是访问其主机名,这个有些类似正在运行的进程ID。

完成! 您的系统上不需要Python或requirements.txt中的任何内容,构建或运行此镜像也不需要在系统上安装它们。看起来你并没有真正建立一个PythonFlask的环境,但你已经拥有了一切。

构建这个应用

我们已经准备号构建这个应用了。确保你当前所处位置在工作目录的顶层。下面的ls命令应有的结果:

$ ls
Dockerfile		app.py			requirements.txt

现在运行构建命令。我们可以使用--tag选项为将要创建的镜像命令,也可以使用短名选项-t代替。

docker build --tag=friendlyhello .

在哪儿可以看到你构建的镜像呢?它保存在你本地docker的镜像仓库里:

$ docker image ls

REPOSITORY            TAG                 IMAGE ID
friendlyhello         latest              326387cea398

注意镜像默认TAG为latest。如果要指定其他的TAG,可以使用完成标记进行定义,如:--tag=friendlyhello:v0.0.1.

Linux用户的故障排除

代理服务器配置

代理服务器可以在启动并运行后阻止与Web应用程序的连接。如果您位于代理服务器后面,请将以下行添加到Dockerfile,使用ENV命令指定代理服务器的主机和端口:

# 设置代理服务器, 将host:port替换成你的服务器信息
ENV http_proxy host:port
ENV https_proxy host:port

DNS配置

DNS配置错误可能会导致pip出现问题。您需要设置自己的DNS服务器地址才能使pip正常工作。 您将需要更改Docker守护程序的DNS设置,可以在/etc/docker/daemon.json配置文件(文件可能不存在,需要自行创建)中编辑(创建)dns关键字,如下所示:

{
  "dns": ["your_dns_address", "8.8.8.8"]
}

在上面的示例中,列表的第一个元素是你自己DNS服务器的地址。第二项是Google的DNS,可在第一项无法使用时使用。

完成上述修改,保存daemon.json文件,重启Docker

sudo service docker restart

修复后,重新尝试build命令。

运行应用

运行应用,并使用-p选项将容器开发的80端口映射到外部的4000端口上:

docker run -p 4000:80 friendlyhello

你可以看到python在本机的80端口(http://0.0.0.0:80)上服务。但是这个设置是在容器内的环境中有效,容器内部并不知道你使用了外部的4000端口映射内部的80端口,所以你在外部访问该服务是需要使用http://localhost:4000

在浏览器中输入这个地址,你就可以看到该服务提供的一个web页面。

浏览器中展示

注意:如果你是在window7上使用Docker Toolbox,则需要使用Docker的宿主机ip来替换localhost,例如:http://192.168.99.100:4000/。可以使用docker-machine ip命令来获取宿主机ip。

你可以在终端中使用curl命令来查看相同的内容。

$ curl http://localhost:4000

<h3>Hello World!</h3><b>Hostname:</b> 8fc990912a14<br/><b>Visits:</b> <i>cannot connect to Redis, counter disabled</i>

在执行docker run -p 4000:80命令分别对应着该服务在外部提供服务的端口和容器内Dockerfile中使用EXPOSE指令对外开发的端口。这是该操作后就可以在本地(http://localhost)使用4000端口访问容器的80端口了。

在终端中按CTRL+C退出。

在window中显式停止容器

在window系统中,CTRL+C不能直接停止容器。所以,你应该首先按下CTRL+C查看相关提示或者打开一个新的终端,然后执行docker container ls查看正在运行的容器列表,最后使用docker container stop <容器NAME或者ID>停止相应容器。否则,在你下一次运行该容器的时候,守护进程将会抛出错误信息。

现在我们来让应用以独立模式背后运行:

docker run -d -p 4000:80 friendlyhello

你将获得一个应用运行的容器长ID,然后返回终端,应用将在背后运行。你还可以使用docker container ls命令查看容器的短ID(在运行命令是可以互换):

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED
1fa4ab2cf395        friendlyhello       "python app.py"     28 seconds ago

请注意,CONTAINER ID匹配http:// localhost:4000上的内容。

现在使用docker container stop 命令停止进程,以CONTAINER ID作为参数,如下:

docker container stop 1fa4ab2cf395

分享镜像

为了证明我们所创建镜像的便携性,我们将上传该镜像,然后在任意地方运行。为此,你需要知道如何将想要发布到生产环境的镜像推送到镜像注册服务。

镜像注册服务就是一个仓库集合,想Github那样的镜像仓库,不同点在于docker仓库的代码是已经编译过的。一个账号可以创建若干个仓库,docker命令默认使用Docker公共注册服务。

注意:我们在这里使用Docker的公共注册服务只是因为它是免费和预配置的,另外还有许多公共注册服务可供选择,您甚至可以使用Docker Trusted Registry设置自己的私有注册服务。

登录注册服务

如果你还没有Docker账号,可以到hub.docker.com注册一个,然后记住你的用户名。

在本机登录Docker公共注册服务:

$ docker login

给镜像打标签

将本地镜像与注册服务上的仓库相关联的表达式为:username/repository:tagtag为可选项,但还是建议显式声明,因为它注册服务用来区分镜像版本的机制。为镜像分配仓库并使用一个有意义的名字,比如:get-started:part2。这个镜像将放入get-started仓库中,并标记为part2版本。

现在我们来为镜像打标签。使用你的用户名、仓库名以及标签名来执行docker tag image命令,以便确定该镜像上传的目的地。完整命令如下:

docker tag image username/repository:tag

例如:

docker tag friendlyhello gordon/get-started:part2

执行docker image ls查看你最近标记的镜像。

$ docker image ls

REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
friendlyhello            latest              d9e555c53008        3 minutes ago       195MB
gordon/get-started         part2               d9e555c53008        3 minutes ago       195MB
python                   2.7-slim            1c7128a655f6        5 days ago          183MB
...

发布镜像

上传你标记的镜像到仓库中:

docker push username/repository:tag

一旦完成上述操作,你的镜像将会作为公共镜像。当你登录Docker Hub后,你就可以看到你新上传的镜像,以及相关pull命令。

从远程仓库下载镜像并运行

从先在开始,你可以使用docker run命令在任何机器上运行你的应用,具体命令如下:

docker run -p 4000:80 username/repository:tag

如果本机不存在该镜像,docker将从远程仓库自动下载(或使用docker pull命令手动下载)。

$ docker run -p 4000:80 gordon/get-started:part2
Unable to find image 'gordon/get-started:part2' locally
part2: Pulling from gordon/get-started
10a267c67f42: Already exists
f68a39a6a5e4: Already exists
9beaffc0cf19: Already exists
3c1fe835fb6b: Already exists
4c9f1fa8fcb8: Already exists
ee7d8f576a14: Already exists
fbccdcced46e: Already exists
Digest: sha256:0601c866aab2adcc6498200efd0f754037e909e5fd42069adeff72d1e2439068
Status: Downloaded newer image for gordon/get-started:part2
 * Running on http://0.0.0.0:80/ (Press CTRL+C to quit)

无论在什么地方执行docker run,都将获取你的镜像,连同python和requirements.txt中的依赖,并运行你的代码。这些都将整合到一个很小的镜像包里面,你不需要额外安装任何东西就可以使用docker运行它。

结论

本文全部介绍完毕。下一本将会介绍如何在服务中扩展我们的应用。

——————————————————————————
行路不知花开处,蓦然回首芷兰香。